七讲下来,你已经能用汇编指挥 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 的指令面板会同时显示每条指令的机器码。

本讲三句话与全系列总结

  1. 定长 32 位、六种格式(R/I/S/B/U/J),opcode + funct3/7 定身份
  2. 立即数在 B/J 型里被打乱排列——共享连线,硬件换软件的忍让
  3. 会编码就会解码:objdump 与你的手算遵循同一个循环

八讲到此收官。你现在拥有的,是从寄存器到机器码的完整分层图景——接下来最好的路是把这套知识带进真实战场:去 App 的闯关模式打完第一幕,再进阶特权级与中断(那是另一段旅程的起点)。

免费下载 HI RISC-V App,从 addi 到 Bootloader,39 关等你。