示例: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 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
查表可知 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]
,也就是说,偏移量 0x1f
和 0x1e
具有上述相同的二进制表示。
由于 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 位)
- 访存指令的偏移量仅支持访存数据位宽的正整数倍
- a0–a5、s0–s1、sp 和 ra 这 10 个寄存器访问频率远高于其他寄存器
- RV64 基本上是 RV32 的超集,唯一例外是压缩指令。
- RV64C 更换了若干 RV32C 代码大小指令,因为对于 64 位地址,更换后的指令能压缩更多代码。
- RV64C 不支持压缩版本的跳转并链接(c.jal)和整数与浮点字存取指令(c.lw、c.sw、c.lwsp、c.swsp、c.flw、c.fsw、c.flwsp 和 c.fswsp)。
- 所以
lw
、sw
之类的指令在 RV64GC 中只有 32 位,没有 16 位
- 所以
- 作为替代,RV64C 支持更常用的字加减指令(c.addw、c.addiw、c.subw)以及双字存取指令(c.ld、c.sd、c.ldsp、c.sdsp)。
- 此时,一条指令是 16 位或者 32 位的
示例:addi sp, sp, 0x30
压缩指令
addi 相应的压缩指令有多条,如果使用 c.addi
1
2
3
4
5
6
7
8
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
1
2
3
4
5
6
7
8
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 倍立即数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
实际遇到的情况:在退栈时移动栈指针
1
2
3
4
; 反编译的汇编代码
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
1
2
3
4
5
6
7
8
9
10
11
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
1
2
3
4
5
6
7
8
9
10
11
汇编指令
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
压缩指令
1
2
3
4
5
6
7
8
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;
指令都是固定长度的,其立即数也是有范围限制的,所以直接存储一个大数到寄存器需要分成多个指令:
1
2
80200028: 79 65 lui a0, 0x1e # a0 = 0x1e000 (写入高位)
8020002a: 9b 05 05 24 addiw a1, a0, 0x240 # a1 = 0x1e240 = 123456 (写入低位)
一旦这个数字已经在寄存器内,把它存储到栈上就不必重新生成,而是可以直接复制过去
1
8020002e: 23 22 b4 fe sw a1, -0x1c(s0) # a1 的大小在 32 个字节之内,直接从寄存器复制到栈