ARM逆向基础:ARM与x86的区别、ARM指令集(Thumb/ARM)、ARM函数调用约定
好,我们直接进入正题。ARM 和 x86 这两兄弟,虽然都是做 CPU 的,但脾气秉性完全不同。你要是习惯了 x86 那一套,刚接触 ARM 逆向时,肯定会觉得“这玩意儿怎么不按套路出牌?” 嗯,我当年第一次逆向一个 ARM 固件时,就被它的指令长度和调用约定搞得晕头转向。今天我们就来把这些坑填平。
ARM 与 x86 的核心区别
说白了,最大的区别就两点:指令集架构 和 设计哲学。
- 指令长度:x86 是可变长度指令(1-15 字节),ARM 在 ARM 模式下是固定 4 字节,Thumb 模式下是固定 2 字节(或 4 字节的 Thumb-2)。
- 寄存器数量:x86 通用寄存器少(8 个/16 个),ARM 有 16 个通用寄存器(R0-R15),其中 R13 是 SP,R14 是 LR,R15 是 PC。
- 寻址方式:x86 支持复杂的寻址模式(基址+变址+偏移),ARM 相对简洁,但支持条件执行(每条指令都可以带条件码)。
- 内存访问:x86 允许很多指令直接操作内存,ARM 则遵循“加载-存储”架构——只有 LDR/STR 指令能访问内存,其他指令只操作寄存器。
我个人习惯:在逆向 ARM 时,我会先看指令长度。如果看到 4 字节对齐的指令,基本就是 ARM 模式;如果看到大量 2 字节指令,那就是 Thumb 模式。这个判断比看任何标志位都直观。
ARM 指令集:ARM 模式 vs Thumb 模式
ARM 处理器可以在两种指令集模式下运行。你想想看,为什么要有两套指令集?
ARM 模式:每条指令 32 位,功能强大,几乎每条指令都能条件执行。但代码密度低,占空间大。
Thumb 模式:每条指令 16 位(Thumb-2 扩展后也有 32 位),代码密度高,适合嵌入式设备。但功能受限,比如条件执行能力弱很多。
在实际逆向中,你经常会在同一个二进制里看到两种模式混用。我记得有一次分析一个手机 bootloader,它启动时用 ARM 模式初始化硬件,然后切到 Thumb 模式跑主逻辑。如果你没注意到模式切换,反汇编出来的代码就是一团乱麻。
避坑指南:我曾经在分析一个固件时,IDA 自动识别成了 ARM 模式,但实际代码是 Thumb 模式。结果反汇编出来的指令全是错的。后来我手动检查了入口点的地址最低位——如果地址最低位是 1,说明是 Thumb 模式(比如 0x10001),如果是 0,则是 ARM 模式。这个技巧帮我省了好几个小时。
条件执行的区别
ARM 模式下的每条指令都可以带条件码,比如 ADDEQ R0, R1, R2 表示“如果上一条指令结果为相等(EQ),则执行加法”。Thumb 模式下只有分支指令(B 系列)有条件执行。所以你在逆向 ARM 模式代码时,会看到很多条件指令,而 Thumb 模式则多用条件分支跳转。
ARM 函数调用约定
ARM 的调用约定和 x86 差别很大。x86 通常用栈传参(stdcall/cdecl),而 ARM 主要用寄存器传参。这就是所谓的 ATPCS(ARM Thumb Procedure Call Standard)。
| 参数位置 | 寄存器 | 说明 |
|---|---|---|
| 第1个参数 | R0 | 同时也是返回值寄存器 |
| 第2个参数 | R1 | |
| 第3个参数 | R2 | |
| 第4个参数 | R3 | |
| 第5个参数及以上 | 栈 | 从 SP+0 开始压栈 |
| 返回值 | R0(32位)或 R0+R1(64位) | |
| 链接寄存器 | R14 (LR) | 保存返回地址 |
| 栈指针 | R13 (SP) |
这里有个关键点:ARM 的返回地址不压栈,而是存在 LR 寄存器里。函数返回时,直接 BX LR 或 MOV PC, LR 就行。这和 x86 的 RET 指令完全不同。x86 的返回地址是存在栈上的,所以 RET 会从栈顶弹出地址。ARM 这种设计减少了内存访问,速度更快。
注意:如果函数内部调用了其他函数,就必须先保存 LR 到栈上(通常用 PUSH {LR}),否则内层函数返回时会覆盖 LR。我见过很多新手逆向时,看到 PUSH {R4-R7, LR} 就懵了,其实这就是在保存寄存器现场和返回地址。
实战中的调用约定识别
在逆向时,如何快速识别函数调用?看 BL 或 BLX 指令。BL 是带链接的跳转,它会将下一条指令的地址存入 LR,然后跳转到目标地址。BLX 类似,但会切换指令集模式(ARM <-> Thumb)。
; ARM 模式下的函数调用示例
BL func_name ; 跳转到 func_name,LR = 当前PC+4
; 返回后执行这里
; 被调用函数内部
func_name:
PUSH {R4-R7, LR} ; 保存寄存器
; ... 函数体 ...
POP {R4-R7, PC} ; 恢复寄存器并返回(注意这里 POP 到 PC 相当于 BX LR)
你可能会问:为什么最后是 POP {R4-R7, PC} 而不是 POP {R4-R7, LR}; BX LR?嗯,这是 ARM 的一个小技巧——PC 是 R15,直接 POP 到 PC 就相当于跳转,同时完成了恢复和返回。我在逆向时看到这种写法,基本就能确定是函数结尾了。
知识体系结构图
下面这张图总结了 ARM 逆向基础的核心逻辑,你可以对照着梳理思路:
逆向中的实用技巧
最后分享几个我自己的经验:
- 识别函数起始:ARM 函数通常以
PUSH {R4-R11, LR}或PUSH {R7, LR}开头(Thumb 模式常用 R7 做帧指针)。看到这种指令序列,基本就是函数入口。 - 注意 PC 的用法:在 ARM 模式下,PC 的值是当前指令地址 +8(因为流水线)。这在计算偏移时容易出错。我刚开始逆向时,经常算错 PC 相对寻址的偏移量,后来干脆用 IDA 自动计算。
- Thumb 模式下的 BL:Thumb 模式的 BL 指令是 32 位的(两条 16 位指令组合),用于长跳转。逆向时如果看到连续的
0xF000 0xF800之类的字节,基本就是 BL 指令。
我个人习惯:在 IDA 中,我会先按 Alt+G 手动设置处理器模式,确保反汇编正确。然后重点关注 R0-R3 的赋值,因为它们是参数传递的关键。如果你看到某个函数大量操作 R0-R3,那它很可能是在准备调用另一个函数。
好了,ARM 逆向的基础就这些。说白了,就是记住三点:模式判断、寄存器传参、LR 返回。把这三点吃透,大部分 ARM 代码你都能看懂个七七八八。
公众号:蓝海资料掘金营,微信deep3321