本教程的目标是:
- 了解如何使用语义函数实现指令语义。
- 了解语义函数与 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);
然后,为名为 c
的 uint32_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 opt
和 copts = ["-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
形式:
add
:a + b
and
:a & 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 指令:lui
和 auipc
。前者
将预移位的源操作数直接复制到目标。后者
在将指令地址写入到
目标。指令地址可通过 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
实例和一个可调用的对象,例如返回 bool
的 std::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] >
本教程到此结束。希望以上内容对您有所帮助。