本教程的目标是:
- 了解生成的 ISA 和二元解码器如何协同工作。
- 编写必要的 C++ 代码,为 RiscV 创建全指令解码器 结合了 ISA 和二进制解码器的 RV32I。
了解指令解码器
在给定指令地址的情况下,指令解码器负责读取
从内存获取指令字词,并返回
表示该指令的 Instruction
。
顶级解码器实现如下所示的 generic::DecoderInterface
:
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
如您所见,只需实现一种方法:cpp
virtual Instruction *DecodeInstruction(uint64_t address);
现在,我们来看看生成的代码提供了哪些功能以及需要哪些功能。
首先,考虑文件中的顶级类 RiscV32IInstructionSet
riscv32i_decoder.h
:在
ISA 解码器。要重新查看内容,请导航到
重新生成全部教程
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
现在将您的目录更改回代码库的根目录,
生成的源代码为此,请将目录更改为
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_decoder.h
。这里有三个类,
请参阅以下内容:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
请注意类的命名。所有类都是根据
“isa”中指定的名称的 Pascal 大小写形式声明:
isa RiscV32I { ... }
我们先从 RiscVIInstructionSet
类开始。如下所示:
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
此类中没有虚拟方法,因此这是一个独立的类,
请注意两点首先,构造函数会获取一个指向
RiscV32IInstructionSetFactory
类。这是一个由
解码器将使用它来创建 RiscV32Slot
类的实例,
解码为 slot RiscV32
定义的所有指令,如
riscv32i.isa
文件。其次,Decode
方法接受一个额外的参数,
是指向 RiscV32IEncodingBase
的指针,这是一个提供
第一个教程中生成的 ISA 解码器与
解码器。
RiscV32IInstructionSetFactory
类是一个抽象类,我们从
必须为完整解码器派生我们自己的实现。在大多数情况下
非常简单:只需提供一个方法来调用每个
我们在 .isa
文件中定义的 slot 类。在这个例子中,这非常简单,就像
只有一个这样的类:Riscv32Slot
(名称为 riscv32
的 Pascal 大小写形式)
与 Slot
串联在一起)。系统不会为您生成该方法,
一些高级用例,在这些用例中,派生子类可能会很有用
并调用其构造函数。
在本单元稍后的部分中,我们将介绍最后一个类 RiscV32IEncodingBase
。
因为这是另一个练习的主题。
定义顶级指令解码器
定义工厂类
如果您针对第一个教程重新构建了项目,请务必改回
riscv_full_decoder
目录中。
打开文件 riscv32_decoder.h
。所有必要的包含文件都具有
并且命名空间已设置完毕。
在标记为 //Exercise 1 - step 1
的注释后,定义类
RiscV32IsaFactory
从 RiscV32IInstructionSetFactory
继承。
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
接下来,定义 CreateRiscv32Slot
的替换项。由于我们并未使用
派生了 Riscv32Slot
的类,因此只需使用以下代码即可分配新实例:
std::make_unique
。
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处。
定义解码器类
构造函数、析构函数和方法声明
接下来,定义解码器类。在上述同一文件中,前往
RiscV32Decoder
的声明。将声明扩展为类定义
其中,RiscV32Decoder
继承自 generic::DecoderInterface
。
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
接下来,在编写构造函数之前,我们快速看一下代码
关于二进制解码器的第二版教程中生成的示例。除了
Extract
函数,则还有函数 DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
该函数接受需要解码的指令字词,并返回
与该指令匹配的操作码。另一方面,
DecodeInterface
类RiscV32Decoder
仅在
地址。因此,RiscV32Decoder
类必须能够访问内存
读取要传递给 DecodeRiscVInst32()
的指令字词。在此项目中
访问内存的方法是通过
.../mpact/sim/util/memory
恰如其分地命名为 util::MemoryInterface
,如下所示:
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
此外,我们还需要能够将 state
类实例传递给
其他解码器类的构造函数。相应的状态类
riscv::RiscVState
类,派生自 generic::ArchState
,添加了
RiscV 的功能。也就是说,我们必须声明构造函数,
可以获取指向 state
和 memory
的指针:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
删除默认构造函数并替换析构函数:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
接下来声明 DecodeInstruction
方法,我们需要替换
generic::DecoderInterface
。
generic::Instruction *DecodeInstruction(uint64_t address) override;
如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处。
数据成员定义
RiscV32Decoder
类需要私有数据成员来存储
构造函数形参和指向工厂类的指针。
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
它还需要一个指向从上派生的编码类的指针
RiscV32IEncodingBase
,我们将其称为 RiscV32IEncoding
(
(在练习 2 中)。此外,它还需要一个指向
RiscV32IInstructionSet
,因此请添加:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
最后,我们需要定义与内存接口搭配使用的数据成员:
generic::DataBuffer *inst_db_;
如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处。
定义解码器类方法
接下来,您需要实现构造函数、析构函数和
DecodeInstruction
方法结合使用。打开文件 riscv32_decoder.cc
。空
方法以及命名空间声明中,
共 using
项声明。
构造函数的定义
构造函数只需要初始化数据成员。首先,初始化
state_
和 memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
接下来,分配每个解码器相关类的实例,传入 适当的参数
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
最后,分配 DataBuffer
实例。它是使用工厂分配的
可通过 state_
成员访问。我们分配一个数据缓冲区
单个 uint32_t
,因为这是指令字词的大小。
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
析构函数定义
析构函数很简单,只需释放我们在构造函数中分配的对象即可,
但要注意一个方面数据缓冲区实例是对引用计数的,因此
针对该指针调用 delete
后,我们对该对象执行 DecRef()
操作:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
方法定义
在本例中,此方法的实现非常简单。我们假设 地址已正确对齐,且无需进行额外的错误检查 必填字段。
首先,必须使用内存从内存中提取指令字词
接口和 DataBuffer
实例。
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
接下来,我们调用 RiscVIEncoding
实例来解析指令字词,
必须在调用 ISA 解码器本身之前完成。回想一下,ISA
解码器直接调用 RiscVIEncoding
实例来获取操作码
和运算数指定。我们还没有实施
类,不过让我们使用 void ParseInstruction(uint32_t)
作为该方法。
riscv_encoding_->ParseInstruction(iword);
最后,我们调用 ISA 解码器,传入地址和 Encoding 类。
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处。
编码类
编码类会实现解码器类使用的接口 来获取指令运算码及其源运算数和目标运算数,以及 资源运算数。这些对象都依赖于二进制文件中的信息 格式解码器(例如操作码)、 它与解码器类分开,以保持其 不限定编码,并且支持多种不同的编码方案 。
RiscV32IEncodingBase
是一个抽象类。我们需要使用一组方法,
在派生类中实现的功能如下所示。
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
乍一看,这有点复杂,特别是 但对于像 RiscV 这样的简单架构,我们实际上忽略了大部分 这些参数,因为其值隐含其中。
下面我们将依次介绍每种方法。
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
GetOpcode
方法会返回当前的 OpcodeEnum
成员
指令,用于标识指令操作码。OpcodeEnum
类
(在生成的 ISA 解码器文件 riscv32i_enums.h
中定义)。该方法采用
这两个参数,对于我们的目的而言,可以忽略这两个参数。第 1 个
是槽类型(也在 riscv32i_enums.h
中定义的枚举类),
由于 RiscV 只有一个槽,因此它只有一个可能的值:
SlotEnum::kRiscv32
。第二个是槽的实例编号(如果
该槽有多个实例,这可能在某些 VLIW 中出现
架构)。
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
接下来的两种方法用于对处理器中的硬件资源进行建模
以提高周期准确性。在教程练习中,我们不会使用
因此,在实现过程中,系统会对它们执行桩,返回 nullptr
。
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
这三个方法会返回
指令语义函数来访问任意指令的值
谓词运算数,每个指令源运算数,并写入新的
值转换为指令目标运算数。由于 RiscV 不使用
指令谓词,该方法只需返回 nullptr
。
这些函数的参数模式是类似的。首先,就像
GetOpcode
表示传入槽位和条目。然后是
为其创建操作数的指令。只有当
不同的操作码需要针对同一操作数返回不同的操作数对象
但此 RiscV 模拟器并非如此。
接下来是谓词、来源、目标、操作数枚举条目,
标识需要创建的操作数。这些信息来自
riscv32i_enums.h
中的 OpEnums,如下所示:
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
如果您回顾一下
riscv32.isa
您会注意到,它们对应于
在每个指令的声明中使用的操作数名称。通过使用不同的
表示不同位字段和操作数的操作数的操作数名称
它使得编写编码类变得更加轻松,因为枚举成员是唯一的
确定要返回的确切操作数类型,且无需
请考虑 slot、entry 或 opcode 参数的值。
最后,对于源操作数和目标操作数, 操作数会被传入(同样,我们可以忽略此操作),而对于目的地, 操作数,指令与命令提示符之间经过的延迟时间(以周期为单位) 且目标结果可用于后续指令。 在我们的模拟器中,延迟时间为 0,这意味着指令写入 系统会将结果立即输出到寄存器中。
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
最后一个函数用于获取特定目的地的延迟时间
操作数,前提是已在 .isa
文件中将其指定为 *
。这种情况并不常见
和 不用于此 RiscV 模拟器,因此我们对此函数的实现
则会返回 0。
定义编码类
头文件 (.h)
方法
打开文件 riscv32i_encoding.h
。所有必要的包含文件都具有
并且命名空间已设置完毕。所有代码添加操作均为
已关注评论“// Exercise 2.
”
首先,我们来定义一个类 RiscV32IEncoding
,该类继承自
生成的接口
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
接下来,构造函数应接受指向状态实例的指针,在本例中为
指向 riscv::RiscVState
的指针。应使用默认析构函数。
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
在添加所有接口方法之前,我们先添加由
RiscV32Decoder
用于解析指令:
void ParseInstruction(uint32_t inst_word);
接下来,让我们添加在丢弃 不使用的参数的名称:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
最后,添加公共接口的其余方法替换项,但使用 遵循 .cc 文件的实现。
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
为了简化每个操作数 getter 方法的实现
我们将创建两个 Callables(函数对象)数组,这些对象按
SourceOpEnum
和 DestOpEnum
成员的数值。
这样,方法的正文可简化为调用
用于传入并返回其返回值的枚举值的函数对象
值。
为了组织这两个数组的初始化,我们定义了两个私有数组, 将通过构造函数调用的方法,如下所示:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
数据成员
所需的数据成员如下所示:
state_
用于保存riscv::RiscVState *
值。uint32_t
类型的inst_word_
,用于存储当前值 说明文字。opcode_
,用于存储当前指令的运算码,ParseInstruction
方法。类型为OpcodeEnum
。source_op_getters_
是一个数组,用于存储用于获取来源的 Callables 操作数对象。数组元素的类型是absl::AnyInvocable<SourceOperandInterface *>()>
。dest_op_getters_
是一个数组,用于存储获取 目标操作数对象。数组元素的类型是absl::AnyInvocable<DestinationOperandInterface *>()>
。xreg_alias
:RscV 整数寄存器 ABI 名称的数组,例如“zero”和 “ra”而非“x0”和“x1”。
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处。
源文件 (.cc)。
打开文件 riscv32i_encoding.cc
。所有必要的包含文件都具有
并且命名空间已设置完毕。所有代码添加操作均为
已关注评论“// Exercise 2.
”
辅助函数
首先,我们将编写几个辅助函数,
源寄存器和目标寄存器运算数。这些将在
该寄存器类型,并且将调用 RiscVState
对象以获取
注册对象,然后在寄存器对象中调用操作数工厂方法。
我们先从目标运算数帮助程序开始:
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
如您所见,有两个辅助函数。第二个需要额外
参数 op_name
,允许操作数具有不同的名称或字符串
表示法,而不是底层寄存器。
同样,对于源运算数帮助程序:
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
构造函数和接口函数
构造函数和接口函数非常简单。构造函数 只会调用两个初始化方法来初始化以下对象的 Callables 数组: 操作数 getter 的方法。
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
先存储指令字,然后存储指令字和
从调用二进制解码器生成的代码中获取的。
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
最后,操作数 getter 从它调用的 getter 函数返回值 基于使用目标/源操作数枚举值的数组查找。
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
数组初始化方法
您可能已经猜到,大部分工作是初始化 getter
数组,但不必担心,这是使用简单的重复模式完成的。让我们
请先从 InitializeDestinationOpGetters()
开始,因为只有
几个目标操作数。
调用 riscv32i_enums.h
中生成的 DestOpEnum
类:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
对于 dest_op_getters_
,我们需要初始化 4 个条目,kNone
各一个,
kCsr
、kNextPc
和kRd
。为方便起见,每个条目都使用
lambda。签名
的 lambda 为 void(int latency)
。
到目前为止,我们还没有说过
在 MPACT-Sim 中定义的操作数。在本练习中,我们将仅使用
类型:generic::RegisterDestinationOperand
(在
register.h
,
和 generic::DevNullOperand
中定义的
devnull_operand.h
。
目前,这些运算数的细节并不是很重要,
前者用于写入寄存器,后者会忽略所有写入操作。
kNone
的第一个条目很简单 - 只需返回一个 nullptr 作为可选的
记录错误。
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
下一条是kCsr
。在这里,我们将进行一些作弊。“hello world”节目
不依赖于任何实际的 CSR 更新,但有一些样板代码
执行 CSR 指令。解决方法是使用
名为“CSR”的常规寄存器并向其传送所有此类写入
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
接下来是 kNextPc
,它指的是“pc”注册。它用作
。名称在 RiscVState
中的定义为
kPcName
。
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
最后是 kRd
目标操作数。在 riscv32i.isa
中,操作数
rd
仅用于引用在“rd”中编码的整数寄存器字段
因此不会造成任何歧义。那里
只是一项复杂功能。寄存器 x0
(abi 名称 zero
)通过硬连接为 0,
因此对于该寄存器,我们使用 DevNullOperand
。
因此,在此 getter 中,我们首先使用rd
从 .bin_fmt 文件生成的 Extract
方法。如果值为 0,我们
返回“DevNull”运算数,否则我们将返回正确的寄存器运算数,
注意将适当的寄存器别名用作操作数名称。
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
现在进入 InitializeSourceOperandGetters()
方法,其中模式为
大致相同,但细节略有不同。
首先,我们看一下从 Cloud Functions 函数生成的 SourceOpEnum
riscv32i.isa
:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
通过检查,除了kNone
,他们分为两个组。一个
是立即运算数:kBimm12
、kImm12
、kJimm20
、kSimm12
、kUimm20
和 kUimm5
。另一个是寄存器运算数:kCsr
、kRs1
和 kRs2
。
kNone
运算数的处理方式与目标运算数的处理方式相同 - 返回
nullptr。
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
接下来,我们来处理寄存器运算数。我们将处理与kCsr
类似的
对应目标运算数的处理方式 - 只需调用
使用“CSR”的辅助函数作为寄存器名称。
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
运算数 kRs1
和 kRs2
的处理方式与 kRd
等效,不同之处在于
虽然我们不想更新 x0
(或 zero
),但我们需要确保,
始终从该操作数读取 0。为此,我们将使用
generic::IntLiteralOperand<>
类在
literal_operand.h
。
此运算数用于存储字面值(与模拟的
立即值)。否则,模式是相同的:首先提取
指令字词中的 rs1/rs2 值,如果为零,则返回字面量
操作数为 0,否则返回常规寄存器
使用辅助函数的源操作数,使用 abi 别名作为操作数
名称。
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
最后,我们处理不同的立即操作数。即时值包括
存储在以下类中定义的 generic::ImmediateOperand<>
类的实例中:
immediate_operand.h
。
直接运算数的不同 getter 之间的唯一区别
使用哪个提取器函数,以及存储类型是带符号还是
无符号。
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
如果您需要帮助(或想要检查您的工作),完整的解决方案是 此处。
本教程到此结束。希望这些内容对您有所帮助。