指令语义函数教程

本教程的目标是:

  • 了解如何使用语义函数实现指令语义。
  • 了解语义函数与 ISA 解码器说明的关系。
  • 为 RiscV RV32I 指令编写指令语义函数。
  • 通过运行小型“Hello World”测试最终模拟器可执行文件。

语义函数概览

MPACT-Sim 中的语义函数用于实现以下运算 使其在模拟状态下的副作用可见 同样地,在 硬件。每个已解码指令的模拟器内部表示法 包含一个 Callable 函数,用于调用该对象的语义函数 指令。

语义函数具有签名 void(Instruction *),即 函数,该函数接受指向 Instruction 类实例的指针,以及 返回 void

Instruction 类在 instruction.h

在编写语义函数时, 使用 Source(int i)Destination(int i) 方法调用。

源运算数和目标运算数接口如下所示:

// 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;
};
// 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;
};

正态 3 运算数编写语义函数的基本方法 指令(例如 32 位 add 指令)如下所示:

void MyAddFunction(Instruction *inst) {
  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);
  uint32_t c = a + b;
  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, c);
  db->Submit();
}

我们来详细了解一下此函数的各个组成部分。该代码段中的 函数正文从源操作数 0 和 1 读取数据。AsUint32(0) 调用 将底层数据解释为 uint32_t 数组,并获取第 0 个 元素。无论底层寄存器或值是 数组是否有值。源操作数的大小(以元素为单位)可以是 从源运算数方法 shape() 获取,该方法会返回一个向量 包含每个维度中元素的数量。该方法会返回 {1} 表示标量,{16} 表示 16 元素矢量,{4, 4} 表示 4x4 数组。

  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);

然后,为名为 cuint32_t 临时分配值 a + b

下一行可能需要更多解释:

  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();

DataBuffer 是一个引用计数对象,用于将值存储在 模拟状态(例如寄存器)。它是相对非类型的,不过它具有 具体取决于为其分配数据的对象的大小。在本示例中,该尺寸 sizeof(uint32_t)。该语句为 destination 是此目标操作数的目标,在本例中为 32 位整数寄存器。DataBuffer 还通过 指令的架构延迟时间这是在指令期间指定的 解码。

下一行将数据缓冲区实例视为 uint32_t 数组, 将存储在 c 中的值写入第 0 个元素。

  db->Set<uint32_t>(0, c);

最后,最后一个语句将数据缓冲区提交到模拟器以供使用 作为目标机器状态(在本例中为寄存器)的新值, 解码指令时所设置的指令的延迟时间, 已填充目标操作数矢量。

