NEMU 添加指令
前言
前面讨论了 nemu 执行一条指令的过程,在源码中,可以看到它目前可以解析的指令有限:
1 |
|
因此它并不能运行某些程序,因为程序中存在的某些指令不能被 nemu 解析,我们要做到的,就是添加指令。这意味着要看手册,研究各个指令的语义,再来实现它。
手册的话,我看的是 Chapter 24 RV32/64G Instruction Set Listings,这一章会列出很多指令、指令类型。
addi
1 |
|
运行之后发现 00000413 无法解析,查看反汇编,发现是 li s0,0:
1 |
|
通过查阅,得知 li 是伪指令。它实际上会被翻译成 addi。因此,我们只需要将 li 当成 addi 的别名就行了。翻译指令的时候,nemu 只会查看 00000413 的特殊字段,只要解析了 00000413,li 自然就会执行。这里需要搞清楚的是,这里是可以实现伪指令的(但最好不要,因为要考虑到指令复杂性、正交性等等)。
li s0,0 指令的含义是:addi a0, s0, 0。也就是 a0 = s0 + src1,因此我们只需查一下 addi 的指令格式:
31 | 27 | 26 | 25 | 24 | 23-20 | 19-15 | 14-12 | 11-7 | 6-0 |
---|---|---|---|---|---|---|---|---|---|
imm[11:0] | rs1 | 000 | rd | 0010011 |
然后就可以模仿示例进行解析和执行了:
1 |
|
重新编译运行:
1 |
|
可以看到,接下来是 00c000ef 指令无法解析,这个指令是:jal ra,80000018。说明上一个指令已经解析成功了。接着解析 jal。
jal
在 RISC-V 指令集中,jal
指令代表 “Jump and Link”。这是一种用于实现无条件跳转的控制流指令,同时它会将跳转后的下一条指令的地址保存到一个寄存器中,通常用于函数调用的实现。jal
的作用是将程序的执行从当前指令跳转到一个指定的目标地址,并且将返回地址(即跳转指令后的下一条指令的地址)保存在一个寄存器中。
指令格式
jal
指令在 RISC-V 中是一种 J-type 格式的指令。其操作和细节如下:
- 操作:
jal rd, offset
rd
:这是目标寄存器,用于保存跳转后应该返回的地址(下一条指令的地址)。offset
:这是一个立即数,表示相对于当前指令的字节偏移量,用于计算跳转目标地址。
使用场景
jal
指令常用于函数调用。在调用函数前,它将返回地址保存到寄存器(通常是 ra
,即返回地址寄存器),然后跳转到函数的起始地址执行。函数执行完毕后,可以通过跳转回保存的返回地址来继续执行调用后的指令。
指令格式为:
31 | 27 | 26 | 25 | 24 | 23-20 | 19-15 | 14-12 | 11-7 | 6-0 |
---|---|---|---|---|---|---|---|---|---|
imm[20|10:1|11|19:12] | rd | 1101111 |
在执行指令的时候,首先是 fetch:
1 |
|
因此,在进行 decode 的时候,这时候 snpc 已经得到了更新,并且解析的时候,已经先将 dnpc 设置为 snpc,后面根据实际情况,比如是否跳转再由指令语义做决定修改。
返回地址的地址就是 cpu.pc or s->pc(当前执行的地址)+4;而跳转的指令就是 cpu.pc or s->pc+offset。这里建议统一访问 s而不是 cpu。
因此,指令的执行分为两步:
- 保存返回地址到特定寄存器:s->dnpc = s->pc + imm;
- 设置跳转地址:R(rd) = s->pc + 4;
这里还需要设置取 imm 的宏,由于这里是 J-type,因此得自己设置。
设置宏定义
首先看看示例指令auipc 是如何做到的:
1 |
|
首先 BITMASK(bits) 顾名思义就是求掩码,比如 BITMASK(3),那就是 1 << 3 ==> 1000-1==>111,很好理解。
接着 BITS(x,hi,lo) 就是取一个字中的某些位,因为这涉及到和掩码与的操作,比如解析auipc sp,0x9 的时候,x 为 0x00009117,二进制就是:00000000 00000000 10010001 00010111。其中[31:12]为 imm去要提取出来。那么 BITS(00000000 00000000 10010001 00010111, 31, 12)就是:
- x 左移 12:00000000 00000000 00000000 00001001
- 和掩码与运算:00000000 00000000 00000000 00001001 & 00000000 00000000 00000011 1111 1111 = 00000000 00000000 00000000 00001001
- 接着扩展,SETX(00000000 00000000 00000000 00001001,20): 值在 32 位中不变。
- 接着右移补零:00000000 00000000 00000000 00001001 << 12: 00000000 00000000 10010000 00000000
那么就成功得提取出了 imm:00000000 00000000 10010000 00000000。可见,取 imm 的最终就是将指令字中的imm 取出拼凑成无符号数。
那么 J-type 就很好设置了,J-type 的指令格式为:imm[20|10:1|11|19:12],那么就可以如此提取:
1 |
|
因此指令为:
1 |
|
继续编译和运行:
1 |
|
可以看到,接下来是 00112623 指令无法解析,这个指令是:sw ra,12(sp)。说明上一个指令已经解析成功了。接着解析 sw。
sw
在 RISC-V 指令集中,sw
指令表示“Store Word”。这是一种存储指令,用于将一个寄存器的内容写入到内存中。sw
是 S-type(存储类型)指令的一部分,用于处理内存的写操作。
指令格式
sw
指令的基本格式是:
1 |
|
rs1
:基址寄存器,提供存储地址的基础部分。offset
:一个立即数偏移量,与基址寄存器的内容相加得到最终的内存地址。rs2
:源寄存器,其内容将被写入到由rs1
和offset
计算出的内存地址中。
操作
sw
指令将寄存器 rs2
中的内容存储到从寄存器 rs1
读取的地址加上 offset
的位置。存储的数据大小是一个字(word),在 RISC-V 标准中,一个字通常是 32 位(4 字节)。
使用场景
sw
指令常用于将计算结果或变量的值保存回内存,或在函数调用中保存寄存器的内容到栈中(如果使用基于栈的调用约定)。这对于实现复杂的数据处理、函数调用以及操作系统功能等都是必需的。
指令格式为:
31 | 27 | 26 | 25 | 24 | 23-20 | 19-15 | 14-12 | 11-7 | 6-0 |
---|---|---|---|---|---|---|---|---|---|
imm[11:15] | rs2 | rs1 | 000 | imm[4:0] | 0100011 |
因此,sw 这样执行:M(R[rs1] + offset) = R[rs2]。
我们有接口,因此可以直接调用:
1 |
|
即为:vaddr_write(rs1+imm,4,rs2)。但是这个函数名太长,我们直接定义宏:
1 |
|
因此可以这样解析和执行:
1 |
|
继续编译和执行:
1 |
|
可以看到,接下来是 00008067 指令无法解析,这个指令是:ret。说明上一个指令已经解析成功了。接着解析 ret.
ret
在 RISC-V 指令集中,ret
指令是一个伪指令,用于从一个函数返回。它通常被解释为 jalr
指令的一个特定形式,具体来说就是 jalr x0, 0(ra)
。这个操作意味着它会从寄存器 ra
(返回地址寄存器)进行跳转,其中 x0
是一个永远为零的寄存器,因此不会对返回地址进行任何修改。
功能解释
jalr
指令:jalr
是一个跳转并链接寄存器指令,它将当前指令的下一个地址(即跳转指令的地址 + 4)保存到指定的寄存器中,并跳转到目标寄存器加上一个立即数的地址。在ret
指令的情况下,这个操作简化为从ra
寄存器指定的地址进行跳转,因为立即数为0。x0
寄存器: 这是一个特殊的寄存器,其值永远是0。在ret
指令中使用x0
作为目标寄存器意味着跳转指令的返回地址不会被保存,这是因为在函数返回时,我们不需要在ra
寄存器之外再保存一个返回地址。
使用场景
ret
指令通常用在函数的末尾,实现从函数调用返回到调用者的代码位置。在大多数高级语言中,这对应于函数的结束括号或者 return
语句。
示例
如果有一个函数 foo
调用了另一个函数 bar
,并且 bar
函数执行完毕后需要返回到 foo
,那么在 bar
的末尾就会有一个 ret
指令,如下所示:
1 |
|
这样,ret
指令确保程序从 bar
函数跳回 foo
函数中 jal
指令之后的位置继续执行。这是现代计算机体系结构中实现函数调用和返回机制的一个基本组成部分。
因此,我们要实现 jalr 指令的解析和执行:
- 下一条指令的地址即 s->pc + 4 保存:R(rd) = s->pc + 4);
- 并跳转到目标寄存器加上一个立即数(x0=0)的地址:s->dnpc = (src1 + imm) & ~(word_t)1; // 在 RISC-V 架构中,指令地址必须对齐到偶数地址
因此,ret 地址可以这样被解析和执行:
1 |
|
编译和执行:
1 |
|
根据讲义,做到这里,说明第一阶段已经成功了~