ARM汇编⼊门指南
本篇⽂章的⽬的是希望以⼀个例⼦的⽅式,能够不那么枯燥的的给⼤家简单介绍⼀下Android或iOS这些移动终端上ARM架构的CPU是如何执⾏ARM汇编指令的。如果说程序员在学习任何⼀门语⾔的起点都是从学习写helloworld程序开始的,那么本篇⽂章希望的就是成为你学习ARM汇编的那第⼀篇⼊门教程,⼿把⼿的带着你⽤ARM汇编⼿写⼀个helloworld程序。
Hello, ARM
⾸先我们这⾥是准备⽤GNU ARM汇编来⼿写⼀个ARM64架构的helloworld程序,那么需要先准备如下⼏个东西:
⼀个⽂本编辑器,这⾥我们⽤vim .
⼀个ARM64的编译器,这⾥我们⽤的是Android NDK⾥⾯⾃带的clang.
伪指令
以上准备好了,我们就可以开始新建⼀个⽂件名为main.S的纯⽂本⽂件,然后⽤任意⾃⼰最⼼爱的⽂本编辑器( 对于我⽽⾔它永远是vim) 来打开它,咱们先来起个头:
.text
.file 'main.c'
.globl main  // -- Begin function main
.p2align 2
这⾥我们使⽤是GNU ARM汇编,其中以.开头的是汇编指令 (Assembler Directive ) ⼜或被称为伪指令( Pseudo-operatio),因为它们不属于ARM指令,因此被称为伪指令,这⾥我们先尽量忽略它们,因为我们的主要学习⽬的是学习真正的ARM汇编指令,⽽不是这些伪东西,如果想了解它们可以参考⽂末的附录(伪指令参考表),这⾥只需要看懂其中的⼀句伪指令即可:
.globl main
这⼀句伪指令它定义了最重要的事情:在我们这个⽂件⾥⾯有⼀个叫做main名称的导出函数,它就是我们helloworld程序的⼊门函数。
main函数
然后我们就可以来书写我们的helloworld程序的main函数:
.typemain,@function
main: // @main
// %bb.0:
subsp, sp, #32 // =32
stpx29, x30, [sp, #16] // 16-byte Folded Spill
addx29, sp, #16 // =16
movw8, wzr
sturwzr, [x29, #-4]
adrpx0, .L.str
addx0, x0, :lo12:.L.str
strw8, [sp, #8] // 4-byte Folded Spill
blprintf
ldrw8, [sp, #8] // 4-byte Folded Reload
movw0, w8
ldpx29, x30, [sp, #16] // 16-byte Folded Reload
addsp, sp, #32 // =32
ret
在GNU ARM汇编⾥⾯所有以:结尾的都会视为标签 ( label ),在这⾥我们定义⼀个叫做main的标签,并且使⽤.type伪指令定义这个标签的类型是⼀个函数(function),到此我们就定义了我们的main函数。
汇编指令
上⾯这⼀段ARM汇编⽬前就和天书⼀样,你不认识它,它不认识你,没关系,接下来我们会⼀⾏⼀⾏的来学习它们究竟是什么意思,在看完这篇⽂章后,当你再看到它们时,它们会和你学过的任何⼀门语⾔的helloworld⼀样简单的。
下⾯我们先窥视⼀下第⼀⾏写得是什么东西:
sub sp, sp, #32
这⾥我们需要先了解⼀下ARM汇编的格式,ARM指令使⽤的是三地址码 , 它的格式如下:
<opcode> {<cond>} {S} <Rd>,<Rn>,<shifter_operand>
其中我们⽬前只需关注⼏个重要的:
opcode: 为指令,在我们第⼀句的指令是sub ,表⽰减法。
Rd: 为指令操作⽬的寄存器,在我们第⼀句中是sp寄存器。
Rn: 为指令第⼀源操作数,在我们第⼀句中是sp寄存器
shifter_operand: 数据处理指令,这⾥我们第⼀句是⽴即数寻址,即#32。
那么这句话汇编翻译成⼈话就是: '将sp寄存器的值减去32' ,例如伪代码:
那么这句话汇编翻译成⼈话就是: '将sp寄存器的值减去32' ,例如伪代码:
sp = sp - 32
我们现在虽然知道了这句汇编在做什么运算,但是它究竟是什么意思还是⼀头雾⽔,因为我们还不熟悉另外⼏个预备知识:ARM64架构下的的寄存器和内存布局。
寄存器
要读懂ARM汇编,⾸先就必须对ARM寄存器有⼀个基础的认知,在ARM64架构下,CPU提供了33个寄存器, 其中前31个(0~30)是通⽤寄存器 (general-purpose integer registers),最后2个(31,32)是专⽤寄存器(sp寄存器和pc寄存器)。
前⾯0~30个通⽤寄存器的访问⽅式有2种:
当将其作为32bit寄存器的时候,使⽤W0 ~ W30来引⽤它们。(数据保存在寄存器的低32位)
当将其作为64bit寄存器的时候,使⽤X0 ~ X30来引⽤它们。
第31个专⽤寄存器的访问⽅式有4种:
当将其作为32bit栈帧指针寄存器(stack pointer) 的时候,使⽤WSP来引⽤它。
当将其作为62bit栈帧指针寄存器(stack pointer) 的时候,使⽤SP来引⽤它。
当将其作为32bit零寄存器( zero register )的时候,使⽤WZR来引⽤它。
当将其作为62bit零寄存器( zero register )的时候,使⽤ZR来引⽤它。
另外需要注意的,像FP (X29) ,LR(X30) 寄存器都不能和SP(x31)寄存器⼀样⽤名字来访问,⽽只能使⽤数字索引来访问它们。
其实还有第32个专⽤寄存器,它就是PC ( x32)寄存器,但是在ARM的汇编⽂档⾥⾯说明了,你⽆法在汇编中使⽤PC名称的⽅式或者⽤X32数字索引的访问它,因为它不是给汇编⽤的,⽽是给CPU执⾏汇编指令时⽤的,它永远记录着当前CPU正在执⾏哪⼀句指令的地址。
在众多寄存器中,我们⽬前只需要了解其中⼏个重要的作⽤即可:
寄存器说明
X0寄存器⽤来保存返回值(或传参)
X1 ~ X7寄存器⽤来保存函数的传参
X8寄存器也可以⽤来保存返回值
X9 ~ X28寄存器⼀般寄存器,⽆特殊⽤途
x29(FP)寄存器⽤来保存栈底地址
X30 (LR)寄存器⽤来保存返回地址
X31(SP) 寄存器⽤来保存栈顶地址
X31(ZR)寄存器零寄存器,恒为0
X32(PC)寄存器⽤来保存当前执⾏的指令的地址
内存布局
在了解完了ARM架构的寄存器以后,我们接下来还需要⼤概了解⼏个ARM64的内存布局,⾸先⼀个ARM64的进⾏会拥有⼀个⾮常⼤的虚拟内存映射空间,其中⼜分为两⼤块:
内核地址(0xffff_ffff_ffff_ffff ~ 0xffff_0000_0000_0000范围的256TB的寻址空间),
⽤户地址 (0x0000_ffff_ffff_ffff ~ 0x0000000000000_0000范围的256TB的寻址空间) 。
这⾥我们只关⼼⽤户地址,其中有分为两⼤块:
栈内存( Stack),从⾼位向低位⽣长。
堆内存 ( Heap ), 从低位向⾼位⽣长。
其中我们知道栈内存⾸先是按照线程为单元的,每个线程都有⾃⼰的栈内存块,著名的StackOverflow所指的就是线程的栈溢出。然后每个线程的栈内存⼜可以根据函数的调⽤层级关系分为不同的栈帧( Stack Frame )。因为这⾥咱不讲编程基础,本⽂默认读者已经拥有相关的编程基础知识,就不在赘述。
line #1
在了解了ARM64架构下的寄存器和内存布局后,我们再回头⼀⾏⾏的来理解main函数,先看第⼀句汇编:
sub sp, sp, #32
它作为我们main函数的第⼀句,即在栈上⾯开启了⼀个全新的栈帧stack frame,那么第⼀件事情就是申请这个栈帧(或者函数)⾥⾯所需的栈内存空间,因为我们知道栈内存的⽣长⽅式是从⾼位向低位⽣长的,那么从基地址做减法就是增长,做加法就是收缩。在这⾥我们的main函数⼤概需要 32 bytes 的栈
空间来实现⼀个helloworld的功能,所以先将栈帧指针sp向下移动了⼀点内存空间出来,即可在函数中使⽤栈来分配内存,放置我们的局部变量等。
从下⾯开始,我们在讲解每⼀句汇编时,都会主要通过下⾯的图标形式来说明,我们重点关注的是CPU是如何使⽤寄存器和内存来做计算的,
从下⾯开始,我们在讲解每⼀句汇编时,都会主要通过下⾯的图标形式来说明,我们重点关注的是CPU是如何使⽤寄存器和内存来做计算的,因此只需要关注每执⾏⼀⾏汇编指令后,寄存器和内存的变化即可(红⾊标注的),例如我们进⼊到main函数时的初始状态下,内存和寄存器是这样的:
其中我们重点关注的是sp寄存器,因为我们这⼀句汇编主要就是修改sp寄存器的值来达到申请栈内存空间的⽬的。
我们的第⼀⾏汇编会将sp栈帧往低位移动 32 bytes,因此在CPU执⾏完这⼀句汇编指令后,内存和寄存器会变成如下的状态:
1
NOTE: 栈扩⼤32bytes内存空间
line #2
在我们开辟了新的栈内存后,我们就开始⽤这些栈内存来保存数据了,这⾥我们的helloworld程序的逻辑其实很简单,那就是在main函数⾥⾯调⽤printf来打印⼀⾏Hello World!的信息出来。
那么现在我们在main函数⾥⾯,准备去调⽤另⼀个函数printf,这就意味着我们需要在main函数这个栈帧⾥⾯开启⼀个新的栈帧来调⽤printf。我们在【内存布局】的⼀节已经提到了,每个线程的栈内存其实是按照栈帧 (Stack Frame )为单位分割的,每个函数都有⼀个单独的栈帧。
随着调⽤栈,在每个栈帧中我们需要⼀些专⽤的寄存器来保存当前的CPU上下⽂,例如我们在每个栈帧(或函数)都需要如下的寄存器来记录这些信息:
pc寄存器,记录当前CPU正在哪个指令。
sp寄存器,记录当前栈顶。
fp寄存器,记录当前栈的栈底。
lr寄存器,记录当前栈的返回地址,即这个函数调⽤完成后应该返回到哪⾥。
其中pc和sp寄存器,随着程序的运⾏,都是实时更新的,但是例如fp和lr寄存器随着程序的调⽤栈,在每个栈帧中的值都不⼀样,例如我们hello world的调⽤栈⼤概会这样的:
#0 printf()
#1 main()      <- current pc
#2 libc.init()
当前我们正处在main函数中,我们的lr寄存器记录的是main函数的返回值地址,即它的调⽤者的地址,在执⾏完main函数后,我们是需要返回到这个地址去的。
但是现在我们准备在main函数中调⽤printf函数,那么到printf函数中后,例如lr寄存器就需要⽤来保存main函数的地址作为返回地址,因为printf函数执⾏完了以后,我们希望能回到它的调⽤者即main函数中来继续执⾏main函数⾥⾯后⾯的指令。
因此,为了能让printf函数能使⽤lr和fp寄存器,可以修改它⽤来保存它栈帧的上下⽂状态,那么就需要在main函数⾥⾯,在准备调⽤printf函数之前,将现在咱们main函数的lr和fp寄存器(以及其他所有需要保存的寄存器)的数据都先备份到栈内存上⾯,那么printf函数就可以⾃由使⽤这些寄存器,执⾏⾃⼰的逻辑,并在执⾏完毕后通过lr寄存器返回到main函数中来,这时我们就可以再将之前备份到栈上⾯的旧的寄存器的值重新还原到寄存器中。
所以我们的第⼆句汇编,就是备份fp和lr两个寄存器的值,例如lr寄存器⾥⾯,现在保存着main函数的返
回地址 (即它的调⽤者__libc_init()函数的地址),我们将这些寄存器的值从寄存器⾥⾯保存到栈内存上去。
在ARM64汇编⾥⾯,以ST开头的指令都是将寄存器的值Store到内存地址上。
stpx29, x30, [sp, #16] // 16-byte Folded Spill
2
NOTE: 备份x29(fp)寄存器的值到栈上内存NOTE: 备份x30(lr)寄存器的值到栈上内存
line #3
在我们备份了fp寄存器的值到栈内存上之后,我们就可以开始修改fp寄存器的值了,将它设置成新的栈帧的栈底,即调⽤printf函数这个栈帧的栈底,在printf函数中,就可以通过fp寄存器来获取到它的栈帧基地址。
addx29, sp, #16                    // =16
3
NOTE: ⽤x29(fp)寄存器保存新的栈底地址,准备调⽤⼦函数
line #4
然后,我们希望调⽤printf函数,这个函数是有返回值的,类型为⼀个int值,在调动完printf函数后,printf函数会希望能把它的返回值传递给它的调⽤者(即我们的main函数),那么⼀般情况下都是通过寄存器传值的,例如这⾥我们提前将w8寄存器的值重置为0,printf函数就可以将返回值放到w8寄存器中,它的调⽤者main函数就可以通过读取w8寄存器来接收到printf函数的返回值。
这⾥我们通过MOV指令,将零寄存器(其值永远是0)的值移动到w8寄存器上,说⼈话就是将w8寄存器⾥⾯的值都设置为0 , 这个操作和我们写代码时,初始化⼀个int型的变量,将其先设置为0⼀样,然后将其传⼊到被调⽤的函数中去,被调⽤的函数将返回值设置到该变量上的逻辑是⼀样的。
movw8, wzr
4
NOTE: 将w8寄存器重置为0,准备⽤它来接收调⽤的⼦函数的返回值
line #5
使⽤STUR指令,将栈上的⼀个32bit的内存全部重置为0 .
sturwzr, [x29, #-4]
5
NOTE: 将[x29, #-4]地址的内存重置为0
line #6
在调⽤⼀个函数前,我们准备了接收和保存函数的返回值,接下来我们就准备去真正去调⽤printf函数了,但是我们还忘了⼀点,那就是函数的传参,printf函数需要能接收到我们的参数,即printf函数的第⼀个参数:⼀个⽤于打印的字符串,在我们这⾥就是 'Hello World!' 这个字符串,因为我们的字符串是⼀个字⾯量,它是⼀个静态全局的字符串,已经保存到内存⾥⾯了,我们只需要查到这个字符串的地址即可。
我们通过ADRP指令去查这个字符串的所在内存的页的基地址,我们的字符串的标签是.L.str,它的.type类型是⼀个object的字符串。(这部分是由伪指令定义的,具体可查看⽂末完整的汇编代码)
adrpx0, .L.str
6
NOTE: 将字符串 “hello world”所在的页的基地址加载到x0寄存器中
line #7
上⼀句,我们得到的只是字符串所在的页的基地址,我们还需要通过偏移地址计算出这个字符串的具体内存地址在哪⾥。我们通过在上⼀句查出来的基地址的基础上再增加⼀个偏移量即得到字符串的内存地址,并且我们⽤w0寄存器来保存它,⽤于将这个字符串作为printf函数的参数传递进去。
addx0, x0, :lo12:.L.str
7
NOTE: 计算“hello world”的偏移地址保存到x0寄存器中
line #8
虽然我们在 line #4 ⾥⾯重置了w8寄存器⽤于接收printf函数的返回值,但当我们通过寄存器接收到返回值后,我们还需要栈上的⼀个内存空间来保存这个返回值,因此在调⽤这个函数前提前在栈内存上为它准备⼀个内存地址来存放函数的返回值(即w8寄存器⾥的值)。
这⾥我们也是通过MOV指令,将零寄存器(WZR )的值(即0)移动到栈内存的32bit内存空间,说⼈话就是初始化⼀个32bit的内存空间,将这个内存块的数据都清零,准备⽤来保存printf函数的返回值。
strw8, [sp, #8] // 4-byte Folded Spill
8
NOTE: 将w8寄存器中的值保存到[sp, #8]的内存地址上
line #9
⼀切准备好了,我们就可以真正使⽤BL指令来调⽤printf函数了,printf函数的地址是通过linker链接到的libc内的printf函数,⼀般来说调⽤指令有多个,例如B指令,就是单纯的跳转到另⼀个地⽅去执⾏了,不准备返回了,是⼀张单程船票,⽽这⾥我们使⽤的BL指令在跳转到另⼀个地⽅,会先将当前指令的地址保存到lr寄存器中,便于跳转到另⼀个地⽅之后还有坐标可以传送回来,是⼀张往返的套票。
blprintf
blprintf
9
NOTE: x0寄存器保存着printf函数的传参,即指向字符串“hello world”的地址NOTE: 调⽤并跳转到printf函数之前,将当前的地址作为返回地址保存在x30(lr)寄存器中
line #10
在printf函数执⾏完了以后,它会把函数的返回值(⼀个32bit的int值)放在w8寄存器中,就和电影⾥⾯的特务接头⼀样,我们按照事前约定好的去某个指定的地⽅(这⾥是w8寄存器)⾥⾯去拿结果,即可得到最新的情报(即printf函数的返回值),并且我们使⽤LDR指令将w8寄存器的这个返回值保存到栈内存上。
ldrw8, [sp, #8] // 4-byte Folded Reload
10
NOTE: 将w8寄存器的值保存到[sp,#8]的内存地址上
line #11
这⾥使⽤MOV指令,将w8寄存器的值移动到w0寄存器上,即将之前⽤于传参的w0寄存器重置回了0了。
movw0, w8
11
NOTE: 将w8寄存器的值移动到w0寄存器上
line #12
到这⾥,我们的main函数已经通过调⽤printf函数在屏幕上打印出来的Hello World!的⽂字,printf函数已经返回到了我们的main函数,我们也重置了⽤于传参的寄存器,接下来我们还需要恢复在调⽤printf函数之前备份的寄存器的值。printf函数的作用是向终端
之前我们将fp和lr两个寄存器的值,保存在栈内存上,现在我们做⼀个反操作,将栈内存上保存的值通过LD指令还原到寄存器中去。
ldpx29, x30, [sp, #16] // 16-byte Folded Reload2
12
NOTE: 还原之前保存在栈内存上的FP的值到x29(fp)寄存器中NOTE: 还原之前保存在栈内存上的LR的值到x30(lr)寄存器中
line #13
咱们的main函数已经完成了它的历史使命,成功的打印出了Hello World!,它作为⼀个栈帧也准备退出了,在进⼊main函数⼀开头的时候,我们在第⼀句汇编⾥⾯,通过SUB指令申请了⼀个32 Bytes⼤⼩的栈内存空间⽤来搞事情,现在事情办妥了以后,我们有借有还,把申请的32 Bytes栈内存空间通过ADD指令给还回去,将栈顶还原到调⽤main函数之前的位置,我们轻轻的来轻轻的⾛,不带着⼀byte的内存。
addsp, sp, #32                    // =32
13
NOTE: 全部出栈,栈缩⼩32bytes的内存空间。
line #14
最后⼀步,我们使⽤RET指令退出函数,它就是我们的helloworld程序⾥main函数的return语句。到此我们的程序就写完了。
ret
14
NOTE: 函数返回,返回值通过x0寄存器返回给调⽤者
结语
在写下这14句汇编以后,我们就可以使⽤clang编译器将其编译成可执⾏的⼆进制⽂件:
$ aarch64-linux-android29-clang -o main_arm main.S
然后我们可以将它放到任何⼀台ARM64 CPU的机器,如⼤部分的Android机器,或者树莓派等单⽚机上运⾏了,我们就可以看见学习⼀门语⾔

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。