虽然这是一个相当简单的函数,但它确实含有一些样板代码, 代码。 此外,它还可能会掩盖指令的实际语义。订单 以进一步简化大多数指令的语义函数的编写, 下方代码中定义了许多模板化 helper 函数, instruction_helpers.h。 这些帮助程序会隐藏 1、2 或 3 指令的样板代码 源操作数和一个目标操作数。我们来看看 操作数辅助函数:

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument1, Argument2)> operation) {
  Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
  Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument, Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Result, Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

您会发现,不是使用如下语句:

  uint32_t a = inst->Source(0)->AsUint32(0);

该辅助函数使用:

generic::GetInstructionSource<Argument>(instruction, 0);

GetInstructionSource 是一系列模板化辅助函数, 用于为说明源提供模板化访问方法 操作数。如果没有它们,每个指令辅助函数 专门用于每种类型,以便使用正确的 As<int type>() 函数。您可以看到这些模板的定义 函数 instruction.h

如您所见,有三种实现方式 具体取决于来源是 操作数类型与目的地相同,无论目的地是否 或者它们是否全都不同。每个版本的 该函数会获取一个指向指令实例的指针,以及一个可调用的 (包括 lambda 函数)。这意味着,我们现在可以重写 add 如下所示:

void MyAddFunction(Instruction *inst) {
  generic::BinaryOp<uint32_t>(inst,
                              [](uint32_t a, uint32_t b) { return a + b; });
}

在 build 中使用 bazel build -c optcopts = ["-O3"] 进行编译时 此文件应该完全内联,不会产生开销, 简洁易用,而且不会降低任何性能

正如前面提到的,针对一元、二元和三元标量提供了辅助函数 以及矢量等效项。它们还可用作有用的模板 。


初始构建

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

$  bazel build :riscv32i
...<snip>...

不会生成任何文件,因此这只是对 确保一切井然有序


添加三个运算数 ALU 指令

现在,我们来为一些通用的 3 运算数 ALU 添加语义函数 操作说明。打开 rv32i_instructions.cc 文件,并确保 缺失的定义会添加到 rv32i_instructions.h 文件中。

我们将添加的说明如下:

  • add - 32 位整数加法。
  • and - 32 位按位和.
  • or - 32 位按位或。
  • sll - 32 位逻辑左移。
  • sltu - 32 位无符号集小于号。
  • sra - 32 位算术右移。
  • srl - 32 位逻辑右移。
  • sub - 32 位整数相减。
  • xor - 32 位按位异或。

如果您学过之前的教程,可能还记得我们区分了 寄存器-寄存器指令和 解码器。对于语义函数,我们不再需要这样做。 操作数接口将从任意一个操作数中读取操作数值。 是、寄存器或立即,且语义函数完全无关 底层源操作数究竟是什么。

除了 sra 之外,上述所有说明均可被视为在 32 位无符号值,因此对于这些值,我们可以使用 BinaryOp 模板函数 一个模板类型参数。填写 函数正文相应地位于 rv32i_instructions.cc 中。请注意,只有 移位指令的第二个操作数的位用于移位。 金额。否则,所有运算都采用 src0 op src1 形式:

  • adda + b
  • anda & b
  • or:a | b
  • sll:a << (b & 0x1f)
  • sltu:(a < b) ? 1 : 0
  • srl:a >> (b & 0x1f)
  • sub:a - b
  • xor:a ^ b

对于 sra,我们将使用包含三个参数的 BinaryOp 模板。观察 模板中,第一个类型参数是结果类型 uint32_t。第二个是 源操作数 0 的类型(在本例中为 int32_t),最后一个是类型 源操作数 1 的值,在本例中为 uint32_t。这使得 sra 的正文 语义函数:

  generic::BinaryOp<uint32_t, int32_t, uint32_t>(
      instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });

继续进行更改并构建。你可以对照 rv32i_instructions.cc


添加两个运算数 ALU 指令

只有两条由 2 个运算数组成的 ALU 指令:luiauipc。前者 将预移位的源操作数直接复制到目标。后者 在将指令地址写入到 目标。指令地址可通过 address() 方法访问 Instruction 对象的 ID。

由于只有一个源运算数,因此我们不能使用 BinaryOp,而是改用 我们需要使用 UnaryOp。由于我们可以同时处理来源和 目标运算数作为 uint32_t,我们可以使用单个参数模板 版本。

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

lui 语义函数的正文尽可能简单, 只需返回来源即可。auipc 的语义函数引入了一个次要 问题,因为您需要在 Instruction 中访问 address() 方法 实例。答案是将 instruction 添加到 lambda 捕获,使其 可以在 lambda 函数正文中使用。lambda 的编写形式应为 [instruction](uint32_t a) { ... },而不是像之前一样的 [](uint32_t a) { ... }。 现在,可以在 lambda 正文中使用 instruction

继续进行更改并构建。你可以对照 rv32i_instructions.cc


添加控制流更改说明

您需要实现的控制流更改说明分为 转换为有条件的分支指令(如果运行的是较短的分支, 比较适用)和跳转和链接指令, 实施函数调用(通过设置链接 寄存器为零,从而使这些写入操作无操作)。

添加条件分支说明

没有适用于分支指令的辅助函数,因此有两种选项。 从头开始编写语义函数,或编写局部辅助函数。 由于我们需要实现 6 个分支指令,因此后者似乎值得 努力。在此之前,我们先看看分支的实现 指令语义函数。

