rCore (RISC-V):指令的二进制格式

时间:2024-04-20

示例:beq x1 x2 0x1f

| 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|---|---|---|---|---|---|---|---|---|---|
| 0  | 0  | 0  | 0  | 0  | 0  | 0  | 0  | 0  | 0  | 1  | 0  | 0  | 0  | 0  | 0  | 1  | 0  | 0  | 0  | 1  | 1  | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 |

查表可知 RV32I 的 beq 指令格式
31 <- 0
imm[12|10:5] rs2 rs1 000 imm[4:1|11] 1100011

offset: 假设为 0000 0000 0000 0000 0000 0000 0001 1111 (imm = 0x1f)
imm[12] = 0
imm[10:5] = 000 000
imm[4:1] = 1111
imm[11] = 0

各个部分:
opcode: 1100011 (B-type)
imm[11] = 0
imm[4:1] = 1111
func3: 000 (beq)
rs1: 00001
rs2: 00010
imm[10:5] = 000 000
imm[12] = 0

最终的指令格式(二进制)
=> binary format: 00000000001000001000111101100011 

https://luplab.gitlab.io/rvcodecjs/#q=beq+x1,+x2,+0x01f&abi=false&isa=RV32I

由于该格式并不考虑 imm[0],也就是说,偏移量 0x1f0x1e 具有上述相同的二进制表示。

由于 RISC-V 指令长度必须是两字节的倍数,分支指令的寻址方式将 12 位立即数乘以 2,符号扩展后与 PC 相加。

src: 《RISC-V 开放架构设计之道 1.0.0》 (原著 The RISC-V Reader: An Open Architecture Atlas)

因此,这个偏移量实际上被要求为 2 的倍数,所以并不会出现我们在这假设的 0x1f,以上二进制所包含的偏移量也仅为 0x1e,并且原偏移量为 0xf (0x1e = 0xf * 2)。

压缩指令

压缩指令的长度为 16 位。

  • RV64 中,虽然寄存器的宽度为 64 位,但指令的长度与 RV32 一样都为 32 位。
  • 对于 riscv64gc-(unknown-none-elf) 目标平台标识符,它具有 C (指令压缩)拓展,使某些 32 位的指令有相应的 16 位的压缩指令
    • 此时,一条指令是 16 位或者 32 位的
      • 从二进制来看,32 位指令以 11 开头,16 位指令以 00、01、10 开头
      • 压缩指令多了 c. 前缀,比如 addi 的压缩指令为 c.addi
      • 压缩指令的类型也多了 C 前缀,比如 J 型指令的压缩情况为 CJ 型
      • 在执行指令前,译码器将所有 16 位指令翻译成相应的 32 位指令,以供 CPU 处理
        • 在 CPU 的高速缓存优势下,译码器的开销可以被忽略不计
    • 指令能被压缩成 16 位,是因为
      • a0–a5、s0–s1、sp 和 ra 这 10 个寄存器访问频率远高于其他寄存器
        • 指令操作码表中使用 rd'、rs1' 和 rs2' 来表示它们:有些压缩指令只针对它们
        • 所以指令中,每个寄存器编号此时只需要 3 位来表示(相比于正常的 5 位,多了 2 位用来表示指令类型)
      • 很多指令会覆写其中一个源操作(寄存器)
        • 压缩指令并不只针对那 10 个常用寄存器设计,依然有的压缩指令支持所有寄存器(但隐式地覆写其中一个源操作数)
      • 立即数从 12 位变得更小了(比如 6 位、5 位)
        • 访存指令的偏移量仅支持访存数据位宽的正整数倍
    • RV64 基本上是 RV32 的超集,唯一例外是压缩指令。
      • RV64C 更换了若干 RV32C 代码大小指令,因为对于 64 位地址,更换后的指令能压缩更多代码。
      • RV64C 不支持压缩版本的跳转并链接(c.jal)和整数与浮点字存取指令(c.lw、c.sw、c.lwsp、c.swsp、c.flw、c.fsw、c.flwsp 和 c.fswsp)。
        • 所以 lwsw 之类的指令在 RV64GC 中只有 32 位,没有 16 位
      • 作为替代,RV64C 支持更常用的字加减指令(c.addw、c.addiw、c.subw)以及双字存取指令(c.ld、c.sd、c.ldsp、c.sdsp)。

