ARM汇编基础下

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ARM汇编基础下相关的知识,希望对你有一定的参考价值。

这篇博客躺在我的草稿箱里都快发霉了,续命篇,哈哈

这一篇主要就总结一下ARM汇编中一些指令相关的知识点,指令相关的东西还是挺多的,所以只能挑一些重点来记录。在上一篇中有说指令集相关的内容,这里的话主要是讨论arm指令集,也就是4字节的。

首先,先来看一下指令的一般编码格式,固定占用四字节

对于上图的符号以及参数说明如下:

opcode : 指令操作符编码
cond   : 指令执行的条件编码
S      : 决定指令的操作是否影响CPSR的值(是否影响标志寄存器)
Rd     : 目标寄存器编码
Rn     : 包含第1个操作数的寄存器编码
shifter_operand  :  表示第2个操作数

也可以看出来,ARM是三地址的指令语法格式,指令的基本格式如下

<opcode><cond>S  <Rd>,<Rn>,<shifter_operand>

这里的符号说明其实和上面差不多的,因为其指令语法格式和对应的二进制编码并不是顺序对应的

<opcode>  :  指令助记符,如ADD表示算术加操作指令
<code>  :  表示指令执行的条件
S       :  决定指令的操作是否影响标志寄存器
<Rd>      :  表示目标寄存器
<Rn>      :  表示包含第一个操作数的寄存器
<shifter_operand>  :  表示第2个操作数

对于第二个操作数而言,通常有下面三种格式

1.立即数方式
2.寄存器方式
3.寄存器移位方式

先来看立即数方式,每一个立即数由一个8位的常数循环右移得到,而循环右移多少次是由一个4位的二进制数的2倍来表示

如果立即数记作<immediate>,8位常数记作immed_8,4位的循环右移值记作rotate_imm,那么有如下公式

<immediate> = immed_8 >-> (rotate_imm * 2)   //>->表示循环右移,自定义,方便一下书写而已

那么也就是说如果一个立即数可以构造出上面的表达式,那么就是一个合格的立即数,否则就不是合法的,这里也体现出来ARM定长指令的弊端了,比较指令就占4字节,所以其立即数表示的范围有限。

如下就是一些合格的立即数
0xFF  = 0xFF >-> (0*2)
0x104 = 0x41 >-> (0xF*2)  //这里循环右移30为相当于左移2位即可
0xFF0 = 0xFF >-> (0xE*2)  //相当于左移4位

如下一些操作数不是合法的
0x101,0x102,0xFF1

在上面,其实对于这个常数和循环的次数如何决定是编译器做的事,我们只需明白在编写汇编代码时出现一些错误心中有数即可。

注意,在某些时候,我们会认为一些不合法的操作数但是编译通过了,这里并不是说上面的公式有误,而是有可能编译器做了指令的等价替换,简单说不适用上面的规则了,毕竟某些指令我们可以将整个第二操作数都拿来当立即数(12位),只是此时操作符编码会不一样(指令不一致)。

下面再来看寄存器的方式,这种比较好理解,在寄存器方式下,操作数即为寄存器的值

ADD r0,r1,r2  @r0 = r1 + r2

再来看最后一种,寄存器移位方式,寄存器移位方式的操作数为寄存器的数值做相应的移位(或循环移位)得到,具体的移位方式有如下几种

ASR : 算术右移
LSL : 逻辑左移
LSR : 逻辑右移
ROR : 循环右移
RRX : 扩展的循环右移

移位的位数可以用立即数方式或者寄存器的方式进行表示

MOV R0,R1,LSL #3      @R0 = R1 * 2^3   这里mov未使用第一操作数
ADD R0,R1,R1,LSL #3   @R0 = R1 + R1 * 2^3
SUB R0,R1,R2,LSR #4   @R0 = R1 - R2 / 2^4
MOV R0,R1,ROR R2      @R0 = R1 循环右移R2位

下面再来看<code>域,这里表示ARM指令的条件码域,大多数ARM指令都可以有条件的执行,也就是根据CPSR中的标志位决定是否执行该指令。当条件满足时执行该指令,条件不满足时该指令被当作一条NOP指令。从最上面的编码格式图中可以看出,其在编码格式中在最高的四位,那么也就是说可以表示16种情况

这里在ARM5之前的版本中,所有的指令都是由条件执行的,从ARMv5版本开始,引入了一些必须无条件执行的指令。