void MyConditionalBranchGreaterEqual(Instruction *instruction) {
  int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
  int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
  if (a >= b) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0,m target);
    db->Submit();
  }
}

不同分支指令的唯一不同之处在于分支 条件以及两者的数据类型(有符号 32 位整数与无符号 32 位整数) 源运算数。也就是说,我们需要为 源运算数。辅助函数本身需要接受 Instruction 实例和一个可调用的对象,例如返回 boolstd::function 作为参数传递。辅助函数如下所示:

template <typename OperandType>
static inline void BranchConditional(
    Instruction *instruction,
    std::function<bool(OperandType, OperandType)> cond) {
  OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
  OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
  if (cond(a, b)) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0, target);
    db->Submit();
  }
}

现在,我们可以编写 bge(有符号分支大于或等于)语义函数 以:

void RV32IBge(Instruction *instruction) {
  BranchConditional<int32_t>(instruction,
                             [](int32_t a, int32_t b) { return a >= b; });
}

其余分支说明如下所示:

  • Beq - 分支等于。
  • Bgeu - 大于或等于的分支(无符号)。
  • Blt - 小于(有符号)的分支。
  • Bltu - 小于(无符号)的分支。
  • Bne - 分支不相等。

接下来进行更改以实现这些语义功能 重新构建。你可以对照 rv32i_instructions.cc

为跳转和链接编写辅助函数没有意义 因此我们需要从头开始编写这些内容我们先来 来研究它们的指令语义。

jal 指令从源操作数 0 中获取偏移量,并将其添加到 current pc(指令地址)以计算跳转目标。跳转目标 写入目标操作数 0。退货地址是指 下一个序列指令。可以通过将当前的 指令的大小调整为相应的地址。系统会将退货地址写入 目标操作数 1。务必在代码中添加说明对象指针, lambda 捕获。

jalr 指令将基寄存器作为源运算数 0 和偏移量 作为源操作数 1,将它们相加以计算跳转目标。 否则,与 jal 指令相同。

根据指令语义的描述,编写两个语义 函数和构建。你可以对照 rv32i_instructions.cc


添加内存存储指令

我们需要实现三种存储指令:存储字节 (sb)、存储半字 (sh) 和存储字词 (sw)。商店说明 与目前为止实施的说明有所不同 写入本地处理器状态。而是会写入系统资源 主内存MPACT-Sim 不会将内存视为指令操作数, 因此必须使用其他方法执行内存访问。

答案是向 MPACT-Sim ArchState 对象添加内存访问方法, 或者更合适的做法是,创建一个派生自 ArchState 的新 RiscV 状态对象 可添加此属性的位置ArchState 对象管理核心资源,例如 寄存器和其他状态对象。它还管理用于 缓冲目标操作数数据缓冲区,直到可以写回 注册对象。大部分说明都可以在不知道 但有一些,例如内存操作和其他特定系统, 指令要求具备在此状态对象中的功能。

我们来看一下 fence 指令的语义函数, 已在 rv32i_instructions.cc 中实现。fence 指令持有指令问题,直到某些内存操作 已完成。它用于保证指令之间的内存排序 在那条指令之前和之后执行的那条消息。

// Fence.
void RV32IFence(Instruction *instruction) {
  uint32_t bits = instruction->Source(0)->AsUint32(0);
  int fm = (bits >> 8) & 0xf;
  int predecessor = (bits >> 4) & 0xf;
  int successor = bits & 0xf;
  auto *state = static_cast<RiscVState *>(instruction->state());
  state->Fence(instruction, fm, predecessor, successor);
}

fence 指令语义函数的关键部分是最后两条 代码。首先,使用 Instruction 中的方法提取状态对象 类,并将 downcast<> 应用于特定于 RiscV 的派生类。然后是 Fence 调用 RiscVState 类的 方法以执行围栏操作。

商店说明的运作方式与此类似。首先是 通过基址和偏移指令源运算数计算内存访问, 则将从下一个源操作数中提取要存储的值。接下来, RiscV 状态对象是通过 state() 方法调用获取的, static_cast<>,系统会调用相应的方法。

