RiscV ISA 解码器

本教程的目标是:

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

概览

在 MPACT-Sim 中,目标指令被解码,并存储在内部 使其信息更加丰富, 执行速度更快。这些指令实例在指令中缓存 以便减少频繁执行的指令 。

说明类

在我们开始之前,稍微了解一下说明 MPACT-Sim 卡中Instruction 类在 mpact-sim/mpact/sim/generic/instruction.h.

说明类实例包含 模拟指令“执行”时,例如:

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

这些实例通常存储在指令(实例)缓存中,并且 会在重新执行该指令时重复使用该 ID。这有助于提升效果 错误。

除了指向上下文对象的指针之外,其他所有内容都由 指令解码器。为此 但没必要了解这些内容的详情, 直接使用它们相反,您可以大致了解它们的使用方式。 。

可调用的语义函数是 C++ 函数/方法/函数对象 (包括 lambda)。对于 例如,对于 add 指令,它会加载每个源运算数,将两个 运算数,并将结果写入单个目标运算数。以下主题的主题: 语义函数教程对语义函数进行了深入介绍。

指令运算数

指令类包括指向三种类型的运算数接口的指针: 谓词、来源和目标。这些接口允许语义函数 独立于底层指令的实际类型 操作数。例如,访问寄存器和立即数的值 通过相同的界面访问数据也就是说,执行相同操作 运算,但在不同的运算数(例如寄存器与 immedates)上可以 语义函数来实现。

谓词操作数接口,适用于支持谓词的 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 指令的延迟时间可以为 1 则可能是 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(指令集架构)定义了抽象模型 是软件与硬件进行交互的途径。它定义了 可用的指令、数据类型、寄存器以及其他机器状态 指令及其行为(语义)。对于 因此 ISA 不包含指令的实际编码。 请单独处理。

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

ISA 描述文件由 isa-解析器解析,后者会生成 不限定表示法的指令解码器。该解码器负责 来填充说明对象的字段。比如 目标寄存器编号,通过特定于格式的指令获取 解码器。其中一个解码器是二进制解码器, 下一个教程。

本教程介绍了如何为简单标量编写 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-Case 名称生成的 并将其与 EncodingBase 串联起来。声明 slots { riscv32; } 指定 RiscV32I 中只有一个指令槽 riscv32 ISA(而不是 VLIW 指令中的多个槽位),并且 有效指令是指在 riscv32 中定义的要执行的指令。

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

它指定任何拆解代码段的第一个反汇编片段 将显示为左对齐,格式为 15 个字符 宽字段。任何后续片段都将附加到此字段 任何其他间距。

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

槽位可用于将说明分解为多个单独的组,然后可将这些组 合并为一个广告位请注意表示法 : riscv32i, zicsrriscv32此属性指定广告位 riscv32 继承 槽位 zicsrriscv32i 中定义的所有指令。RiscV 32 位 ISA 由名为 RV32I 的基本 ISA 组成,其中可能有一组可选的扩展 资源。槽机制允许 然后根据需要进行组合,以定义 整体 ISA。在本示例中,RiscV“I”群组 与“zicsr”中的代码不同。可以定义其他群组 搜索“M”(乘/除)、“F”(单精度浮点数)、“D” (双精度浮点数)、“C”(紧凑的 16 位指令)等命令 所需的最终 RiscV ISA 所需的资源。

// 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 个周期。请注意,指令的大小 是计算 在模拟的 处理器。该值可能与 指令表示。

槽定义的核心是操作码部分。可以看到,其中只有两个 操作码(指令)fenceebreak 目前已在 riscv32ifence 操作码通过指定名称 (fence) 进行定义, 操作数规范 ({: imm12 : }),后跟可选反汇编代码 格式 ("fence") 以及要作为语义内容绑定的 Callable 函数 函数 ("&RV32IFence")。

指令运算数指定为三元组,每个组成部分 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) host - 对于其他主机,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/decoder/mpact_sim_isa.bzl 加载 在 mpact-sim 代码库中。在本教程中, 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_isaincludes 部分用于 用于指定源文件可能包含的其他 .isa 文件。通过 isa_name 用于指定具体的 ISA,如果有多个 ID,则需要 在要为其生成解码器的源文件中。


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

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

31 月 25 日 20 月 24 日 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 - 按位异或。

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