示例:addi sp, sp, 0x30 压缩指令

addi 相应的压缩指令有多条,如果使用 c.addi

c.addi rd, imm => addi rd, rd, imm => x[rd] = x[rd] + sext(imm)
15 <- 0
000 imm[5] rd imm[4:0] 01

sp = x2 = 00010
0x30 = 0011 0000

000 1 00010 10000 01 => 0x1141 => 41 11 (LE)

使用 c.li

c.li rd, imm => addi rd, x0, imm => x[rd] = sext(imm)
15 <- 0
010 imm[5] rd imm[4:0] 01

sp = x2 = 00010
0x30 = 0011 0000

010 1 00010 10000 01 => 0101 0001 0100 0001 => 0x5141 => 41 51 (LE)

使用 c.addi16sp (栈指针加 16 倍立即数)

c.addi16sp imm => addi x2, x2, imm => x[2] = x[2] + sext(imm)
15 <- 0 
011 imm[9] 00010 imm[4|6|8:7|5] 01

0x30 = 0011 0000
  imm[4|5] = 1

011 0 00010 10001 01 => 0110 0001 0100 0101 => 6 1 4 5

imm 的取值范围只考虑 9:4 位,去除一个最高的符号位,范围为
* 正数的最大范围为 2^8-1,即 255
* 负数的最大范围为 -2^8,即 -256
* 且不含上述 [-256, 255] 中区间中的 [-8, 7] 范围
  * 因为最低 3 位被该指令舍弃了
  * 所以不包括 11111111111111111111111111111000 ~ 00000000000000000000000000000111

实际遇到的情况:在退栈时移动栈指针

; 反编译的汇编代码
8020004c: 45 61         addi    sp, sp, 0x30

45 61 (LE) => 0x6145 => 0110 0001 0100 0101 => 011 ... 01 (与 c.addi16sp 0x30 所表示的一致)

拓展一下:在建栈时移动栈指针 addi sp, sp, -0x30,即 c.addi16sp -0x30

c.addi16sp imm => addi x2, x2, imm => x[2] = x[2] + sext(imm)
011 imm[9] 00010 imm[4|6|8:7|5] 01

0x30 = 0011 0000 = 00000000000000000000000000110000
-0x30 = 11111111111111111111111111001111 + 1 = 11111111111111111111111111010000
  imm[5] = 0

011 1 00010 11110 01 => 0111 0001 0111 1001 => 7 1 7 9 => 0x7179 => 79 71 (LE)

实际汇编验证:
80200020: 79 71         addi    sp, sp, -0x30

示例: c.li a0, 0

汇编指令 
8020003a: 01 45         li      a0, 0x0

c.li rd, imm => addi rd, x0, imm => x[rd] = sext(imm)
15 <- 0
010 imm[5] rd imm[4:0] 01
a0 = x10 = 0xa = 1010
010 0 01010 00000 01 => 0100 0101 0000 0001

验证:
45 01 (LE) => 0100 0101 0000 0001

示例:ret 压缩指令

ret = jalr x0, 0(x1) = c.jr x1 
100 0 rs1 00000 10

x1 = 00001 
100 0 00001 00000 10 => 1000 0000 1000  0010 => 8 0 8 2 => 0x8082 => 82 80 (LE)

验证:
8020004e: 82 80         ret

示例:let a = 123456;

指令都是固定长度的,其立即数也是有范围限制的,所以直接存储一个大数到寄存器需要分成多个指令:

80200028: 79 65         lui   a0, 0x1e      # a0 = 0x1e000 (写入高位)
8020002a: 9b 05 05 24   addiw a1, a0, 0x240 # a1 = 0x1e240 = 123456 (写入低位)

一旦这个数字已经在寄存器内,把它存储到栈上就不必重新生成,而是可以直接复制过去

8020002e: 23 22 b4 fe   sw  a1, -0x1c(s0) # a1 的大小在 32 个字节之内,直接从寄存器复制到栈