汇编-函数本质

时间:2021-7-3 作者:qvyue

  • 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)

    汇编-函数本质
    image.png

SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址。
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!(没有出现函数嵌套调用的时候不需要fp,相当于分界点)
    ⚠️:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldrldp strstpARM64里面 对栈的操作是16字节对齐的!!

ARM64是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容(编译期就已经确定大小)。不存在push操作。在iOS中栈是往低地址开辟空间

汇编-函数本质
image.png

函数调用栈

常见的函数调用开辟和恢复的栈空间:

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

恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。

内存读写指令

⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x020x03

str(store register)指令
将数据从寄存器中读出来,存到内存中。

ldr(load register)指令
将数据从内存中读出来,存到寄存器中。

ldrstr 的变种 ldpstp 还可以操作2个寄存器。

堆栈操作案例

使用32个字节空间作为这段程序的栈空间,然后利用栈将x0x1的值进行交换。

.text
.global _C

_C:
    sub  sp, sp, #0x20        ;拉伸栈空间32个字节
    stp  x0, x1, [sp, #0x10]  ;sp 偏移 16字节存放 x0和x1  []的意思是寻址。这sp并没有改变
    ldp  x1, x0, [sp, #0x10]  ;将sp偏移16个字节的值取出来,放入x1 和 x0。这里内存相当于temp 交换了 x0 和 x1。寄存器中的值交换了,内存中的值不变。
    add  sp, sp, #0x20  ;恢复栈空间
    ret

这段代码相当于 x0,x1遍历,sp和内存没有变。
栈空间分配:

汇编-函数本质
image.png

断点调试
0x102e6e518断点处对x0x1分别赋值0xa0xb。然后单步执行:

汇编-函数本质
image.png

拉伸后sp也变了。

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
      sp = 0x000000016cf95b30
(lldb) register read sp
      sp = 0x000000016cf95b10
(lldb) 

看下0x000000016cf95b10的空间:

汇编-函数本质
image.png

目前还没有写入内存,是脏数据。接着单步执行:

汇编-函数本质
image.png

数据写入了内存。接着单步执行数据读取放入x0x1

汇编-函数本质
image.png

这个时候x0x1的数据完成了交换。内存的数据并没有变化。
继续单步执行:

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
      sp = 0x000000016cf95b30
(lldb) register read sp
      sp = 0x000000016cf95b10
(lldb) register read sp
      sp = 0x000000016cf95b30
(lldb) 

sp还原了,栈空间释放,这时候0xa0xb还依然存在内存中,等待下次拉伸栈空间写数据覆盖:

汇编-函数本质
image.png

bl和ret指令

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

b就是跳转,l将下一条指令的地址放入lr(x30)寄存器。

汇编-函数本质
image.png

lr相当于保存的”回家的路“。

ret

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

ret只会看lr

ARM64平台的特色指令,它面向硬件做了优化处理。

x30寄存器

x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
一个嵌套调用的案例,汇编代码如下:

.text
.global _C, _D

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

_D:
    mov x0,#0xbbbb
    ret

ViewController.m中调用:

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

C();打断点执行,进入C中:

汇编-函数本质
image.png

这个时候lr指向viewDidLoad下一条指令,接着执行跳入D中:

汇编-函数本质
image.png

这个时候lr指向C的下一条指令0x104c8e4f8,执行完D返回C可以看到lr仍然指向0x104c8e4f8没有改变

汇编-函数本质
image.png

继续执行发现一直在0x104c8e4f80x104c8e4fc中跳转返不回去viewDidLoad中了,发生了死循环。

->  0x104c8e4f8 :  mov    x0, #0xaaaa
    0x104c8e4fc : ret  

那么如果要返回,就必须将viewDidLoad中下一条指令告诉lr,这个时候就必须在bl之前保护lr寄存器(遇到bllr就会改变。需要保护“回家的路”)。那么这个时候能不能把lr保存到其它寄存器?这里我们没法保证其它寄存器不会被使用。这个时候唯一属于当前函数的也就是自己的栈区了。保存到栈区应该就能解决了。
可以看下系统是怎么实现的,写一个c函数断点调试看下:

void c() {
    d();
    return;;
}

void d() {
    
}

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

系统的实现如下:

汇编-函数本质
image.png
TestDemo`c:
//边开辟空间边写入 x29(fp) 和 x30(lr) 的值。[sp, #-0x10]!  !代表赋值给sp,相当于 sp -= 0x10
->  0x102a21e84 :  stp    x29, x30, [sp, #-0x10]!
    0x102a21e88 :  mov    x29, sp
    0x102a21e8c :  bl     0x102a21e98               ; d at ViewController.m:34:1
//将sp所指向的地址读取给x29,x30。[sp], #0x10 等价于 sp += 0x10
    0x102a21e90 : ldp    x29, x30, [sp], #0x10
    0x102a21e94 : ret

可以看到系统先开辟栈空间,然后将x29x30寄存器的值存入栈区。在ret之前恢复x29x30的值。

  • stp x29, x30, [sp, #-0x10]!:开辟空间并将x29x30存入栈区。!代表赋值给sp,相当于 sp -= 0x10
  • ldp x29, x30, [sp], #0x10:将栈区的值给x29x30并回收空间。[sp], #0x10 等价于 sp += 0x10

那么对于CD的案例自己实现下保存和恢复lr寄存器。

.text
.global _C, _D

_C:
    //sub sp,sp,#0x10
    //str x30,[sp] ;等价
    str x30, [sp,#-0x10]! ;16字节对齐,必须最小0x10。
    mov x0,#0xaaaa
    bl _D
    mov x0,#0xaaaa
    //ldr x30,[sp]
    //add sp,#0x10 ;等价
    ldr x30,[sp],#0x10
    ret

_D:
    mov x0,#0xbbbb
    ret
汇编-函数本质
viewDidload中下一条指令
汇编-函数本质
fp指向的内存
汇编-函数本质
栈中存储的上一个函数对应`lr`值

这个时候进入Dlr值已经发生变化。

汇编-函数本质
D中lr值

返回C中继续执行,这个时候执行到ret的时候lr已经恢复了。

汇编-函数本质
C中恢复lr值

继续执行正常返回viewDidload了,这个时候死循环就已经解决了。

⚠️:在函数嵌套调用的时候,需要将x30入栈!开辟空间需要16字节对齐。如果开辟8字节再读的时候会坏地址访问。写的时候没问题。

汇编-函数本质
image.png

函数的参数和返回值

先看下系统的实现:

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

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

汇编-函数本质
image.png

可以看到变量1020分别存入了w0w1
sum调用如下(release模式下编译器会优化):

TestDemo`sum:
    //开辟空间
->  0x100121e68 :  sub    sp, sp, #0x10             ; =0x10
    //w0 和 w1 存入栈中
    0x100121e6c :  str    w0, [sp, #0xc]
    0x100121e70 :  str    w1, [sp, #0x8]
    //从栈中读取参数
    0x100121e74 : ldr    w8, [sp, #0xc]
    0x100121e78 : ldr    w9, [sp, #0x8]
    //参数相加存入w0
    0x100121e7c : add    w0, w8, w9
    //恢复栈空间
    0x100121e80 : add    sp, sp, #0x10             ; =0x10
    //返回
    0x100121e84 : ret

从上面可以看出返回值在w0中。那么自己实现sum函数的汇编代码:

.text
.global _suma

_suma:
    add x0,x0,x1
    ret

调用:

int suma(int a, int b);
- (void)viewDidLoad {
    [super viewDidLoad];
    printf("%d",suma(10,20));
}
汇编-函数本质
image.png

⚠️ARM64下,函数的参数是存放在X0X7(W0W7)这8个寄存器里面的。如果超过8个参数就会入栈。那么oc的方法最好不要超过6个(selfcmd)。
函数的返回值是放在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);
}

汇编-函数本质
image.png

可以看到前8个参数分别保存在w0~w7寄存器中,第9个参数先保存在w10中,然后写入x8中(这个时候x8指向sp,相当于第9个参数写入了当前函数栈中)。

TestDemo`-[ViewController viewDidLoad]:
    //拉伸栈空间,保存fp lr
    0x100f09e5c :   sub    sp, sp, #0x40             ; =0x40 
    0x100f09e60 :   stp    x29, x30, [sp, #0x30]

    //fp指向 sp+0x30
    0x100f09e64 :   add    x29, sp, #0x30            ; =0x30 
    //fp-0x8 存放x0
    0x100f09e68 :  stur   x0, [x29, #-0x8]
    //fp-0x10 存放x1
    0x100f09e6c :  stur   x1, [x29, #-0x10]
    //fp-0x8 给到 x8
    0x100f09e70 :  ldur   x8, [x29, #-0x8]
    //sp+0x10 指针给到 x9
    0x100f09e74 :  add    x9, sp, #0x10             ; =0x10 
    //x8写入 sp+0x10
    0x100f09e78 :  str    x8, [sp, #0x10]

    //adrp = address page 内存中取数据
    0x100f09e7c :  adrp   x8, 4
    0x100f09e80 :  add    x8, x8, #0x418            ; =0x418 
    //x8所指向的内容去出来
    0x100f09e84 :  ldr    x8, [x8]
    //x8写入栈中,这个时候x9指向地址,这个时候是一个新的x8
    0x100f09e88 :  str    x8, [x9, #0x8]
    0x100f09e8c :  adrp   x8, 4
    0x100f09e90 :  add    x8, x8, #0x3e8            ; =0x3e8 
    0x100f09e94 :  ldr    x1, [x8]
    0x100f09e98 :  mov    x0, x9
    0x100f09e9c :  bl     0x100f0a568               ; symbol stub for: objc_msgSendSuper2

    //sp 一直没有改变过,w0~w7 分别存放前8个参数
    0x100f09ea0 :  mov    w0, #0x1
    0x100f09ea4 :  mov    w1, #0x2
    0x100f09ea8 :  mov    w2, #0x3
    0x100f09eac :  mov    w3, #0x4
    0x100f09eb0 :  mov    w4, #0x5
    0x100f09eb4 :  mov    w5, #0x6
    0x100f09eb8 :  mov    w6, #0x7
    0x100f09ebc :  mov    w7, #0x8
    //x8 指向 sp
    ->  0x100f09ec0 : mov    x8, sp
    //参数 9 存入 w10
    0x100f09ec4 : mov    w10, #0x9
    //w10 存入 x8地址中,也就是sp栈底中
    0x100f09ec8 : str    w10, [x8]

    0x100f09ecc : bl     0x100f09de4               ; test at ViewController.m:41
    0x100f09ed0 : ldp    x29, x30, [sp, #0x30]
    0x100f09ed4 : add    sp, sp, #0x40             ; =0x40 
    0x100f09ed8 : ret    
汇编-函数本质
viewDidLoad栈空间变化

接着往下直接跳转到test函数中:

TestDemo`test:
    //开辟空间48字节
    0x100f09de4 :   sub    sp, sp, #0x30             ; =0x30 

    //从viewDidLoad栈中取数据 第9个参数(读写往高地址)
    0x100f09de8 :   ldr    w8, [sp, #0x30]

    //参数入栈,分别占4个字节
    0x100f09dec :   str    w0, [sp, #0x2c]
    0x100f09df0 :  str    w1, [sp, #0x28]
    0x100f09df4 :  str    w2, [sp, #0x24]
    0x100f09df8 :  str    w3, [sp, #0x20]
    0x100f09dfc :  str    w4, [sp, #0x1c]
    0x100f09e00 :  str    w5, [sp, #0x18]
    0x100f09e04 :  str    w6, [sp, #0x14]
    0x100f09e08 :  str    w7, [sp, #0x10]
    0x100f09e0c :  str    w8, [sp, #0xc]

->  0x100f09e10 :  ldr    w8, [sp, #0x2c]
    0x100f09e14 :  ldr    w9, [sp, #0x28]
    0x100f09e18 :  add    w8, w8, w9
    0x100f09e1c :  ldr    w9, [sp, #0x24]
    0x100f09e20 :  add    w8, w8, w9
    0x100f09e24 :  ldr    w9, [sp, #0x20]
    0x100f09e28 :  add    w8, w8, w9
    0x100f09e2c :  ldr    w9, [sp, #0x1c]
    0x100f09e30 :  add    w8, w8, w9
    0x100f09e34 :  ldr    w9, [sp, #0x18]
    0x100f09e38 :  add    w8, w8, w9
    0x100f09e3c :  ldr    w9, [sp, #0x14]
    0x100f09e40 :  add    w8, w8, w9
    0x100f09e44 :  ldr    w9, [sp, #0x10]
    0x100f09e48 : add    w8, w8, w9
    0x100f09e4c : ldr    w9, [sp, #0xc]
    //最终相加结果给 w0
    0x100f09e50 : add    w0, w8, w9
    //栈平衡
    0x100f09e54 : add    sp, sp, #0x30             ; =0x30 
    0x100f09e58 : ret  
汇编-函数本质
test栈空间

最终函数返回值放入w0中,如果在release模式下test不会被调用(被优化掉,因为没有意义,有没有对app没有影响。)

自己实现一个简单有参数并且嵌套调用的汇编:

.text
.global _func,_sum

_func:
    //sub sp,sp,#0x10
    //stp x29,x30,[sp]
    stp x29,x30,[sp, #-0x10]!
    bl _sum
    //ldp x29,x30,[sp]
    //add sp,sp,#0x10
    ldp x29,x30,[sp],#0x10
    ret
_sum:
    add x0,x0,x1
    ret

返回值

函数的返回值一般是一个指针,不会超过8字节。X0寄存器就完全够用了。如果要返回一个结构体类型超过8字节。
下面的例子(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);
}

汇编代码:

TestDemo`-[ViewController viewDidLoad]:
    0x1042b5e58 :   sub    sp, sp, #0x50             ; =0x50 
    0x1042b5e5c :   stp    x29, x30, [sp, #0x40]
    0x1042b5e60 :   add    x29, sp, #0x40            ; =0x40 
    0x1042b5e64 :  stur   x0, [x29, #-0x8]
    0x1042b5e68 :  stur   x1, [x29, #-0x10]
    0x1042b5e6c :  ldur   x8, [x29, #-0x8]
    0x1042b5e70 :  add    x9, sp, #0x20             ; =0x20 
    0x1042b5e74 :  str    x8, [sp, #0x20]
    0x1042b5e78 :  adrp   x8, 4
    0x1042b5e7c :  add    x8, x8, #0x418            ; =0x418 
    0x1042b5e80 :  ldr    x8, [x8]
    0x1042b5e84 :  str    x8, [x9, #0x8]
    0x1042b5e88 :  adrp   x8, 4
    0x1042b5e8c :  add    x8, x8, #0x3e8            ; =0x3e8 
    0x1042b5e90 :  ldr    x1, [x8]
    0x1042b5e94 :  mov    x0, x9
    0x1042b5e98 :  bl     0x1042b6564               ; symbol stub for: objc_msgSendSuper2
    //x8指向栈空间的区域,预留足够的空间
    0x1042b5e9c :  add    x8, sp, #0x8              ; =0x8 
    0x1042b5ea0 :  mov    w0, #0x1
    0x1042b5ea4 :  mov    w1, #0x2
    0x1042b5ea8 :  mov    w2, #0x3
    0x1042b5eac :  mov    w3, #0x4
    0x1042b5eb0 :  mov    w4, #0x5
    0x1042b5eb4 :  mov    w5, #0x6
    0x1042b5eb8 :  bl     0x1042b5e04               ; getStr at ViewController.m:59
->  0x1042b5ebc : ldp    x29, x30, [sp, #0x40]
    0x1042b5ec0 : add    sp, sp, #0x50             ; =0x50 
    0x1042b5ec4 : ret   

str函数:

    TestDemo`getStr:
->  0x1001d1e04 :  sub    sp, sp, #0x20             ; =0x20 
    //参数分别放入栈中
    0x1001d1e08 :  str    w0, [sp, #0x1c]
    0x1001d1e0c :  str    w1, [sp, #0x18]
    0x1001d1e10 : str    w2, [sp, #0x14]
    0x1001d1e14 : str    w3, [sp, #0x10]
    0x1001d1e18 : str    w4, [sp, #0xc]
    0x1001d1e1c : str    w5, [sp, #0x8]

    //取出来放入w9,
    0x1001d1e20 : ldr    w9, [sp, #0x1c]
    //存入x8,也就是上一个栈中直到写完
    0x1001d1e24 : str    w9, [x8]
    0x1001d1e28 : ldr    w9, [sp, #0x18]
    0x1001d1e2c : str    w9, [x8, #0x4]
    0x1001d1e30 : ldr    w9, [sp, #0x14]
    0x1001d1e34 : str    w9, [x8, #0x8]
    0x1001d1e38 : ldr    w9, [sp, #0x10]
    0x1001d1e3c : str    w9, [x8, #0xc]
    0x1001d1e40 : ldr    w9, [sp, #0xc]
    0x1001d1e44 : str    w9, [x8, #0x10]
    0x1001d1e48 : ldr    w9, [sp, #0x8]
    0x1001d1e4c : str    w9, [x8, #0x14]
    //栈平衡,这里没有以 x0 作为返回值,已经全部写入上一个函数栈x8中。
    0x1001d1e50 : add    sp, sp, #0x20             ; =0x20 
    0x1001d1e54 : ret    

这里没有使用X0作为返回值,而是使用了栈空间。

汇编-函数本质
结构体返回值栈空间

如果返回值大于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));
}

⚠️:有两个函数 A BA -> B,在B执行完后A传递给B的参数释放了么?
在上面的例子中910没有释放,相当于A的局部变量。

对应的汇编代码:

TestDemo`-[ViewController viewDidLoad]:
    //函数开始
    0x100c31ee4 :   sub    sp, sp, #0x60             ; =0x60 
    0x100c31ee8 :   stp    x29, x30, [sp, #0x50]
    0x100c31eec :   add    x29, sp, #0x50            ; =0x50 

    //参数入栈
    0x100c31ef0 :  stur   x0, [x29, #-0x8]
    0x100c31ef4 :  stur   x1, [x29, #-0x10]
    //x8获取参数x0
    0x100c31ef8 :  ldur   x8, [x29, #-0x8]
    //x9指向 x29 - 0x20
    0x100c31efc :  sub    x9, x29, #0x20            ; =0x20 
    //x8 存入 x29 - 0x20
    0x100c31f00 :  stur   x8, [x29, #-0x20]

    //address page 内存中取数据
    0x100c31f04 :  adrp   x8, 4
    0x100c31f08 :  add    x8, x8, #0x418            ; =0x418 
    //x8 所指的内存取出来
    0x100c31f0c :  ldr    x8, [x8]
    0x100c31f10 :  str    x8, [x9, #0x8]
    0x100c31f14 :  adrp   x8, 4
    0x100c31f18 :  add    x8, x8, #0x3e8            ; =0x3e8 
    0x100c31f1c :  ldr    x1, [x8]
    0x100c31f20 :  mov    x0, x9
    0x100c31f24 :  bl     0x100c32584               ; symbol stub for: objc_msgSendSuper2
    //x8指向 sp + 0x8
    0x100c31f28 :  add    x8, sp, #0x8              ; =0x8 
    0x100c31f2c :  mov    w0, #0x1
    0x100c31f30 :  mov    w1, #0x2
    0x100c31f34 :  mov    w2, #0x3
    0x100c31f38 :  mov    w3, #0x4
    0x100c31f3c :  mov    w4, #0x5
    0x100c31f40 :  mov    w5, #0x6
    0x100c31f44 :  mov    w6, #0x7
    0x100c31f48 : mov    w7, #0x8
    //sp的值给x9
    0x100c31f4c : mov    x9, sp
    //9 给 w10
    0x100c31f50 : mov    w10, #0x9
    //w10写入 x9 所指向的地址
    0x100c31f54 : str    w10, [x9]
    //10 给 w10
    0x100c31f58 : mov    w10, #0xa
    //w10写入 x9 所指向的地址 偏移4个字节
    0x100c31f5c : str    w10, [x9, #0x4]
    //跳转getStr
    0x100c31f60 : bl     0x100c31e58               ; getStr at ViewController.m:31

    //函数结束
->  0x100c31f64 : ldp    x29, x30, [sp, #0x50]
    0x100c31f68 : add    sp, sp, #0x60             ; =0x60 
    0x100c31f6c : ret      

str:

TestDemo`getStr:
    //开辟空间
    0x100c31e58 :   sub    sp, sp, #0x30             ; =0x30 
    //从上一个栈空间 获取9 和 10
    0x100c31e5c :   ldr    w9, [sp, #0x30]
    0x100c31e60 :   ldr    w10, [sp, #0x34]
    //参数入栈
    0x100c31e64 :  str    w0, [sp, #0x2c]
    0x100c31e68 :  str    w1, [sp, #0x28]
    0x100c31e6c :  str    w2, [sp, #0x24]
    0x100c31e70 :  str    w3, [sp, #0x20]
    0x100c31e74 :  str    w4, [sp, #0x1c]
    0x100c31e78 :  str    w5, [sp, #0x18]
    0x100c31e7c :  str    w6, [sp, #0x14]
    0x100c31e80 :  str    w7, [sp, #0x10]
    0x100c31e84 :  str    w9, [sp, #0xc]
    0x100c31e88 :  str    w10,[sp, #0x8]

    //获取参数分别存入上一个栈x8所指向的地址中
->  0x100c31e8c :  ldr    w9, [sp, #0x2c]
    0x100c31e90 :  str    w9, [x8]
    0x100c31e94 :  ldr    w9, [sp, #0x28]
    0x100c31e98 :  str    w9, [x8, #0x4]
    0x100c31e9c :  ldr    w9, [sp, #0x24]
    0x100c31ea0 :  str    w9, [x8, #0x8]
    0x100c31ea4 :  ldr    w9, [sp, #0x20]
    0x100c31ea8 :  str    w9, [x8, #0xc]
    0x100c31eac :  ldr    w9, [sp, #0x1c]
    0x100c31eb0 :  str    w9, [x8, #0x10]
    0x100c31eb4 :  ldr    w9, [sp, #0x18]
    0x100c31eb8 :  str    w9, [x8, #0x14]
    0x100c31ebc : ldr    w9, [sp, #0x14]
    0x100c31ec0 : str    w9, [x8, #0x18]
    0x100c31ec4 : ldr    w9, [sp, #0x10]
    0x100c31ec8 : str    w9, [x8, #0x1c]
    0x100c31ecc : ldr    w9, [sp, #0xc]
    0x100c31ed0 : str    w9, [x8, #0x20]
    0x100c31ed4 : ldr    w9, [sp, #0x8]
    0x100c31ed8 : str    w9, [x8, #0x24]
    //恢复栈
    0x100c31edc : add    sp, sp, #0x30             ; =0x30 
    0x100c31ee0 : ret    
汇编-函数本质
参数超过8个&返回值大于8字节

和之前的猜测相符。

函数的局部变量

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

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

对应的汇编指令:

TestDemo`func1:
->  0x104bc5e40 :  sub    sp, sp, #0x10             ; =0x10 
    0x104bc5e44 :  str    w0, [sp, #0xc]
    0x104bc5e48 :  str    w1, [sp, #0x8]
   //局部变量c存入自己的栈区
    0x104bc5e4c : mov    w8, #0x6
    0x104bc5e50 : str    w8, [sp, #0x4]
    0x104bc5e54 : ldr    w8, [sp, #0xc]
    0x104bc5e58 : ldr    w9, [sp, #0x8]
    0x104bc5e5c : add    w8, w8, w9
    0x104bc5e60 : ldr    w9, [sp, #0x4]
    0x104bc5e64 : add    w0, w8, w9
    0x104bc5e68 : add    sp, sp, #0x10             ; =0x10 
    0x104bc5e6c : ret  

函数的局部变量放在栈里面!(自己的栈)
那么有嵌套调用呢?

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);
}

对应的汇编:

TestDemo`func1:
    //函数的开始
->  0x100781d9c :  sub    sp, sp, #0x30             ; =0x30 
    0x100781da0 :  stp    x29, x30, [sp, #0x20]
    0x100781da4 :  add    x29, sp, #0x20            ; =0x20 

    //参数入栈
    0x100781da8 : stur   w0, [x29, #-0x4]
    0x100781dac : stur   w1, [x29, #-0x8]

    //局部变量入栈
    0x100781db0 : mov    w8, #0x6
    0x100781db4 : stur   w8, [x29, #-0xc]

    //读取参数和局部变量
    0x100781db8 : ldur   w0, [x29, #-0x4]
    0x100781dbc : ldur   w1, [x29, #-0x8]
    0x100781dc0 : ldur   w2, [x29, #-0xc]

    //执行func2
    0x100781dc4 : bl     0x100781df8               ; func2 at ViewController.m:86
    //func2 返回值入栈
    0x100781dc8 : str    w0, [sp, #0x10]

    //读取参数和局部变量
    0x100781dcc : ldur   w0, [x29, #-0x4]
    0x100781dd0 : ldur   w1, [x29, #-0x8]
    0x100781dd4 : ldur   w2, [x29, #-0xc]

    //第二次执行func2
    0x100781dd8 : bl     0x100781df8               ; func2 at ViewController.m:86

    //func2 返回值入栈
    0x100781ddc : str    w0, [sp, #0xc]

    //读取两次 func2 返回值
    0x100781de0 : ldr    w8, [sp, #0x10]
    0x100781de4 : ldr    w9, [sp, #0xc]
    //相加存入w0返回上层函数
    0x100781de8 : add    w0, w8, w9

    //函数的结束
    0x100781dec : ldp    x29, x30, [sp, #0x20]
    0x100781df0 : add    sp, sp, #0x30             ; =0x30 
    0x100781df4 : ret  

可以看到参数被保存到栈中。
⚠️:现场保护包含:FPLR参数返回值

总结

    • 是一种具有特殊的访问方式的存储空间(后进先出,LIFO)
    • SP和FP寄存器
      • sp寄存器在任意时刻保存栈顶的地址
      • fp(x29)寄存器属于通用寄存器,在某些时刻利用它保存栈底的地址(嵌套调用)
    • ARM64里面栈的操作16字节对齐
    • 栈读写指令
      • 读:ldr(load register)指令LDR、LDP
      • 写:str(store register)指令STR、STP
    • 汇编练习
      • 指令:
        • sub sp, sp,#0x10 ;拉伸栈空间16个字节
        • stp x0,x1,[sp];往sp所在位置存放x0和x1
        • ldp x0,x1,[sp];读取sp存入x0和x1
        • add sp,#0x10;恢复栈空间
      • 简写:
        • stp x0, x1,[sp,#-0x10]!;前提条件是正好开辟的空间放满栈。先开辟空间,存入值,再改变sp的值。
        • ldp x0,x1,[sp],#0x10
  • bl指令
    • 跳转指令:bl标号,转到标号处执行指令并将下一条指令的地址保存到lr寄存器
    • B代表跳转
    • L代表lr(x30)寄存器
  • ret指令
    • 类似函数中的return
    • 让CPU执行lr寄存器所指向的指令
    • 有跳转需要“保护现场”
  • 函数
    • 函数调用栈
      • ARM64中栈是递减栈,向低地址延伸的栈
      • SP寄存器指向栈顶的位置
      • X29(FP)寄存器指向栈底的位置
    • 函数的参数
      • ARM64中,默认情况下参数是放在X0~X7的8个寄存器中
      • 如果是浮点数,会用浮点寄存器
      • 如果超过8个参数会用栈传递(多过8个的参数在函数调用结束后参数不会释放,相当于局部变量,属于调用方,只有调用方函数执行结束栈平衡后才释放。)
    • 函数的返回值
      • 一般情况下函数的返回值使用X0寄存器保存
      • 如果返回值大于了8个字节(放不下),就会利用内存。写入上一个调用栈内部,用X8寄存器作为参照。
    • 函数的局部变量
      • 使用栈保存局部变量
    • 函数的嵌套调用
      • 会将X29,X30寄存器入栈保护。
      • 同时现场保护的还有:FP,LR,参数,返回值。
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。