RiscV ISA 解码器

本教程的目标如下:

  • 了解 MPACT-Sim 模拟器如何表示指令。
  • 了解 ISA 描述文件的结构和语法。
  • 为 RiscV RV32I 指令子集编写 ISA 说明

概览

在 MPACT-Sim 中,目标指令会被解码并存储在内部表示法中,以便更方便地获取其信息,并加快执行语义。这些指令实例会缓存在指令缓存中,以减少经常执行的指令的执行次数。

指令类

在开始之前,我们先简单了解一下指令在 MPACT-Sim 中的表示方式,会很有帮助。Instruction 类在 mpact-sim/mpact/sim/generic/instruction.h 中定义。

Instruction 类实例包含在“执行”指令时模拟指令所需的所有信息,例如:

  1. 指令地址、模拟指令大小,即 .text 格式的大小。
  2. 指令操作码。
  3. 谓词操作数接口指针(如果适用)。
  4. 源运算数接口指针的矢量。
  5. 目标运算数接口指针的向量。
  6. 语义函数可调用。
  7. 指向架构状态对象的指针。
  8. 指向上下文对象的指针。
  9. 指向子级和下一个 Instruction 实例的指针。
  10. 反汇编字符串。

这些实例通常存储在指令(实例)缓存中,并且每当重新执行指令时都会重复使用。这样可以提高运行时的性能。

除了指向上下文对象的指针之外,所有其他指针都由根据 ISA 说明生成的指令解码器进行填充。在本教程中,我们无需了解这些项的详细信息,因为我们不会直接使用它们。只需大致了解它们的使用方式即可。

语义函数可调用项是实现指令语义的 C++ 函数/方法/函数对象(包括 lambda)。例如,对于 add 指令,它会加载每个源操作数,对这两个操作数进行加法运算,并将结果写入单个目的操作数。语义函数教程中详细介绍了语义函数这一主题。

指令运算数

指令类包括指向三种类型的运算数接口的指针:谓词、源和目标。这些接口支持独立于底层指令运算数的实际类型编写语义函数。例如,通过相同的接口访问寄存器和立即值。这意味着,执行相同操作但对不同操作数(例如寄存器与立即值)的操作可以使用相同的语义函数来实现。

对于支持基于条件执行指令的 ISA(对于其他 ISA,它为 null),谓词运算数接口用于根据谓词的布尔值确定是否应执行给定指令。

// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
 public:
  virtual bool Value() = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
  virtual ~PredicateOperandInterface() = default;
};

源运算数接口允许指令语义函数从指令运算数中读取值,而不考虑底层运算数类型。接口方法同时支持标量值和矢量值运算数。

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};

目的操作数接口提供了用于分配和处理 DataBuffer 实例(用于存储寄存器值的内部数据类型)的方法。目标运算数也有与之关联的延迟时间,即等待指令语义函数分配的数据缓冲区实例用于更新目标寄存器的值的周期数。例如,add 指令的延迟时间可能为 1,而 mpy 指令的延迟时间可能为 4。有关语义函数的本教程中对此进行了详细介绍。

// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

ISA 说明

处理器的 ISA(指令集架构)定义了软件与硬件互动的抽象模型。它定义了可用的指令集、数据类型、寄存器和其他指令的机器状态,以及它们的行为(语义)。对于 MPACT-Sim 而言,ISA 不包含指令的实际编码。请单独处理。

处理器 ISA 以描述文件的形式表示,该描述文件在与编码无关的抽象级别描述指令集。说明文件会枚举一组可用指令。对于每条指令,必须列出其名称、操作数的数量和名称,以及其与实现其语义的 C++ 函数/可调用的绑定。此外,还可以指定反汇编格式字符串,以及该指令对硬件资源名称的使用。前者可用于生成指令的文本表示形式,以用于调试、跟踪或交互式用途。后者可用于在模拟中提高周期准确性。

ISA 描述文件由 isa-parser 解析,该解析器会为不依赖于表示法的汇编指令解码器生成代码。此解码器负责填充指令对象的字段。具体值(例如目标寄存器编号)是从特定于格式的指令解码器获取的。二进制解码器就是其中一种解码器,下一教程将重点介绍它。

