七讲下来,你已经能用汇编指挥 CPU。但每条汇编指令进入 CPU 时,其实是 32 个 0 和 1。终讲我们把指令拆开:看懂六种编码格式,亲手编两条机器码——这一讲之后,objdump 反汇编里的十六进制对你不再是天书。
定长 32 位:一切的前提
x86 指令长度 1 到 15 字节不等,译码器要先猜长度才能解析;RISC-V 每条指令恒定 32 位(C 扩展的 16 位压缩指令是另外的家族)。定长意味着:取指器永远知道下一条在哪、译码电路规整、流水线好排。这是所有 RISC 设计的第一块基石。
32 位里要塞下「做什么 + 两三个寄存器编号 + 可能的立即数」,不同指令需要的字段不同,于是有了六种格式:
| 格式 | 代表指令 | 字段布局 |
|---|---|---|
| R | add, sub | opcode, rd, funct3, rs1, rs2, funct7 |
| I | addi, lw, jalr | opcode, rd, funct3, rs1, imm[11:0] |
| S | sw, sb | opcode, imm[11:5], rs2, rs1, funct3, imm[4:0] |
| B | beq, bne | opcode, imm 打乱, rs2, rs1, funct3, imm 打乱 |
| U | lui, auipc | opcode, rd, imm[31:12] |
| J | jal | opcode, rd, imm 打乱 |
opcode(7 位)定指令大类,funct3/funct7 是大类内的细分;寄存器编号各占 5 位(2⁵ = 32,正好)。
亲手编码一条 addi
addi a0, zero, 42,查表:a0 = x10、zero = x0、addi 的 opcode 是 0010011、funct3 = 000:
imm[11:0] rs1 funct3 rd opcode
000000101010 00000 000 01010 0010011
连成 32 位,每 4 位转一个十六进制:
0000 0010 1010 0000 0000 0101 0001 0011
0 2 A 0 0 5 1 3 → 0x02A00513
0x02A00513 就是 addi a0, zero, 42。 反过来从机器码读指令也一样机械:取低 7 位 opcode 定类,拆 rd/rs1,读立即数。objdump 做的就是这个循环。
beq:真正的硬骨头
beq a0, a1, 8(相等则跳过 8 字节)。opcode = 1100011,funct3 = 000,rs1 = 10,rs2 = 11,立即数 = 8 = 0b1000。
麻烦在于 B 型的立即数是 12 位但要塞进不连续的位置:
imm: [12] [10:5] ...... [4:1] [11]
把 8 拆开:bit12=0、bit10:5=000000、bit4:1=100、bit11=0,按格式填入:
imm12 imm10:5 rs2 rs1 f3 imm4:1 imm11 opcode
0 000000 01011 01010 000 0100 0 1100011
连起来转十六进制 = 0x00B50463。你可以对这段二进制逐段验证——rs2=01011(11)、rs1=01010(10)都在正确的位置。
为什么立即数要「打乱」
初见 B 型的人都会问:老老实实排不行吗?不行——这是故意的。
观察两个事实:① B 型和 S 型的立即数最低 5 位所在的位置是共用的;② 无论怎么排,bit 0 永远是 0(分支目标都是 2 字节对齐)。
于是硬件工程师发现:把立即数的几位分散摆,可以让「addi、load、sw、beq」共享同一套位连线来提取立即数,译码器少一大把多路选择器。代价是写反汇编工具的人头疼,收益是每条路径上的门电路都变少。
这个设计哲学和第 01 讲的
zero寄存器一脉相承:为了硬件简单,让软件(人)多忍一点。 RISC 的全部美学,就在这类取舍里。
动手验证
用 Python 做你的第一个「汇编器片段」,验证上面的手算:
def enc_addi(rd, rs1, imm):
return (imm & 0xFFF) << 20 | rs1 << 15 | rd << 7 | 0x13
print(hex(enc_addi(10, 0, 42))) # 0x2a00513 —— 和手算一致
再自己挑战 add a2, a0, a1(R 型,funct7=0,opcode=0x33),然后在模拟器里看这条指令的机器码是否与你的结果一致。
免费下载 HI RISC-V App,App 的指令面板会同时显示每条指令的机器码。
本讲三句话与全系列总结
- 定长 32 位、六种格式(R/I/S/B/U/J),opcode + funct3/7 定身份
- 立即数在 B/J 型里被打乱排列——共享连线,硬件换软件的忍让
- 会编码就会解码:objdump 与你的手算遵循同一个循环
八讲到此收官。你现在拥有的,是从寄存器到机器码的完整分层图景——接下来最好的路是把这套知识带进真实战场:去 App 的闯关模式打完第一幕,再进阶特权级与中断(那是另一段旅程的起点)。
免费下载 HI RISC-V App,从 addi 到 Bootloader,39 关等你。