Caiwen的博客

RiscV指令集

2026-03-29 09:14

0. 数据格式

RiscV 中字长为 3232 位。

1. 寄存器

寄存器 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 位的。

2. 指令集

2.1 R 格式

一些在寄存器上进行算数操作的指令为 R 格式:opname rd, rs1, rs2

其中 opcode 恒为 0110011。指令进行的操作由 funct3funct7 来决定:

| 指令 | 描述 | | ------------------- | ---------------------------------------------------------------------------- | ---- | | 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 | 将 rs1rs2 视为有符号数。如果 rs1 < rs2,则 rd = 1,否则 rd = 0 | | sltu rd, rs1, rs2 | 将 rs1rs2 视为无符号数。如果 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 |

其中,addsub 用的电路是差不多的,所以 funct3 一样,只有 funct7 不同以区分。srlsra 也是同理。

只提供 slt。如果需要 >,可以把操作数调换一下。如果需要 <=,可以先判 >,然后再异或上 1 取反。RiscV 没有像 X86 的 OFOFCFCF 这种标识位,而是整了这样的指令。

2.2 I 格式

涉及到立即数的指令为 I 格式。

imm 是补码表示,也就是这里立即数的范围只有 [2048,2047][-2048, 2047]。后面会说如何表示更大的立即数。

2.2.1 算数

opcode0010011

| 指令 | 描述 | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | | 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

对于移位指令,由于移位的位数会对 3232 取模,因此 imm 只有低 5 位有用。剩下的 7 位类似 funct7,来区分是算数右移还是逻辑右移。

2.2.2 load

opcode0000011

指令 描述
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

2.2.3 jalr

jalr rd, rs1, imm

  • opcode1100111funct3000
  • 先把下一条指令的地址(PC + 4)放到 rd 中,然后跳到 rs1 + imm

2.3 S 格式

用于 store 指令。

这类指令由于不需要写寄存器,所以原来 rd 的地方改为 imm 的低位部分。

opcode0100011

指令 描述
sb rs2, imm(rs1) rs2 的低位 1 字节放入 imm + rs1 这个内存地址
sh rs2, imm(rs1) rs2 的低位 2 字节放入 imm + rs1 这个内存地址
sw rs2, imm(rs1) rs2 放入 imm + rs1 这个内存地址

没有 sbushu 这种指令。

2.4 B/SB 格式

用于条件跳转指令。

opcode1100011

条件跳转指令使用 PC 相对寻址。

由于 RiscV 中每条指令都是 32 位的,所以理论上 imm 只需要表示 44 的倍数即可。但是 RiscV 还支持压缩指令,这种指令只有 16 位。并且 RiscV 约定扩展指令集的指令长度为 1616 的倍数。所以 imm 表示的是 22 的倍数。

imm 的组成比较特殊:

  • imm[12|10:5]zxxxxxx
  • imm[4:1|11]wwwwy
  • 最后组成的 immzyxxxxxxwwww0(即 imm[4:1|11] 的最后一位表示第 1111 位)

这样的设计会减少硬件的开销。

imm 表示是汇编中的 Label。

指令 描述
beq rs1, rs2, label 如果 rs1 == rs2,则跳转到 label
bne rs1, rs2, label 如果 rs1 != rs2,则跳转到 label
blt rs1, rs2, label rs1rs2 视为有符号整数,如果 rs1 < rs2,则跳转到 label
bge rs1, rs2, label rs1rs2 视为有符号整数,如果 rs1 >= rs2,则跳转到 label
bltu rs1, rs2, label rs1rs2 视为无符号整数,如果 rs1 < rs2,则跳转到 label
bgeu rs1, rs2, label rs1rs2 视为无符号整数,如果 rs1 >= rs2,则跳转到 label

><= 可以通过交换操作数来实现,因此不存在 bgtble 指令。

1212 位的 imm ,再加上有一半的地址都是无效指令,所以上述指令只能到达 PC 前后 ±210\pm 2^{10} 条指令。

如果需要到达更远的地方,需要进行如下转换:

Unknown
1
2
3
4
5
6
7
8
beq x10, x0, far # next instr # -----> bne x10, x0, next # 条件取反 j far next: # next instr

2.5 J/UJ 格式

仅用于 jal 指令:

opcode1101111

注意这里的 imm 也跟上面的 B 格式一样,位不是按顺序的。

imm 表示是汇编中的 Label。

jal rd, label

  • 使用 PC 相对寻址

  • 将下一条指令的地址 (PC + 4)放入到 rd

  • 调到 label

imm2020 位,有一半的地址是无效指令,所以上述指令能到达 PC 前后 ±218\pm 2^{18} 条指令。

2.6 U 格式

用于 luiauipc 两个指令。

和 J 格式很像,但这里的 imm 的位是有序的,并且语义上也有差别,表示的是一个 3232 位数的高 2020 位。

  • lui rd, imm

    • opcode0110111

    • 会将 rd 的高 2020 位置成 imm,然后低 1212 位清空

    • luiaddi 相配合可以将一个 32 位立即数放到寄存器

      Unknown
      1
      2
      lui x10, 0x87654 # x10 = 0x87654000 addi x10, x10, 0x321 # x10 = 0x87654321

      注意的是,addiimm 是补码,加到寄存器之前会先进行符号扩展,所以有可能发生这种情况:

      所以当低 1212 位的部分的最高位为 11 时,需要让 lui 的立即数多 1。

  • auipc rd, imm

    • opcode0010111
    • rd = PC + (imm << 12)
    • 可以将 PC 的值取出来放到通用寄存器中,比如 auipc x5, 0

3. 伪指令

指令 描述 等价于
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 如果是绝对地址,那么使用 luiaddi。如果是 PC 相对地址,那么使用 auipcaddi
call label 调用函数 如果与当前 PC 相差不远,则 jalr ra, label 即可。如果相差很远,则需要 auipc ra, addr[31: 12] 然后 jalr ra, addr[11:0]

值得注意的是,基础指令集中没有乘法和除法的指令。像是 mul rd, rs1, rs2 指令是属于 M 扩展里的。不过即使是 M 扩展也没有 muli,因为 riscv 里面立即数的字段位数不太长,可能不实用,而且把立即数加载到寄存器也不是什么麻烦事。

4. 函数调用约定

调用者保存寄存器 ra、a0-a7、t0-t6

被调用者保存寄存器 sp、s0-s11

zero 由于恒为 0,不需要考虑是调用者保存还是被调用者保存。gp 在整个程序的运行过程中应该是不变的,tp 在同一个线程中应该也是不变的,所以也不需要考虑是调用者保存还是被调用者保存。

a0-a7 用来存放参数。

如果函数的返回值是 32 位的,那么只需要把返回值存在 a0 里即可。如果返回值是 64 位的话,a0 存底 32 位,a1 存高 32 位。