本教程介绍如何为简单标量架构编写 ISA 描述文件。我们将使用 RiscV RV32I 指令集的子集来说明这一点,并与其他教程一起构建能够模拟“Hello World”程序的模拟器。如需详细了解 RiscV ISA,请参阅 Risc-V 规范

首先打开文件: riscv_isa_decoder/riscv32i.isa

该文件的内容分为多个部分。首先是 ISA 声明:

isa RiscV32I {
  namespace mpact::sim::codelab;
  slots { riscv32; }
}

这会将 RiscV32I 声明为 ISA 的名称,并且代码生成器将创建一个名为 RiscV32IEncodingBase 的类,用于定义生成的解码器将用来获取操作码和操作数信息的接口。此类的名称通过以下方式生成:将 ISA 名称转换为 Pascal 大小写形式,然后将其与 EncodingBase 串联。声明 slots { riscv32; } 指定 RiscV32I ISA 中只有一个指令槽 riscv32(而 VLIW 指令中有多个槽),并且唯一有效的指令是定义为在 riscv32 中执行的指令。

// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};

这指定了任何反汇编规范(详见下文)的第一个反汇编 fragment 将在 15 个字符宽的字段中左对齐。任何后续 fragment 都会附加到此字段,而不会添加任何额外的空格。

下面是三个槽位声明:riscv32izicsrriscv32。根据上面的 isa 定义,只有为 riscv32 槽定义的指令会成为 RiscV32I 的一部分。另外两个空档有什么用?

槽可用于将指令分解为单独的组,然后在最后将这些组合并为单个槽。请注意 riscv32 槽声明中的表示法 : riscv32i, zicsr。这表示槽位 riscv32 继承槽位 zicsrriscv32i 中定义的所有指令。RiscV 32 位 ISA 由一个名为 RV32I 的基本 ISA 组成,您可以向其添加一组可选扩展。借助槽机制,可以单独指定这些扩展中的指令,然后根据需要在最后组合这些指令以定义整个 ISA。在本例中,RiscV“I”组中的指令与“zicsr”组中的指令是分开定义的。根据所需的最终 RiscV ISA,可以为“M”(乘/除)、“F”(单精度浮点)、“D”(双精度浮点)、“C”(紧凑的 16 位指令)等定义其他组。

// The RiscV 'I' instructions.
slot riscv32i {
  ...
}

// RiscV32 CSR manipulation instructions.
slot zicsr {
  ...
}

// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
  ...
}

无需更改 zicsrriscv32 槽定义。不过,本教程的重点是向 riscv32i 槽添加必要的定义。我们来详细了解一下此广告位中当前定义了什么:

// The RiscV 'I' instructions.
slot riscv32i {
  // Include file that contains the declarations of the semantic functions for
  // the 'I' instructions.
  includes {
    #include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
  }
  // These are all 32 bit instructions, so set default size to 4.
  default size = 4;
  // Model these with 0 latency to avoid buffering the result. Since RiscV
  // instructions have sequential semantics this is fine.
  default latency = 0;
  // The opcodes.
  opcodes {
    fence{: imm12 : },
      semfunc: "&RV32IFence"c
      disasm: "fence";
    ebreak{},
      semfunc: "&RV32IEbreak",
      disasm: "ebreak";
  }
}

首先是 includes {} 部分,其中列出了在最终 ISA 中直接或间接引用此槽位时需要包含在生成的代码中的头文件。此外,您可以在全局范围的 includes {} 部分中列出包含文件,在这种情况下,此类文件始终包含在内。如果必须向每个槽位定义添加相同的 include 文件,这会非常方便。

default sizedefault latency 声明定义如下:除非另有指定,否则指令的大小为 4,且目标运算数写入的延迟时间为 0 个周期。请注意,此处指定的指令的大小是用于计算要在模拟处理器中执行的下一个序列指令的地址的程序计数器增量的大小。这可能与输入可执行文件中的指令表示法以字节为单位的大小相同,也可能不同。

空档定义的核心是操作码部分。如您所见,riscv32i 中目前只定义了两个操作码(指令):fenceebreak。定义 fence 操作码时,需指定名称 (fence) 和运算数规范 ({: imm12 : }),后跟可选反汇编格式 ("fence"),以及要作为语义函数 ("&RV32IFence") 绑定的 Callable 函数。

