最近笔者刚刚加入了一个项目组,需要用到ARM架构的东西,和ARM pwn也有一定关系,因此一不做二不休,决定开始学习ARM pwn,顺便熟悉项目前置知识,一举两得。
ARM与x86分属不同架构,指令集不同,需要从头开始学习,本文从寄存器、指令方面对x86-64和ARM架构下的汇编语言做比较与学习。(配图选自清华大学出版社《ARM Cortex-M3与Cortex-M4权威指南》,侵删)
寄存器是汇编语言的核心,在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
,相当于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
。
ARM的指令集和x86-64有一些相似之处,但也有一些不同,需要注意的是,ARM的立即数前面需要加上#标识,如#0x12345678。下面的指令均为32位系统下的指令。
与x86相同,ARM使用MOV
系列指令进行寄存器与寄存器(立即数)之间的数据传送:
MOV/MOVS reg1,
:赋值reg1
为reg2/imm8
MOVW ,
:赋值reg32
的低16位为imm16
MOVT ,
:赋值reg32
的高16位为imm16
MVN reg1,
:将reg2
的值取反之后赋值给reg1
LDR , =
①:赋值reg32
为imm32
备注:
① 这里的指令并不是一条真正的指令,而是一条伪指令。ARM汇编器会将字符数据汇总组成一个称为 “文字池” 的数据块,与x86-64不同,后者如果需要实现将立即数赋值到寄存器,会直接将立即数写死到指令中。这里的LDR
指令实际是做了寻址操作,将文字池地址中的数据赋值到寄存器中。如果需要将32位立即数赋值到32位寄存器,可以使用这条指令,也可以将MOVW
和MOVT
指令配合使用分别赋值前16位和后16位。
不同于x86使用mov指令可实现寄存器、立即数和内存空间的数据交换,ARM使用单独的指令集进行寄存器和内存空间的数据交换,其中基址可以选择任意一个通用寄存器或PC寄存器,变址也可以使用任意一个通用寄存器,较x86更加灵活:
LDRB/LDRH/LDR reg1, [, ]
:赋值8/16/32位reg2+imm32
地址的数据到reg1
,如果指令后面有叹号,表示指令执行后reg2
值更新为reg2+imm32
,有叹号可等同于 LDRB/LDRH/LDR reg1, [],
,这种形式称为后序指令。LDRD reg1, , [, ]
:赋值64位reg3+imm32
地址的数据到reg1
和reg2
,有叹号可等同于 LDRD reg1, , [reg3],
LDRSB/LDRSH reg1, [, ]
:有符号传送8/16位reg2+imm32
地址的数据到reg1
,目标寄存器会进行32位有符号扩展,有叹号可等同于 LDRSB/LDRSH reg1, [],
STRB/STRH/STR reg1, [, ]
:保存寄存器reg1
的8/16/32位值到reg2+imm32
地址,有叹号可等同于 STRB/STRH/STR reg1, [],
STRD reg1, , [reg3, ]
:保存寄存器reg1
和reg2
的64位值值到reg3+imm32
地址,有叹号可等同于 STRD reg1, , [reg3],
LDRB/LDRH/LDR reg1, [, reg3{, LSL }]
:赋值寄存器reg1
的值为reg2/PC+(reg3{<地址处的8/16/32位值
LDRD reg1, , [, {, LSL }]
:赋值寄存器reg1
和reg2
的值为reg3/PC+(reg4-32{<地址处的64位值
STRB/STRH/STR reg1, [, reg3{, LSL }]
:保存寄存器reg1
的8/16/32位值到reg2+(reg3{<地址
LDMIA/LDMDB reg1,
:将reg1
地址的值按照顺序保存到reg-list
中的寄存器中,如果reg1
后有叹号,则在保存值后自动增加(LDMIA
)或减少(LDMDB
)reg1
。如LDMIA R0, {R1-R5}
,LDMIA R0, {R1, R3, R6-R9}
STMIA/STMDB reg1,
:向reg1
地址存入寄存器组中的多个字。如果reg1
后有叹号,则在保存值后自动增加(STMIA
)或减少(STMDB
)reg1
。注意:后序指令不能使用PC寻址。
虽然ARM与x86都使用push和pop指令进行入栈和出栈,但ARM可以实现一条指令多次出入栈。
PUSH
:将寄存器组中的寄存器值依次入栈,reg-list
中可以有PC、LR寄存器。POP
:将出栈的值依次存入寄存器组中的寄存器,reg-list
中可以有PC、LR寄存器。不同于x86指令的大多数算术运算使用两个寄存器,ARM指令的算数运算指令通常包含3个寄存器,实现运算后的自由赋值而不是x86中必须赋值给目标寄存器且目标寄存器必须参与运算。
ADD/SUB reg1, ,
:计算(+/-)
将结果保存到reg3
ADC/SBC reg1, , reg3
:计算(+/-)reg3+(进位/借位)
将结果保存到reg3
ADC ,
:计算reg32+imm32+进位
将结果保存到reg32
SBC reg1, ,
:计算-imm32-借位
将结果保存到reg1
RSB reg1, ,
:计算-
将结果保存到reg1
MUL reg1, , reg3
:计算*reg3
将结果保存到reg1
UDIV/SDIV reg1, , reg3
:计算/reg3
(无符号/有符号)将结果保存到reg1
,如果除以0,则结果为0MLA reg1, , reg3,
:计算reg1=*reg3+
MLS reg1, , reg3,
:计算reg1=-*reg3+
ARM支持x86格式的逻辑运算以及3运算符的逻辑运算。
AND/ORR/BIC/EOR reg1, {, }
:如果reg3/imm
存在,则表示reg1=(&/|/&~/^)
,否则表示reg1=reg1(&/|/&~/^)
(与/或/与非/异或)ORN reg1, ,
:表示reg1=|~
(或非)ASR/LSL/LSR reg1, {, }
:如果reg3/imm
存在,则表示reg1=(>>/<<)
,否则表示reg1=reg1(>>/<<)
(算数右移、逻辑左移、逻辑右移)ROR reg1, {, reg3}
:如果reg3
存在,则表示reg1=(>>)reg3
,否则表示reg1=reg1(>>)
(循环右移)对应于x86中的movsx和movzx指令。
SXTB/SXTH reg1, {, ROR }
:右移
位后有符号扩展
的低8/16位并赋值给reg1
UXTB/UXTH reg1, {, ROR }
:右移
位后无符号扩展
的低8/16位并赋值给reg1
将寄存器中的值按字节进行反转。
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字节。位域操作允许机器指令对寄存器中的特定位进行处理,在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
。与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/B.W
:无条件跳转到指定位置,B.W
跳转范围更大。BX reg
:寄存器跳转。BL
:跳转到指定位置/寄存器值,且将返回地址保存到LR
寄存器中,类比x86的call
指令。一般在函数开头都会首先将BL
寄存器的值保存到栈中便于返回时获取。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,
:比较寄存器的值为0/不为0时跳转(只支持前向跳转)