ARM基础(一):ARM架构寄存器、指令集概览、Thumb/Thumb-2模式

做Pwn的人,迟早要碰ARM。不管是IoT设备、手机内核,还是云原生场景下的边缘计算节点,ARM架构几乎无处不在。我个人最早接触ARM是在分析一个路由器漏洞时,那时候我连ARM有多少个寄存器都说不清,调试器里看到一堆R0~R15直接懵了。后来踩了不少坑,才慢慢摸清楚这套体系。

这一章我们先把ARM的底子打牢。寄存器怎么用、指令集长什么样、Thumb和Thumb-2到底有什么区别——这些搞明白了,后面做沙箱绕过和异架构利用才能顺手。

ARM寄存器体系

ARM的寄存器,说白了就是CPU内部的一小块高速存储空间。跟x86不一样,ARM的寄存器数量多,而且大部分是通用寄存器。这在漏洞利用时其实是个好事——你会有更多的“临时变量”可以用。

寄存器 别名 用途说明
R0 - R3 参数/返回值寄存器 函数调用时传参,R0也存返回值
R4 - R11 通用寄存器 局部变量、临时数据,函数内需保存
R12 IP(Intra-Procedure) 过程间调用临时寄存器,链接器可能用
R13 SP(Stack Pointer) 栈指针,指向当前栈顶
R14 LR(Link Register) 保存函数返回地址
R15 PC(Program Counter) 程序计数器,指向当前指令+8

关键点:ARM的PC指向的是当前指令地址+8(ARM模式下)。这是因为ARM采用三级流水线:取指、译码、执行。PC在取指阶段就已经更新了。做ROP时计算偏移一定要把这个+8算进去,否则你的gadget地址全错。

我记得第一次写ARM的ROP链时,死活调不对地址,后来才发现是PC的偏移量没处理。嗯,这个坑我替你们踩过了。

ARM指令集概览

ARM指令集是固定32位长度的,每条指令占4字节。这跟x86那种变长指令完全不同。好处是解码简单,坏处是代码密度低——同样的功能,ARM的代码体积比Thumb大不少。

常用的指令其实不多,我列几个做Pwn必须掌握的:

  • LDR/STR:内存加载和存储。LDR R0, [R1] 表示从R1指向的地址读4字节到R0。
  • MOV:寄存器间传值。MOV R0, #0x10 把立即数0x10赋给R0。
  • ADD/SUB:加减法。ADD R0, R1, R2 就是R0 = R1 + R2。
  • B/BL/BX:分支跳转。B是无条件跳转,BL会保存返回地址到LR,BX可以切换指令集模式。
  • PUSH/POP:压栈和出栈。PUSH {R4-R7, LR} 一次压多个寄存器,POP则相反。

小技巧:做ARM ROP时,POP {R0, PC} 这种gadget非常常见。它一次弹出两个值,一个给R0控制参数,一个给PC控制执行流。找gadget时优先找这种。

Thumb模式与Thumb-2

ARM模式每条指令32位,代码密度低。为了省空间,ARM推出了Thumb模式。Thumb指令是16位的,代码体积能缩小30%~40%。但代价是功能受限——很多操作做不了,比如不能直接访问所有寄存器。

后来ARM搞了Thumb-2,这是个混合指令集。它既有16位指令,也有32位指令,解码器自动识别。Thumb-2基本覆盖了ARM模式的功能,同时保持了高代码密度。现在大部分ARM程序都跑在Thumb-2模式下。

怎么切换模式?看BX指令的目标地址最低位:

  • 如果最低位是0,切换到ARM模式
  • 如果最低位是1,切换到Thumb模式

举个例子:BX R0,如果R0 = 0x8000,则跳转到ARM模式;如果R0 = 0x8001,则跳转到Thumb模式。这个最低位不会被当成地址的一部分,它只是个模式标志。

避坑指南:我曾经在分析一个固件时,反汇编出来的代码全是乱码。后来发现是反汇编器默认用了ARM模式,但固件实际跑在Thumb模式。如果你看到反汇编结果明显不对,先检查一下模式设置。IDA里按Alt+G可以手动切换。

ARM与Thumb指令对比

特性 ARM模式 Thumb模式 Thumb-2
指令长度 固定32位 固定16位 16/32位混合
代码密度 低 高 高
寄存器访问 全部16个 受限(R0-R7为主) 基本全部
条件执行 几乎所有指令 仅分支指令 部分指令
常见场景 启动代码、中断处理 早期嵌入式系统 现代ARM程序(默认)

做漏洞利用时,你大概率会遇到Thumb-2代码。但ARM模式下的gadget有时更好用,因为指令功能更全。我个人习惯是两种模式都搜一遍gadget,看哪个组合更顺。

知识体系结构图

下面这张图把ARM基础的核心脉络画出来了。寄存器、指令集、模式切换,这三块是后面所有内容的基础。

ARM基础核心知识结构 ARM基础核心知识结构 寄存器体系 指令集概览 Thumb/Thumb-2 R0-R15 通用寄存器 SP/LR/PC 特殊用途 LDR/STR 内存访问 B/BL/BX 分支跳转 16位指令 高密度 Thumb-2 混合模式 核心要点:寄存器是基础,指令集是工具,模式切换是ARM特有的坑 做Pwn时,三者必须同时掌握才能写出正确的ROP链 理解寄存器 掌握指令集 识别模式切换 构造ROP链 (最终目标)

实践:如何快速识别当前模式

拿到一个ARM二进制文件,第一步就是确定它跑在什么模式下。我一般用这几个方法:

  1. 看入口地址:如果入口地址最低位是1,说明是Thumb模式。比如0x8001,那肯定是Thumb。
  2. 看指令长度:反汇编出来如果全是4字节对齐的32位指令,那就是ARM模式。如果看到大量2字节指令,那就是Thumb或Thumb-2。
  3. 看BX指令的目标:在代码里搜索BX Rn,看它跳转的目标地址最低位。这是最直接的方法。

实战技巧:用objdump反汇编时,加参数 -m arm 或 -m thumb 可以指定模式。我习惯先用 -m arm 反汇编一次,如果发现大量非法指令,再换 -m thumb 试试。IDA Pro会自动识别,但偶尔也会翻车,手动确认一下更保险。

好了,ARM的基础框架就搭到这里。寄存器怎么用、指令集有哪些、Thumb和Thumb-2怎么切换——这些是后面所有章节的基石。你想想看,如果连PC指向哪里都搞不清楚,ROP链怎么可能写得对?