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时,就是靠这个技巧从一堆乱码里定位到了非法指令。
数据处理指令: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——标志位根本没变。
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和移位——说白了就是个轻量级流密码。
移位操作指令: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的立即数不是所有32位值都能直接用的。它采用"编码模式",只有特定模式的值才能直接出现在指令里。如果遇到非法立即数错误,先检查是不是用了不支持的值。
- 标志位更新:不是所有指令都更新标志位。只有带S后缀的(如SUBS、ADDS)才会更新。我见过有人用ADD做比较,结果条件跳转永远不触发。
- 移位修饰符:移位量不能超过寄存器位宽(64位)。LSL #64是未定义行为,编译器不会报错,但运行结果不可预测。