这章内容将会进一步解析更多的指令,此外将解析指令的过程拆分为一个单独的类,采用表格驱动的方式,将数据和逻辑分离,降低了 if else 嵌套层数。
这部分依旧改动不多,只增加了七个指令。此外代码中细碎的变动没有完全列出来,下面只是主体部分的更新,可以尝试自己动手实现,如果简单抄一遍是没有成长的,总之需要在解决问题中加深印象。
可以参考这个分支的代码:
接下来首先将指令解析拆分为一个单独的类 InstructionExecutor ,用来专门解析指令。
class InstructionExecutor {
public:
static std::optional execute(Cpu& cpu, uint32_t inst);
};
1.2 Cpu::execute
将 CPU 中的 execute 方法改为下面的形式:
std::optional Cpu::execute(uint32_t inst) {
auto exe = InstructionExecutor::execute(*this, inst);
if (exe.has_value()) {
return exe;
}
return std::nullopt;
}
此前将所有指令解析都放入了一个 switch 来维护,但是解析指令的个数一增加就难以维护了。
1.3 InstructionExecutor::execute
接下来讲解InstructionExecutor::execute如何实现表格驱动的方式来解析指令:
std::optional executeAddi(Cpu& cpu, uint32_t inst) {
uint32_t rd = (inst >> 7) & 0x1f;
uint32_t rs1 = (inst >> 15) & 0x1f;
int64_t immediate = static_cast(inst & 0xfff00000) >> 20;
std::cout << "ADDI: x" << rd << " = x" << rs1 << " + " << immediate << std::endl;
cpu.regs[rd] = cpu.regs[rs1] + immediate;
return cpu.update_pc();
}
std::optional InstructionExecutor::execute(Cpu& cpu, uint32_t inst) {
uint32_t opcode = inst & 0x7f;
uint32_t funct3 = (inst >> 12) & 0x7;
// x0 is hardwired zero
cpu.regs[0] = 0;
std::cout << "Executing instruction: 0x" << std::hex << opcode <<
", funct3: 0x" << funct3 << std::dec << std::endl;
std::unordered_map<
std::tuple,
std::function(Cpu&, uint32_t)>
> instructionMap = {
{std::make_tuple(0x13, 0x0), executeAddi},
{std::make_tuple(0x13, 0x1), executeSlli},
{std::make_tuple(0x13, 0x2), executeSlti},
{std::make_tuple(0x13, 0x3), executeSltiu},
{std::make_tuple(0x13, 0x4), executeXori},
{std::make_tuple(0x13, 0x5), executefunct70X5},
{std::make_tuple(0x13, 0x6), executeOri},
{std::make_tuple(0x13, 0x7), executeAndi},
{std::make_tuple(0x33, 0x0), executeAdd},
};
auto it = instructionMap.find({opcode, funct3});
if (it != instructionMap.end()) {
return it->second(cpu, inst);
}
// 确保所有可能的执行路径都有明确的返回值
}
其中维护了一张哈希表,key 是有 opcode 和 funct3 组成,value 对应解析指令的函数。
当执行的时候会根据解析出来 opcode 和 funct3 用来进一步跳转到对应的指令。
此外采用 C++17 optional 来控制处理错误,这也是为什么最后一行找不到的时候会返回return std::nullopt;。
这部分内容可以进一步阅读这篇文章:C++17 optional其中给出了 optional 出来之前是如何处理的,存在哪些问题,出现之后又是如何处理的。
1.2 funct7
注意{std::make_tuple(0x13, 0x5), executefunct70X5},对应了多个指令。
因为所有的指令都需要 opcode 和 funct3 定位,但有时候需要 funct7 进一步区分。下面的函数就是做了进一步的跳转。
std::optional executefunct70X5(Cpu& cpu, uint32_t inst) {
uint32_t funct7 = (inst & 0xfe000000) >> 25;
std::cout << "Executing srli or srai funct7: 0x" << std::hex << funct7 << std::dec << std::endl;
switch (funct7) {
// srli
case 0x00: {
return executeSrli(cpu, inst);
}
// srai
case 0x20: {
return executeSrai(cpu, inst);
}
default:
return std::nullopt;
}
}
从下面的维护的哈希表中我们已经能够看到接下来需要进一步解析的指令,此前 addi 和 add 已经解析完成了的。
std::unordered_map<
std::tuple,
std::function(Cpu&, uint32_t)>
> instructionMap = {
{std::make_tuple(0x13, 0x0), executeAddi},
{std::make_tuple(0x13, 0x1), executeSlli},
{std::make_tuple(0x13, 0x2), executeSlti},
{std::make_tuple(0x13, 0x3), executeSltiu},
{std::make_tuple(0x13, 0x4), executeXori},
{std::make_tuple(0x13, 0x5), executefunct70X5},
{std::make_tuple(0x13, 0x6), executeOri},
{std::make_tuple(0x13, 0x7), executeAndi},
{std::make_tuple(0x33, 0x0), executeAdd},
};
新增加的指令都属于RISC-V指令集中的I(立即数)类型指令和R(寄存器-寄存器)类型指令的一部分,用于进行基本的整数运算和逻辑操作。以下是每个指令的功能和类别:
Slli (Shift Left Logical Immediate)Slti (Set Less Than Immediate)Sltiu (Set Less Than Immediate Unsigned)Xori (XOR Immediate)Ori (OR Immediate)Andi (AND Immediate)Srli (Shift Right Logical Immediate)Srai (Shift Right Arithmetic Immediate)
这些指令提供了基本的算术运算和位操作,用于实现诸如加法、减法、逻辑运算等基本操作,是RISC-V指令集中用于处理整数数据的关键部分。
2.2 SLLI 指令格式
RISC-V 指令SLLI(Shift Left Logical Immediate)用于将寄存器中的值左移指定的位数,然后将结果存储回寄存器。下面是SLLI指令的内部组成以及一个文本图形化的表示:
31 20 15 10 6 0
+----------------+---------+-----+---------+----------+
| imm[11:0] | shamt | rd | funct3 | opcode | I-type
+----------------+---------+-----+---------+----------+
例子:
假设有以下SLLI指令:
SLLI x1, x2, 4
这表示将寄存器x2中的值左移 4 位,并将结果存储回x1。在文本图形化的内部表示中:
000000000100 10000 00001 001 0110011
imm[11:0] shamt rd funct3 opcode
因此,SLLI x1, x2, 4的二进制表示为0010110011。
使用场景:
SLLI指令通常用于位操作,例如在实现算法时需要将某个寄存器中的值左移一定位数,以进行乘法或其他算术运算。这在编写低级别的系统软件或底层硬件控制程序时可能会经常遇到。例如,在实现加密算法或图形处理器中,位操作是常见的操作之一。
2.3 SLTI
slti是一条有符号立即数比较指令,用于将一个寄存器的值与一个立即数进行比较。下面是slti指令的内部组成的文本图形表示:
[ immediate ] [ rs1 ] [ funct3 ] [ rd ] [ opcode ]
31 20 19 15 14 12 11 7 6 0
具体来说,slti的操作是将rs1中的值与有符号的immediate相比较,如果rs1的值小于immediate,则将目标寄存器rd设置为 1,否则设置为 0。
以下是一个例子,假设我们有如下 RISC-V 汇编代码:
slti x3, x1, 10
这条指令的意思是将寄存器x1中的值与立即数10进行比较,如果x1的值小于10,则将寄存器x3设置为 1,否则设置为 0。这样,x3将存储比较的结果,表示x1 < 10的情况。
2.4 SRAI
“SRAI” 的完整展开是 “Shift Right Arithmetic Immediate”,其中:
因此,”SRAI” 用于对有符号整数执行算术右移操作,移动的位数由一个立即数值指定。
下面是一个 RISC-V 汇编指令的示例:
SRAI x1, x2, 2
这意味着:进行算术右移立即数操作,取寄存器x2中的值,将其算术右移 2 位,然后将结果存储在寄存器x1中。
因为上一部分已经增加了编译和运行汇编代码的工具函数,接下来可以直接调用:
TEST(RVTests, TestSlli) {
std::string code = start +
"addi x2, x0, 5 \n" // Load 5 into x2
"slli x1, x2, 3 \n"; // x1 = x2 << 3
Cpu cpu = rv_helper(code, "test_slli", 2);
// Verify if x1 has the correct value
EXPECT_EQ(cpu.regs[1], 5 << 3) << "Error: x1 should be the result of SLLI instruction";
}
// Test slti instruction
TEST(RVTests, TestSlti) {
std::string code = start +
"addi x2, x0, 8 \n" // 将 8 加载到 x2 中
"slti x1, x2, 10 \n"; // x1 = (x2 < 10) ? 1 : 0
Cpu cpu = rv_helper(code, "test_slti", 2);
// 验证 x1 的值是否正确
EXPECT_EQ(cpu.regs[1], 1) << "Error: x1 should be the result of SLTI instruction";
}
上面只是一部分内容,变动没有完全列出,需要参考代码来实现。