02-汇编基础(2)

2021-04-02  本文已影响0人  深圳_你要的昵称

前言

本篇文章主要以汇编的角度,分析函数的本质,在分析函数的过程中,就会解决上篇文章最后的死循环问题。

一、基础知识点

接着上篇文章01-汇编基础(1)的内容,我们再介绍几个常见的基础知识点。

1.1 栈

在讲函数之前,先来看看,因为函数的实现代码的作用域就对应在内存的中。
栈是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)。我们都知道,栈的操作无非就是👇

1.1.1 栈的结构

栈的结构就好比只有一个口的管子,如下图👇

那么问题来了 👉 系统怎么知道开辟多大的空间呢?

编译器决定的,因为你的代码被编译后,编译器就知道要申请多少空间大小的栈了。

1.1.2 SP FP 寄存器

根据SP 和 FP 寄存器可以查看栈的空间大小,因为👇

⚠️ ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp。
ARM64里面 对栈的操作是16字节对齐的!!

ARM64中,是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容,原因上面说过,编译期就已经确定大小,所以不存在push操作,同时, iOS中内存是高地址向低地址的方向开辟栈空间的。

1.2 函数调用栈

我们都知道,函数的实现是在栈中进行的,函数执行完毕后栈的空间自动释放,那么在汇编中,常见的函数调用栈的开辟和恢复的代码👇

