前言
逆向安卓或者其他arm平台应用的时候,阅读arm汇编是必不可少的。这里对arm汇编知识点做一个总结和记录。
arm汇编基础
arm9内核寄存器组
arm9内核工作模式
当前程序状态寄存器CPSR
arm指令的格式
由于arm属于精简指令集,因此它的译码机制相对于x86这些可能简单一些。对于arm的程序来说,为了实现相同的功能,程序就需要用到相对更多的操作达到相同的功能。
与x86不同,和mips(精简指令集)相似,arm的指令是定长的。下图展示了arm指令的基本格式:
条件执行
首先arm的opcode
部分保存操作码,表示要进行的操作。cond
表示条件码,对应字节的第一个部分,这个部分处理器会访问CPSR
程序状态寄存器的值来判断是否要执行指令。常用的条件编码有这些:
S后缀表示操作的结果是否要影响状态寄存器的值(类似于在x86中进行cmp
、test
等指令会做的操作),后面三个不用多说。注意,{}
的部分不是一定要存在的。因此我们可以发现,精简指令集相对于x86这种来说,指令是非常灵活的。
立即寻址
可以看到每个部分的长度是固定的。因此每个部分的数目是有上限的。
特别看到操作数2
只有12个比特位,那么这就引出一些问题:当我们进行大的立即数操作的时候很可能是非法的(比如12位的立即数显然无法表示32位的地址空间)。因此arm引入了一些机制来实现大范围的立即数:
在操作数2
的12个比特位中,前4位表示循环右移值,后八位表示常数。arm会对8位常数
在32位空间中进行循环右移前四位的循环右移值的两倍
来达到32位的效果。例如:
1 | mov r0, #0x0000f200 |
这里的机器代码为:
1 | E3A00CF2 |
可以猜到,E
为二进制数1110
,对应的条件码为AL
,也就是默认的一定执行。3A
表示操作码,也就是mov
操作。0
表示目的寄存器r0
,紧接着的0
表示缺省的操作数1寄存器
,最后的CF2
才是操作数2
。那么CF2
是如何表示0x0000F200
的呢?我们看下面的图:
我们很清楚的看到,在32位的空间下进行循环右移,想要让F2
表示F200
就是要让立即数左移8
位,对应的循环右移就是32 - 8
位。对应的4位循环右移值就是24 // 2
:1100
。
我们很自然就会意识到,正如图中所提到的,这样的操作只能将8位常数扩展到32位的空间,但是无法表示所有的32位常数。因此arm提供了ldr
伪指令来帮我们加载我们想要的任何32位常数到某一个通用寄存器当中:
1 | ldr r1, =0x12345678 |
通过这条伪指令,编译器会帮我们生成一段指令来计算出我们想要的常数并加载到某一个寄存器中。
可以选择的移位方式
符号 | 含义 |
---|---|
lsl | 逻辑左移 |
lsr | 逻辑右移 |
asl | 算数左移,和lsl一样 |
asr | 算数右移(右移补0/1位) |
ror | 循环右移 |
rrx | 带扩展的循环右移,循环右移1位后左端用c填充,这种方式只移一位,所以无须指定移位位数 |
寄存器寻址
寄存器寻址说白了就是操作数放在寄存器中。我们经常看到这样的指令:
1 | add r0, r1, r2 |
这里将r1 + r2
存入r0
中。除此之外,在前面提到的机器码格式中,操作数2
拥有最长的12位宽度,因此操作数2
可以有很多的玩法。例如在arm中第二个操作数为寄存器时(例如这里的r2),我们可以对第二操作数寄存器进行一些操作来简化程序:
1 | add r3, r2, r1, lsr #2 |
这里的操作就是将r2 + r1 // 4
存入r3
中。移位的数可以是5位立即数或某个寄存器内的值。需要强调的是,执行完后第二操作数寄存器(例如这里的r1
)中的数值不会改变。
寄存器间接寻址
这里与x86间接寻址类似。
数据传送类的load/store类指令都是用寄存器间接寻址方式。
例如:
1 | ldr r0, [r1] ; r0 <- mem32[r1] |
寄存器偏移地址间接寻址
例:
1 | ; 前变址 |
寄存器偏移地址
1 | ldr r0, [r1, r2] ; r0 <- mem32[r1 + r2] |
多寄存器及块拷贝寻址
一条指令完成多字数据或数据块的传送
基本指令:ldm
、stm
基址寄存器变化方式:
IA:操作完地址增
IB:地址增再操作
DA:操作完递减
DB:递减再操作
以ldm
为例:
1 | ldmia r0, {r1-r4, r6} |
这里的操作就是:将r0
地址开始的内存,先ldr r1, [r0]
,然后ldr r2, [r0 + 4]
,依次最终ldr r6, [r0 + 16]
。
r0
作为基址寄存器,其值自动更新(但是执行完之后其值不会改变!除非在r0后面加上!
)
使用这两个指令,可以实现内存的拷贝。先加载某个地址开始的几个字,然后再将几个寄存器里的值存储到另一块内存区域。
堆栈寻址
数据栈与寄存器组之间批量数据传输,采用r13(sp)
寄存器作为堆栈指针,sp
指向栈顶。
不同计算机系统的堆栈组织生长方式:
FD/ED:满递减/空递减
FA/EA:满递增/空递增
满
的含义:栈顶指针指向最后压入的数据。空
则意味着栈顶指针指向空内存地址
递增递减就好理解了。我们熟知的x86就是满递减
的堆栈组织方式。然而arm对于这四种方式都支持,但是用的比较多的还是满递减方式。
如下例子:
这里lr
寄存器也叫r14
寄存器。
这里堆栈为满递减方式,因此stmfd
会将寄存器组从右往左(先入后出)压入栈顶
而ldmfd
会将栈顶依次弹出到寄存器组(后入先出)。
实际上压栈出栈不一定要用堆栈寻址,也可以用多寄存器块拷贝:
1 | ; 压栈 |
ARM指令集
存储器访问(L/S)指令
1 | ldr r1, [r5] ; 寄存器间接加载 |
数据处理类指令
数据传送指令
1 | mov r1, r0 |
与x86不同,arm寄存器之间使用mov,寄存器与存储器之间使用L/S指令。
算数逻辑运算指令
1 | ; 64位整数加法 |
看一个例子程序:
1 | mov r11, #20 |
这段程序对20维矢量作标量积
跳转指令
跳转指令可完成从当前指令向前或向后32MB的地址空间跳转
基本跳转指令:b
带返回的跳转指令:bl
带状态切换的跳转指令(arm与Thumb之间):bx
带返回和状态切换的跳转指令:blx
1 | bl label ; 程序无条件跳转至label,同时保存当前pc值至r14(lr) |
程序状态寄存器访问指令
cpsr可分为4个8位独立域:
_f
:标志域
_s
:状态域
_x
:扩展域
_c
:控制域
这样在操作cpsr时可以指定域,更加安全。
例:清除cpsr标志位
1 | mrs r0, cpsr ; r0 <- cpsr |