既然在指令里面就有执行的条件,那么说明在一些简单的分支判断中可以使用条件域做无分支优化

	cmp R0,#1
	moveq r1,#2
	movne r2,#3 

上面三条指令的意思表示比较R0寄存器的值是否为1,为1则R1寄存器给2,否则r2寄存器给3。下面两条指令肯定会有一条指令当做NOP执行,这样子达到了一个无分支的优化,但是当分支里面的主体代码比较多时,这样子就不太划算了。

 

下面再来看一下寻址方式,总共有九种寻址方式

1.寄存器寻址
2.立即寻址
3.寄存器移位寻址
4.寄存器间接寻址
5.基址寻址
6.多寄存器寻址
7.堆栈寻址
8.块拷贝寻址
9.相对寻址

前面的几种其实都是比较好理解的,就简单的记录一下,主要堆栈的寻址相对会复杂一些,因为有4种类型的堆栈

先来看寄存器寻址,操作数的值在寄存器中,指令执行时直接取出寄存器值操作

mov r1,r0     @ r1 = r0
add r1,r1,r0  @ r1 += r0

立即寻址,操作是立即数,使用#表示前缀,十六进制值前面需要加0x

mov r0,#0xf0  @ r0 = 0xf0
add r1,r1,#5  @ r1 += 5

寄存器移位寻址,这里也就是上面说的第二操作数可以使用寄存器移位来表示

mov r0,r1,LSL #1      @ r0 = r1 * 2
add r2,r0,r1, LSL #2  @ r2 = r0 + r1 * 4 这里很像数组的寻址公式,首地址+下标*步长

具体移位的方式可以参考上面,还有这里需要说明一下,移位并不占指令周期,在CPU取指令之前,会先预处理这个移位操作计算出结果(硬件-移位器)

下面看寄存器间接寻址,操作数保存在寄存器指定地址的存储单元中,即寄存器为操作数的地址指针

ldr r0,[sp] @取出sp指向的四字节内容给r0

