本节摘要:寻址模式在教科书里是文字描述,在机器里是冷冰冰的位。本节以 RISC-V 的单一寻址模式与 x86 的 ModRM 加 SIB 机制为标本,看"操作数在内存哪里"这条信息如何被压缩进固定位段。读完你应当能手工填写 ModRM 与 SIB 字节,并解释 64 位模式下 RIP 相对寻址的编码细节。
指令要访问内存,就得说清地址。地址 = 基址 + 变址 × 比例 + 位移,这个通用公式大家都会背;编码的问题在于:这条公式里哪些成分写进指令、各占几位、不写时怎么表示缺省。不同 ISA 给出两种极端答案。
RISC-V 的答案极简:访存指令只有一种寻址模式——基址寄存器加 12 位有符号偏移(Load/Store 架构的克制,2.1 节见过)。偏移不够宽怎么办?用别的指令先把地址算进寄存器。比如要把地址 0x12345678 处的数读进 t0:
lui t2, 0x12345 ; t2 = 0x12345000(高 20 位立即数装进寄存器) addi t2, t2, 0x678 ; t2 = 0x12345678(低 12 位补齐) lw t0, 0(t2) ; 读内存:基址 t2 加零偏移
三条指令协作完成一次远地址访问——寻址模式本身零复杂度,复杂度被推给了指令组合。这种"模式少、靠组合"的风格让解码端只认一种内存操作数形状,代价是地址合成的代码稍长。x86 的答案相反:把公式整体搬进指令,用 ModRM 与 SIB 字节逐项描述。
x86 的内存操作数由 ModRM 字节主导,8 位切成三段:最高两位 mod(寻址形态)、中间三位 reg(寄存器号或操作码延伸)、最低三位 rm(基址或辅助说明)。reg 段一物两用——在需要第二个寄存器操作数时放寄存器号,在指令延伸时放功能编号(3.2 节的地层之一);rm 段同样身兼数职:rm=100 时表示"后面跟 SIB 字节",mod=00 且 rm=101 时表示"用 32 位位移或 RIP 相对"。SIB 字节(Scale-Index-Base)再切三段:两位比例、三位变址寄存器、三位基址寄存器,专门负责"变址乘系数"这类成分。

把寻址表达式装配成字节,比想象中规律。看同一条 mov eax, [ecx+位移] 在三种位移宽度下的身材:无位移时 ModRM 用 mod=00,指令只省成 8B 01 两字节;小位移(-128 到 127)用 mod=01,多出一个位移字节,8B 41 04;大位移用 mod=10,四个位移字节跟上,8B 81 78 56 34 12。汇编器自动挑最省的档位,手工编码时你也在做同样的判断——位移宽度是变长产线代码密度的主要来源。
变址乘比例则由 SIB 补位。mov eax, [ebx+esi*4+8] 的完整编码是 8B 84 B3 08 00 00 00:操作码 8B 说明"读 32 位进寄存器";ModRM=0x84(mod=10 表大位移,reg=000 表目标 eax,rm=100 表后随 SIB);SIB=0xB3(scale=10 表乘 4,index=110 表 esi 变址,base=011 表 ebx 基址);末尾四个字节是小端存放的位移 8。逐段对号入座,没有一位是多余的。这种数组访问 a[i](基址加变址乘元素宽度)正是 SIB 的设计动机——C 代码里最热的天际线。
64 位模式还藏着一个高频特例:mod=00 且 rm=101 时,指令不再解释为绝对 32 位位移,而是 RIP 相对寻址——目标地址按"下一条指令地址加位移"计算。全局变量的访问因此用这条捷径:位置无关代码(现代系统库的默认形态)正是靠它避免绝对地址依赖,加载器才能把代码映射到任意地址。
3.3 节若只讲 x86 会失之偏颇,把 RISC-V 的对应做法摆上台面才算完整。它的访存指令只有"基址加 12 位偏移"一种形态,遇到更宽的偏移就分解成多步:先用 lui 把 20 位高位常数装进临时寄存器,再用 addi 补上 12 位低位,最后以该寄存器为基址执行加载。看似繁琐,实则有笔隐藏的账:绝大多数访存的偏移都在 12 位有符号范围内(正负 2048 字节覆盖了绝大多数结构体字段与局部数组),只有访问大数组的远端才需要合成——为少数场景支付三条指令,换来解码端永远只认一种内存操作数。这笔账的精髓是为高频路径保持极简,为低频路径保留出路,第 5 章解码车间你会看到这条原则如何在流水线上兑现成稳定的时序。
它还带来一个编译器视角的红利:寻址模式单一,意味着指令选择算法简单——不需要在"多个等价寻址方案"之间搜索最优。x86 的编译器后端则要在"位移塞不塞、塞多大、要不要借 SIB"之间做成本模型决策,寻址选择本身成了优化问题。工具链复杂度的天平,在编码格式定下的那一刻就已经倾斜。
手工填 ModRM 最容易错在三处。一是 reg 段的方向语义:8B(读进寄存器)与 89(写出到内存)的目标、来源在 ModRM 里互换,填反了数据流向就反——很多手写编码错误源于此。二是 mod=00 rm=101 这个特殊格子在 32 位与 64 位模式下的含义不同(绝对位移对 RIP 相对),拿 32 位直觉写 64 位代码会静默出错。三是 SIB 里 base=101 也有一层特殊含义(配合 mod 段表示无基址),虽然低频,但反汇编对不上时值得怀疑到这里。建议把本节的三个阶梯案例亲手抄进反汇编器各跑一遍,字节级对照——这一课的肌肉记忆,3.4 节马上要用。