2. ARM64体系结构基础

ARM64指令集,说白了就是处理器能听懂的基本命令。我刚开始接触时也觉得这东西很抽象,但后来发现,只要你理解了指令的编码格式,后面的数据处理、逻辑运算其实都是套路。

指令编码格式:机器码的骨架

ARM64的指令固定32位长。这一点比x86舒服多了——每条指令都是4字节,对齐简单,解码也快。

指令编码的核心结构是这样的:

31 30 29 28  27 26 25 24  23 ... 0
+--+--+--+--+--+--+--+--+-----------+
|op0|op1|  op2  |  op3  |   operand |
+--+--+--+--+--+--+--+--+-----------+

其中op0和op1决定指令大类。比如:

  • 00xx:数据处理(算术、逻辑)
  • 1x0x:内存访问(LDR/STR)
  • 101x:分支跳转(B/BL)

我个人习惯在逆向时先看op0和op1,这样能快速缩小指令范围。有一次我在分析一个崩溃dump时,就是靠这个技巧从一堆乱码里定位到了非法指令。

小技巧:ARM64指令的bit[31:25]是"家族位"。你只要记住这7个bit,就能识别90%的常见指令。

数据处理指令:ADD/SUB/MUL

这三条指令是逆向分析里出现频率最高的。我统计过,一个典型的ARM64二进制里,它们能占到指令总数的15%-20%。

ADD:加法指令

ADD Xd, Xn, Xm      // Xd = Xn + Xm
ADD Xd, Xn, #imm    // Xd = Xn + 立即数

注意,ARM64的ADD支持三种操作数形式:寄存器、立即数、移位后的寄存器。比如:

ADD X0, X1, X2, LSL #3   // X0 = X1 + (X2 << 3)

这个移位加法的组合在数组索引访问时特别常见。我在逆向一个视频编解码库时,就经常看到这种模式——说白了就是在算像素偏移。

SUB:减法指令

SUB Xd, Xn, Xm      // Xd = Xn - Xm
SUBS Xd, Xn, #1     // Xd = Xn - 1,同时更新标志位

这里有个坑:SUBS带S后缀的版本会更新条件标志位,而SUB不会。我曾经在调试一个循环时,发现循环条件死活不成立,最后发现是用了SUB而不是SUBS——标志位根本没变。

注意:ARM64里,CMP指令其实是SUBS的别名——它做减法但不保存结果,只更新标志位。所以看到CMP时,心里要清楚它背后就是一条SUBS。

MUL:乘法指令

MUL Xd, Xn, Xm      // Xd = Xn * Xm
SMULL Xd, Wn, Wm    // 有符号32位乘,结果64位
UMULL Xd, Wn, Wm    // 无符号32位乘,结果64位

MUL指令在ARM64里是3周期延迟。如果你在性能敏感代码里看到连续的MUL,那基本就是优化空间所在。我记得有一次调优一个加密算法,把MUL替换成查表+ADD的组合,吞吐量提升了30%。

指令 操作 延迟(周期) 常见场景
ADD 加法 1 地址计算、循环计数
SUB 减法 1 栈指针调整、循环递减
MUL 乘法 3 索引计算、加密算法

逻辑运算指令:AND/ORR/EOR

逻辑指令在逆向分析里经常被忽略,但它们是实现位掩码、标志位操作的关键。

AND:按位与

AND X0, X1, #0xFF       // 取低8位
AND X0, X1, X2          // X0 = X1 & X2

你想想看,AND最常见的用途就是位掩码提取。比如从状态寄存器里取某个标志位:

AND W0, W1, #0x80       // 提取bit 7

ORR:按位或

ORR X0, X1, X2          // X0 = X1 | X2
ORR X0, XZR, #0x100     // 加载立即数到寄存器

这里有个技巧:ORR X0, XZR, #imm其实是MOV指令的另一种写法。XZR是零寄存器,ORR和零做或运算,结果就是立即数本身。我在逆向时经常看到这种模式,编译器喜欢这么生成。

EOR:按位异或

EOR X0, X1, X2          // X0 = X1 ^ X2
EOR X0, X1, #-1         // X0 = ~X1(按位取反)

EOR和-1做异或,相当于按位取反。这个技巧在加密算法里用得特别多。我曾经逆向过一个自定义加密算法,整个核心就是一堆EOR和移位——说白了就是个轻量级流密码。

实战经验:在逆向分析时,如果看到连续的AND/ORR/EOR组合,大概率是在做位域操作。这时候可以画个位图,把每个bit的用途标出来,分析效率会高很多。

移位操作指令:LSL/LSR/ASR/ROR

移位指令在ARM64里不是独立的,而是作为操作数修饰符嵌入到其他指令中。这一点和x86很不一样。

助记符 全称 操作 示例
LSL 逻辑左移 左移,低位补0 ADD X0, X1, X2, LSL #3
LSR 逻辑右移 右移,高位补0 SUB X0, X1, X2, LSR #2
ASR 算术右移 右移,高位补符号位 MOV X0, X1, ASR #4
ROR 循环右移 右移,低位补到高位 EOR X0, X1, X2, ROR #8

嗯,这里要注意:ASR和LSR的区别。ASR保留符号位,适合有符号数除法;LSR高位补0,适合无符号数。我在分析一个音频解码器时,就遇到过ASR用错导致负数被当成正数处理的bug。

知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速参考:

ARM64指令集入门知识体系 ARM64指令集 指令编码格式 数据处理指令 逻辑运算指令 移位操作指令 op0/op1分类 32位固定长度 ADD SUB MUL AND ORR EOR LSL/LSR ASR/ROR 核心要点 指令固定32位 → 编码决定分类 → 操作数决定行为 移位操作通常作为其他指令的修饰符出现

避坑指南

最后分享几个我踩过的坑:

  • 立即数范围:ARM64的立即数不是所有32位值都能直接用的。它采用"编码模式",只有特定模式的值才能直接出现在指令里。如果遇到非法立即数错误,先检查是不是用了不支持的值。
  • 标志位更新:不是所有指令都更新标志位。只有带S后缀的(如SUBS、ADDS)才会更新。我见过有人用ADD做比较,结果条件跳转永远不触发。
  • 移位修饰符:移位量不能超过寄存器位宽(64位)。LSL #64是未定义行为,编译器不会报错,但运行结果不可预测。