ARM汇编基础上

Posted 嘻嘻兮

tags:

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

ARM是一个精简指令集处理器,其指令集的设计是定长的,也就是其汇编对应的机器码是定长的(2字节或者4字节)。那么对于定长而言,其优点就是更快的被执行,因为这样CPU取指令译码的速度相对x86的CPU会快一些,但是缺点也比较明显,毕竟定长,那么表示其指令有限(指令少),所以一定程度上会软件的复杂度(需要几条指令才能完成一个功能)。

ARM的指令集一开始设计定长都是2字节的,随着时间的发展,会发现指令可能已经不够表达了,所以后面又出现了4字节的指令集。对于2字节的指令集称为thumb指令集,而4字节的称为arm指令集。此时我们可以自由选择,如果需要体积小那么我们可以选择thumb编译,而需要性能更好可以选择arm指令集。

但是对于某些指令而言,比如加法,那么使用2字节和4字节的指令区别其实并不大,所以就出现了thumb2指令集,其指令集算是变长了(要么2字节要么4字节),算是集合了上面两种的指令集的优点,现在的ndk编译器只能编译出thumb2指令集合arm指令集了。

下面来说说ARM处理器的工作状态,也就是说CPU如何按几个字节来取指令译码,毕竟存在多套指令集

1.ARM状态
2.Thumb状态

那么如何给CPU指令集指明状态呢?其实在标志寄存器中有一位来表示哪种状态,具体可以看下面的标志寄存器。

下面就可以说到寄存器了,如下图

上图表示了不同的模式下对于寄存器的访问,那么什么是CPU的工作模式,简单来说也就是x86下的权限级别划分,也就是多少环的意思,在arm里共有7种模式,其实本质上说还是2种,也就是三环和0环,只是把0环拆分的更加细致了,所以多了这么多的模式出来。

对于ARM的寄存器,目前我们只需关心用户模式,只需记住以下两种,共17个寄存器可用

1.通用寄存器 R0-R15
2.标志寄存器 cpsr

对于其他模式的寄存器,又会有其对应的寄存器,也就是上图中颜色比较深的那几个,但是对于寄存器的访问而言是一样的,比如说当进入了管理模式,那么我们使用SPSP寄存器访问的是SPSP_svc寄存器,而到了中止模式,我们还是只需使用SPSP寄存器,但是此时本质上访问的是SPSP_abt寄存器,无需关心真实的寄存器变化和名字。

通过上图也可以发现,不管在哪种模式下,其R0-R7,R15寄存器都是通用的。

下面再来稍微细说一下通用寄存器和程序计数器,在上图的寄存器在汇编中名字可以发现,其后面都有一个括号,表示的是寄存器别名,如下两指令是等价的

mov r15,#0
mov pc,#0

一般而言,对于R13,R14,R15这三个寄存器都使用别名来编码,因为这样子可读性更好(有特殊含义),其余都使用R0-R12表示

PC -  r15 程序寄存器,类似于eip
LR -  r14 链接寄存器,类似于CALL指令,下条指令的地址在r14
SP -  r13 堆栈指针

r13和r15应该比较好理解,对于r14,在x86里面使用call指令会将返回地址压入堆栈,而这里表示返回地址在LR寄存器。但是该寄存器只有一个,表明当函数内部还有函数调用时,我们首先需要先保存LR寄存器,防止寄存器值被覆盖。

对于函数返回而言,此时就比较简单了,只需如下汇编指令就可返回

mov pc,lr

下面就可以来说一下标志寄存器了

N : 当该位为1时表示负数,为0时表示正数
Z : 当该位为1时表示两数相等,为0时表示两数不相等
C : 当该位为1时,若为加法运算表示产生进位,否则该位为0 (减法运算中产生借位则该位置0,否则为1)
V : 使用加法/减法运算时,表示有符号溢出,否则该位为0
I : 当该位为1时,IRQ中断被禁止
F : 当该位为1时,FIQ中断被禁止
T : 当该位为1时,处理器处于Thumb状态下运行;该位为0时,处理器处于ARM状态下运行

重点说一下对于C位,加法运算和减法运算其产生的值是不一样的。而对于T位,就是上面有说CPU使用何种工作状态。

