ARM 汇编学习记录

1. 简介

根据 CPU 架构不同,汇编主要可以分为两种:模拟器上的x86 汇编、真机上的 arm64 汇编,主要从下面三个模块进行学习:

  • 寄存器
  • 指令
  • 堆栈

在学习之前,推荐阅读汇编语言入门教程,可以对于这三个部分有个初步的认识。

2. 寄存器

2.1 寄存器的分类

寄存器有多种类型,下面按照不同的作用,分别来介绍一下,之后会在 iOS 项目中进行查看。

2.1.1 通用寄存器

x0-x28属于通用寄存器,每个寄存器都是64 位的,说明最多可以存放8 个字节的数据。
w0-w2832 位的寄存器(为了兼容 32 位的 CPU),属于 x0-x28低 32 位,如下图所示:

截屏2021-03-02 下午10.59.01.png

x0-x7通常用来存放函数的参数,如果参数更多,就使用堆栈(下面会有讲到)来传递。

通过 lldb 命令,我们也可以验证出他们的关系,如下图所示:


image.png

x0 在函数运行最后,通常被用来存放函数的返回值(如果有的话)。

2.1.2 程序计数器寄存器

也就是pc (Program Counter Register)寄存器,存放当前CPU 正在执行的指令的地址,类似于 8086 汇编的 ip 寄存器

2.1.3 堆栈寄存器

堆栈寄存器是用来控制函数分配的栈空间的,下面会有讲到。
sp (Stack Pointer)寄存器,指向栈顶。
fp (Frame Pointer)寄存器,指向栈底。

2.1.4 链接寄存器

lr (Link Register),也就是x30,存放着函数的返回地址
使用bl指令跳转时,会自动将bl指令下面的一条指令的地址,存放到lr寄存器中,如果 bl指令跳转之后,遇到了ret指令ret指令会将lr寄存器存放的指令地址给pc寄存器,然后 CPU 就会执行pc寄存器存储的地址所指向的指令,相当于函数返回了。

