RiscV 中字长为 位。
| 寄存器 | ABI 名称 | 描述 |
|---|---|---|
| x0 | zero | 恒为零 |
| x1 | ra | 存放函数调用返回地址 |
| x2 | sp | 栈指针 |
| x3 | gp | 保存指向全局数据区附近的指针(比如 .bss),便于访问大量的全局/静态变量。(具体怎么用由编译器决定) |
| x4 | tp | 保存指向 Thread Local 存储的指针。(具体怎么用由编译器决定) |
| x5-x7 | t0-t2 | 临时寄存器 |
| x8 | s0/fp | 即可以作为一个保存寄存器(saved register),也可以作为栈帧指针(即指向栈的起始位置)的寄存器。 |
| x9 | s1 | 保存寄存器 |
| x10-x11 | a0-a1 | 存放函数的参数/返回值 |
| x12-x17 | a2-a7 | 存放函数的参数 |
| x18-x27 | s2-s11 | 保存寄存器 |
| x28-x31 | t3-t6 | 临时寄存器 |
riscv 的寄存器都是 32 位的。
一些在寄存器上进行算数操作的指令为 R 格式:opname rd, rs1, rs2。
其中 opcode 恒为 0110011。指令进行的操作由 funct3 和 funct7 来决定:
| 指令 | 描述 |
| ------------------- | ---------------------------------------------------------------------------- | ---- |
| add rd, rs1, rs2 | rd = rs1 + rs2 |
| sub rd, rs1, rs2 | rd = rs1 - rs2 |
| sll rd, rs1, rs2 | rd = rs1 << rs2,逻辑左移 |
| slt rd, rs1, rs2 | 将 rs1 和 rs2 视为有符号数。如果 rs1 < rs2,则 rd = 1,否则 rd = 0 |
| sltu rd, rs1, rs2 | 将 rs1 和 rs2 视为无符号数。如果 rs1 < rs2,则 rd = 1,否则 rd = 0 |
| xor rd, rs1, rs2 | rd = rs1 ^ rs2 |
| srl rd, rs1, rs2 | rd = rs1 >> rs2,逻辑右移 |
| sra rd, rs1, rs2 | rd = rs1 >> rs2,算数右移 |
| or rd, rs1, rs2 | rd = rs1 | rs2 |
| and rd, rs1, rs2 | rd = rs1 & rs2 |
其中,add 和 sub 用的电路是差不多的,所以 funct3 一样,只有 funct7 不同以区分。srl 和 sra 也是同理。
只提供 slt。如果需要 >,可以把操作数调换一下。如果需要 <=,可以先判 >,然后再异或上 1 取反。RiscV 没有像 X86 的 、 这种标识位,而是整了这样的指令。
涉及到立即数的指令为 I 格式。
imm 是补码表示,也就是这里立即数的范围只有 。后面会说如何表示更大的立即数。
opcode 为 0010011 。
| 指令 | 描述 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| addi rd, rs, imm | rd = rs + imm |
| slti rd, rs, imm | 将 rd 按有符号数解释。如果 rs < imm,则设置 rd = 1,反之设置 rd = 0 |
| sltiu rd, rs, imm | 将 rd 按无符号数解释。由于 imm 是有符号数,所以在比较的时候需要先把 imm 符号扩展 成 32 位,再把 imm 视为无符号数去跟 rd 比较。如果 rs < imm,则设置 rd = 1,反之设置 rd = 0 |
| xori rd, rs, imm | rd = rs ^ imm |
| ori rd, rs, imm | rd = rs | imm |
| andi rd, rs, imm | rd = rs & imm |
| slli rd, rs, imm | rd = rs << imm |
| srli, rd, rs, imm | rd = rs >> imm,逻辑右移 |
| srai, rd, rs, imm | rd = rs >> imm,算数右移 |
由于减去一个数,相当于加上一个数的相反数,所以没有 subi。
对于移位指令,由于移位的位数会对 取模,因此 imm 只有低 5 位有用。剩下的 7 位类似 funct7,来区分是算数右移还是逻辑右移。
opcode 为 0000011。
| 指令 | 描述 |
|---|---|
lb rd, imm(rs1) |
加载 rs1 + imm 地址处的一个字节,并符号扩展到 32 位放入 rd 中 |
lh rd, imm(rs1) |
加载 rs1 + imm 地址处的两个字节(半字),并符号扩展到 32 位放入 rd 中 |
lw rd, imm(rs1) |
加载 rs1 + imm 地址处的四个字节(字)放入 rd 中 |
lbu rd, imm(rs1) |
加载 rs1 + imm 地址处的一个字节,并零扩展到 32 位放入 rd 中 |
lhu rd, imm(rs1) |
加载 rs1 + imm 地址处的两个字节(半字),并零扩展到 32 位放入 rd 中 |
jalr rd, rs1, imm
opcode 为 1100111,funct3 为 000PC + 4)放到 rd 中,然后跳到 rs1 + imm 处用于 store 指令。
这类指令由于不需要写寄存器,所以原来 rd 的地方改为 imm 的低位部分。
opcode 为 0100011。
| 指令 | 描述 |
|---|---|
sb rs2, imm(rs1) |
把 rs2 的低位 1 字节放入 imm + rs1 这个内存地址 |
sh rs2, imm(rs1) |
把 rs2 的低位 2 字节放入 imm + rs1 这个内存地址 |
sw rs2, imm(rs1) |
把 rs2 放入 imm + rs1 这个内存地址 |
没有 sbu 和 shu 这种指令。
用于条件跳转指令。
opcode 为 1100011。
条件跳转指令使用 PC 相对寻址。
由于 RiscV 中每条指令都是 32 位的,所以理论上 imm 只需要表示 的倍数即可。但是 RiscV 还支持压缩指令,这种指令只有 16 位。并且 RiscV 约定扩展指令集的指令长度为 的倍数。所以 imm 表示的是 的倍数。
imm 的组成比较特殊:
imm[12|10:5] 为 zxxxxxximm[4:1|11] 为 wwwwyimm 为 zyxxxxxxwwww0(即 imm[4:1|11] 的最后一位表示第 位)这样的设计会减少硬件的开销。
imm 表示是汇编中的 Label。
| 指令 | 描述 |
|---|---|
beq rs1, rs2, label |
如果 rs1 == rs2,则跳转到 label |
bne rs1, rs2, label |
如果 rs1 != rs2,则跳转到 label |
blt rs1, rs2, label |
将 rs1 和 rs2 视为有符号整数,如果 rs1 < rs2,则跳转到 label |
bge rs1, rs2, label |
将 rs1 和 rs2 视为有符号整数,如果 rs1 >= rs2,则跳转到 label |
bltu rs1, rs2, label |
将 rs1 和 rs2 视为无符号整数,如果 rs1 < rs2,则跳转到 label |
bgeu rs1, rs2, label |
将 rs1 和 rs2 视为无符号整数,如果 rs1 >= rs2,则跳转到 label |
> 和 <= 可以通过交换操作数来实现,因此不存在 bgt 和 ble 指令。
位的 imm ,再加上有一半的地址都是无效指令,所以上述指令只能到达 PC 前后 条指令。
如果需要到达更远的地方,需要进行如下转换:
Unknown12345678beq x10, x0, far # next instr # -----> bne x10, x0, next # 条件取反 j far next: # next instr
仅用于 jal 指令:
opcode 为 1101111。
注意这里的 imm 也跟上面的 B 格式一样,位不是按顺序的。
imm 表示是汇编中的 Label。
jal rd, label
使用 PC 相对寻址
将下一条指令的地址 (PC + 4)放入到 rd 中
调到 label
imm 有 位,有一半的地址是无效指令,所以上述指令能到达 PC 前后 条指令。
用于 lui 和 auipc 两个指令。
和 J 格式很像,但这里的 imm 的位是有序的,并且语义上也有差别,表示的是一个 位数的高 位。
lui rd, imm
opcode 为 0110111
会将 rd 的高 位置成 imm,然后低 位清空
lui 和 addi 相配合可以将一个 32 位立即数放到寄存器
Unknown12lui x10, 0x87654 # x10 = 0x87654000 addi x10, x10, 0x321 # x10 = 0x87654321
注意的是,addi 的 imm 是补码,加到寄存器之前会先进行符号扩展,所以有可能发生这种情况:
所以当低 位的部分的最高位为 时,需要让 lui 的立即数多 1。
auipc rd, imm
opcode 为 0010111rd = PC + (imm << 12)PC 的值取出来放到通用寄存器中,比如 auipc x5, 0| 指令 | 描述 | 等价于 |
|---|---|---|
mv rd, rs1 |
令 rd = rs1 |
addi rd, rs1, 0 |
not rd, rs |
将 rs1 按位取反放入 rd 中 |
xori rd, rs, -1 |
li rd, imm |
将立即数 imm 放入 rd 寄存器中 |
根据 imm 的位数来决定是直接 addi rd, x0, imm 还是需要上 lui |
j label |
跳转到 label |
jal x0, label |
jr rs1 |
跳转到 rs1 存储的地址处 |
jalr x0, rs1 |
ret |
从当前函数返回 | jr ra,又等价于 jalr x0, ra |
nop |
无操作 | addi x0, x0, 0 |
la rd, label |
将 label 的地址加载到寄存器 rd 中 |
如果是绝对地址,那么使用 lui 和 addi。如果是 PC 相对地址,那么使用 auipc 和 addi |
call label |
调用函数 | 如果与当前 PC 相差不远,则 jalr ra, label 即可。如果相差很远,则需要 auipc ra, addr[31: 12] 然后 jalr ra, addr[11:0] |
值得注意的是,基础指令集中没有乘法和除法的指令。像是 mul rd, rs1, rs2 指令是属于 M 扩展里的。不过即使是 M 扩展也没有 muli,因为 riscv 里面立即数的字段位数不太长,可能不实用,而且把立即数加载到寄存器也不是什么麻烦事。
调用者保存寄存器 ra、a0-a7、t0-t6
被调用者保存寄存器 sp、s0-s11
zero 由于恒为 0,不需要考虑是调用者保存还是被调用者保存。gp 在整个程序的运行过程中应该是不变的,tp 在同一个线程中应该也是不变的,所以也不需要考虑是调用者保存还是被调用者保存。
a0-a7 用来存放参数。
如果函数的返回值是 32 位的,那么只需要把返回值存在 a0 里即可。如果返回值是 64 位的话,a0 存底 32 位,a1 存高 32 位。