RiscVState 对象的 StoreMemory 方法相对简单,但具有 几个方面需要注意:

  void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);

我们可以看到,该方法带有三个参数,即指向 指令本身、存储的地址,以及指向 DataBuffer 的指针 实例。请注意,无需尺寸, DataBuffer 实例本身包含一个 size() 方法。不过,您无需 可用于 分配适当大小的 DataBuffer 实例。我们需要改为 使用从 db_factory() 方法获取的 DataBuffer 工厂 Instruction 实例。工厂有一个 Allocate(int size) 方法 该函数会返回所需大小的 DataBuffer 实例。下面是一个示例 如何使用它为半字存储区分配 DataBuffer 实例 (请注意,auto 是 C++ 功能,可从右侧推导出类型 ):

  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(uint16_t));

有了 DataBuffer 实例后,我们就可以像往常一样向其写入数据:

  db->Set<uint16_t>(0, value);

然后将其传递给内存存储接口:

  state->StoreMemory(instruction, address, db);

我们还没有完全完成。DataBuffer 实例被计为引用。这个 通常由 Submit 方法理解和处理, 尽可能简单地使用最常见的应用场景不过,StoreMemory 并非 这样写出来的它会在 DataBuffer 实例运行时执行 IncRef 操作 对其执行上述操作,完成后按 DecRef 即可。但是,如果语义函数 DecRef 是它自己的引用,永远不会被收回。因此,最后一行 是:

  db->DecRef();

存储函数有三个,唯一的区别是存储函数的大小 内存访问听起来,这对本地人来说是个大好机会 模板化辅助函数。商店函数的唯一不同之处是 商店值的类型,因此模板必须将该值作为参数。 除此之外,只需传入 Instruction 实例:

template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->Set<ValueType>(0, value);
  state->StoreMemory(instruction, address, db);
  db->DecRef();
}

接下来,完成存储语义函数并构建。您可以查看您的 抵消 rv32i_instructions.cc


添加内存加载指令

需要实现的加载指令如下所示:

  • lb - 加载字节,用符号扩展字词。
  • lbu - 将无符号的零扩展字节加载到字词中。
  • lh - 加载半个字词,然后在字词中附加符号。
  • lhu - 将半个字的无符号零扩展加载到一个字词中。
  • lw - 加载字词。

加载指令是我们要建模的最复杂指令 本教程。它们与商店说明类似,因为它们需要 访问 RiscVState 对象,但会增加每次加载的复杂性 指令分为两个独立的语义函数。第一个是 与存储指令类似,因为它会计算有效地址, 并启动内存访问第二个是 访问完成,并将内存数据写入寄存器目标 操作数。

首先,我们来看看 RiscVState 中的 LoadMemory 方法声明:

  void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
                  Instruction *child_inst, ReferenceCount *context);

StoreMemory 方法相比,LoadMemory 会额外使用两个 形参:指向 Instruction 实例的指针 引用计数了 context 对象。前者是指令, 实现寄存器回写(如 ISA 解码器教程中所述)。它 使用当前 Instruction 实例中的 child() 方法访问。 后者是指向从 ReferenceCount,在本示例中,将存储一个 DataBuffer 实例 包含已加载的数据。您可以通过 Instruction 对象中的 context() 方法(尽管对于大多数说明而言) 则设为 nullptr)。

RiscV 内存加载的上下文对象定义为以下结构:

// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
  explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
  ~LoadContext() override {
    if (value_db != nullptr) value_db->DecRef();
  }

  // Override the base class method so that the data buffer can be DecRef'ed
  // when the context object is recycled.
  void OnRefCountIsZero() override {
    if (value_db != nullptr) value_db->DecRef();
    value_db = nullptr;
    // Call the base class method.
    generic::ReferenceCount::OnRefCountIsZero();
  }
  // Data buffers for the value loaded from memory (byte, half, word, etc.).
  DataBuffer *value_db = nullptr;
};

除了数据大小(字节、 半字和单词)以及所加载的值是否会进行符号扩展。通过 后者仅会考虑 child 指令。我们先来创建一个 辅助函数。它非常类似于 存储指令,但该指令不会通过访问源运算数来获取值, 并创建一个上下文对象。