指令运算数指定为三元组,每个组成部分以分号 predicate ':' 源操作数列表 ':' 目标操作数列表分隔。源操作数列表和目标操作数列表是逗号分隔的操作数名称列表。如您所见,fence 指令的指令操作数不含谓词操作数,只有一个源操作数名称 imm12,且没有目标操作数。RiscV RV32I 子集不支持谓词执行,因此在本教程中的谓词操作数将始终为空。

语义函数被指定为用于调用语义函数的 C++ 函数或 Callable 函数所需的字符串。语义函数/Callable 的签名为 void(Instruction *)

反汇编规范由以英文逗号分隔的字符串列表组成。通常仅使用两个字符串,一个用于操作码,一个用于操作数。设置格式时(使用说明中的 AsString() 调用),系统将根据上述 disasm widths 规范在字段中设置每个字符串的格式。

以下练习可帮助您向 riscv32i.isa 文件添加足够的指令,以便模拟“Hello World”程序。如果时间紧迫,您可以在 riscv32i.isarv32i_instructions.h 中找到解决方案。


执行初始构建

如果您尚未将目录更改为 riscv_isa_decoder,请立即执行此操作。然后,按如下方式构建项目,此构建应该会成功。

$ cd riscv_isa_decoder
$ bazel build :all

现在,将您的目录更改回代码库根目录,然后看一下生成的源代码。为此,请将目录更改为 bazel-out/k8-fastbuild/bin/riscv_isa_decoder(假设您使用的是 x86 主机;对于其他主机,k8-fastbuild 将是另一个字符串)。

$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder

在此目录中,除了其他文件之外,还会生成以下 C++ 文件:

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

我们在浏览器中点击 riscv32i_enums.h,来看看它。您应该会看到其中包含类似以下内容:

#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H

namespace mpact {
namespace sim {
namespace codelab {
  enum class SlotEnum {
    kNone = 0,
    kRiscv32,
  };

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kCsr = 1,
    kImm12 = 2,
    kRs1 = 3,
    kPastMaxValue = 4,
  };

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kRd = 2,
    kPastMaxValue = 3,
  };

  enum class OpcodeEnum {
    kNone = 0,
    kCsrs = 1,
    kCsrsNw = 2,
    kCsrwNr = 3,
    kEbreak = 4,
    kFence = 5,
    kPastMaxValue = 6
  };

  constexpr char kNoneName[] = "none";
  constexpr char kCsrsName[] = "Csrs";
  constexpr char kCsrsNwName[] = "CsrsNw";
  constexpr char kCsrwNrName[] = "CsrwNr";
  constexpr char kEbreakName[] = "Ebreak";
  constexpr char kFenceName[] = "Fence";
  extern const char *kOpcodeNames[static_cast<int>(
      OpcodeEnum::kPastMaxValue)];

  enum class SimpleResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class ComplexResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class AttributeEnum {
    kPastMaxValue = 0
  };

}  // namespace codelab
}  // namespace sim
}  // namespace mpact

#endif  // RISCV32I_ENUMS_H

如您所见,在 riscv32i.isa 文件中定义的每个槽位、操作码和运算数都是在枚举类型之一中定义的。此外,还有一个 OpcodeNames 数组,用于存储所有操作码的名称(在 riscv32i_enums.cc 中定义)。其他文件包含生成的解码器,我们将在另一个教程中详细介绍该解码器。

Bazel 构建规则

Bazel 中的 ISA 解码器目标使用名为 mpact_isa_decoder 的自定义规则宏进行定义,该宏从 mpact-sim 代码库中的 mpact/sim/decoder/mpact_sim_isa.bzl 加载。在本教程中,riscv_isa_decoder/BUILD 中定义的构建目标为:

mpact_isa_decoder(
    name = "riscv32i_isa",
    src = "riscv32i.isa",
    includes = [],
    isa_name = "RiscV32I",
    deps = [
        "//riscv_semantic_functions:riscv32i",
    ],
)

