2. ARM64基础回顾
寻址模式,说白了就是 CPU 怎么找到它要操作的数据。ARM64 的寻址方式不算多,但每个都很实用。我刚开始从 x86 转到 ARM64 时,最不适应的就是它那种「规整感」——每条指令做什么、数据从哪里来,写得清清楚楚。
这一节我们先把三种最基础的寻址模式讲透。它们是后续所有复杂寻址的基石。
2.1 寄存器寻址:最快、最直接
寄存器寻址,操作数直接放在寄存器里。CPU 拿到指令,直接读寄存器文件,没有内存访问,没有计算偏移。这是最快的寻址方式。
核心特点: 操作数在寄存器中,零内存访问,单周期完成。
// 寄存器寻址示例
add x0, x1, x2 // x0 = x1 + x2
sub x3, x4, x5 // x3 = x4 - x5
mul x6, x7, x8 // x6 = x7 * x8
and x9, x10, x11 // x9 = x10 & x11
我在项目中遇到过一个问题:某段热路径代码性能始终上不去,查了半天发现是编译器生成了大量内存到内存的操作。手动改成寄存器寻址后,吞吐量直接翻倍。嗯,寄存器就是 CPU 的「手边工具」,能用的尽量用。
个人习惯: 写汇编时,我会先列一遍当前可用的寄存器,尽量把频繁使用的变量留在寄存器里。你想想看,一次内存访问几十个周期,寄存器访问才一个周期,差距太大了。
2.2 立即数寻址:常数直接嵌在指令里
立即数寻址,常数直接编码在指令中。ARM64 的立即数有点特殊——它不像 x86 那样可以随便放 32 位立即数。ARM64 指令是固定 32 位长的,能留给立即数的位很有限。
// 立即数寻址示例
mov x0, #42 // x0 = 42
add x1, x2, #100 // x1 = x2 + 100
cmp x3, #0 // 比较 x3 与 0
and x4, x5, #0xFF // x4 = x5 & 0xFF
ARM64 的立即数编码规则是这样的:一个 8 位值,可以循环右移偶数位。所以不是所有 32 位常数都能直接放进去。比如 #0x12345678 就不能直接用 mov 加载。
| 立即数类型 | 编码方式 | 示例 | 是否可直接编码 |
|---|---|---|---|
| 小整数 (0-65535) | 直接编码 | #42 |
是 |
| 移位模式 | 8位值 + 循环右移 | #0xFF00 |
是 |
| 任意 32 位 | 需拆分为多条指令 | #0x12345678 |
否 |
| 64 位立即数 | 需 movz + movk 组合 |
#0xABCDEF0123456789 |
否 |
我曾经踩过的坑: 写了一个驱动,用 mov x0, #0x10000000 加载外设基地址,结果汇编器报错。查了半天手册才明白——这个值不能直接编码。后来改用 movz x0, #0x1000, lsl #16 才搞定。记住:大常数要用 movz/movk 组合加载。
2.3 基址寻址:访问内存的起点
基址寻址是 ARM64 访问内存最常用的方式。格式是 [基址寄存器 + 偏移]。偏移可以是立即数,也可以是另一个寄存器。
// 基址寻址示例
ldr x0, [x1] // x0 = *x1,偏移为 0
ldr x0, [x1, #8] // x0 = *(x1 + 8)
ldr x0, [x1, x2] // x0 = *(x1 + x2)
str x3, [x4, #16] // *(x4 + 16) = x3
基址寻址有三种变体,我画了一张图帮你理解:
2.4 三种变体的实战对比
前变址和后变址在栈操作中特别有用。我写一个栈帧管理的例子:
// 前变址:压栈(先减栈指针,再存数据)
stp x29, x30, [sp, #-16]! // sp -= 16, 然后 *(sp+0)=x29, *(sp+8)=x30
// 后变址:出栈(先取数据,再加回栈指针)
ldp x29, x30, [sp], #16 // x29=*(sp+0), x30=*(sp+8), 然后 sp += 16
// 普通基址:访问栈上局部变量
ldr x0, [sp, #8] // 读取 sp+8 处的局部变量
我建议: 写栈操作时,优先用前变址/后变址。它们把「修改指针」和「访问数据」合并成一条指令,既省代码又省周期。我在优化一个网络协议栈时,把 6 条栈操作指令压缩成 3 条,栈帧处理速度提升了 30%。
2.5 寻址模式选择指南
| 场景 | 推荐寻址模式 | 理由 |
|---|---|---|
| 算术运算 | 寄存器寻址 | 最快,无内存延迟 |
| 加载小常数 | 立即数寻址 | 常数嵌在指令中,无额外加载 |
| 结构体成员访问 | 基址 + 立即数偏移 | 偏移固定,编译器可优化 |
| 数组遍历 | 基址 + 索引寄存器 | 索引可变,适合循环 |
| 栈帧管理 | 前变址/后变址 | 一条指令完成指针调整和数据访问 |
| 加载大常数 | movz/movk 组合 |
突破立即数编码限制 |
注意: 前变址和后变址会修改基址寄存器。如果你后续还需要原来的基址值,记得先保存。我曾经在中断处理程序中用了后变址出栈,结果把栈指针搞乱了,系统直接崩溃。调试了一整天才发现是寄存器被意外修改了。
这三种基础寻址模式,是 ARM64 汇编的「ABC」。寄存器寻址最快,立即数寻址最方便但有编码限制,基址寻址最灵活。实际项目中,90% 的数据访问都靠基址寻址完成。掌握好它们,后面的复杂寻址模式就水到渠成了。