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,@functionmain: // @main// %bb.0:subsp, sp, #32 // =32stpx29, x30, [sp, #16] // 16-byte Folded Spilladdx29, sp, #16 // =16movw8, wzrsturwzr, [x29, #-4]adrpx0, .L.straddx0, x0, :lo12:.L.strstrw8, [sp, #8] // 4-byte Folded Spillblprintfldrw8, [sp, #8] // 4-byte Folded Reloadmovw0, w8ldpx29, x30, [sp, #16] // 16-byte Folded Reloadaddsp, sp, #32 // =32ret在 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 = 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)寄存器零寄存器,恒为0X32(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是如何使用寄存器和内存来做计算的,因此只需要关注每执行一行汇编指令后,寄存器和内存的变化即可(红色标注的),例如我们进入到 main 函数时的初始状态下,内存和寄存器是这样的:

0其中我们重点关注的是 sp 寄存器,因为我们这一句汇编主要就是修改 sp 寄存器的值来达到申请栈内存空间的目的。我们的第一行汇编会将 sp 栈帧往低位移动 32 bytes,因此在CPU执行完这一句汇编指令后,内存和寄存器会变成如下的状态:

1NOTE: 栈扩大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

2NOTE: 备份x29(fp)寄存器的值到栈上内存NOTE: 备份x30(lr)寄存器的值到栈上内存line #3在我们备份了 fp 寄存器的值到栈内存上之后,我们就可以开始修改 fp 寄存器的值了,将它设置成新的栈帧的栈底,即 调用 printf 函数这个栈帧的栈底,在 printf 函数中,就可以通过 fp 寄存器来获取到它的栈帧基地址。addx29, sp, #16 // =16

3NOTE: 用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

4NOTE: 将w8寄存器重置为0,准备用它来接收调用的子函数的返回值line #5使用 STUR 指令,将栈上的一个 32bit 的内存全部重置为 0 .sturwzr, [x29, #-4]