此规则会调用 ISA 解析器工具和生成器来生成 C++ 代码,然后将生成的代码编译为其他规则可以使用标签 //riscv_isa_decoder:riscv32i_isa 依赖的库。includes 部分用于指定源文件可能包含的其他 .isa 文件。isa_name 用于指定在生成解码器的源文件中,如果指定了多个 IIS,则此为必填。


添加寄存器-寄存器 ALU 指令

现在,可以向 riscv32i.isa 文件中添加一些新指令了。第 1 组指令是寄存器-寄存器 ALU 指令,例如 addand 等。在 RiscV32 上,这些指令都使用 R 型二进制指令格式:

31..25 24..20 19 月 15 日 12 月 14 日 11..7 6.0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd 操作码

虽然 .isa 文件用于生成与格式无关的解码器,但考虑二进制格式及其布局来引导条目仍然很有用。如您所见,有三个字段与用于填充指令对象的解码器相关:rs2rs1rd。此时,我们将选择将这些名称用于所有指令中同一指令字段中以相同方式(位序列)编码的整数寄存器。

我们要添加的说明如下:

  • add - 整数加法。
  • and - 按位和。
  • or - 按位或。
  • sll - 左移逻辑。
  • sltu - 设置小于号、无符号。
  • sub - 整数相减。
  • xor - 按位异或。

其中每条指令都将添加到 riscv32i 槽定义的 opcodes 部分。回想一下,我们必须为每个指令指定名称、操作码、反汇编和语义函数。这个名称很简单,我们只使用上面的运算码名称。此外,它们都使用相同的运算数,因此我们可以使用 { : rs1, rs2 : rd} 作为运算数规范。这意味着,由 rs1 指定的寄存器源操作数在指令对象的源操作数矢量中的索引为 0,由 rs2 指定的寄存器源操作数的索引为 1,由 rd 指定的寄存器目标操作数将是目标操作数矢量中的唯一元素(索引为 0)。

接下来是语义函数规范。为此,您可以使用关键字 semfunc 和一个 C++ 字符串,指定可用于分配给 std::function 的可调用对象。在本教程中,我们将使用函数,因此可调用字符串将为 "&MyFunctionName"。根据 fence 指令建议的命名方案,这些命名方案应为 "&RV32IAdd""&RV32IAnd" 等。

最后是反汇编规范。它以关键字 disasm 开头,后跟逗号分隔的字符串列表,用于指定应如何以字符串形式输出指令。如果在运算数名称前面使用 % 符号,则表示使用该运算数的字符串表示形式进行字符串替换。对于 add 指令,应为:disasm: "add", "%rd, %rs1,%rs2"。这意味着,add 指令的条目应如下所示:

    add{ : rs1, rs2 : rd},
      semfunc: "&RV32IAdd",
      disasm: "add", "%rd, %rs1, %rs2";

接下来,修改 riscv32i.isa 文件,并将所有这些说明添加到 .isa 说明中。如果您需要帮助(或想检查自己的工作),请点击此处查看完整说明文件。