最低五位表示CPU处理何种工作模式,也就是用于表示上面说的7种模式(权限),如用户模式则表示 10000。

对于编码而言,一般我们只需关心最高四位和T位即可。

 

下面就可以开始入手写一下ARM汇编代码,首先我们可以先写一份C代码,然后使用ndk编译出一份汇编代码,观察其框架结构

#include<stdio.h>

int main(int argc,char* argv[])

    puts("hello arm");
    return 0;

编译生成汇编的命令如下:

	armv7a-linux-androideabi22-clang -E Hello.c -o Hello.i
	armv7a-linux-androideabi22-clang -S Hello.i -o Hello.s

注意这里需要添加一下ndk的环境变量,这里我的目录如下,大家根据目录对应修改添加即可

D:\\Android-NDK-r21\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin

生成的.s文件就是对应的汇编文件,由于这里生成的汇编代码,有很多无用的伪指令(.开头的指令),所以我就精简一下,精简后的汇编代码如下:

	.text  @代码段
	.fpu	neon       @浮点协处理器 soft表示软件模拟
	.globl	main       @全局符号为main
	.p2align	2  @对齐值,表示2^n
	.type	main,%function    @指明main的类型,这里是一个函数
	.code	32                @32表示arm指令集,16表示thumb指令集
main:
	push	r11, lr
	mov	r11, sp
	sub	sp, sp, #16
	ldr	r2, .LCPI0_0
.LPC0_0:
	add	r2, pc, r2
	mov	r0, r2
	bl	puts
	movw	r1, #0
	mov	r0, r1
	mov	sp, r11
	pop	r11, pc
	.p2align	2
.LCPI0_0:
	.long	.L.str-(.LPC0_0+8)  @定义数据
.Lfunc_end0:
	.size	main, .Lfunc_end0-main  @函数的大小

	.type	.L.str,%object          @ @.str
	.section	.rodata.str1.1,"aMS",%progbits,1  @定义rodata段
.L.str:
	.asciz	"hello arm"  @字符串
	.size	.L.str, 10

	.section	".note.GNU-stack","",%progbits @表示栈不能执行代码

在ARM里面,使用的是@进行注释,中间部分就是main的函数代码了,没有太多的注释,剩余的伪指令基本上都在后面注释了,下面先编译运行,看看结果是否正确,编译运行命令如下,注意需要先启动一个安卓模拟器或者真机

	//编译链接生成可执行文件
	armv7a-linux-androideabi22-clang -c Hello.s -o Hello.o
	armv7a-linux-androideabi22-clang Hello.o -o Hello

	//运行可执行文件
	adb push Hello /data/local/tmp
	adb shell chmod 777 /data/local/tmp/Hello
	adb shell /data/local/tmp/Hello

下面就解释一下上面的汇编代码,首先我们可以使用.section来定义一个段,在系统中预定了以下一些段、

.text:表示代码段
.data:表示初始化的数据段
.bss:表示未初始化的数据段
.rodata:表示只读数据段

再来看对齐值伪指令,在ARM指令集中不可能有1字节的指令集,所以指令最低是2字节,任何段都是2的n次方,上面的汇编代码表示4字节对齐。

.code伪指令,如果是32,那么表示arm指令集,也就是生成的字节码都是4字节的,而16则表示thumb2指令集。这里可以修改为16然后使用IDA反汇编观察一下字节码即可。

再下面就是函数的汇编代码了,首先看首尾部分,这里和x86中汇编函数框架很类似,都是保存环境,提示堆栈的操作

push r11, lr @保存r11和lr寄存器,因为内部有函数调用,lr表示函数的返回地址,所以需要保存
mov	r11, sp    @保存sp
sub	sp, sp, #16  @提升堆栈,立即数前面需要加#

@....


mov	sp, r11   @还原堆栈
pop	r11, pc @还原环境,这里原先lr保存的值给pc,表示返回

需要额外注意的是原先保存的lr值最后是给了pc相当于上面说的返回

mov pc,lr @lr中保存着函数返回地址,给了pc相当于返回

再来看中间的这串汇编代码,也就是调用puts函数

	ldr	r2, .LCPI0_0