5NOTE: 将[x29, #-4]地址的内存重置为0line #6在调用一个函数前,我们准备了接收和保存函数的返回值,接下来我们就准备去真正去调用 printf 函数了,但是我们还忘了一点,那就是函数的传参,printf 函数需要能接收到我们的参数,即 printf 函数的第一个参数:一个用于打印的字符串,在我们这里就是 'Hello World!' 这个字符串,因为我们的字符串是一个字面量,它是一个静态全局的字符串,已经保存到内存里面了,我们只需要查到这个字符串的地址即可。我们通过 ADRP 指令去查找这个字符串的所在内存的页的基地址,我们的字符串的标签是 .L.str ,它的 .type 类型是一个 object的字符串。(这部分是由伪指令定义的,具体可查看文末完整的汇编代码)adrpx0, .L.str

6NOTE: 将字符串 “hello world”所在的页的基地址加载到x0寄存器中line #7上一句,我们得到的只是字符串所在的页的基地址,我们还需要通过偏移地址计算出这个字符串的具体内存地址在哪里。我们通过在上一句查出来的基地址的基础上再增加一个偏移量即得到字符串的内存地址,并且我们用 w0 寄存器来保存它,用于将这个字符串作为 printf 函数的参数传递进去。addx0, x0, :lo12:.L.str

7NOTE: 计算“hello world”的偏移地址保存到x0寄存器中line #8虽然我们在 line #4 里面重置了 w8 寄存器用于接收 printf 函数的返回值,但当我们通过寄存器接收到返回值后,我们还需要栈上的一个内存空间来保存这个返回值,因此在调用这个函数前提前在栈内存上为它准备一个内存地址来存放函数的返回值(即 w8 寄存器里的值)。这里我们也是通过 MOV 指令,将零寄存器(WZR )的值(即0)移动到栈内存的32bit内存空间,说人话就是初始化一个 32bit 的内存空间,将这个内存块的数据都清零,准备用来保存 printf 函数的返回值。strw8, [sp, #8] // 4-byte Folded Spill

8NOTE: 将w8寄存器中的值保存到[sp, #8]的内存地址上line #9一切准备好了,我们就可以真正使用 BL 指令来调用 printf 函数了,printf 函数的地址是通过 linker 链接到的 libc 内的 printf 函数,一般来说调用指令有多个,例如 B 指令,就是单纯的跳转到另一个地方去执行了,不准备返回了,是一张单程船票,而这里我们使用的 BL 指令在跳转到另一个地方,会先将当前指令的地址保存到 lr 寄存器中,便于跳转到另一个地方之后还有坐标可以传送回来,是一张往返的套票。blprintf

9NOTE: 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

10NOTE: 将w8寄存器的值保存到[sp,#8]的内存地址上line #11这里使用 MOV 指令,将 w8 寄存器的值移动到 w0 寄存器上,即将之前用于传参的 w0 寄存器重置回了 0 了。movw0, w8

11NOTE: 将w8寄存器的值移动到w0寄存器上line #12到这里,我们的 main 函数已经通过调用 printf 函数在屏幕上打印出来的 Hello World! 的文字,printf 函数已经返回到了我们的 main 函数,我们也重置了用于传参的寄存器,接下来我们还需要恢复在调用 printf 函数之前备份的寄存器的值。之前我们将 fp 和 lr 两个寄存器的值,保存在栈内存上,现在我们做一个反操作,将栈内存上保存的值通过 LD 指令还原到寄存器中去。ldpx29, x30, [sp, #16] // 16-byte Folded Reload2

12NOTE: 还原之前保存在栈内存上的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

13NOTE: 全部出栈,栈缩小32bytes的内存空间。line #14最后一步,我们使用 RET 指令退出函数,它就是我们的 helloworld 程序里 main 函数的 return 语句。到此我们的程序就写完了。ret

14NOTE: 函数返回,返回值通过x0寄存器返回给调用者结语在写下这14句汇编以后,我们就可以使用 clang 编译器将其编译成可执行的二进制文件:$ aarch64-linux-android29-clang -o main_arm main.S然后我们可以将它放到任何一台 ARM64 CPU的机器,如大部分的Android机器,或者树莓派等单片机上运行了,我们就可以看见学习一门语言最亲切的打印语句了,这里我们使用的是Android自带的 LLDB 调试器在真机上运行的:$ gdbclient.py -r /data/local/tmp/main_arm$ Hello World!到此你就基本学会了如何用ARM汇编手写一个 helloworld 程序,希望这篇文章真的能带大家走进ARM汇编的世界里一起学习,路漫漫兮。附录本文中完整的汇编代码:.text.file'main.c'.globlmain // -- Begin function main.p2align2.typemain,@functionmain: // @main// %bb.0:subsp, sp, #32 // =32 申请32bytes的栈空间stpx29, x30, [sp, #16] // 16-byte Folded Spill 将 FP(x29), LR(x30) 保存在栈上addx29, sp, #16 // =16 缩小栈大小16bytesmovw8, wzr// 将 zero寄存器的值0 移动到 w8 寄存器sturwzr, [x29, #-4]//adrpx0, .L.straddx0, x0, :lo12:.L.strstrw8, [sp, #8] // 4-byte Folded Spillblprintfldrw8, [sp, #8] // 4-byte Folded Reloadmovw0, w8ldpx29, x30, [sp, #16] // 16-byte Folded Reloadaddsp, sp, #32 // =32ret.Lfunc_end0:.sizemain, .Lfunc_end0-main// -- End function.type.L.str,@object // @.str.section.rodata.str1.1,'aMS',@progbits,1.L.str:.asciz'Hello World!\n'.size.L.str, 14.ident'Android (7155654, based on r399163b1) clang version 11.0.5 (https://android.googlesource.com/toolchain/llvm-project 87f1315dfbea7c137aa2e6d362dbb457e388158d)'.section'.note.GNU-stack','',@progbits本文汇编对应的C源码:#include <stdio.h>int main() {printf('Hello World!\n');return 0;}伪指令参考表(节选):asm含义.textTells as to assemble the following statements onto the end of the text subsection numbered subsection, which is an absolute expression. If subsection is omitted, subsection number zero is used..file.file (which may also be spelled .app-file') tellsasthat we are about to start a new logical file. string is the new file name. In general, the filename is recognized whether or not it is surrounded by quotes'';.globl.global makes the symbol visible to ld. If you define symbol in your partial program, its value is made available to other partial programs that are linked with it. Otherwise, symbol takes its attributes from a symbol of the same name from another file linked into the same program..p2alignPad the location counter (in the current subsection) to a particular storage boundary. The first expression (which must be absolute) is the number of low-order zero bits the location counter must have after advancement. For example `.p2align 3' advances the location counter until it a multiple of 8. If the location counter is already a multiple of 8, no change is needed..sectionUse the .section directive to assemble the following code into a section named name..asciz.asciz is just like .ascii, but each string is followed by a zero byte. The 'z' in `.asciz' stands for 'zero'.参考资料GNU ARM Assembler Quick ReferenceGNU AssemblerARM A64 General Instructions- EOF -

(0)

相关推荐

  • 这是什么骚代码,我服了!!!

    神秘代码 今天给大家看个有意思的东西! 不仅有意思,还能学到知识. 话题从两行(准确的说是一行)神奇的代码聊起: // main.c #include <stdio.h> int main ...

  • GNU AS汇编器伪指令大全

    GNU AS汇编器伪指令大全

  • AWDPwn 漏洞加固总结

    AWD简介AWD(Attack With Defense,攻防兼备) 模式需要在一场比赛里要扮演攻击方和防守方,利用漏洞攻击其他队伍进行得分,修复漏洞可以避免被其他队伍攻击而失分.也就是说,攻击别人的 ...

  • 栈溢出漏洞原理详解与利用

    0x01 前言 和我一样,有一些计算机专业的同学可能一直都在不停地码代码,却很少关注程序是怎么执行的,也不会考虑到自己写的代码是否会存在栈溢出漏洞,借此机会我们一起走进栈溢出. 0x02 程序是怎么运 ...

  • 6,免费好用!修图神器 Snapseed 入门指南,教你用手机修出大片 | 爱范儿

    本文来自微博@摄影师钟楼怪人 ,原标题<Tutorial | Snapseed 照片编辑完全手册(1)>,AppSo 经授权发布,后面的连载我们也会长期跟进,感兴趣的朋友可以保持关注. 每 ...

  • 隶书经典《曹全碑》入门指南,写的太绝了,新手必看!

    隸書作為壹種最為靈動和高古的字體,不少人由隸入門也是無可厚非.說起入門的隸書選擇,不少人第壹想到<曹全碑>,那麽曹全碑到底好在哪裏?今天書思就重點和大家說說. 1.<曹>字保持 ...

  • 快速入门指南到,教你超捷径轻松上手SMOOTH-Q3

    拿到新机「SMOOTH-Q3手机稳定器」不知道该从何下手?别烦恼,快速入门指南.按键介绍.模式介绍教程都为你准备好了.照着做,轻松玩转SMOOTH-Q3那都不是事. 1 快速入门指南 安装手机 / 调 ...

  • 研究僧入门指南(含科研)

    本文由考研斯基原创 本文约2000字,预计需要7分钟 大家好,我是考研斯基. 目前大部分同学都已经收到了拟录取通知,学长在这里先恭喜大家啦,但同时也想告诉大家上岸并不是终点,而是新的起点.这几天中科院 ...

  • 再见了,被文献检索折磨的日子!因为这篇入门指南了,我上岸了!

    解螺旋公众号·陪伴你科研的第2549天 文献筛选 文献浩如烟海,却是我们站在巨人肩上前行的资本,毕竟,众多科研思路.研究套路均隐藏在海面之下,静待挖掘.所以,要快速确定课题的研究方向,关键就在于多读文 ...

  • 视频号小白直播带货入门指南!从0-1只需要这些步骤

    随着视频号直播的频繁更新迭代,直播带货也走上快速发展的道路,越来越多机构.企业.个人投入视频号直播蓝海,越快进入,越早抢占视频号直播的红利市场. 视频直播带货如此火爆,那么对视频号小白来说,要怎么做直 ...

  • GitHub 热榜:C 快速入门指南!

    人工智能算法与Python大数据 致力于提供深度学习.机器学习.人工智能干货文章,为AI人员提供学习路线以及前沿资讯 23篇原创内容 公众号 点上方人工智能算法与Python大数据获取更多干货 在右上 ...

  • 「入门指南」一盔一带进行时,你的头盔选好了吗?

    本内容来源于@什么值得买APP,观点仅代表作者本人 |作者:BigDguokr 先声明一下,本文面向的对象,是广大电动车骑士(含新国标和超标车),不是摩托骑士.好了,正文开始 一盔一带的这个政策,个人 ...

  • 新手入门指南 | 报告酱教你写研报

    前两周聊了一下如何读研报,如果你能多看多写,日积月累,水平当然也会慢慢提高. 最近还有小伙伴会问彤彤,如果自己写研报,要怎么下手?思路和框架是什么呢? 这个问题看上去有点泛,其实也不难回答.报告酱就来 ...