前言

逆向安卓或者其他arm平台应用的时候,阅读arm汇编是必不可少的。这里对arm汇编知识点做一个总结和记录。

arm汇编基础

arm9内核寄存器组

image-20220710104847044

arm9内核工作模式

image-20220710105026605

当前程序状态寄存器CPSR

image-20220710105123125

arm指令的格式

由于arm属于精简指令集,因此它的译码机制相对于x86这些可能简单一些。对于arm的程序来说,为了实现相同的功能,程序就需要用到相对更多的操作达到相同的功能。

与x86不同,和mips(精简指令集)相似,arm的指令是定长的。下图展示了arm指令的基本格式:

image-20220629220206767

条件执行

image-20220629220632158

首先arm的opcode部分保存操作码,表示要进行的操作。cond表示条件码,对应字节的第一个部分,这个部分处理器会访问CPSR程序状态寄存器的值来判断是否要执行指令。常用的条件编码有这些:

image-20220629221136438

S后缀表示操作的结果是否要影响状态寄存器的值(类似于在x86中进行cmptest等指令会做的操作),后面三个不用多说。注意,{}的部分不是一定要存在的。因此我们可以发现,精简指令集相对于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的呢?我们看下面的图:

image-20220629222123910

我们很清楚的看到,在32位的空间下进行循环右移,想要让F2表示F200就是要让立即数左移8位,对应的循环右移就是32 - 8位。对应的4位循环右移值就是24 // 21100

我们很自然就会意识到,正如图中所提到的,这样的操作只能将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
2
ldr r0, [r1]	; r0 <- mem32[r1]
str r0, [r1] ; r0 -> mem32[r1]

寄存器偏移地址间接寻址

例:

1
2
3
4
5
6
; 前变址
ldr r0, [r1, #4] ; r0 <- mem32[r1 + 4]
; 后变址
ldr r0, [r1], #4 ; r0 <- mem32[r1] && r1 <- r1 + 4
; 自动变址
ldr r0, [r1, #4]! ; r0 <- mem32[r1 + 4] && r1 <- r1 + 4

寄存器偏移地址

1
2
ldr r0, [r1, r2]	; r0 <- mem32[r1 + r2]
ldr r0, [r1, r2, lsl #2] ; r0 <- mem32[r1 + r2 * 4]

多寄存器及块拷贝寻址

一条指令完成多字数据或数据块的传送

基本指令:ldmstm

基址寄存器变化方式:

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对于这四种方式都支持,但是用的比较多的还是满递减方式。

如下例子:

image-20220702165800061.png

这里lr寄存器也叫r14寄存器。

这里堆栈为满递减方式,因此stmfd会将寄存器组从右往左(先入后出)压入栈顶

ldmfd会将栈顶依次弹出到寄存器组(后入先出)。

实际上压栈出栈不一定要用堆栈寻址,也可以用多寄存器块拷贝:

1
2
3
4
; 压栈
stmdb sp!, {r0-r12, lr}
; 出栈
ldmia sp!, {r0-r12, lr}

ARM指令集

存储器访问(L/S)指令

1
2
3
4
5
ldr r1, [r5]	; 寄存器间接加载
str r1, [r0, #0x04] ; r1 -> mem32(r0 + 0x04)

ldrb r3, [r2], #1 ; 加载一个字节
strh r1, [r0, #2]! ; 存储r1中半字(低两个字节)到r0+2的地址,且r0更新

数据处理类指令

数据传送指令

1
2
3
mov r1, r0
mov r1, r0, lsl #3 ; r1 <- r0 * 8
mvn r0, #0 ; r0 <- ~0,r0 = -1

与x86不同,arm寄存器之间使用mov,寄存器与存储器之间使用L/S指令。

算数逻辑运算指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
; 64位整数加法
; r0/r1 与 r2/r3 分别存放两个加数的低/高32位,r4/r5 存放结果的低/高32位

adds r4, r0, r2 ; 带s后缀表示结果影响cpsr中的标志位c
adc r5, r1, r3 ; 带进位的加法,c标志位参与运算
; 有进位c置1


; 64位整数减
subs r4, r0, r2 ; r0 - r2 -> r4
sbc r5, r1, r3 ; 带借位的减法,r1减r3再减借位放入r5


; 逆向减法
rsb r0, r1, r2 ; r0 = r2 - r1
rsc r0, r1, r2 ; 在上行指令基础上再减c标志位的反码


; 逻辑运算
and r0, r0, #3 ; 保持r0的0、1位,其余清0(r0 <- r0 & 0x03)
orr r0, r0, #3 ; 置位r0的0、1位,其余不变
eor r0, r0, #3 ; 反转r0的0、1位,其余不变(异或)
bic r0, r0, #3 ; 清零r0的0、1位,其余不变(位清除,相当于和0x03的反码相与)


; 比较指令
cmp r1, r0 ; x86同理,r1 - r0 结果影响cpsr中标志位,但不保留运算结果
cmn r0, #1 ; 判断r0的值是否为1的补码,是则z置位(r0 - -0x01),经常被用来判断是否为一个值的补码


; 测试指令
tst r1, #3 ; 按位与,结果影响cpsr
teq r1, r2 ; 按位异或,结果影响cpsr


; 乘法指令
; mul: 32位乘法
; mla: 三操作数乘法,将操作数1与操作数2相乘,结果加第三个操作数,存入目的寄存器
mla rd, rm, rs, rn ; rd <- rm * rs + rn
; 注意,rd和rm不能是同一个寄存器!

看一个例子程序:

1
2
3
4
5
6
7
8
	mov r11, #20
mov r10, #0
loop:
ldr r0, [r8], #4 ; 偏移地址在中括号外代表后变址
ldr r1, [r9], #4
mla r10, r0, r1, r10
subs r11, r11, #1
bne loop

这段程序对20维矢量作标量积

跳转指令

跳转指令可完成从当前指令向前或向后32MB的地址空间跳转

基本跳转指令:b

带返回的跳转指令:bl

带状态切换的跳转指令(arm与Thumb之间):bx

带返回和状态切换的跳转指令:blx

1
2
3
4
bl label	; 程序无条件跳转至label,同时保存当前pc值至r14(lr)

; 跳转范围不受限制方式(使用伪指令直接加载地址至pc寄存器)
ldr pc, =label

程序状态寄存器访问指令

cpsr可分为4个8位独立域:

_f:标志域

_s :状态域

_x:扩展域

_c:控制域

这样在操作cpsr时可以指定域,更加安全。

例:清除cpsr标志位

1
2
3
mrs  r0, cpsr	; r0 <- cpsr
bic r0, r0, #0xF0000000 ; 清除高4位
msr cpsr_f, r0 ; cpsr_f <- r0

此外,arm指令集中通常还包括异常产生指令、协处理器指令等

⬆︎TOP