template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->set_latency(0);
  auto *context = new riscv::LoadContext(db);
  state->LoadMemory(instruction, address, db, instruction->child(), context);
  context->DecRef();
}

可以看到,它们的主要区别在于分配的 DataBuffer 实例 都会作为参数传递给 LoadMemory 调用,并存储在 LoadContext 对象。

child 指令语义函数都非常相似。首先, 通过调用 Instruction 方法 context() 获得 LoadContext; 静态类型转换为 LoadContext *。其次,该值(根据数据 type)从 load-data DataBuffer 实例中读取。第三, 从目标操作数分配 DataBuffer 实例。最后, 系统会将加载的值写入新的 DataBuffer 实例,并对加载的值执行 Submit 处理。 同样,模板化辅助函数也是一个很好的想法:

template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
  auto *context = down_cast<riscv::LoadContext *>(instruction->context());
  uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, value);
  db->Submit();
}

接下来,实现最后几个辅助函数和语义函数。付款 注意您在模板中为每个辅助函数使用的数据类型 调用,它对应于负载的大小和有符号/无符号性质 指令。

你可以对照 rv32i_instructions.cc


构建并运行最终模拟器

至此,我们已经完成了所有艰难步骤,可以构建最终的模拟器了。通过 将这些教程中的所有工作结合在一起的顶级 C++ 库 位于 other/。您无需费心查看该代码。周三 会在今后的高级教程中探讨该主题。

将工作目录更改为 other/,然后进行构建。它在构建时无需 错误。

$ cd ../other
$ bazel build :rv32i_sim

该目录中有一个简单的“hello world”文件中 hello_rv32i.elf。如需在此文件上运行模拟器并查看结果,请执行以下操作:

$ bazel run :rv32i_sim -- other/hello_rv32i.elf

您应该会看到如下几行内容:

INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$

您还可以使用命令 bazel run :rv32i_sim -- -i other/hello_rv32i.elf 在交互模式下运行模拟器。系统会显示一个简单的 命令 Shell。在提示符处输入 help 以查看可用命令。

$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000   addi           ra, 0, 0
[0] > help


    quit                           - exit command shell.
    core [N]                       - direct subsequent commands to core N
                                     (default: 0).
    run                            - run program from current pc until a
                                     breakpoint or exit. Wait until halted.
    run free                       - run program in background from current pc
                                     until breakpoint or exit.
    wait                           - wait for any free run to complete.
    step [N]                       - step [N] instructions (default: 1).
    halt                           - halt a running program.
    reg get NAME [FORMAT]          - get the value or register NAME.
    reg NAME [FORMAT]              - get the value of register NAME.
    reg set NAME VALUE             - set register NAME to VALUE.
    reg set NAME SYMBOL            - set register NAME to value of SYMBOL.
    mem get VALUE [FORMAT]         - get memory from location VALUE according to
                                     format. The format is a letter (o, d, u, x,
                                     or X) followed by width (8, 16, 32, 64).
                                     The default format is x32.
    mem get SYMBOL [FORMAT]        - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem SYMBOL [FORMAT]            - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem set VALUE [FORMAT] VALUE   - set memory at location VALUE(1) to VALUE(2)
                                     according to FORMAT. Default format is x32.
    mem set SYMBOL [FORMAT] VALUE  - set memory at location SYMBOL to VALUE
                                     according to FORMAT. Default format is x32.
    break set VALUE                - set breakpoint at address VALUE.
    break set SYMBOL               - set breakpoint at value of SYMBOL.
    break VALUE                    - set breakpoint at address VALUE.
    break SYMBOL                   - set breakpoint at value of SYMBOL.
    break clear VALUE              - clear breakpoint at address VALUE.
    break clear SYMBOL             - clear breakpoint at value of SYMBOL.
    break clear all                - remove all breakpoints.
    help                           - display this message.

_start:
80000000   addi           ra, 0, 0
[0] >

本教程到此结束。希望以上内容对您有所帮助。