.LPC0_0:
	add	r2, pc, r2
	mov	r0, r2
	bl	puts  @bl指令表示函数调用

对于puts函数而言,需要传递的应该是一个地址值,也就是字符串的首地址,对于ARM汇编中,mov是没有办法操作内存的,只有str/ldr指令用于存储和读取内存。由于一个地址值可能就需要占用4个字节,所以可能无法直接表示,那么我们就需要自己来计算该字符串的地址

pc + offset

我们可以使用当前的指令的地址值加上对于的偏移,这里子就能计算出字符串的首地址了,所以LCPI0_0中其实存放的是偏移,然后使用ldr指令来加载偏移值,下一条add指令加上pc就是结果了。

.LCPI0_0:
	.long	.L.str-(.LPC0_0+8)  @定义数据  .L.str-.LPC0_0-8

.long表示定义4字节的数据,我们还可以使用.byte和.short来分别来定义单字节和双字节数据。

只是奇怪的是,对于偏移值而言,为什么最后还需要减8呢,这里因为在ARMCPU中,pc指向的不是当前的指令,而是下下条指令,也就是三级流水线。

地址
100: mov r0,pc  @r0=108
104: mov r1,r2
108: mov r2,r3

假设如上代码,其r0执行完的结果并不是100,而是108,具体的原因可以看下图

所以因为PC指向的是下下条指令,在arm指令中,每条指令占用4字节,所以减去8之后其偏移值+PC就正常了。那么如果对于.code伪指令的值改为16的话,那么这个值就需要修改为4了,这样子才能保证偏移值的正确。

OK,最后就可以说说函数的调用约定了,这里的约定是比较简单的,前四个寄存器使用R0~R3进行传递参数,剩余不够的使用堆栈传递参数,所以你会发现该字符串的地址值最后赋值给了r0。

好了,下面我们自己来添加一个函数,并尝试调用

	.text
	.fpu	neon
	.globl	main                    @ -- Begin function main
	.p2align	2
	.type	main,%function
	.code	32                      @ @main
main:
	push	r11, lr
	mov	r11, sp
	sub	sp, sp, #16
	mov r0,#5  @传递参数一
	mov r1,#8  @传递参数二
	bl MyAdd   @调用函数
	ldr	r2, .LCPI0_0
.LPC0_0:
	add	r2, pc, r2
	movw	r3, #0
	mov	r0, r2  @参数一  参数二在MyAdd的返回值中,正好是r1
	bl	printf  @修改调用printf函数
	movw	r1, #0
	str	r0, [sp]                @ 4-byte Spill
	mov	r0, r1
	mov	sp, r11
	pop	r11, pc
	.p2align	2
.LCPI0_0:
	.long	.L.str-(.LPC0_0+8)
.Lfunc_end0:
	.size	main, .Lfunc_end0-main
	
	.type MyAdd,%function
	.code 32
MyAdd:  @添加的add函数
	add r1,r0,r1  @这里我的返回值放在r1
	mov pc,lr  @返回
.LMyAdd_end:
	.size MyAdd,.LMyAdd_end-MyAdd
	
	.type	.L.str,%object          @ @.str
	.section	.rodata.str1.1,"aMS",%progbits,1
.L.str:
	.asciz	"hello arm:%d"  @修改字符串
	.size	.L.str, 13

	.section	".note.GNU-stack","",%progbits

上面的代码中,对于Add函数,我就是偷懒了,把返回值放在r1寄存器中,因为在返回后,也没有对r1进行修改,这样子再调用printf函数时会自动取r1作为第二个参数。

执行结果如下:

adb push Hello /data/local/tmp
Hello: 1 file pushed. 0.7 MB/s (6560 bytes in 0.009s)
adb shell chmod 777 /data/local/tmp/Hello
adb shell /data/local/tmp/Hello
hello arm:13

最后显示的就是打印的结果了,13是正确的,说明两个函数都被正确调用了。

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

GNU ARM 汇编基础笔记

ARM汇编基础下

ARM汇编基础下

汇编语言ARM扩展资料汇编语言开发

汇编语言ARM扩展资料汇编语言开发

ARM汇编基础--ADS1.2安装与第一个ARM汇编程序