再来看基址寻址,将基址寄存器的值与偏移量相加,形成操作数的有效地址,基址寻址用于访问基址附近的存储单元,常用于查表、数组操作、功能寄存器访问等

    ldr r0,[sp, #4] @r0 = [sp+4]
    ldr r0,[sp],#4  @r0 = [sp] , add sp,sp,#4

注意一下上面两种的区别,下面那一种在执行完毕后sp会自动+4。

下面就是多寄存器寻址,一次传送多个寄存器值,允许一条指令传送 16 个寄存器的任何子集或所有寄存器。

push r0-r3    @push r0 到 r3的值
pop r1,r2,r3  @pop到 r0 - r3

注意,这里的寄存器子集只能由小到大进行顺序排列,否则会编译错误。

再来看堆栈寻址,堆栈寻址使用堆栈指针SP,即R13,指向堆栈的栈顶,首先堆栈分为2种

向上生长:向高地址方向生长,称为递增堆栈
向下生长:向低地址方向生长,称为递减堆栈

这里也就是说向堆栈压入数据时,这个堆栈地址是要加还是减,使用push指令是默认递减的,也就是往低地址,x86也是这种情况。

然后对于堆栈而言,还有如下两个情况区分

堆栈指针指向最后压入的有效数据项,称为满堆栈
堆栈指针指向下一个要放入的空位置,称为空堆栈

这里其实简单的来说就是sp指向的数据是否是有效的,对于满堆栈,其sp指向的数据就是刚刚压入的数据,所以是有效的,而空堆栈则是先放数据然后提升堆栈,那么此时SP指向的数据肯定无效拉。默认情况下使用的是满堆栈。

所以总共的情况就共有四种了,默认使用的就是满递减堆栈。

1.满递增:堆栈地址向上增长,堆栈指针指向有效数据的最高地址。如 LDMFA,STMFA
2.空递增:堆栈地址向上增长,堆栈指针指向堆栈上的第一个空位置。如 LDMEA,STMEA
3.满递减:堆栈地址向下增长,堆栈指针指向有效数据项的最低地址。如 LDMFD,STMFD
4.空递减:堆栈地址向下增长,堆栈指针指向堆栈下的第一个空位置。如 LDMED,STMED 

其实对于上面的push和pop而言,其实就是个伪指令,真实反汇编中对应的就是STMFD和LDMFD。

.text:B014139C 00 48 2D E9                 STMFD           SP!, R11,LR  @push R11,LR
.text:B01413B0 00 88 BD E8                 LDMFD           SP!, R11,PC  @pop R11,PC

在上面的指令中,可以发现sp后面有加了一个!,这里表示回写的意思,也就是影响sp的值,如果不加!,那么数据压入栈,而SP是不会变化的。

下面一种就是块拷贝寻址,用于将一块数据从存储器的某一位置拷贝到另一位置。这种情况其实和上面那种情况很类似,也有4种情况,因为上面那种情况只是单独针对堆栈而言的,那么堆栈的话就是操作SP寄存器,而这里可以操作其他寄存器做递增增减的存放和读取数据操作。

其指令格式一般如下四种情况

STMXX - 写入数据
LDMXX - 读取数据

XX共有四种情况
DA - 地址递减在操作数据之后
IA - 地址递增在操作数据之后
DB - 地址递减在操作数据之前
IB - 地址递增在操作数据之前

;--------说明-------
D - dec 减
I - inc 加
A - after  之后
B - before 之前

其实上面的意思和堆栈寻址是一样的,先表示地址是否向高地址还是低地址生长,然后判断满(之后操作)和空(之前操作)。

在IDA的反汇编中,只会显示堆栈寻址相关的指令,并不会显示上述的指令,下面我们来对应着堆栈寻址测试一下。

先看空堆栈的情况

	STMIA sp!,r0 @STMEA
	STMDA sp!,r0 @STMED
	
	LDMDB sp,r0  @LDMEA
	LDMIB sp,r0  @LDMED


对应的IDA反汇编代码如下

.text:AF0513A0 01 00 8D E8 STMEA           SP, R0  @STMIA  r0->[sp]  sp->sp+4
.text:AF0513A4 01 00 0D E8 STMED           SP, R0  @STMDA  r0->[sp]  sp->sp-4
.text:AF0513A8 01 00 1D E9 LDMEA           SP, R0  @LDMDB  sp->sp-4  r0->[sp]
.text:AF0513AC 01 00 9D E9 LDMED           SP, R0  @LDMIB  sp->sp+4  r0->[sp]

再来看满堆栈的情况

	STMIB sp,r0 @STMFA
	STMDB sp,r0 @STMFD
	
	LDMDA sp!,r0 @LDMFA
	LDMIA sp!,r0 @LDMFD


对应的IDA反汇编代码如下

.text:B3D323A0 01 00 8D E9 STMFA           SP, R0 @STMIB  sp->sp+4  r0->[sp]
.text:B3D323A4 01 00 0D E9 STMFD           SP, R0 @STMDB  sp->sp-4  r0->[sp]
.text:B3D323A8 01 00 1D E8 LDMFA           SP, R0 @LDMDA  [sp]->r0  sp->sp-4
.text:B3D323AC 01 00 9D E8 LDMFD           SP, R0 @LDMIA  [sp]->r0  sp->sp+4

上面使用SP寄存器来作为目标操作数只是为了方便理解和好测试,这里可以将SP寄存器换成其他任意寄存器,只是注意该寄存器里面存着的应该是一个有效的地址。

还有对于上面的操作而言,取数据其表达的地址增减是取反的,比如看最后一个指令LDMFD,这里很明显看指令意思D是递减的原因,因为这里D是针对存数据而言的,所以取数据的时候需要加(取反),而在存数据的时候直接按照指令字面意思理解即可。

再来看最后一种情况,相对寻址,相对寻址是基址寻址的一种变通,由程序计数器 PC 提供基准地址,指令中的地址码字段作为偏移量,两者相加后得到有效地址。

	B LABLE1  @跳转到 LABLE1 标号处
	mov r0,#0
	mov r1,#1
	mov r2,#2
	mov r3,#3
LABLE1:

观察其IDA中的反汇编代码

.text:000003A0 03 00 00 EA                 B               LABLE1  @注意这里机器码中是 3
.text:000003A4             ; ---------------------------------------------------------------------------
.text:000003A4 00 00 A0 E3                 MOV             R0, #0
.text:000003A8 01 10 A0 E3                 MOV             R1, #1
.text:000003AC 02 20 A0 E3                 MOV             R2, #2
.text:000003B0 03 30 A0 E3                 MOV             R3, #3
.text:000003B4
.text:000003B4             LABLE1

注意上面的跳转偏移值不是跳过多少字节,而是跳过多少条指令,因为PC指向的是下下条指令,也就是3A8的位置(包括该条),那么三条指令过后就是LABLE标签了。那么如果中间一条指令都没有的话,其值应该就是-1,因为PC指向下下条,此时还需回退一条指令才正确。

下面再来讲一下跳转指令,因为该类指令会比较常见,而且有些指令还比较细节,主要就以下四种

1. B   <label>     - 跳转
2. BL  <label>     - 带链接的跳转
3. BX  Rm          - 跳转并交换
4. BLX <label>/Rm  - 带链接和交换

首先,先来看B指令,也就是跳转指令,这个指令在上面已经有用过了,下面我们使用该指令来实现一个分支的功能,注意该指令同样适用于上面说的条件域。

	TST r0,r0
	BNE LABEL_ELSE  @ r0不为零的时跳转
	mov r1,1
	B LABEL_EN
LABEL_ELSE:
	mov r1,2
LABEL_EN:

下面再来看BL指令,该指令是带链接的跳转,什么是链接?在上一篇博客中有提到过链接寄存器(LR),该寄存器保存着返回地址。那么说明该指令应该是和函数调用相关的,首先,我们先来试一下直接使用B指令来实现函数的调用

	.type	Add,%function
	.code   32
Add:
	add r0,r0,r1
	mov pc,lr   @这里返回

	.type	main,%function
	.code	32                      @ @main
main:
	push	r11, lr
	
	mov r0,#1
	mov r1,#2
	mov lr,pc  @pc指向下下条指令,正好是LABEL_RET的返回地址处
	B Add

LABEL_RET:
	pop	r11, pc

可以看到,使用B指令来实现函数的调用,在调用之前,我们必须自己先给LR寄存器赋值,那么如果我们使用BL指令,其实编译器就会帮我们给LR赋值,仅此而已

	@mov lr,pc  该行代码注释掉即可
	BL Add
LABEL_RET:

OK,下面来看一个问题,假如我们调用的这个函数是Thumb指令集会这么办,比如我们需要调用一个C库函数,而这个C库函数是Thumb指令集的,那么程序会执行错误,我们可以将Add函数修改为thumb指令集然后尝试调用

	.type	Add,%function
	.code   16  @16表示 thumb指令集
Add:
	add r0,r0,r1
	mov pc,lr   @这里返回

其实运行的时候并不会出现什么问题,因为这里编译器帮我们换成了BLX指令。但是在上一篇说过,标志寄存器中的T位用于表示CPU的工作状态,而从ARM指令集的代码跳转到Thumb指令集的代码其T位并不会变化(BLX会变化所以上面不会有问题)。

对于此,所以我们可以想到一种花指令的办法来干扰IDA的反汇编引擎,那就是在ARM的汇编代码中加入Thumb指令,那么自然其反汇编引擎会错误判断。

	B LABEL   @下面的.code 16就是花指令,对抗反汇编引擎
	.code 16
	mov r0,r1
LABEL:

说明IDA的反汇编引擎不够强大(因为它认为其代码地址必然在模4的地址上),也可能是我的IDA版本较低,其实我们可以使用ndk自带的反汇编引擎,它的反汇编引擎会解析正确

arm-linux-androideabi-objdump -D Hello > 1.txt

然后在 1.txt 中搜索 <main>

所以,下面就可以来说说BX指令了,该指令后面只能跟一个寄存器的值,也就是不能跟标号。使用这条指令最主要的原因是可以带状态的切换,也就是说可以从ARM指令集切换到Thumb指令集。

那么如何判断切换呢?因为thumb或者arm都是2字节或者4字节,所以其代码的地址都是%2对齐的,最低位一直都是0,所以可以用最低位来当做T位状态的标志。如果地址值是1,那么跳转过去T位会切换为1,执行thumb指令(必须使用BX)。

如上图所示,Rm[0]就是表示该地址的最低位。还有需要注意的是Bx指令并不会自动给LR赋值

	.type	Add,%function
	.code   16
Add:
	add r0,r0,r1
	bx lr @注意这里必须使用bx来返回,因为使用mov pc的返回方式不会切换T位,这里需要回到ARM指令集

	.type	main,%function
	.code	32                      @ @main
main:
	push	r11, lr
	
	mov r0,#1
	mov r1,#2
	sub r2,pc,#24  @pc指向下下条,也就是lr赋值的指令,中间共7条,因为Add函数的每条指令占2字节,所以总共 5*4 + 2*2 = 24
	add r2,r2,1    @最低位置1,表示跳转后的代码是Thumb指令集
	mov lr,pc
	Bx r2

使用上面的程序就可以发现其T位会自动切换了,需要注意的是在Add函数中的返回,也必须使用bx来返回,因为此时你需要从Thumb指令集回到ARM指令集。

使用完Bx之后,你应该能发现Bx这个指令最难使用的地方了吧,那就是需要自己计算其函数的地址,还有就是LR的赋值。

所以就可以讲最后一条指令了,那就是BXL,这条指令它会帮我们保存LR的值

	@mov lr,pc
	Blx r2

该指令还是会根据其地址的最低位来判断是否需要切换T位,那么计算其函数地址如何解决,对于BLX指令,其后面除了跟一个r2寄存器,还可以跟一个标号

	mov r0,#1
	mov r1,#2
	Blx Add

那么他是如何判断的呢?其实他根本没有判断,只要是后面跟着标号,其T位必定会取反操作,所以此时对于标号情况,如果跳转是同指令集的情况,那么就不能使用blx了。

OK,上面讲完了四条跳转的指令,不过对于上面的有个问题其实还是没有本质解决,那就是当调用一个未知其指令集的库函数时,该如何解决呢?

一般来说库有其导入表,如果是Thumb指令集,那么操作系统在填写其调用地址的时候会将该函数地址进行加一,这样子就和API的调用者就无关了。下面来模拟一下

//Add 函数
.text:0000039C Add                                     ; CODE XREF: j_Add+8↓j
.text:0000039C                                         ; DATA XREF: j_Add+4↓o ...
.text:0000039C                 ADD             R0, R1
.text:0000039E                 BX              LR


//main函数
.text:000003A0 main                                    ; DATA XREF: .text:00000358↑o
.text:000003A0                                         ; .got:main_ptr↓o
.text:000003A0                 STMFD           SP!, R11,LR
.text:000003A4                 MOV             R0, #1
.text:000003A8                 MOV             R1, #2
.text:000003AC                 MOV             LR, PC
.text:000003B0                 B               j_Add  @调用者只需调用j_Add即可,无需关心指令集
.text:000003B4
.text:000003B4 LABEL_RET
.text:000003B4                 LDMFD           SP!, R11,PC


//这里也就是模拟操作系统干的活
.text:000003B8 j_Add                                   ; CODE XREF: main+10↑j
.text:000003B8                 LDR             R12, =(Add+1 - 0x3C4)  @这里对Add做+1操作,因为是Thumb指令集
.text:000003BC                 ADD             R12, PC, R12 ; Add
.text:000003C0                 BX              R12
.text:000003C0 ; End of function j_Add
.text:000003C0
.text:000003C0 ; ---------------------------------------------------------------------------
.text:000003C4 off_3C4         DCD Add+1 - 0x3C4       ; DATA XREF: j_Add↑r

注意如果直接使用LDR指令赋值给PC,那么此时PC也会根据其最低位来切换T位。

下面再来调用一个puts函数,验证其调用的方式

Bl puts(plt)  @plt表示不知道函数地址,从导入表调用 -- 这里不加 plt 也可以

观察IDA中的反汇编代码

.text:000003EC                 BL              puts  @这里是我们的代码,BL不会切换状态,无需关心


.plt:00000340 puts                                    ; CODE XREF: main+14↓p
.plt:00000340                 ADR             R12, 0x348
.plt:00000344                 ADD             R12, R12, #0x1000
.plt:00000348                 LDR             PC, [R12,#(puts_ptr - 0x1348)]! ; __imp_puts


.got:00001FFC puts_ptr        DCD __imp_puts          ; DATA XREF: puts+8↑r

可以看出来,在地址348的位置,就是读取puts_ptr,并进行运算(计算函数地址),而puts_ptr其指向的内容则由操作系统来填写,可能填写puts+0或者puts+1。说明调用任何库或者API都是使用中转的方式来调用,这样子软件就可以在各种环境中跑了,无需关心库是什么指令集。

跳转指令差不多就总结这些了,对于剩余的指令如何使用,具体可以参考文档,ARM官方有提供指令速查卡,还是中文的,很方便查询指令的使用,点击下面的链接地址下载即可(官方链接)。

ARM和Thumb2的指令速查卡(PDF) 

以上是关于ARM汇编基础下的主要内容,如果未能解决你的问题,请参考以下文章

ARM汇编基础下

x86汇编如何查看一个地址的值

汇编SI DI 的用法

ARM汇编基础

关于 GNU ARM 汇编程序的意外警告

ARM汇编基础上