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

基址寻址有三种变体,我画了一张图帮你理解:

ARM64 基址寻址三种变体 基址 + 立即数偏移 ldr x0, [x1, #8] 不修改基址寄存器 前变址(Pre-index) ldr x0, [x1, #8]! 先修改基址,再访问 后变址(Post-index) ldr x0, [x1], #8 先访问,再修改基址 执行流程对比 ① 计算 x1 + 8 ② 访问该地址 ③ x1 保持不变 ① x1 = x1 + 8 ② 访问新 x1 地址 ③ x1 已更新 ① 访问 x1 地址 ② x1 = x1 + 8 ③ x1 已更新 典型应用场景 结构体成员访问(基址+偏移) | 栈操作(前变址压栈/后变址出栈) | 数组遍历(基址+索引寄存器)

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% 的数据访问都靠基址寻址完成。掌握好它们,后面的复杂寻址模式就水到渠成了。