2.1.5 程序状态寄存器
  • cpsr (Current Program Status Register)
    存放当前程序的运行状态,和其他寄存器不同(其他寄存器是用来存放数据或指令的),cpsr 寄存器是按位起作用的,每一位都有专门的含义,记录特定的信息,如下图所示:
    image.png
  - CPSR的低8位(包括I、F、T和M[4~0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位!
  - N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行,意义重大!
  • spsr (Saved Program Status Register)
    异常状态下使用
2.2 寄存器的读写

我们在 ViewController 中写一个方法,然后在该方法中下一个断点,如下图所示:

截屏2021-03-02 下午11.19.25.png

通过register read命令来查看当前所有的寄存器,如下同:

image.png

可以通过上图对之前寄存器的介绍进行分析巩固:
【1】x0 中存放的是sumWithA:b:默认的第一个参数self,也就是方法调用者:ViewController,可以通过下图方式验证:

截屏2021-03-02 下午11.27.41.png

【2】x1 中存放的是sumWithA:b:默认的第二个参数_cmd,就是方法名称,从上图中可以直接看出来
【3】x2x3中分别存放的是sumWithA:b:第 3 和第 4 个参数,也就是 a 和 b,对应的值为a=0x00001=1b=0x0000002=2,跟我们传递的参数值是一样的。
【4】lr(x30)存放的是函数的返回地址,当sumWithA:b:执行完成之后,还需要返回 viewDidLoad 方法中继续执行,所以lr(x30)中存放的是 viweDidLoad 的地址,从上图中可以很直观的看出来。
【5】pc 寄存器存放的是 CPU 当前执行指令的地址,也就是sumWithA:b:方法,从上图中可以很直观的看出来。

这里没有用到sp fp 寄存器,下面在介绍堆栈时会讲到,另外的一些寄存器有时候会当临时存储使用,所以可能会存储任何数据,不做介绍。

我们还可以向指定寄存器写入数据,读取单个寄存器的数据,如下:

// 修改寄存器x1 存储的数据
(lldb) register write x1 0x0000000000000001
// 读取 x1 单个寄存器的数据
(lldb) register read x1
      x1 = 0x0000000000000001

上面介绍的寄存器用来存放指令和数据,控制寄存器的读取则是需要指令来控制的。

3.指令

我们先来学习下如何在项目中编写汇编指令,然后再介绍一些指令,可以自己写着练习一下。

内嵌汇编格式:

__asm__ [关键词]( 
    指令
    : [输出操作数列表]
    : [输入操作数列表]
    : [被污染的寄存器列表]
);

如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // volatile 关键字表示禁止编译器对汇编代码进行再次优化
    __asm__ __volatile__(
        "add x0, x0, x1"
    );
}

3.1 传送指令

  • mov:move
    使用最频繁的指令,相当于高级语言中的赋值语句。

使用格式如下:

指令 描述
mov x1, x0; 将寄存器 x0 的值赋给 x1
mov x0, #0x8 将 0x8 存储到x1

3.2 运算指令

指令 描述
add x0,x1,x2; x0 = x1 + x2
sub x0,x1,x2; x0 = x1 - x2
mul x0,x1,x2; x0 = x1 * x2
sdiv x0,x1,x2; x0 = x1 / x2;
and x0,x0,#0xF; x0 = x0 & #0xF (与操作)
orr x0,x0,#9; x0 = x0 或 #9 (或操作)
eor x0,x0,#0xF; x0 = x0 ^ #0xF (异或操作)
3.3 寻址指令

涉及到内存寻址的问题。


image.png
3.3.1 取值指令
  • ld (load) 开头的是取值指令,如 :
    • ldr
      将内存中的数据,放到另一个寄存器中,偏移值是正值
    • ldur
      ldr相同,偏移值为负值
    • ldpload pair ,一对寄存器
      根据地址读取一对数据,按地址顺序赋值给一对寄存器
指令 描述
ldr x0,[x1]; 从 x1 指向的地址里面取出一个 64 位大小的数据存入 x0
ldur w8,[x29, #-0x8]; 从x29-0x8的地址里面取出一个32位的数,存入w8
ldp x1,x2,[x10,#0x10]; 从 x10+0x10指向的地址开始取出 2 个 64 位的数,分别存入 x1,x2
3.3.2 赋值指令
  • st (store)开头的是存值指令,如:
    • str
      往内存中写数据(偏移值为正)
    • stur
      往内存中写数据(偏移值为负)
    • stp
      存放一对数据,从右边那个地址开始按顺序存放,寄存器写在左边,内存寻址写右边
指令 描述
str x5,[sp,#24]; 往内存中写数据(偏移值为正),把 x5 的值(64位的数值)存到 sp + 24指向的地址
stur w0,[x29, #0x8] ; 往内存中写数据(偏移值为负),将 w0 的值存储到 x29 - 0x8 这个地址里
stp x29,x30,[sp, #-16]!; 把 x29、x30 的值存到 sp-16 的地址上,并且把sp-=16 Note:后面有个感叹号的,然后没有stup这个指令哈

寻址方式:

  1. [x10, #0x10] 表示从 x10 + 0x10 的地址取值
  2. [sp, #-16]表示从 sp - 16地址取值,取完后再把 sp-16 写回 sp
  3. [sp], #16 表示从 sp 取值,取完后把 sp+16写回 sp

3.4 cmp 指令、cpsr 寄存器

  • cmp指令:compare比较
  • cpsr:Current Program Status Register 程序状态寄存器
  • spsr: Saved Program Status Register 异常状态下使用

cmp 是比较指令,其功能相当于是减法指令,只是不会保存结果,cmp 执行完成之后会对cpsr状态寄存器产生影响,其他指令通过读cpsr状态寄存器的条件标志位来得知比较结果。

示例:

  • cmp x0,x1:意思是将寄存器x0的值与寄存器x1的值相减,并根据结果设置 cpsr 的标志位
  • cmp x0, #0x88:意思是将寄存器 x0 的值与立即数 0x88 相减,并根据结果设置 cpsr 的标志位
image.png

示例:


image.png

可以看到我们将 x0 设置了 0x3,将 x2 设置了 0x2,然后使用 cmp 指令,最后 cpsr 寄存器的值为 0x20000000,转换成二进制位 00100000000000000000000000000000,按照上面那张图对比,可以发现第 31 30 位都是 0,说明计算结果为正数,结果非 0。

3.5 跳转指令

  • b指令
    跳转到目标地址执行函数,可以带条件跳转

  • bl指令:(blret配合使用)
    跳转到目标地址执行完函数后返回原代码,继续往下执行,相当于在代码中调用一个函数,函数调用完后继续往下执行原代码。

上面介绍过lr寄存器,当 bl 跳转之前,会将下一行要执行的代码的地址存到lr寄存器

  • ret 指令:返回指令
    将函数返回,也就是将lr(x30)寄存器的存储的指令地址赋给pc寄存器,然后继续执行。

有返回的意思是会存lr(x30),意味着可以返回到本方法继续执行,一般用于不同方法直接的调用。
无返回的一般是方法内的跳转,如while 循环if else

跳转指令一般还伴随着条件,以实心点. 开头的都是表示条件,如 b.ne,一般用于 if else。常见的条件码有以下这些:

image.png

示例:


image.png

根据上上图,beq指令执行时,去会看cpsr中的z位看是否为1。

带 if else 的汇编:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self test];
}

- (void)test{
    int a = 5;
    int b = 3;
    if(a > b){
        printf("a > b");
    }else{
        NSLog(@"a <= b");
    }
}

汇编代码:


image.png

4. 内存模型

4.1 堆

由于寄存器只能存放少量数据,在大多数的时候,CPU 指挥寄存器跟内存交换数据,所以除了寄存器还必须了解内存是怎么存储数据的。

程序运行的时候,操作系统会给它分配一段内存,用来存储程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x10000x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。

image.png

程序运行过程中,对于动态占用请求(比如新建对象,或者使用 malloc),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从其实地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。
举例来说,用户申请 10 个字节的内存,那么从起始地址0x1000开始给他分配,一直分配到 0x100A,如果再申请 22 个字节,那么就分配到0x1020

image.png

这种因为用户主动请求而划分出来的内存区域,叫做堆(Heap)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

4.2 栈

除了堆(Heap) 以外,其他的内存占用叫做栈(Stack)。简单来说,栈是由于函数运行而临时占用的内存区域,是一种往下(低地址)生长的数据结构。

image.png
int main(){
    int a = 2;
    int b =3;
}

上面的代码中,系统开始执行main 函数的时候,会为它在内存里面建立一个帧(frame),所有 main 的内部变量(比如a 和 b)都保存在这个帧里面。main 函数执行结束之后,该帧就会被回收,释放所有的内部变量,不再占用空间。

image.png

如果 main 函数内部又调用了其他函数,又是怎样呢?

int main() {
   int a = 2;
   int b = 3;
   return test(a, b);
}

上面的代码中,main函数内部调动了 test函数。当执行到这一步的时候,系统也会为test 新建一个帧,用来存储它的内部变量。也就是说,此时同时存在两个帧:main 和 test。一般来说,调用栈有多少层,就有多少帧。

image.png

等到 test 运行结束,它的帧就会被回收,系统会回到函数 main 刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。
栈(Stack)是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配的。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0
image.png

5. 举例

了解完以上的基础知识之后,下面就用一个简单的例子了解汇编的栈操作。


// hello.c
#include <stdio.h>
int test(int a, int b) {
  int res = a + b;
  return res;
}
int main() {
  int res = test(1, 2);
  return 0;

使用clang命令将其编译成arm64指令集汇编代码:

clang -S -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` hello.c

可以看到完整的汇编如下:

.section __TEXT,__text,regular,pure_instructions
.build_version ios, 13, 2 sdk_version 13, 2
.globl _test                   ; -- Begin function test
.p2align 2
_test:                                  ; @test
.cfi_startproc
; %bb.0:
sub sp, sp, #16             ; =16
.cfi_def_cfa_offset 16
str w0, [sp, #12]
str w1, [sp, #8]
ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1
str w0, [sp, #4]
ldr w0, [sp, #4]
add sp, sp, #16             ; =16
ret
.cfi_endproc
                                        ; -- End function
.globl _main                   ; -- Begin function main
.p2align 2
_main:                                  ; @main
.cfi_startproc
; %bb.0:
sub sp, sp, #32             ; =32
stp x29, x30, [sp, #16]     ; 16-byte Folded Spill
add x29, sp, #16            ; =16
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
stur wzr, [x29, #-4]
orr w0, wzr, #0x1
orr w1, wzr, #0x2
bl _test
str w0, [sp, #8]
mov w0, #0
ldp x29, x30, [sp, #16]     ; 16-byte Folded Reload
add sp, sp, #32             ; =32
ret
.cfi_endproc
                                        ; -- End function
.subsections_via_symbols
    1. 第一部分
.section __TEXT,__text,regular,pure_instructions
.build_version ios, 13, 2 sdk_version 13, 2
.globl _test                   ; -- Begin function test
.p2align 2

代码中类似.section 或者.globl等以 '.' 开头的被称为编译指令,用于告知编译器相关的信息或者特定操作。

指令 介绍
__TEXT,__text .section里面的 __TEXT,__text字段用来存放代码指令
.build_version 是编译版本信息
.globl_test 声明了全局变量(函数)
; -- Begin function test 分号后面是注释
.p2align 2用于指定程序的对齐方式,这类似于结构体的字节对齐,为的是加速程序的执行速度,p2align 的单位是指数,即按照 2 的 n 次方对齐,这里的 .p2align 2 表示按照 2^2 = 4 字节对齐,如果单行指令数据长度不足4字节,将用 0 补全,超过 4 但不是 4 的倍数,则按照最小倍数补全
_test、_main 称之为标签(label),用于辅助定位代码或者资源地址,也方便开发者理解和记忆
    1. 函数开始和结束
指令 介绍
.cfi_startproc; 定义函数开始
.cfi_endproc; 定义函数结束
.cfi_xxx; call frame information xxx, cfi 是 DWARF 2.0 定义的函数栈信息,用来告诉编译器生成响应的 DWARF 调试信息,主要是和函数有关。
    1. 方法头和方法尾

汇编中的如下部分被称为方法头(prologue),用于保存上一个方法调用栈帧的帧头,以及预留部分用于局部变量的栈空间

sub sp, sp, #32             ; =32 // 将栈顶指针向下移动 32 个字节,开辟栈空间,用来存储参数或者局部变量
stp x29, x30, [sp, #16]     ; 16-byte Folded Spill
add x29, sp, #16            ; =16

汇编中的如下部分被称为方法尾(epilogue),用于取出方法头中栈帧信息及方法的返回地址,并将栈恢复到调用前的位置

ldp x29, x30, [sp, #16]     ; 16-byte Folded Reload
add sp, sp, #32             ; =32 // 将栈空间释放
ret
    1. test函数的实现
//源代码
 int test(int a, int b) {
    int res = a + b;
    return res;
 }
 
 
 //汇编
 sub    sp, sp, #16             ; =16
 .cfi_def_cfa_offset 16
 str    w0, [sp, #12]
 str    w1, [sp, #8]
 ldr    w0, [sp, #12]
 ldr    w1, [sp, #8]
 add    w0, w0, w1
 str    w0, [sp, #4]
 ldr    w0, [sp, #4]
 add    sp, sp, #16             ; =16
 ret

在编译器生成汇编时,会首先计算需要的栈空间大小,并利用sp (stack pointer)指针指向低地址开辟相应的空间。从test函数可以看到这里涉及了3个变量,分别是a、b、resint变量占据4个字节,因此需要12个字节,但 ARM64汇编为了提高访问效率要求按照16字节进行对齐,因此需要16 byte的空间,也就是需要在栈上开辟16字节的空间,可以看汇编的第一句,正是将sp 指针下移16字节

sp (stack pointer)是栈顶指针,永远指向栈顶!

sub sp, sp, #16             ; =16
image.jpeg

接下来看:

str w0, [sp, #12]
str w1, [sp, #8]

2句的意思是,将w0存储在sp+12 的地址指向的空间,w1存储在sp+8 存储的空间里,寄存器x0~x7用于子程序调用时的参数传递,按顺序入参。 x0w0 是同一个寄存器的不同尺寸形式,x08字节,w0x0的前4个字节,因此w0是函数的第一个入参aw1是函数的第二个入参b,上文栈一节我们提到, 栈的分配是从高地址向低地址,所以a在这里分配sp+16~sp+12这块内存空间, 存储是从低地址到高地址,也就是存在sp+12 ~ sp+16 这4个字节的空间里,b将占据 sp+8 ~sp+124个字节的空间,栈结构图变为如下所示:

image.png

接下来test 函数内部将ab进行相加,需要注意的是,只有寄存器才能参与运算,因此接下来的汇编代码又将变量的值从内存中读出来,再进行相加运算。


ldr w0, [sp, #12]
ldr w1, [sp, #8]
add w0, w0, w1

到这里可能会纳闷,先存储在读取后运算,感觉这一步很多余,确实是这样的,因为这是没有进行编译优化的结果,为了是能够更好的学习和了解汇编的工作机制。
计算完成之后将结果存储到了w0寄存器,地址是sp+4

str w0, [sp, #4]
image.png

接下来就要进行返回操作了,上文中我们提到,函数的返回值一般存储在x0/w0 寄存器中返回的,这里也可以看到它将返回值res载入到了x0/w0 寄存器了:

ldr w0, [sp, #4]

最后就是将栈还原,并返回到函数调用处继续向下执行。

add sp, sp, #16             ; =16
ret

显然,经过这样的操作,栈被完全还原到了函数调用以前的样子,需要注意的细节是,栈空间中的内存单元并未被清空,这就导致下一次使用栈时,未初始化单元的值是不确定的,这也就是局部变量不初始化会出现随机值的根本原因。

    1. main函数汇编实现
      接着,再看看main 函数的汇编代码就变得很好理解了:
sub sp, sp, #32             ; =32
stp x29, x30, [sp, #16]     ; 16-byte Folded Spill
add x29, sp, #16            ; =16
----------------------------------------------------prologue-----------------------------------------------------------------
.cfi_def_cfa w29, 16
.cfi_offset w30, -8
.cfi_offset w29, -16
stur wzr, [x29, #-4]        
orr w0, wzr, #0x1
orr w1, wzr, #0x2
bl _test
str w0, [sp, #8]
mov w0, #0
----------------------------------------------------epilogue-----------------------------------------------------------------
ldp x29, x30, [sp, #16]     ; 16-byte Folded Reload
add sp, sp, #32             ; =32
ret

参考资料:
阮一峰-汇编语言入门教程
x86 与 ARM 的爱恨情仇
ARM64汇编基础
为什么使用汇编可以 Hook objc_msgSend
ARM64 汇编
iOS 的内嵌汇编

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容