【iOS内功】ARM黑魔法—栈桢的入栈和出栈

2,231 阅读7分钟

栈桢之谜

调用一个子函数,在内存上会入一个新的栈桢。子函数执行完了,当前栈桢会出栈。在运行时,栈桢的出栈和入栈的逻辑是怎么实现的呢?

这是一个很有趣的问题,也是一个重要的知识点,它是排查疑难Crash的必备技能。

ARM64特殊寄存器

栈桢的入栈和出栈依赖于3个特殊寄存器,它们是fp、lr、sp,在ARM汇编里对应的是X29、X30、x31

特殊寄存器作用
LR (X30)link register 链接寄存器,保存返回上一层调用函数的地址
FP (X29)Frame point 指向栈底,保存栈桢的地址
SP (x31)Stack point 指向栈顶, 可以用来寻址
PC指向当前执行的代码的地址,我们无法访问PC寄存器
CPSR状态寄存器。不同于编程语言里面的if else.在汇编中就需要根据状态寄存器中的一些状态来控制分支的执行。

案例分析

下面基于一个Demo来分析

void func2() {
    
}

int func1(int a, int b,int c) {
    int x = 7+18;
    int y = a + b + c;
    func2();
    return y;
}

int main(int argc, char * argv[]) {
    func1(5,7,9);
}

调试汇编代码:

XCode设置Debug->Debug Workflow->Always Show Disassembly,然后真机调试运行Demo,就可以查看到每一个方法的ARM64汇编指令。

main函数汇编代码

