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