//开辟栈空间
sub    sp, sp, #0x40             ; 拉伸0x40(64字节)空间
stp    x29, x30, [sp, #0x30]     ;x29\x30 寄存器入栈保护
add    x29, sp, #0x30            ; x29指向栈帧的底部
... 
//恢复栈空间
ldp    x29, x30, [sp, #0x30]     ;恢复x29/x30 寄存器的值
add    sp, sp, #0x40             ;栈平衡
ret

上面的汇编代码的执行过程,如下图所示👇

  1. 通过sub减指令来开辟空间,此时sp指向低地址位置,x29也是fp指向栈底,即高地址位置
  2. 函数ret即调用完毕返回之前,需要通过add加指令,恢复sp寄存器地址指向,这就是所谓的栈平衡
  3. 恢复后数据并不销毁,下次再拉伸栈空间后,会先覆盖再读取。如果先读取,读取的就是垃圾数据

二、内存读写指令

注意⚠️ 读/写数据是都是往高地址进行操作。

读/写的指令主要有2个👇

  1. str(store register)指令 👉 将数据从寄存器中读出来,存到内存中.
  2. ldr(load register)指令 👉 将数据从内存中读出来,存到寄存器中

str ldr 是内存寄存器交互的专门的指令。

还有2个也很常用的指令stp和 ldp,意思是可以同时操作2个寄存器的读和写。

练习

写一个函数,功能很简单,就是x0和x1 交换数据,目的是熟悉上面的stp ldp指令意思。代码如下👇

.text
.global _C

_C:
    sub  sp, sp, #0x20        ;拉伸栈空间32个字节
    stp  x0, x1, [sp, #0x10]  
    ldp  x1, x0, [sp, #0x10]  
    add  sp, sp, #0x20        ;恢复栈空间
    ret

上面代码,第一行和倒数第二行,常规操作,对栈空间的拉伸与恢复,重点是中间2句代码(汇编代码从右往左看)👇

至此,上面的代码就完成了x0和x1寄存器中值的交换,以前我们知道将a和b值交换时,需要用到第三个temp变量,那么这里内存就充当了temp的角色,如下图所示👇

示例调试

但是,注意⚠️内存中的值是没有变化的,sp寄存器的指向地址也没变,变化的只是 x0 和 x1寄存器中的值,不信?接下来我们可以调试看看。

上图在0x104631c8c断点处,对x0和x1分别赋值0xa和0xb,再读取sp的地址值,然后接着单步往下执行下面👇

上图发现,x0和x1已交换完毕,但是再次读取sp地址时,是没有变化的,依旧是0x000000016b7d1190。再继续单步执行👇

sp还原了,栈空间释放,这时候0xa,0xb还依然存在内存中,并没有释放,会有问题吗?其实仔细想想,我们每次sub拉伸栈空间后,都是通过str或stp对内存空间的值进行写数据覆盖的,所以不会有问题。我们可以通过view memory查看内存👇

上图中,输入0x000000016b7d1190地址查看,果然发现a和b均在内存中没有释放。

2.1 bl和ret指令

接下来我们看看bl和ret指令。

bl标号

b就是跳转,l就是将下一条指令的地址放入lr(x30)寄存器。还是看上面的例子,查看跳转C函数后,lr寄存器的地址,如下图👇

lr相当于保存的回家的路

ret

默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

ret只会看lr
注意⚠️ ARM64平台的特色指令,它面向硬件做了优化处理。

2.2 x30寄存器

x30寄存器,就是我们上面说的lr寄存器,存放的是函数的返回地址。当ret指令执行时就会去寻找x30寄存器保存的地址值

案例演示

我们还是用一个案例演示给大家看看,很简单,C函数中bl跳转到D函数(C函数调用D函数)👇

.text
.global _C, _D

_C:
    mov x0,#0xaaaa
    bl _D
    mov x0,#0xaaaa
    ret

_D:
    mov x0,#0xbbbb
    ret

调用的代码👇

int C();
int D();
- (void)viewDidLoad {
    [super viewDidLoad];
    printf("C");
    C();
    printf("D");
}

C();这行打上断点,run,查看汇编👇

当前lr指向的是bl c()这条指令的地址,接着step into进入到C()中👇

然后跳转到D()中👇

此时lr的地址又发生了变化,变成了0x00000001021e5c78,接着往下执行,回到C()中👇

lr的地址和在D()中的一样,没变化,继续执行下去,你会发现,断点执行一直在0x1021e5c780x1021e5c7c这两句中跳转,返不回去viewDidLoad中了,发生了死循环。

这个就是我们上一篇01-汇编基础(1)中最后碰到的死循环问题,现在我们来分析一下:

既然我们知道了bl指令的作用,就是保存回去的地址(回家的路),那我们得想办法保存回到viewDidLoad的地址,而且必须在bl之前进行保存,因为上图中的现象可见 👉 遇到bl,lr就会改变

现在我们知道了何时保存,即bl之前,但是保存在哪里呢?

如果保存到其它寄存器,是没法保证系统是否会覆盖其它寄存器的地址值的,那么接得想办法保存在自己的一个私有的区域,这个区域是哪里呢?很显然,就是函数本身的栈区

至此,我们知道了,在bl之前将lr的地址保存在函数自己的栈区内

接下来,就是如何写汇编实现这个保存操作了。既然不会写,那不如我们不写汇编,写OC,然后看汇编底层是如何处理的。

void a() {
    b();
    return;;
}

void b() {
    
}

- (void)viewDidLoad {
    [super viewDidLoad];
    a();
}

step into进入查看a()的汇编👇

看来,重点就是第一条和ret前一条的指令了,我们先来看第一条指令的含义,老规矩,从右往左看👇

分析完第一句指令后,再来看ldp指令,就没那么难了👇

系统的整明白了,再回到自己定义的C()和D()中,照着写就行了👇

.text
.global _C, _D

_C:
    str x30, [sp,#-0x10]!
    mov x0,#0xaaaa
    bl _D
    mov x0,#0xaaaa
    ldr x30,[sp],#0x10
    ret

_D:
    mov x0,#0xbbbb
    ret

run,调试看看👇

step into进入C函数👇

接着step into进入D函数👇

接着单步往下执行👇

原来lr中保存的0x0000000100c7dc50,就是保存回到C函数的地址。我们再看看sp寄存器地址,是0x000000016f1851a0,通过view memory看看里面的值👇

我们知道,sp寄存器是指向栈顶的地址,再回过头来看看ViewDidLoad中bl跳转C()函数的汇编👇

上图中0x0100c7dcd0,不就是bl跳转C()函数的下一条指令的地址吗,这就验证了我们之前分析的,ViewDidLoad的lr寄存器的值被保存到了它自己的栈里面。

然后继续往下执行ldr x30,[sp],#0x10,x30的值就取到了0x0100c7dcd0,就能跳转回ViewDidLoad了,这个时候死循环就已经解决了。

综上所述
⚠️ 在函数嵌套调用的时候,需要将x30入栈

如果拉伸的是8字节

如果只拉伸8字节的空间,结果会怎样?👇

_C:
    str x30, [sp,#-0x8]!
    mov x0,#0xaaaa
    bl _D
    mov x0,#0xaaaa
    ldr x30,[sp],#0x8
    ret

这里 str x30, [sp,#-0x8]!只拉伸8字节,run👇

错误是报在ldr x30,[sp],#0x8这行,说明,拉伸空间没问题,但是要恢复内存,返回ViewDidLoad时报错了,即从内存读数据,存到x30寄存器的时候报错了

所以,栈中一定要保持16字节对齐的原则!

三、函数的参数和返回值

接下来,我们看看汇编是怎么处理带有参数和返回值的函数。例如👇

int sum(int a, int b) {
    return a + b;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    sum(10,20);
}

sum(10,20);这行打上断点,查看汇编👇

上图红框处,对w0,w1赋值的不就是10跟20吗,接着step into查看sum函数的汇编👇

最终,返回ViewDidLoad之前,结果是保存在寄存器w0中。于是,我们自己实现一个sum函数的汇编,可以这么写👇

.text
.global _sum

_sum:
    add x0,x0,x1
    ret

x0 = x0 + x1,因为参数就是保存在 x0和x1之中。

调用的👇

int sum(int a, int b);
- (void)viewDidLoad {
    [super viewDidLoad];
    printf("%d",sum(10,20));
}

运行看看👇

  1. ARM64下,函数的参数是存放在X0到X7(W0到W7)这8个寄存器里面的。
  2. 如果超过8个参数,就会入栈
  3. 函数的返回值是放在X0寄存器里面的。
参数超过8个的情况
int test(int a, int b, int c ,int d, int e, int f, int g, int h, int i) {
    return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}

参数分布与sp指向如下图所示👇

接着我们step into去到test函数中👇

整个累加的过程如下图所示👇

最终函数返回值放入w0中。

如果在release模式下test不会被调用(被优化掉,因为没有意义,且对app没有影响。)

返回值

请看下面的实例👇

// str结构体占用24字节大小
struct str {
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
};

struct str getStr(int a, int b, int c, int d, int e, int f) {
    struct str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    struct str str2 = getStr(1,2,3,4,5,6);
}

打上断点,查看汇编👇

接着step into进入到getStr函数中👇

getStr整个汇编赋值的过程如下图所示👇

最终会发现,这里并没有以 x0 作为返回值,而是将返回值写入上一个函数(ViewDidLoad函数)的栈x8寄存器中。

综上,如果返回值大于8字节,返回值会保存在上一个函数栈空间

结构体成员超过8个

如果结构体成员超过8个呢,是个什么情况?

struct str {
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
    int i;
    int j;
};

struct str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
    struct str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    str1.g = g;
    str1.h = h;
    str1.i = i;
    str1.j = j;
    return str1;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    struct str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    printf("%d",func(10,20));
}

ViewDidLoad汇编👇

ASMPrj`-[ViewController viewDidLoad]:
    // 拉伸栈空间6*16=96字节大小
    0x100f31c80 <+0>:   sub    sp, sp, #0x60             ; =0x60 
    // 将x29 和x30的值存到栈中,地址是sp+0x50 👉 `保存回家的路`
    0x100f31c84 <+4>:   stp    x29, x30, [sp, #0x50]
    // x29指向sp+0x50这个地址
    0x100f31c88 <+8>:   add    x29, sp, #0x50            ; =0x50 
    // 参数x0和x1 入栈
    0x100f31c8c <+12>:  stur   x0, [x29, #-0x8]
    0x100f31c90 <+16>:  stur   x1, [x29, #-0x10]
    // x8存入参数x0的值
    0x100f31c94 <+20>:  ldur   x8, [x29, #-0x8]
    // x9指向 x29 - 0x20
    0x100f31c98 <+24>:  sub    x9, x29, #0x20            ; =0x20 
    // x8 存入 x29 - 0x20
    0x100f31c9c <+28>:  stur   x8, [x29, #-0x20]
    // adrp 👉 address page 内存中取数据
    // ADRP指令
    // * 编译时,首先会计算出当前PC到exper的偏移量#offset_to_exper 
    // * pc的低12位清零,然后加上偏移量,给register
    // * 得到的地址,是含有label的4KB对齐内存区域的base地址;
    0x100f31ca0 <+32>:  adrp   x8, 4 // 此处的偏移量是4

    // x8 所指的内存取出来
    0x100f31ca4 <+36>:  add    x8, x8, #0x4e0            ; =0x4e0 
    0x100f31ca8 <+40>:  ldr    x8, [x8]
    0x100f31cac <+44>:  str    x8, [x9, #0x8]
    0x100f31cb0 <+48>:  adrp   x8, 4
    0x100f31cb4 <+52>:  add    x8, x8, #0x458            ; =0x458 
    0x100f31cb8 <+56>:  ldr    x1, [x8]
    0x100f31cbc <+60>:  mov    x0, x9
    0x100f31cc0 <+64>:  bl     0x100f32524               ; symbol stub for: objc_msgSendSuper2

    // x8指向 sp + 0x8
    0x100f31cc4 <+68>:  add    x8, sp, #0x8              ; =0x8 
    0x100f31cc8 <+72>:  mov    w0, #0x1
    0x100f31ccc <+76>:  mov    w1, #0x2
    0x100f31cd0 <+80>:  mov    w2, #0x3
    0x100f31cd4 <+84>:  mov    w3, #0x4
    0x100f31cd8 <+88>:  mov    w4, #0x5
    0x100f31cdc <+92>:  mov    w5, #0x6
    0x100f31ce0 <+96>:  mov    w6, #0x7
    0x100f31ce4 <+100>: mov    w7, #0x8
    // sp的值给x9
    0x100f31ce8 <+104>: mov    x9, sp
    // w10中存储 9
    0x100f31cec <+108>: mov    w10, #0x9
    // w10中保存x9的地址
    0x100f31cf0 <+112>: str    w10, [x9]
    // w10中存储 10
    0x100f31cf4 <+116>: mov    w10, #0xa
    // x9偏移4字节,再存入w10
    0x100f31cf8 <+120>: str    w10, [x9, #0x4]
    // 跳转getStr函数
    0x100f31cfc <+124>: bl     0x100f31bf4               ; getStr at ViewController.m:30
    0x100f31d00 <+128>: ldp    x29, x30, [sp, #0x50]
    0x102499d04 <+132>: add    sp, sp, #0x60             ; =0x60 
    0x102499d08 <+136>: ret  

接着看getStr汇编👇

ASMPrj`getStr:
->  0x1004ddbf4 <+0>:   sub    sp, sp, #0x30             ; =0x30 
    // 从上一个栈空间 获取9 和 10
    0x1004ddbf8 <+4>:   ldr    w9, [sp, #0x30]
    0x1004ddbfc <+8>:   ldr    w10, [sp, #0x34]
    // 参数入栈
    0x1004ddc00 <+12>:  str    w0, [sp, #0x2c]
    0x1004ddc04 <+16>:  str    w1, [sp, #0x28]
    0x1004ddc08 <+20>:  str    w2, [sp, #0x24]
    0x1004ddc0c <+24>:  str    w3, [sp, #0x20]
    0x1004ddc10 <+28>:  str    w4, [sp, #0x1c]
    0x1004ddc14 <+32>:  str    w5, [sp, #0x18]
    0x1004ddc18 <+36>:  str    w6, [sp, #0x14]
    0x1004ddc1c <+40>:  str    w7, [sp, #0x10]
    0x1004ddc20 <+44>:  str    w9, [sp, #0xc]
    0x1004ddc24 <+48>:  str    w10, [sp, #0x8]
    // 获取参数分别存入上一个栈x8所指向的地址中
    0x1004ddc28 <+52>:  ldr    w9, [sp, #0x2c]
    0x1004ddc2c <+56>:  str    w9, [x8]
    0x1004ddc30 <+60>:  ldr    w9, [sp, #0x28]
    0x1004ddc34 <+64>:  str    w9, [x8, #0x4]
    0x1004ddc38 <+68>:  ldr    w9, [sp, #0x24]
    0x1004ddc3c <+72>:  str    w9, [x8, #0x8]
    0x1004ddc40 <+76>:  ldr    w9, [sp, #0x20]
    0x1004ddc44 <+80>:  str    w9, [x8, #0xc]
    0x1004ddc48 <+84>:  ldr    w9, [sp, #0x1c]
    0x1004ddc4c <+88>:  str    w9, [x8, #0x10]
    0x1004ddc50 <+92>:  ldr    w9, [sp, #0x18]
    0x1004ddc54 <+96>:  str    w9, [x8, #0x14]
    0x1004ddc58 <+100>: ldr    w9, [sp, #0x14]
    0x1004ddc5c <+104>: str    w9, [x8, #0x18]
    0x1004ddc60 <+108>: ldr    w9, [sp, #0x10]
    0x1004ddc64 <+112>: str    w9, [x8, #0x1c]
    0x1004ddc68 <+116>: ldr    w9, [sp, #0xc]
    0x1004ddc6c <+120>: str    w9, [x8, #0x20]
    0x1004ddc70 <+124>: ldr    w9, [sp, #0x8]
    0x1004ddc74 <+128>: str    w9, [x8, #0x24]
    // 栈平衡
    0x1004ddc78 <+132>: add    sp, sp, #0x30             ; =0x30 
    0x1004ddc7c <+136>: ret   

整个执行的过程如下图所示👇

上图所示,参数和返回值都存在上一个函数(ViewDidLoad)的栈中,并且返回值的地址在高位,参数在低位。

四、函数的局部变量

最后,我们来看看函数的局部变量,先看下面示例👇

int func(int a, int b) {
    int c = 6;
    return  a + b + c;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    func(10, 20);
}

首先看看func的汇编👇

上图可知 👉 函数的局部变量放在函数自己的栈里面!

嵌套调用

如果是嵌套调用的场景呢?会是怎样的情况,例如👇

int func1(int a, int b) {
    int c = 6;
    int d = func2(a, b, c);
    int e = func2(a, b, c);
    return  d + e;
}

int func2(int a, int b, int c) {
    int d = a + b + c;
    printf("%d",d);
    return d;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    func1(10, 20);
}

汇编代码👇

上图可见,参数和返回值依然被保存到栈中。

现场保护包含:FP,LR,参数,返回值。

总结

上一篇下一篇

猜你喜欢

热点阅读