RISC-V 指令
发布时间 2023-07-02 23:56:00作者: 可达达鸭
1. 寄存器相关
- RISC-V 架构可选32bit(RV32)/64bit(RV64),可选32个寄存器(I架构)/16个寄存器(E架构)。
- 考虑整数通用寄存器组:其中0寄存器被预留为常数0,其余31/15个寄存器为通用整数寄存器。
- 考虑浮点寄存器组(一般为F/D类扩展指令集),有32个通用浮点寄存器;寄存器位宽由扩展指令集类型决定,F类:单精度、32bit,D类:双精度、64bit。
- RISC-V 的汇编语言是对寄存器的数据进行处理。
- 算术运算(加、减、立即数加)。
- 数据传输(取:从存储器取数据到寄存器;存:从寄存器读数据存到存储器,更多关于存储器访问指令下面有介绍)。
- 逻辑运算(与,或,异或...)。
- 移位操作(逻辑左/右移,算数左/右移)。
- 条件分支(相等、不等、大于、小于跳转)。
- 无条件跳转(跳转 - 链接)。
- 存储器访问指令
- 对于更复杂的数据结构(例如数组和结构体)包含比寄存器更多的数据元素,只能存放于存储器中,使用数据传输指令进行读取。
- 与RISC架构策略一致,只有Load和Store指令可以访问存储器,其它指令都不可访问存储器。
- 存储器读写指令基本单位是字节(Byte)。
- RISC-V架构存储模式只支持小端格式。
- RISC-V推荐使用地址对齐的存储器读写操作,但也支持地址非对齐。
- RISC-V架构的存储器读写指令不支持地址自增/自减的模式。
2. RV32I 指令格式
- R-type
- 实现rs1,rs2两个寄存器的值运算,并将运算后的结果存在rd中。opcode 和 func 共同决定实现哪种运算。
- I-type
- 可以实现带一个常数的算数指令以及Load指令。 注意immediate字段为补码值。
- 对于Load指令:可以取相对于rd中的基地址+imm字节的数据。
- 例如下面的指令,从x22寄存器中获取基地址,并加上偏移地址(64/8bit = 8Byte) ,取出字,放到x9寄存器中。
- 其中(x)22放在rs1寄存器中,(x)9放在rd字段,64放在imm字段。
lw x9, 64(x22)
- 考虑到逻辑操作中的移位,使用的也是I型指令格式,考虑到寄存器位宽为32bit,所以shamt也不会超过32,所以imm只需要划出5bit空间,其余几bit作为额外的操作码字段。
- S-type
- 用于Store指令,immediate字段仍为补码值。
- 存在两个源寄存器,一个存放基址,一个存放数据。
- 设计选择与R-type保持类似的指令格式,将imm进行拆分,可以一定程度降低硬件的复杂性。
- 例如下面的指令,x9中写着要存入的值,x10中写着存储器基地址,并加上偏移地址(240/8bit = 30Byte)。
- 其中(x)9放在rs2字段中,(x)10放在rs1字段中,240放在imm字段中(要被拆分)。
sw x9, 240(x10)
- U-type
- I、S型指令格式中已经有12bit imm,但是需要考虑有时常数很大,12bit不够,此时可以使用LUI指令。
- 将高20bit常数加载到寄存器的第31-12bit,低12bit用常数0填充。之后可以使用 addi 指令与低12bit立即数相加,以实现对32bit寄存器数值的配置。
- 上面介绍的ADDI和LUI指令结和还需注意一个问题,即如果ADDI的立即数将本来一个数据为设置为一个符号位,可能会造成一个数据缺口,需要进行补值。相关处理可以看这篇文章。
- B-type
- 比较两个源寄存器rs1和rs2,并进行跳转。
- 例子如下:
- 指令格式上可以看到imm[0]被舍弃掉了,因为该bit被设置为0;汇编器根据label的地址计算出相对的偏移量为16bytes = 'b1_0000,映射如下图。
beq x19, x10, label
- 看到这里,其实就会有一个疑问,为什么B型和J型立即数在inst中分散的奇奇怪怪,但是寄存器却是固定位置的,知乎上搜到了这个问题的一些回答。我认为其中有一个解释的很有道理,为了平衡(寄存器索引到读出寄存器值的时间)和(通过inst生成参与操作的立即数选择逻辑的时间),提高整体的运算速度。
- J-type