1. ARM64 寻址模式概述
ARM64 的寻址模式,说白了就是 CPU 怎么找到它要操作的数据。我刚开始从 x86 切到 ARM64 时,最不适应的就是这块——x86 的寻址方式太灵活了,而 ARM64 做了大量精简。但精简不代表简单,反而更讲究套路。
咱们先看一张总览图,把六种寻址模式的关系理清楚。
1.1 寄存器寻址
这是最直接的一种。操作数就在寄存器里,CPU 直接拿过来用,不涉及内存访问。
ADD X0, X1, X2 // X0 = X1 + X2
SUB X3, X4, X5 // X3 = X4 - X5
AND X6, X7, X8 // X6 = X7 & X8
寄存器寻址的速度是最快的——没有访存延迟。我优化过一段视频编解码的汇编代码,里面大量使用寄存器寻址,把临时变量全部塞进寄存器,性能直接提升了 30%。
1.2 立即数寻址
立即数就是直接写在指令里的常数。ARM64 的立即数编码有点特殊——不是所有 32 位或 64 位数都能直接写进去。
MOV X0, #0x42 // 合法,16 位立即数
MOV X1, #0x12345678 // 非法!超过编码范围
为什么会这样?ARM64 的指令长度固定 32 位,操作码、寄存器已经占了不少位,留给立即数的空间有限。ARM 的设计思路是:常用的立即数(比如 0-65535、移位后的模式)都能编码,不常用的就用多条指令拼出来。
MOV X0, #0x10000000,汇编器报错。后来改成 MOVZ X0, #0x1000, LSL #16 才搞定。记住:大常数用 MOVZ/MOVK 组合。
1.3 基址寻址
这是内存访问最常用的模式。格式是 [基址寄存器, #偏移],CPU 把基址寄存器的值加上偏移量,得到有效地址,然后访问内存。
LDR X0, [X1, #8] // 从 X1+8 地址加载 8 字节到 X0
STR X2, [X3, #16] // 将 X2 存储到 X3+16 地址
偏移量可以是正数也可以是负数。我调试过一个网络协议栈的 bug,就是偏移量写成了正数,结果访问了错误的内存区域,导致数据包校验失败。
| 偏移类型 | 示例 | 有效地址 |
|---|---|---|
| 正偏移 | LDR X0, [X1, #8] |
X1 + 8 |
| 负偏移 | LDR X0, [X1, #-8] |
X1 - 8 |
| 寄存器偏移 | LDR X0, [X1, X2] |
X1 + X2 |
| 扩展寄存器偏移 | LDR X0, [X1, X2, LSL #3] |
X1 + (X2 << 3) |
1.4 前变址(Pre-index)
前变址和基址寻址很像,但多了一个 ! 后缀。这个感叹号的意思是:先计算地址,然后更新基址寄存器。
LDR X0, [X1, #8]! // 先计算 X1+8,加载到 X0,然后 X1 = X1+8
说白了就是「先改指针,再取值」。这在遍历数组或链表时特别有用。我写过一个内存池分配器,用前变址来管理空闲链表,每次分配节点后指针自动后移,省了一条 ADD 指令。
1.5 后变址(Post-index)
后变址和前变址相反:先取值,再改指针。格式是 [基址寄存器], #偏移。
LDR X0, [X1], #8 // 先从 X1 加载到 X0,然后 X1 = X1+8
嗯,这里要注意:后变址的偏移量不能是寄存器,只能是立即数。这是 ARM64 的硬件限制。
我遇到过一种情况:有人写栈操作时用了后变址,结果函数返回时栈指针没恢复对,导致 crash。后来我建议他用前变址加手动调整,问题就解决了。
| 模式 | 语法 | 执行顺序 | 典型用途 |
|---|---|---|---|
| 基址寻址 | [Xn, #imm] |
不修改基址 | 结构体成员访问 |
| 前变址 | [Xn, #imm]! |
先改基址,再访存 | 栈帧分配、链表遍历 |
| 后变址 | [Xn], #imm |
先访存,再改基址 | 栈弹出、数组顺序访问 |
1.6 PC 相对寻址
PC 相对寻址是 ARM64 里最特殊的一种。它用程序计数器(PC)作为基址,加上一个偏移量来计算目标地址。你想想看,为什么需要这种模式?
因为位置无关代码(PIC)。动态链接库、共享库的代码段可能被加载到任意地址,如果指令里写死绝对地址,换个地方加载就崩了。PC 相对寻址让代码可以在内存中任意移动,只要相对位置不变,就能正常工作。
ADR X0, label // 将 label 的地址加载到 X0(小范围)
ADRP X0, label // 将 label 所在页的基址加载到 X0(大范围)
LDR X0, [PC, #offset] // 从 PC+offset 加载数据
ADR 的寻址范围是 ±1MB,ADRP 可以覆盖 ±4GB(按页对齐)。我调试过的一个 Android 系统服务,启动时总是崩溃,最后发现是 ADR 指令的偏移量超过了 1MB,换成 ADRP 就好了。
1.7 寻址模式选择建议
说了这么多,到底怎么选?我总结了几条经验:
- 能放寄存器就别放内存——寄存器寻址最快,没有之一。
- 小常数用立即数——但注意编码范围,超过 16 位就用 MOVZ/MOVK。
- 结构体访问用基址寻址——偏移量写清楚,代码可读性高。
- 循环遍历用后变址——一条指令完成加载和指针移动。
- 栈操作用前变址/后变址——但一定要配对使用,否则栈会乱。
- 位置无关代码用 PC 相对寻址——ADR/ADRP 是你的好朋友。