接下来是语义函数规范。为此,您可以使用关键字 semfunc 和一个 C++ 字符串,用于指定可用于分配 转换为 std::function。在本教程中,我们将使用函数,因此 Callable 函数 字符串为 "&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 文件后,必须 为每个新语义函数添加函数声明, 引用的文件 rv32i_instructions.h ../semantic_functions/.再次提醒,如果你需要帮助(或想查看你的工作成果) 答案是 此处

完成上述所有操作后,请切换回 riscv_isa_decoder 然后重新构建您随时可以检查生成的源文件。


使用即时指令添加 ALU 指令

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

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 日 20 月 24 日 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-Type 说明如下:

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

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

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

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

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

用于操作码的名称自然地遵循指令名称 (无需构思新模板,这些都是独一无二的)。对于 指定语义函数,回想一下指令对象编码的是 与底层运算数无关的源运算数的接口 类型。这意味着,对于具有相同运算的指令, 可能因操作数类型而异,可以共用同一个语义函数。例如, 如果存在以下情况,addi 指令会执行与 add 指令相同的运算: one 会忽略运算数类型,因此它们可以使用相同的语义函数 规范 "&RV32IAdd"。对 andiorixorislli 也是如此。 其他指令使用了新的语义函数,但应将其命名 基于运算,而非运算数,因此对于 srai,请使用 "&RV32ISra"。通过 U 类型指令 auipclui 没有等效的寄存器,因此可以 以使用 "&RV32IAuipc""&RV32ILui"

反汇编字符串与上一个练习中的反汇编字符串非常相似,但 如您所料,对 %rs2 的引用被替换为 %imm12%uimm5、 或 %uimm20

继续进行更改并构建。检查生成的输出。就像 以前,你可以对照 riscv32i.isarv32i_instructions.h


我们需要添加的分支和跳转-链接指令都使用了目的地 仅隐含在指令本身中的操作数,即下一个 pc 值。在这一阶段,我们将这视为具有名称 next_pc。我们将在后续教程中对此进行进一步定义。

分支说明

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

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

不同的立即数字段会串联成 12 位有符号立即数 值。由于这种形式并非真正具有相关性,因此我们将其称为“立即” bimm12,表示立即启动 12 位分支。我们将在 下一个教程介绍了如何创建二进制解码器。所有 如果分支指令会比较 rs1 和 rs2 指定的整数寄存器 如果条件为 true,则立即值将与当前 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 12 月 19 日 11.7 6.0
1 10 1 8 5 7
CANNOT TRANSLATE CANNOT TRANSLATE CANNOT TRANSLATE CANNOT TRANSLATE RD 操作码

与分支指令一样,20 位立即被分散在 所以我们将其命名为 jimm20。分片并不重要 ,但会在今后 创建二进制解码器的教程。操作数 规范之后会变为 { : jimm20 : next_pc, rd }。请注意 目标操作数、下一个 pc 值和 指令。

与上面的分支说明类似,反汇编格式变为:

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

间接跳转和链接使用具有 12 位立即数的 I-Type 格式。它 将符号扩展的立即值添加到由 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 日 20 月 24 日 19 月 15 日 12 月 14 日 11.7 6.0
7 5 5 3 5 7
CANNOT TRANSLATE rs2 rs1 func3 CANNOT TRANSLATE 操作码

如您所见,这是另一种分散的 12 位立即数, 将其命名为 simm12。存储指令都存储整数的值 通过将 rs2 指定的寄存器添加到内存中的有效地址, 将 rs1 指定的整数寄存器的值转换为 12 位立即数。对于以下参数,操作数格式应为 { : rs1, simm12, rs2 }: 所有商店说明

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

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

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

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

语义函数规范也符合预期:"&RV32ISb", 等等

继续进行更改,然后进行构建。检查生成的输出。只是 与之前一样 riscv32i.isarv32i_instructions.h


添加加载说明

加载指令的建模方式与 模拟器。以便能够对加载延迟时间 加载指令分为两个独立的操作:1) 有效 以及 2) 结果回写。在 模拟器这是通过将加载的语义操作拆分为两部分来实现的, 主指令和指令。此外, 当我们指定操作数时,需要同时为主操作数和 child 指令。为此,您可以将操作数规范视为 三元组列表。相关语法如下:

{(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

感谢您取得今天的成就。希望以上内容对您有所帮助。