4. 常用ARM64反汇编工具

寻址模式,说白了就是CPU怎么找到它要操作的数据。我刚开始做逆向时,经常被各种寻址方式绕晕——明明指令里写的是同一个寄存器,跑出来的结果却天差地别。这一章咱们就把ARM64的五种寻址模式彻底讲透。

核心要点:ARM64的寻址模式决定了数据从哪里来、往哪里去。理解它们,你才能读懂反汇编代码的每一行。

4.1 立即数寻址:数据就在指令里

立即数寻址是最直观的一种——操作数直接编码在指令中。你不需要去内存或寄存器里找,数据就在眼前。

MOV X0, #0x42        // 把立即数0x42加载到X0
ADD X1, X2, #0x10    // X2 + 0x10 → X1
MOVZ X3, #0xABCD, LSL #16  // 带移位加载

嗯,这里要注意:ARM64的立即数不是随便什么值都能用的。它有一个编码规则——必须是可编码立即数。我踩过这个坑:写了个#0x12345678,结果汇编器报错,说这个值无法编码。

我的经验:ARM64的立即数编码采用“8位值+4位移位”的模式。简单说,只有那些能通过循环移位得到的值才是合法的。调试时如果遇到非法立即数错误,先检查这个。

4.2 寄存器寻址:最常用的方式

寄存器寻址,操作数就在寄存器里。这是ARM64中最常见的寻址方式,没有之一。

ADD X0, X1, X2       // X1 + X2 → X0
SUB W3, W4, W5       // W4 - W5 → W3
MUL X6, X7, X8       // X7 * X8 → X6

我个人习惯把寄存器寻址分为三类:

  • 直接寄存器寻址:操作数就是寄存器内容,上面例子都是
  • 寄存器移位寻址:操作前先对寄存器做移位
  • 寄存器扩展寻址:把32位寄存器扩展到64位再操作
// 寄存器移位寻址
ADD X0, X1, X2, LSL #3   // X1 + (X2 << 3) → X0
SUB X3, X4, X5, ASR #2   // X4 - (X5 >> 2) → X3

// 寄存器扩展寻址
ADD X0, X1, W2, UXTW     // X1 + 零扩展的W2 → X0

你想想看,为什么需要移位寻址?我在分析一个数组遍历的代码时,看到LSL #3就知道这是在算索引乘以8——因为数组元素是8字节的。这种模式在逆向中非常常见。

4.3 基址偏移寻址:访问内存的基石

基址偏移寻址是ARM64访问内存的主力。它由一个基址寄存器加上一个偏移量组成,偏移量可以是立即数、寄存器或者移位后的寄存器。

