最近笔者刚刚加入了一个项目组,需要用到ARM架构的东西,和ARM pwn也有一定关系,因此一不做二不休,决定开始学习ARM pwn,顺便熟悉项目前置知识,一举两得。
ARM与x86分属不同架构,指令集不同,需要从头开始学习,本文从寄存器、指令方面对x86-64和ARM架构下的汇编语言做比较与学习。(配图选自清华大学出版社《ARM Cortex-M3与Cortex-M4权威指南》,侵删)
1. 寄存器
寄存器是汇编语言的核心,在x86-64系统中,最为常见的寄存器有以下这些:
64位:
rax, rbx, rcx, rdx
rsi, rdi, rsp, rbp, rip
r8, r9, r10, r11, r12, r13, r14, r15
32位:
eax, ebx, ecx, edx
esi, edi, esp, ebp, eip
r8d, r9d, r10d, r11d, r12d, r13d, r14d, r15d
在大多数程序中,这17个寄存器是最为常用的寄存器,其中
rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip
有专门的作用,但其中的
rax, rbx, rcx, rdx, rsi, rdi
功能相对更加灵活,不像
rsp
只能用于表示栈顶地址,
rbp
只能用于表示栈帧地址,
rip
只能用于表示当前指令地址等。另外的8个寄存器则是通用寄存器,想用来干嘛就干嘛。
那么在ARM架构中,寄存器则是以下这些:
64位:
X0, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12
X13, X14, X15
32位:
R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12
R13, R14, R15
其中
R0~R12
为通用寄存器,共13个,剩下的3个有特殊用途:
-
为栈指针,又称R13
,相当于SP
,在物理上实际上有两个栈指针:主栈指针和进程栈指针,一般的进程只有一个栈指针可见。这个也好理解,就好比在x86-64系统中,内核的栈指针和用户进程的栈指针不同一样。rsp
-
为链接寄存器,又称R14
,用于保存函数调用时的返回值。在x86-64系统中,函数调用的返回值是保存在子函数栈帧的上面,即LR
的位置,在ARM系统中,函数调用同样需要将返回地址保存到栈中,因为rbp+8
在函数返回时会进行自动更新,如果栈中没有返回地址,那么LR
就不知道要更新成什么值了。当然LR
的作用不止这些,在后面遇到具体问题时再进行分析。LR
-
为程序计数器,又称R15
,可读可写。读操作返回当前指令地址+4(由ARM指令集特性决定,ARM指令集中任何一条指令都是偶数长度,与x86-64不同),写操作会导致执行流跳转。PC
的最低有效位(LSB)是一个控制结构,为1时表示进入Thumb状态。当有些时候程序跳转更新PC时需要将新PC值的LSB置1,否则会触发错误异常。这也可以看做是一种程序恶意跳转的保护机制。有时还会将PC
作为基址访问数据。PC
除了这些寄存器之外,两个架构下都各自有各自的特殊寄存器,如x86-64架构下的
rflags
控制寄存器用于保存程序执行的状态。在ARM中同样具有类似功能的控制寄存器:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL4EDN2czY5QGMyMmN3kDO4IWNzQTMyAjYhN2YkJmZwI2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
-
:应用状态寄存器APSR
-
:执行状态寄存器EPSR
-
IPSR
:中断状态寄存器
上面的三个寄存器可以通过一个组合寄存器
访问,在不同的ARM架构中状态寄存器的排布有一定不同:PSR
ARM pwn 入门 (1)2. 指令集
3个中断-异常屏蔽寄存器的功能较少用到,这里先不进行讨论。
CONTROL
寄存器确定了栈指针的选择和线程模式的访问等级,其只能够在特权等级下才能进行修改。
其中具体的细节阐述较为繁琐,不是本文的重点,略过。
另外,在x86-64架构和ARM架构中都有很多的浮点数寄存器,用于进行浮点数计算。在ARM架构中,浮点数寄存器有32个32位寄存器
S0~S31
,其中可以两两组合访问为
D0~D15
,如
S0
和
S1
组合为
D0
。
2. 指令集
ARM的指令集和x86-64有一些相似之处,但也有一些不同,需要注意的是,ARM的立即数前面需要加上#标识,如#0x12345678。下面的指令均为32位系统下的指令。
A. 寄存器传送数据
与x86相同,ARM使用
MOV
系列指令进行寄存器与寄存器(立即数)之间的数据传送:
-
:赋值MOV/MOVS reg1, <reg2/imm8>
为reg1
reg2/imm8
-
:赋值MOVW <reg32>, <imm16>
的低16位为reg32
imm16
-
:赋值MOVT <reg32>, <imm16>
的高16位为reg32
imm16
-
:将MVN reg1, <reg2>
的值取反之后赋值给reg2
reg1
-
①:赋值LDR <reg32>, =<imm32>
为reg32
imm32
备注:
① 这里的指令并不是一条真正的指令,而是一条伪指令。ARM汇编器会将字符数据汇总组成一个称为 “文字池” 的数据块,与x86-64不同,后者如果需要实现将立即数赋值到寄存器,会直接将立即数写死到指令中。这里的
LDR
指令实际是做了寻址操作,将文字池地址中的数据赋值到寄存器中。如果需要将32位立即数赋值到32位寄存器,可以使用这条指令,也可以将
MOVW
和
MOVT
指令配合使用分别赋值前16位和后16位。
B. 存储器传送数据
不同于x86使用mov指令可实现寄存器、立即数和内存空间的数据交换,ARM使用单独的指令集进行寄存器和内存空间的数据交换,其中基址可以选择任意一个通用寄存器或PC寄存器,变址也可以使用任意一个通用寄存器,较x86更加灵活:
-
:赋值8/16/32位LDRB/LDRH/LDR reg1, [<reg2/PC>, <imm32>]<!>
地址的数据到reg2+imm32
,如果指令后面有叹号,表示指令执行后reg1
值更新为reg2
,有叹号可等同于reg2+imm32
,这种形式称为后序指令。LDRB/LDRH/LDR reg1, [<reg2>], <imm32>
-
:赋值64位LDRD reg1, <reg2>, [<reg3/PC>, <imm32>]<!>
地址的数据到reg3+imm32
和reg1
,有叹号可等同于reg2
LDRD reg1, <reg2>, [reg3], <imm32>
-
:有符号传送8/16位LDRSB/LDRSH reg1, [<reg2/PC>, <imm32>]<!>
地址的数据到reg2+imm32
,目标寄存器会进行32位有符号扩展,有叹号可等同于reg1
LDRSB/LDRSH reg1, [<reg2>], <imm32>
-
:保存寄存器STRB/STRH/STR reg1, [<reg2>, <imm32>]<!>
的8/16/32位值到reg1
地址,有叹号可等同于reg2+imm32
STRB/STRH/STR reg1, [<reg2>], <imm32>
-
:保存寄存器STRD reg1, <reg2>, [reg3, <imm32>]<!>
和reg1
的64位值值到reg2
地址,有叹号可等同于reg3+imm32
STRD reg1, <reg2>, [reg3], <imm32>
-
:赋值寄存器LDRB/LDRH/LDR reg1, [<reg2/PC>, reg3{, LSL <imm>}]
的值为reg1
地址处的8/16/32位值reg2/PC+(reg3{<<imm})
-
:赋值寄存器LDRD reg1, <reg2>, [<reg3/PC>, <reg4-32>{, LSL <imm>}]
和reg1
的值为reg2
地址处的64位值reg3/PC+(reg4-32{<<imm})
-
:保存寄存器STRB/STRH/STR reg1, [<reg2>, reg3{, LSL <imm>}]
的8/16/32位值到reg1
地址reg2+(reg3{<<imm})
-
:将LDMIA/LDMDB reg1<!>, <reg-list>
地址的值按照顺序保存到reg1
中的寄存器中,如果reg-list
后有叹号,则在保存值后自动增加(reg1
)或减少(LDMIA
)LDMDB
。如reg1
,LDMIA R0, {R1-R5}
LDMIA R0, {R1, R3, R6-R9}
-
:向STMIA/STMDB reg1<!>, <reg-list>
地址存入寄存器组中的多个字。如果reg1
后有叹号,则在保存值后自动增加(reg1
)或减少(STMIA
)STMDB
。reg1
注意:后序指令不能使用PC寻址。
C. 入栈出栈
虽然ARM与x86都使用push和pop指令进行入栈和出栈,但ARM可以实现一条指令多次出入栈。
-
:将寄存器组中的寄存器值依次入栈,PUSH <reg-list>
中可以有PC、LR寄存器。reg-list
-
:将出栈的值依次存入寄存器组中的寄存器,POP <reg-list>
中可以有PC、LR寄存器。reg-list
D. 算术运算
不同于x86指令的大多数算术运算使用两个寄存器,ARM指令的算数运算指令通常包含3个寄存器,实现运算后的自由赋值而不是x86中必须赋值给目标寄存器且目标寄存器必须参与运算。
-
:计算ADD/SUB reg1, <reg2>, <reg3/imm32>
将结果保存到<reg2>(+/-)<reg3/imm32>
reg3
-
:计算ADC/SBC reg1, <reg2>, reg3
将结果保存到<reg2>(+/-)reg3+(进位/借位)
reg3
-
:计算ADC <reg32>, <imm32>
将结果保存到reg32+imm32+进位
reg32
-
:计算SBC reg1, <reg2>, <imm32>
将结果保存到<reg2>-imm32-借位
reg1
-
:计算RSB reg1, <reg2>, <reg3/imm32>
将结果保存到<reg3/imm>-<reg2>
reg1
-
:计算MUL reg1, <reg2>, reg3
将结果保存到<reg2>*reg3
reg1
-
:计算UDIV/SDIV reg1, <reg2>, reg3
(无符号/有符号)将结果保存到<reg2>/reg3
,如果除以0,则结果为0reg1
-
:计算MLA reg1, <reg2>, reg3, <reg4-32>
reg1=<reg2>*reg3+<reg4-32>
-
:计算MLS reg1, <reg2>, reg3, <reg4-32>
reg1=-<reg2>*reg3+<reg4-32>
E. 逻辑运算
ARM支持x86格式的逻辑运算以及3运算符的逻辑运算。
-
:如果AND/ORR/BIC/EOR reg1, <reg2>{, <reg3/imm32>}
存在,则表示reg3/imm
,否则表示reg1=<reg2>(&/|/&~/^)<reg3/imm32>
(与/或/与非/异或)reg1=reg1(&/|/&~/^)<reg2>
-
:表示ORN reg1, <reg2>, <reg3/imm32>
(或非)reg1=<reg2>|~<reg3/imm32>
F. 移位运算
-
:如果ASR/LSL/LSR reg1, <reg2>{, <reg3/imm32>}
存在,则表示reg3/imm
,否则表示reg1=<reg2>(>>/<<)<reg3/imm32>
(算数右移、逻辑左移、逻辑右移)reg1=reg1(>>/<<)<reg2>
-
:如果ROR reg1, <reg2>{, reg3}
存在,则表示reg3
,否则表示reg1=<reg2>(>>)reg3
(循环右移)reg1=reg1(>>)<reg2>
G. 符号扩展
对应于x86中的movsx和movzx指令。
-
:右移SXTB/SXTH reg1, <reg2>{, ROR <imm>}
位后有符号扩展<imm>
的低8/16位并赋值给<reg2>
reg1
-
:右移UXTB/UXTH reg1, <reg2>{, ROR <imm>}
位后无符号扩展<imm>
的低8/16位并赋值给<reg2>
reg1
H. 数据反转
将寄存器中的值按字节进行反转。
-
:将REV reg1, reg2
中的4字节数据按字节反转后赋值给reg2
(reg1
值不变),原先第0,1,2,3字节的内容被换到了第3,2,1,0字节。reg2
-
:将REV16 reg1, reg2
中的4字节以字单位分为高字和低字分别进行反转后赋值给reg2
(reg1
值不变),原先第0,1,2,3字节的内容被换到了第1,0,3,2字节。reg2
-
:将REVSH reg1, reg2
中的低2字节反转后有符号扩展赋值给reg2
reg1
-
:REVH reg1, reg2
指令的16位表示,只反转低2字节。REV
I. 位域操作
位域操作允许机器指令对寄存器中的特定位进行处理,在x86中好像是也有这样的指令,只是使用频率太低。
-
:将BFD reg1, #lsb, #width
中从第reg1
位开始的连续lsb
位清零。width
-
:将BFI reg1, reg2, #lsb, #width
中最低reg2
位复制到width
中从reg1
位开始的连续lsb
位。width
-
:计算CLZ reg1, reg2
中高位0的个数并赋值给reg2
,多用于浮点数计算。reg1
-
:反转RBIT reg1, reg2
寄存器中的所有位并赋值给reg2
。reg1
-
:取SBFX/UBFX reg1, reg2, #lsb, #width
中从第reg2
位开始的连续lsb
位并有/无符号扩展,赋值给width
。reg1
J. 比较和测试指令
与x86使用
cmp
指令和
test
指令相似,ARM也有关于比较和测试的指令,且实现原理基本相同。
-
:比较两个寄存器或寄存器与立即数,更新标志位APSR。CMP reg1, reg2/imm
-
:比较CMN reg1, reg2/imm
和reg1
或-reg2
,更新标志位APSR。-imm
-
:参照x86的TST reg1, reg2/imm
指令,相与测试,更新N(负数位)和Z(零)标志test
-
:异或测试,更新N和Z标志TEQ reg1, reg2/imm
K. 跳转指令
-
:无条件跳转到指定位置,B/B.W <label>
跳转范围更大。B.W
-
:寄存器跳转。BX reg
-
:跳转到指定位置/寄存器值,且将返回地址保存到BL <label> / BLX reg
寄存器中,类比x86的LR
指令。一般在函数开头都会首先将call
寄存器的值保存到栈中便于返回时获取。BL
- 条件跳转指令族:类比x86指令:
-
BEQ == je
-
BNE == jne
-
(进位标志为1,可表示无符号大于等于)BCS/BHS == jc
-
(进位标志为0,可表示无符号小于)BCC/BLO == jnc
-
(负数标志为1)BMI == js
-
(负数标志为0)BPL == jns
-
(溢出标志为1)BVS == jo
-
(溢出标志为0)BVC == jno
-
(无符号大于)BHI == ja
-
(无符号小于等于)BLS == jbe
-
(有符号大于等于)BGE == jge
-
(有符号小于等于)BLE == jle
-
(有符号大于)BGT == jg
-
(有符号小于)BLT == jl
-
-
:比较寄存器的值为0/不为0时跳转(只支持前向跳转)CBZ/CBNZ reg, <label>