写到现在你可能已经撞见过别扭:addi 的立即数只有 ±2048,想给寄存器装个 100000 怎么办?这一讲填上最后一块拼图:大常数的装配术,顺便盘点整个系列里一直在用的伪指令——它们不是真指令,却让汇编好读了一倍。

lui:往高处放 50 万

lui(load upper immediate)一条指令搞定一半:

lui a0, 24            # a0 = 24 << 12 = 0x18000,低 12 位是 0

它把 20 位立即数放进寄存器的高 20 位,低 12 位补 0。那低 12 位不就丢了?配合 addi 补上:

lui   a0, 24          # a0 = 0x18000
addi  a0, a0, 0x567   # a0 = 0x18567 —— 完整的 32 位常数

两条指令 = 20 位 + 12 位 = 任意 32 位常数。这就是「100000」的装法(li a0, 100000 会自动展开成这两条)。唯一的小坑:低 12 位若是负数(最高位为 1),addi 会借位,装配器要给高 20 位加 1——编译器天天处理的细节,知道有这回事即可。

auipc:以自己为锚

auipc(add upper immediate to pc)是 lui 的近亲,只是加的不是 0 而是 PC:

auipc a0, 0           # a0 = 当前指令自己的地址
auipc a1, 1           # a1 = PC + 0x1000

它是位置无关代码(PIC)的地基:不写死绝对地址,全用「相对我自己的偏移」,代码加载到哪都能跑。现代操作系统里的动态库、内核模块,背后全是 auipc 这类 PC 相对寻址。你在反汇编里看到的 la symbol,展开后就是 auipc + addi 的组合。

伪指令:汇编器的糖

这个系列其实早已用了大量伪指令。伪指令不是真的指令——汇编器看到它,会翻译成一或几条真指令。它们存在的唯一理由是「好读」:

伪指令 真身 意思
nop addi zero, zero, 0 什么都不做
li rd, 100000 lui + addi 装常数
la rd, symbol auipc + addi 装地址
mv rd, rs addi rd, rs, 0 复制寄存器
ret jalr zero, ra, 0 函数返回
j label jal zero, label 无条件跳转
call func auipc + jalr 远调用
not rd, rs xori rd, rs, -1 按位取反
neg rd, rs sub rd, zero, rs 取负
seqz rd, rs sltiu rd, rs, 1 == 0 ? 1 : 0
bgez/bgtz/blez 分支换位组合 ≥0、>0、≤0 时跳

看 nop 的真身:addi zero, zero, 0——往黑洞里加零,什么都不发生,但占一条指令的时间。「什么都不做」居然复用了加法指令,这是第 01 讲 zero 寄存器设计的回收:指令集里根本没有 nop,却人人都有 nop。

真指令有多少

盘点完你会发现 RV32I 的真指令只有 47 条:算术 10 条、访存 8 条、分支 6 条、跳转 2 条、其余是栅栏与系统调用等。相比 x86 动辄上千条指令加上花样寻址模式,RISC-V 把「程序员要记的东西」压到了极限——而伪指令又把 47 条翻译回人类舒服的样子。

硬件做减法,汇编器做加法——这句是 RISC-V 设计美学的钥匙。

动手验证

HI RISC-V App 的模拟器支持伪指令直接输入:分别敲 li a0, 100000 和手写 lui + addi 装配同一常数,看寄存器结果一致;再试试 mv、not、neg,对照第 06 讲的知识验证它们的真身。

免费下载 HI RISC-V App,写完这讲,第一幕闯关可以毕业了。

本讲三句话

  1. 大常数 = lui(高 20 位)+ addi(低 12 位)两条装配
  2. auipc 以 PC 为锚,是位置无关代码与 la 的地基
  3. 伪指令是汇编器的翻译糖:47 条真指令,无限好读

终讲预告:我们已经会「写」汇编了——最后一讲把指令拆开,看 0 和 1 的世界里它们长什么样。