OCSimpleTest`main:
    0x104a5a008 <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x104a5a00c <+4>:  stp    x29, x30, [sp, #0x10]
    0x104a5a010 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x104a5a014 <+12>: stur   w0, [x29, #-0x4]
    0x104a5a018 <+16>: str    x1, [sp]
    0x104a5a01c <+20>: mov    w0, #0x5
    0x104a5a020 <+24>: mov    w1, #0x7
    0x104a5a024 <+28>: mov    w2, #0x9
->  0x104a5a028 <+32>: bl     0x104a59fd4               ; func1 at main.m:60
    0x104a5a02c <+36>: mov    w8, #0x0
    0x104a5a030 <+40>: mov    x0, x8
    0x104a5a034 <+44>: ldp    x29, x30, [sp, #0x10]
    0x104a5a038 <+48>: add    sp, sp, #0x20             ; =0x20 
    0x104a5a03c <+52>: ret    

main函数指令解析

第5行到第7行

mov指令是给寄存器赋值,main函数调用func1时会传递3个参数,因此跳转func1前,要先将3个参数存储到寄存器w0,w1,w2.(w寄存器只占32位,也就是4个字节)

第八行

bl 0x102a41fd8 ; func1 at main.m:60

bl是跳转指令,从main函数跳转到下一个函数func1

函数A汇编代码

OCSimpleTest`func1:
    0x10428dfb8 <+0>:  sub    sp, sp, #0x30             ; =0x30 
    0x10428dfbc <+4>:  stp    x29, x30, [sp, #0x20]
    0x10428dfc0 <+8>:  add    x29, sp, #0x20            ; =0x20 
    0x10428dfc4 <+12>: stur   w0, [x29, #-0x4]
    0x10428dfc8 <+16>: stur   w1, [x29, #-0x8]
    0x10428dfcc <+20>: stur   w2, [x29, #-0xc]
    0x10428dfd0 <+24>: mov    w8, #0x19
->  0x10428dfd4 <+28>: str    w8, [sp, #0x10]
    0x10428dfd8 <+32>: ldur   w8, [x29, #-0x4]
    0x10428dfdc <+36>: ldur   w9, [x29, #-0x8]
    0x10428dfe0 <+40>: add    w8, w8, w9
    0x10428dfe4 <+44>: ldur   w9, [x29, #-0xc]
    0x10428dfe8 <+48>: add    w8, w8, w9
    0x10428dfec <+52>: str    w8, [sp, #0xc]
    0x10428dff0 <+56>: bl     0x10428dfb4               ; func2 at main.m:58:1
    0x10428dff4 <+60>: ldr    w0, [sp, #0xc]
    0x10428dff8 <+64>: ldp    x29, x30, [sp, #0x20]
    0x10428dffc <+68>: add    sp, sp, #0x30             ; =0x30 
    0x10428e000 <+72>: ret       
  

函数A指令解析

第1行

sub sp, sp, #0x30 ; =0x30

sub是减法指令。 SP寄存器的值向低地址偏移48个字节(0x30)。这时候SP已经指向新栈桢的顶部。

第2行

stp x29, x30, [sp, #0x20]

stp是存值指令,存2个值 存储上一个栈桢fp寄存器(x29)和lr寄存器(x30)的值,存储的位置是sp寄存器地址向高地址偏移32个字节(0x20)。

这里存储上一个栈桢fp和lr的值是一个重要的设计,下一个函数执行完,读取这两个值就可以回到原来的逻辑。

偏移的方向和大小(知识点)
因为栈是从高地址向低地址生长,所以入栈时地址偏移都是负向的。ARM64里寄存器是64位,也就是8个字节,这里要存储fp和lr两个寄存器,所以偏移量是16个字节。
思考:fp_A和lrA存储时哪个在前面,哪个在后面,为什么?

第3行

add x29, sp, #0x20 ; =0x20

add是加法指令。 设置fp(x29)寄存器,将其指向sp寄存器向高地址偏移32个字节的位置(0x20)。

此时函数A的栈桢已经布局完成,fp_A指向栈底,sp_A指向栈顶,占了16个字节。上一个栈桢的fp和lr的指针存储在栈桢A之前,也占了16个字节。

思考:为什么栈桢A的空间只有32个字节?

fp到sp之间的内存,主要用来存储寄存器带过来的入参、函数内的局部变量。

函数A3个入参,每个入参占4个字节。2个局部变量,每个4字节,总共20字节。内存有字节对齐,所以总共申请了32个字节的空间。
思考:如果函数A有10几个入参,入参类型除了int,还有其他的类型,这个时候栈桢的空间会是多少呢?

第16行

ldr w0, [sp, #0xc]

ldr是取值指令。 将sp向高地址偏移12个字节(0xc)的值读出来,存储到w0寄存器。sp+0xc存的是“a + b + c”的结果,是函数A要返回的结果y。

第17行

ldp x29, x30, [sp, #0x20]

ldp是取值指令,取2个值 将sp向高地址偏移32个字节的两个值,取出来存储到fp寄存器(x29)和lr寄存器(x30)。

这里和第二行命令是一一对应的,取回main函数的fp和lr

第18行

ret

函数A栈桢出栈,执行lr寄存器指向的指令地址,也就是main函数跳转到fun_A的下一行命令。

小结

main函数调用函数A入栈过程

  1. 将传递给函数A的参数,存储到w0开始的寄存器中
  2. 保存main函数栈底指针fp和返回地址lr。
  3. 对fp和sp指针进行偏移,开辟函数A的栈桢空间

函数A执行完出栈过程

  1. 从内存中取出返回值,储存到w0寄存器里
  2. 从内存中取出main函数的fp和lr
  3. 执行lr的指令

总结

本文介绍了函数入栈和出栈的底层实现,具体使用到哪些的特殊寄存器,如何通过简单的arm指令操作,一步一步完成调用栈的管理。理解这个过程,可以欣赏到Arm设计的精妙之处,也能让我们排查疑难问题时可以俯瞰全局。

为了方便读者理解,前面有一篇文章还介绍了栈的基础概念,栈在内存中的布局,调用栈的内存布局。 【iOS内功】深入解析Crash调用栈的内存布局

参考

FP寄存器及frame pointer介绍

ARM64 Function Calling Conventions

Stack Frames

Procedure Call Standard for the ARM 64-bit Architecture (AArch64)

ARM Reference Manual:

文章汇总

iOS内功系列

【iOS内功】Crash分析模型

【iOS内功】深入解析Crash调用栈的内存布局

【iOS内功】ARM黑魔法—栈桢的入栈和出栈

架构系列

架构师常说的“技术架构”是指什么?

如何系统建设APP稳定性?

思维系列

高绩效工程师的“工作思维模型”