// 立即数偏移
LDR X0, [X1, #8]        // 从 X1+8 地址加载到 X0
STR W2, [X3, #-4]       // 把 W2 存储到 X3-4 地址

// 寄存器偏移
LDR X4, [X5, X6]        // 从 X5+X6 地址加载
STR X7, [X8, X9, LSL #3] // 从 X8 + (X9<<3) 地址存储

避坑指南:我曾经在调试一个崩溃问题时,发现LDR X0, [X1, #8]这条指令执行后X1的值没变。很多新手以为基址偏移会修改基址寄存器——不会!它只是临时计算地址,基址寄存器保持不变。要修改基址,得用前变址或后变址。

4.4 前变址与后变址:自动更新基址

这两个模式是基址偏移的“升级版”——它们会在访问内存的同时更新基址寄存器。这在遍历数组、栈操作时特别有用。

模式 语法 执行顺序 典型用途
前变址 LDR X0, [X1, #8]! 先更新X1 += 8,再访问新地址 栈帧分配、指针前进
后变址 LDR X0, [X1], #8 先访问原地址,再更新X1 += 8 数组遍历、栈弹出
// 前变址:先改指针,再访问
LDR X0, [SP, #-16]!    // SP -= 16,然后从新SP加载
// 等价于:SP = SP - 16; X0 = *SP;

// 后变址:先访问,再改指针
LDR X0, [X1], #8       // 从X1加载,然后X1 += 8
// 等价于:X0 = *X1; X1 = X1 + 8;

我记得有一次分析一个递归函数的反汇编代码,看到连续好几个STP X29, X30, [SP, #-32]!——这就是典型的栈帧创建。前变址的!后缀是关键标志,逆向时看到它就要意识到:基址寄存器被修改了。

调试技巧:在GDB中单步执行前变址/后变址指令时,记得同时观察基址寄存器的变化。我习惯用display/8gx $sp和display $sp同时监控栈内容和栈指针。

4.5 PC相对寻址:位置无关代码的核心

PC相对寻址是ARM64里最“聪明”的寻址方式——它使用程序计数器(PC)作为基址,加上一个偏移量来计算目标地址。这意味着代码可以在内存中任意位置运行,不需要重定位。

ADR X0, label          // 将label的地址加载到X0(小范围)
ADRP X1, label         // 将label所在页的基址加载到X1(大范围)
B label                // 跳转到label(PC相对跳转)
BL func                // 调用func(PC相对调用)

为什么说它重要?你想想看,现代操作系统都使用ASLR(地址空间随机化),每次加载程序时基址都不一样。如果没有PC相对寻址,所有全局变量和函数调用都得在加载时修正——那效率得多低。

// 实际反汇编中常见的PC相对寻址模式
ADRP X0, .LC0          // 获取字符串常量所在页
ADD X0, X0, :lo12:.LC0 // 加上页内偏移,得到完整地址
BL printf              // 调用printf

我在逆向一个位置无关的可执行文件(PIE)时,看到大量ADRP+ADD的组合。这就是ARM64访问全局数据的标准套路——先取页基址,再加页内偏移。

关键区别:PC相对寻址计算的是当前指令地址+偏移,而不是某个固定地址。所以同样的指令,在不同内存位置执行时,访问的目标地址也不同。这正是位置无关代码的精髓。

4.6 寻址模式对比与实战选择

说了这么多,咱们来张图总结一下。下面这张SVG图展示了五种寻址模式的数据流向关系:

ARM64 五种寻址模式数据流向图 CPU 核心 立即数寻址 数据在指令中 寄存器寻址 数据在寄存器中 基址偏移寻址 基址+偏移→内存 前/后变址寻址 访问+更新基址 PC相对寻址 PC+偏移→目标 💡 实战要点 逆向时优先识别寻址模式:!后缀→前变址,无!→基址偏移,ADRP→PC相对

在实际逆向中,我总结了一套选择策略:

  • 访问常量:用立即数寻址,直接编码在指令里
  • 局部变量:用基址偏移寻址,SP/FP加偏移
  • 遍历数组:用后变址寻址,访问完自动前进
  • 栈操作:用前变址寻址,先分配空间再使用
  • 全局变量/函数调用:用PC相对寻址,保证位置无关

常见陷阱:我曾经在分析一个O3优化后的代码时,看到LDR X0, [X0, #8]!——前变址同时修改了源和目标寄存器。这种代码在单步调试时要特别小心,因为X0的值在指令执行前后完全不同。建议在GDB中用si单步,并配合info registers x0观察变化。

4.7 实战:从反汇编代码识别寻址模式

光说不练假把式。咱们来看一段真实的反汇编代码,我标注了每条指令的寻址模式:

// 函数入口:栈帧创建
STP X29, X30, [SP, #-32]!    // 前变址:SP-=32,再存储
MOV X29, SP                   // 寄存器寻址

// 访问局部变量
STR W0, [X29, #28]            // 基址偏移:X29+28
LDR W1, [X29, #28]            // 基址偏移:X29+28

// 调用子函数
ADRP X0, .LC0                 // PC相对寻址:获取页基址
ADD X0, X0, :lo12:.LC0        // PC相对寻址:加页内偏移
BL  printf                    // PC相对寻址:函数调用

// 遍历数组
MOV X2, #0                    // 立即数寻址
.L2:
LDR W3, [X1, X2, LSL #2]     // 基址偏移+寄存器移位
ADD W4, W4, W3                // 寄存器寻址
ADD X2, X2, #1                // 立即数寻址
CMP X2, X5                    // 寄存器寻址
B.LT .L2                      // PC相对寻址:条件跳转

// 函数返回
LDP X29, X30, [SP], #32       // 后变址:先加载,SP+=32
RET                           // 寄存器寻址(隐式使用X30)

你看,一个简单的函数里就包含了所有五种寻址模式。我刚开始学逆向时,就是靠这样逐行标注来训练的。坚持一个月,看到任何反汇编代码都能条件反射地识别出寻址模式。

我的训练方法:找10个不同功能的二进制文件,用objdump反汇编,然后逐行标注寻址模式。标注完再对照ARM手册验证。这个方法虽然笨,但效果极好——肌肉记忆比死记硬背靠谱多了。

好了,这一章的内容就到这里。寻址模式是ARM64汇编的基础中的基础,理解透了,后面的逆向分析才能事半功倍。记住:看到指令先问自己三个问题——数据从哪来?到哪去?基址寄存器变不变?