之前已经写过一篇关于 ARM 的博客了:ARM pwn 环境搭建+基础入门 | Pwn进你的心 (ywhkkx.github.io)
主要简述了以下各方面的知识:(主要是32位)
ARM 的七种工作模式
- 用户模式(USR):正常程序执行模式,不能直接切换到其他模式
- 系统模式(SYS):运行操作系统的特权任务,与用户模式类似,但具有可以直接切换到其他模式等特权
- 快中断模式(FIQ):支持高速数据传输及通道处理,FIQ异常响应时进入此模式
- 中断模式(IRQ):用于通用中断处理,IRQ异常响应时进入此模式
- 管理模式(SVC):操作系统保护模式,系统复位和软件中断响应时进入此模式(由系统调用执行软中断SWI命令触发)
- 中止模式(ABT):用于支持虚拟内存和/或存储器保护,在ARM7TDMI没有大用处
- 未定义模式(UND):支持硬件协处理器的软件仿真,未定义指令异常响应时进入此模式
在所有的寄存器中,有些是各模式共用同一个物理寄存器,有些寄存器是各个模式自己拥有独立的物理寄存器
CPSR 和 SPSR 都是程序状态寄存器,其中 SPSR 是用来保存中断前的 CPSR 中的值,以便在中断返回之后恢复处理器程序状态
ARM 的 NEON 寄存器
常规寄存器
32位 | 64位 | 别名 | 目的 |
---|---|---|---|
R0-R6 | X0-X7 | – | 一般用途 |
X8 | – | 保存子程序返回值 | |
R7 | – | 持有系统调用号 | |
X9-X15 | 临时寄存器 | 子程序使用时不需要保存 | |
R8-R10 | X19-X28 | 临时寄存器 | 子程序使用时必须保存 |
X18 | – | 记录平台信息 | |
R11 | X29 | FP | 帧指针 |
R12 | X16-X17 | IP | 程序内呼叫 |
R13 | X31 | SP | 栈指针 |
R14 | X30 | LR | 链接注册 |
R15 | PC | 程序计数器 | |
CPSR | CPSR | – | 当前程序状态寄存器 |
SPSR | SPSR | – | 程序状态保存寄存器 |
NEON 寄存器
ARM NEON 技术本质上是一种高级的单指令多数据(SIMD)架构扩展,这种扩展仅在一些 ARMv7-A 和 ARMv7-R 架构以及 ARMv8 架构上支持:
- 从 ARMv5 架构开始引入 VFP(vector-floating-point) 指令扩展,可以通过使用短向量指令来加速浮点计算
- 从 ARMv7 架构开始引入 NEON 技术,NEON 技术同样是依靠向量指令来加速计算,VFP 向量指令加速的模式被弃用,因此 VFP 单元有时也称之为 FPU(Floating Point Unit)单元
NEON 寄存器主要是用来存放包含相同数据类型元素的向量:
- 在 ARMv7 架构中(32位), 一共有16个128位寄存器,一个128位寄存器又可以分为两个64位寄存器,以此类推
- 在 ARMv8 架构中(64位),所有寄存器的总数相比 ARMv7 架构翻倍
- Q 寄存器:128位寄存器
- D 寄存器:64位寄存器
- S 寄存器:32位寄存器
- H 寄存器:16位寄存器
- B 寄存器:8位寄存器
32位下 NEON 寄存器:
- 32个S寄存器,S0~S31(单字,32bit)
- 32个D寄存器,D0~D31(双字,64bit)
- 16个Q寄存器,Q0~Q15(四字,128bit)
64位下 NEON 寄存器:
- 32个B寄存器,B0~B31(字节,8bit)
- 32个H寄存器,H0~H31(半字,16bit)
- 32个S寄存器,S0~S31(单字,32bit)
- 32个D寄存器,D0~D31(双字,64bit)
- 32个Q寄存器,V0~V31(四字,128bit)
ARM 的函数调用
测试案例一如下:(64位:循环+选择)
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
从 main 函数开始到 for 循环之间的汇编代码:
1 | ; __unwind { |
- ARM 的 MOV 只支持在寄存器之间转换数据,从内存到 CPU 之间的移动只能通过 LDR/STR 指令来完成
- STP 相当于两个 STR
- W0 就是 X0 的低32位(类似于 RAX 和 EAX 之间的关系)
- ARMv8 有两个0寄存器,分别是 WZR 和 XZR(零寄存器内容为 “0”)
- B 指令是相对跳转指令,根据当前PC寄存器的值加上偏移来实现跳转
从 for 循环到 if 语句的汇编代码:
1 | loc_794 |
- X0 通常作为计数器(地位相当于 x86 中的 RAX)
- B,B.LE 这两个跳转构成了循环
1 | loc_76C |
- B.NE 构成了选择
测试案例二如下:(64位:函数调用)
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
函数 A 调用之前:
1 | 0x5500000814 <main+64> ldr w7, [sp, #0x2c] |
- 前6个参数分别放入 X0-X5 中,后续的参数从右往左依次入栈
函数 A 结束之前:
1 | 0x55000007c8 <A+116> nop |
1 | pwndbg> telescope 0x5501811f50-0x40 |
ldp x29, x30, [sp]
会恢复调用者 main 函数的 X29(FP),X30(LD)- 后面的
#0x40
会使sp+0x40
函数 A 结束之后:
1 | X29 0x5501811f50 —▸ 0x5501811f80 —▸ 0x5501812090 ◂— 0x0 |
ARM 的系统调用
先看两个 libc 函数触发 ARM 系统调用的源码:
1 | int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....); |
32位 arm:
1 | ENTRY(__clone) |
- 系统调用会引起处理器模式切换 USER 切换到 SVC,而 SVC 下的 SP 和 USER 模式的 SP 是不同的
- 因此系统调用无法使用栈传入参数,需要将所有的参数通过寄存器 R0-R5 传递
64位 aarch64:
1 | ENTRY(__clone) |
- 参数传递依靠 X0-X6
svc 和 swi 都是 supervisor call 指令,都是系统调用:
- 在 armv7 之前,用的都是 swi,触发异步异常,进入
vector_swi
异常向量表- 当异步异常产生后,程序会抛出一个信号使异常处理程序进入 CPU 的等待队列,然后由 CPU 在合适的时机去运行异常处理程序
- 在 armv8-arch64 架构下,抛弃 swi 改用 svc,触发的是同步异常,进入同步异常向量表
el1_sync
- 当同步异常产生后,指令流会立刻进入异常处理流程,CPU 立刻执行异常处理程序
- 如果不纠结 CPU 的处理过程,就可以把 swi 和 svc 当成是一回事,只是用的向量表不同而已
当通过系统调用进入内核的时候,内核会进行一系列初始化操作,在内核栈上形成如下的用户空间现场:
pt_regs
:封装了需要在内核入口中保存的最少的状态信息,在系统崩溃时将会提供 debug 信息thread_info
:存储当前进程的基本信息,包括进程描述符task_struct
执行完具体的系统调用代码后,程序会依靠 pt_regs
的数据恢复用户态进程的上下文