ARM体系现在已经有9个版本了,不同体系指令集架构(ISA)和ABI有区别,但一般是向前兼容的,现在网上和书上的资料大多是指第6版ARMv6,本文暂时也只记录ARMv6,之后再慢慢升级。
在此要说一下当前主流应该是ARMv8,在位数上它扩充到了64位,新的指令集叫AArch64,而兼容ARMv7的指令集叫AArch32,再旧的是A32,也是此处介绍ARM32的内容。
工作状态
Arm状态和Thumb状态,前者使用4字节定长指令,后者使用2字节定长指令,重置或异常时恢复为ARM状态,可通过BX/BLX指令转换状态,当目标地址的低位为0时表示转换到thumb状态,否则为arm状态,返回时根据LR恢复状态,在插桩时特别要小心这一点,可通过指令长度确定目标片段的代码类型。
工作模式
这里的工作模式是指当前运行的级别,在ARMv6下有如下七种:
Mode | ModeCode | 说明 |
---|---|---|
User | 10000 | 用户模式,用户进程执行时处于该模式 |
System | 11111 | 普通特权模式,如通常的系统内核执行时会进入该模式 |
Supervisor | 10011 | 重置时的模式,软中断与系统调用(swi指令)时也进入该模式 |
Undefined | 11011 | 获取到不能识别的指令时进入该模式,此时用户可通过软件仿真来模拟自定义的指令(类似中断服务例程) |
IRQ | 10010 | 普通中断模式,一般硬件中断时会进入 |
FIQ | 10001 | 快速中断模式,处理紧急中断/高速数据处理时 |
Abort | 10111 | 访问异常触发保护机制时(如segment fault)会进入该模式 |
(在后来的版本中又增加了3种之后再补充。)
寄存器
一般只提Arm有37个可见的寄存器,但在某一时刻只有部分寄存器能使用,如下所示不同的模式可见的寄存器差不多,但是对应的物理寄存器不一定一致,例如User和Supervisor模式下的R0-R12指向同样的物理寄存器,而R13和R14在两种模式下实际对应的物理寄存器并不一致,所以一般我们在代码中只能看到10多个寄存器,而实际上有30多个:
User | System | Supervisor | Abort | Undefined | IRQ | FIQ | 说明 |
---|---|---|---|---|---|---|---|
R0 | R0 | R0 | R0 | R0 | R0 | R0 | 这一组寄存器在所有模式下指向相同的物理寄存器,其中R0-R3通常用于传参,R0-R1通常用于传输返回值 |
R1 | R1 | R1 | R1 | R1 | R1 | R1 | |
R2 | R2 | R2 | R2 | R2 | R2 | R2 | |
R3 | R3 | R3 | R3 | R3 | R3 | R3 | |
R4 | R4 | R4 | R4 | R4 | R4 | R4 | |
R5 | R5 | R5 | R5 | R5 | R5 | R5 | |
R6 | R6 | R6 | R6 | R6 | R6 | R6 | |
R7 | R7 | R7 | R7 | R7 | R7 | R7 | |
R8 | R8 | R8 | R8 | R8 | R8 | R8_fiq | 这三个寄存器和R11,R12在thumb下不可见 |
R9 | R9 | R9 | R9 | R9 | R9 | R9_fiq | |
R10 | R10 | R10 | R10 | R10 | R10 | R10_fiq | |
R11 | R11 | R11 | R11 | R11 | R11 | R11_fiq | 又叫FP(Frame Pointer)寄存器,通常用于指向栈桢 |
R12 | R12 | R12 | R12 | R12 | R12 | R12_fiq | 又名IP(Intra Procedure call scratch)寄存器,它不会被保存也默认数据不可用,因此可在过程中直接写读 |
R13 | R13 | R13_svc | R13_abt | R13_und | R13_irq | R13_fiq | 又名SP(Stack Pointer)寄存器,保存了栈顶指针 |
R14 | R14 | R14_svc | R14_abt | R14_und | R14_irq | R14_fiq | 又名LR(Linked Register),保存了函数返回地址 |
R15 | R15 | R15 | R15 | R15 | R15 | R15 | 又名PC(Program Counter)寄存器,在计组中它可以保存当前指令位置也可以保存下一指令位置,在3级流水线的ARM系列中(据传未验证)保存了下一条要读取的指令的地址,它的值是当前正在执行的指令的地址+8 |
CPSR | CPSR | CPSR | CPSR | CPSR | CPSR | CPSR | Current Program State Register保存当前程序的状态 |
SPSR_svc | SPSR_abt | SPSR_und | SPSR_irq | SPSR_fiq | Stored Program State Register用于备份CPSR,如出现异常时就会保存并在异常处理完后恢复 |
在这里面CPSR也是按位表示一些状态,它的结构如下(新版会使用更多位):
Bit | 31 | 30 | 29 | 28 | --- | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
N | Z | C | V | --- | I | F | T | M4 | M3 | M2 | M1 | M0 | |
Description | Negative | Zero | Carry | oVerflow | IRQ | FIQ | Thumb | Work Mode |
一般在指令集手册里它们叫ArmRegister,除此之外还有很多不可见的寄存器(我猜的)和协处理器,协处理器用于处理特殊任务,如多媒体任务,算数任务,加解密任务等,它只会执行和它功能相关的指令而忽略其他指令。
指令
本段记录的是ARM指令,Thumb指令以后有需要再补充。ARM指令有如下特点:
- 几乎所有指令都有4位共16种条件标志位,当使用时表示满足条件才执行指令,在汇编上以两个字符的助记符表示
- 很多指令助记符通过S表示该操作会影响程序状态字
注:下面的翻译并不一定正确,我只是为了好记!
编码 | 条件助记符 | 标志位 | 含义 |
---|---|---|---|
0000 | EQ | Z=1 | 相等 |
0001 | NE | Z=0 | 不相等 |
0010 | CS | C=1 | 无符号大于或等于 |
0011 | CC | C=0 | 无符号小于 |
0100 | MI | N=1 | 负值 |
0101 | PL | N=0 | 正值或 0 |
0110 | VS | V=1 | 溢出 |
0111 | VC | V=0 | 无溢出 |
1000 | HI | C=1 且 Z=0 | 无符号大于 |
1001 | LS | C=0 且 Z=1 | 无符号小于或等于 |
1010 | GE | N 和 V 相同 | 有符号大于或等于 |
1011 | LT | N 和 V 不相同 | 有符号小于 |
1100 | GT | Z=0 且 N 等于 V | 有符号大于 |
1101 | LE | Z=1 且 N 不等于 V | 有符号小于或等于 |
1110 | AL | 忽略 | 无条件执行(默认可省略) |
1111 | NV | 忽略 | 从不执行(系统保留) |
类型 | 指令 | 助记 | 说明 | 例子 |
---|---|---|---|---|
跳转指令 | B/BL/BX/BLX | Branch{Link}{eXchange} | 跳转到其他位置,带L将把返回地址放到LR,带X会根据目标切换状态 | BLEQ main;Z标志置位时调用main函数 |
数据处理指令 | MOV/MVN | 数据传输指令与取反传输 | ||
CMP/CVN/TST/TEQ | {算数比较|反值比较|逻辑位测|逻辑位相等} | |||
ADD/ADC/SUB/SBC/RSB/RSC | 加减运算,C表示是否带进位,R表示逆向 | |||
MUL/MULA/{S|U}MULL/{S|U}MUAL | 乘法与乘加指令,所谓乘加就是先乘再加,开头加S或U表示是否有符号,结束加L表示64位 | |||
{L|A}S{L|R}/ROR/RRX | {逻辑|算数}{左|右}移,循环右移,带扩展的循环右移 | |||
AND/ORR/EOR/BIC | 逻辑{与|或|异或|位清除} | |||
PSR处理指令 | MSR/MRS | Move State from Register|... | 可通过该指令读写状态寄存器 | MSR CPSR, R0; 将R0的值赋给CPSR |
加载/存储指令 | {LD|ST}R{|B|H} | {LoaD|STore} Register{Doword|Byte|sHort} | 从加载一个{双字|字节|字}到寄存器或相反 | LDR R0, [SP]; R0=[SP] |
{LD|ST}M | {LoaD|STore}Multi | 从寄存器组存储数据到内存或反向操作 | ||
协处理器指令 | CDP | Coprocessor Data Operation | 让指定编码的协处理器执行指定的指令 | CDP P3, 2, C12, C10, C3, 4; 让协处理器3执行2号指令,后面是它的源目的寄存器等参数 |
LDC/STC | {LoaD|STore} Coprocessor | 让协处理器指定寄存器从指定内存加载/存储数据 | LDC P15, C4, [R0]; 从R0所指位置加载一个dword的数据到15号协处理器的C4寄存器 | |
MCR/MRC | Move Coprocessor from Register|... | 协处理器与ARM寄存器间传送数据 | MCR P3, 3, R0, C4, C5, 6; 将R0的内容传送到P3的C4 C5中 | |
异常产生指令 | SWI | SoftWare Interrupt | 产生软中断,用于系统调用 | SWI 2; 调用编号为2的系统例程 |
BKPT | BreaKPoinT | 断点 | BKPT 0x2233; |
寻址方式
1.立即数寻址:
ADD R0, R0, #0x10 ; R0=R0+0x10,立即数以#开头
2.寄存器寻址:
ADD R0, R1, R2 ; R0=R1+R2
3.寄存器间接寻址:
ADD R0, R1, [R2] ; R0=R1+[R2]
4.基址变址寻址:
ADD R0, [R1, #4] ; R0=[R1+4]
ADD R0, [R1, #4]! ; R0=[R1+4],R1+=4
ADD R0, [R1], #4 ; R0=[R1], R1+=4
ADD R0, [R1, R2] ; R0=[R1+R2]
5.多寄存器寻址:
LDMIA R0, {R1, R2, R3, R4} ; R1=[R0],R2=[R0+4]...
这里的后缀有如下四种,用于表示每执行一个寄存器复制操作后原地址如何变化:
mode | Full Name | 说明 |
---|---|---|
IA | Increase After | 先传送再加 |
DA | Decrease After | 先传送再减 |
IB | Increase Before | 先加再传送 |
DB | Decrease Before | 先减再传送 |
6.相对寻址:
BL main ; call main
7.堆栈寻址:
LDMFD SP!, {R1, R2, R3}; R1=[SP], SP-=4, R2=[SP], SP-=4, R3=[SP], SP-=4
这里的后缀有如下四种,用于表示当前栈指针所指位置是否存放了元素(如若指向的是最后压入的元素则为满栈)以及栈的增长方向(如递减表示由高地址到低地址增长):
mode | Full Name | 说明 |
---|---|---|
FA | Full Ascending | 满递增栈 |
FD | Full Descending | 满递减栈 |
EA | Empty Ascending | 空递增栈 |
ED | Empty Descending | 空递减栈 |
伪指令
伪指令是针对汇编器的,又汇编器转换成某特定结构,因此它大多和架构无关(但也有.thumb
这种是特定于架构的),比如在GCC上那些伪指令在ARM上也能用,因此这里只记录一些在在IDA里常看到的:
类型 | 伪指令 | 助记 | 说明 | 例子 |
---|---|---|---|---|
符号定义伪指令 | GBL<A|L|S> | GloBaL<Arithmetic|Logical|String> var | 定义全局<数字|逻辑|字符串>变量,默认值分别为<0|False|""> | GBLA x;定义一个全局数字变量x它的值为0 |
LCL<A|L|S} | LoCaL<Arithmetic|Logical|String> var | 定义本地<数字|逻辑|字符串}变量,默认值分别为<0|False|""> | LCLA x;定义一个局部数字变量x它的值为0 | |
SET<A|L|S} | SET<Arithmetic|Logical|String> var | 为<数字|逻辑|字符串>变量赋值 | SETA x 233;x=233 | |
RLIST | Register LIST | 将某个寄存器列表定义为一个变量 | r0_5 RLIST {R0-R5};另R0-R5这个寄存器列表为r0_5 | |
数据定义伪指令 | DC<B|W|D|FD|FS|Q>{U} | DataContent<Byte|Word|Dowrd |FloatSingle|FloatDouble|Qword>{Unpad} | 定义各种长度的数据,U表示不对齐 | Str DCB "B3taMa0"; |
SPACE | 定义连续的一片空间,初始化为0 | somespace Sapce 100;定义somespace为100字节长度的空间并初始为0 | ||
MAP | 申明一个数据结构 | MAP 0x10, R0; 在R0+0x10处申明一个结构,之后用Field指令定义结构域 | ||
FIELD | 定义结构里的域 | vtbl FIELD 0x04; 定义一个域名为vtbl长度为4字节 |
调用约定
在ARM中还有个概念易于和ISA混淆,且和ISA密切相关,那就是ABI,ARM支持多种ABI,移动和嵌入式上常见的是EABI,不过这不重要,我们需要关注的是ABI里的调用约定,这里面常用的调用约定是AAPCS(Procedure Call Standard for the Arm Architecture)和AAPCS64,它们分别对应32位和64位,这里介绍前者,在官方文档的调用约定里它的字表示32字节,太奇怪了,它应该叫双字!!!它的寄存器使用如下:
Register | Synonym | Special | 说明 |
---|---|---|---|
r15 | PC | The Program Counter. | |
r14 | LR | The Link Register. | |
r13 | SP | The Stack Pointer. | |
r12 | IP | The Intra-Procedure-call scratch register. | |
r11 | v8 | FP | Frame Pointer or Variable-register 8. |
r10 | v7 | 同v1-v5 | |
r9 | v6 SB TR |
特定于平台,可做不同用途,如在PIC中当SB(Static Base),在TLS中当TR(Thread Register),也可以把它当作v6用。 | |
r4-r8 | v1-v5 | 作为本地变量,需要函数执行前保存结束前恢复。 | |
r0-r3 | a1-a4 | 依次传递参数,若参数大于32位可占用多个连续的寄存器,若4个寄存器不够用则通过栈传输,方向为从右到左入栈;也可保存返回值,返回值为32位保存在r0里,与参数类似若返回值大于32位依次存放 |
而它的栈使用满递减栈,使用时必须4字节对齐,关于字节序arm似乎可以采取大端和小端但默认是小端,根据官方文档可通过一个配置引脚修改,这是ABI的内容,现在一般遇到的其实都是小端序。
参考
- Whirlwind Tour of ARM Assembly
- arm-directives-reference
- ARM and Thumb Instructions
- 《Android 系统安全和反编译实战》-- 刘云[著];朱桂英[著]
- 《ARM处理器开发详解:基于ARM Cortex-A9处理器的开发设计》-- 秦山虎[著];刘洪涛[著]