将指令添加到 riscv32i.isa 文件后,有必要为引用位于 `../semantic_functions/ 中的 rv32i_instructions.h 文件中的每个新语义函数添加函数声明。再次提醒,如果您需要帮助(或想检查自己的工作),请点击此处

完成上述所有操作后,返回 riscv_isa_decoder 目录并重新构建。您随时可以检查生成的源文件。


添加包含立即数的 ALU 指令

我们要添加的下一组指令是 ALU 指令,这些指令使用立即值而不是其中一个寄存器。这类指令分为三组(基于立即字段):具有 12 位有符号立即数的 I 型立即数指令、用于移位的专用 I 型立即数指令,以及具有 20 位无符号立即数值的 U 型立即数。具体格式如下所示:

I-Type 直接格式:

2020 年 31 月 20 日 19..15 12 月 14 日 11..7 6.0
12 5 3 5 7
imm12 rs1 func3 rd 操作码

专用 I-Type 立即格式:

31 月 25 日 24..20 19 月 15 日 12 月 14 日 11..7 6.0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd 操作码

U-Type 直接格式:

12 月 31 日 11.7 6.0
20 5 7
uimm20 RD 操作码

如您所见,操作数名称 rs1rd 与之前相同的位字段相关联,并且用于表示整数寄存器,因此可以保留这些名称。立即值字段具有不同的长度和位置,并且两个(uimm5uimm20)是无符号的,而 imm12 是有符号的。其中每个元素都将使用自己的名称。

因此,I 型指令的运算数应为 { : rs1, imm12 :rd }。对于专用 I 型指令,它应为 { : rs1, uimm5 : rd}。U 类型指令运算数规范应为 { : uimm20 : rd }

我们需要添加的 I 型说明如下:

  • addi - 立即添加。
  • andi - 按位立即,
  • ori - 按位或立即。
  • xori - 按位异或立即数。

我们需要添加的专用 I-Type 指令如下:

  • slli - 按逻辑顺序左移。
  • srai - 按立即数右移算术。
  • srli - 按立即数右移逻辑右移。

我们需要添加的 U-Type 说明如下:

  • auipc - 向 PC 添加上部直接位置。
  • lui - 加载顶部立即。

用于操作码的名称自然地遵循上述指令名称(无需设立新指令,它们都是独一无二的)。在指定语义函数时,请回想一下,指令对象会对不依赖于底层运算符类型的源运算符编码接口。这意味着,对于运算相同但运算数类型可能不同的指令,它们可以共用同一个语义函数。例如,如果忽略运算数类型,则 addi 指令执行与 add 指令相同的运算,因此它们可以使用相同的语义函数规范 "&RV32IAdd"。对 andiorixorislli 也进行了类似的重命名。 其他指令使用新的语义函数,但应根据运算(而不是运算数)来命名,因此对于 srai,请使用 "&RV32ISra"。U 型指令 auipclui 没有等效的寄存器,因此可以使用 "&RV32IAuipc""&RV32ILui"

反汇编字符串与上一个练习中的反汇编字符串非常相似,但对 %rs2 的引用将视情况替换为 %imm12%uimm5%uimm20

继续进行更改并构建。检查生成的输出。与之前一样,您可以对照 riscv32i.isarv32i_instructions.h 检查您的工作。


我们需要添加的分支和跳转和链接指令都使用仅在指令本身中隐含的目标操作数,即下一个 PC 值。在此阶段,我们会将其视为名为 next_pc 的适当运算数。我们将在后续教程中对此进行进一步定义。

分支说明

我们要添加的分支都使用 B 类型编码。

31 30..25 24..20 19 月 15 日 14..12 11 月 8 日 7 6.0
1 6 5 5 3 4 1 7
imm imm rs2 rs1 func3 imm imm 操作码

不同的立即字段会串联成一个 12 位有符号立即值。由于格式实际上并不相关,因此我们将此立即数称为 bimm12,表示 12 位分支立即数。下一篇关于创建二进制解码器的教程中将解决这种碎片化问题。所有分支指令都会比较 rs1 和 rs2 指定的整数寄存器,如果条件为真,立即值会被添加到当前 pc 值,以生成要执行的下一条指令的地址。因此,分支指令的运算数应为 { : rs1, rs2, bimm12 : next_pc }

我们需要添加的分支指令包括:

  • beq - 如果相等,则分支。
  • bge - 如果大于或等于,则分支。
  • bgeu - 如果大于或等于无符号值,则分支。
  • blt - 如果小于,则分支。
  • bltu - 如果小于无符号值,则分支。
  • bne - 如果不等于,则分支。

这些操作码名称都是唯一的,因此可以在 .isa 说明中重复使用。当然,必须添加新的语义函数名称,例如"&RV32IBeq"

现在,反汇编规范有点棘手,因为指令的地址用于计算目的地,但实际上并未包含在指令操作数中。不过,它是存储在指令对象中的信息的一部分,因此是可用的。解决方法是在反汇编字符串中使用表达式语法。您可以输入 %(expression: print format),而不是使用“%”后跟运算数名称。仅支持非常简单的表达式,但“地址 + 偏移”是其中一种,并且针对当前指令地址使用 @ 符号。输出格式与 C 样式的 printf 格式类似,但没有前导 %。然后,beq 指令的反汇编格式将变为:

    disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"

只需添加两个跳转和链接指令即可,即 jal(跳转和链接)和 jalr(间接跳转和链接)。

jal 指令使用 J 类型编码:

31 21 月 30 日 20 19..12 11..7 6.0
1 10 1 8 5 7
imm imm imm imm rd 操作码

与分支指令一样,20 位立即数会分散在多个字段中,因此我们将其命名为 jimm20。碎片化目前并不重要,但将在下一个介绍如何创建二进制解码器的教程中加以讨论。然后,运算数规范将变为 { : jimm20 : next_pc, rd }。请注意,有两个目标运算数:下一个 pc 值和指令中指定的链接寄存器。

与上述分支指令类似,反汇编格式如下所示:

    disasm: "jal", "%rd, %(@+jimm20:08x)"

间接跳转和链接使用 I-Type 格式和 12 位立即数。它会将符号扩展的立即值添加到 rs1 指定的整数寄存器,以生成目标指令地址。链接寄存器是 rd 指定的整数寄存器。

2020 年 31 月 20 日 19..15 12 月 14 日 11..7 6.0
12 5 3 5 7
imm12 rs1 func3 rd 操作码

如果您已经发现了模式,现在就可以推断出 jalr 的操作数规范应为 { : rs1, imm12 : next_pc, rd },以及反汇编规范:

    disasm: "jalr", "%rd, %rs1, %imm12"

继续进行更改,然后进行构建。检查生成的输出。和以前一样,您可以根据 riscv32i.isarv32i_instructions.h 检查您的工作。


添加商店说明

存储指令非常简单。它们都使用 S-Type 格式:

31..25 24..20 19 月 15 日 12 月 14 日 11..7 6.0
7 5 5 3 5 7
imm rs2 rs1 func3 imm 操作码

如您所见,这又是一个分片 12 位立即数的示例,我们将其称为 simm12。存储指令会将 rs2 指定的整数寄存器的值全部存储在内存中的有效地址中,方法是将 rs1 指定的整数寄存器的值与 12 位立即数的符号扩展值相加。对于所有存储指令,运算数格式应为 { : rs1, simm12, rs2 }

需要实现的商店说明如下:

  • sb - 存储字节。
  • sh - 存储半个字词。
  • sw - 商店字词。

sb 的反汇编规范符合您的预期:

    disasm: "sb", "%rs2, %simm12(%rs1)"

语义函数规范也是您预期的:"&RV32ISb" 等。

请继续进行更改,然后进行构建。检查生成的输出。和以前一样,您可以根据 riscv32i.isarv32i_instructions.h 检查您的工作。


添加加载说明

加载指令的建模方式与模拟器中其他指令略有不同。为了能够模拟加载延迟时间不确定的情况,加载指令分为两项独立的操作:1) 有效地址计算和内存访问;2) 结果回写。在模拟器中,这是通过将加载的语义操作拆分为两条单独的指令(主指令和指令)来完成的。此外,在指定运算数时,我们需要为主指令和子指令都指定运算数。具体方法是将运算数规范视为一个三元组列表。相关语法如下:

{(predicate : sources : destinations), (predicate : sources : destinations), ... }

与前面的指令一样,加载指令都使用 I-Type 格式:

2020 年 31 月 20 日 19..15 12 月 14 日 11..7 6.0
12 5 3 5 7
imm12 rs1 func3 rd 操作码

该运算数规范会拆分计算地址所需的运算数,并从加载数据的寄存器目的地启动内存访问:{( : rs1, imm12 : ), ( : : rd) }

由于语义操作分为两条指令,因此语义函数同样需要指定两个 Callable 函数。对于 lw(加载字),应编写如下代码:

    semfunc: "&RV32ILw", "&RV32ILwChild"

反汇编规范更为传统。未提及子指令。对于 lw,它应该是:

    disasm: "lw", "%rd, %imm12(%rs1)"

需要实现的加载指令包括:

  • lb - 加载字节。
  • lbu - 加载无符号字节。
  • lh - 加载半字。
  • lhu - 加载无符号半字。
  • lw - 加载字词。

继续进行更改,然后进行构建。检查生成的输出。与之前一样,您可以对照 riscv32i.isarv32i_instructions.h 检查您的工作。

感谢您耐心阅读。希望以上信息对您有所帮助。