赞
踩
调用一个子函数,在内存上会入一个新的栈桢。子函数执行完了,当前栈桢会出栈。在运行时,栈桢的出栈和入栈的逻辑是怎么实现的呢?
这是一个很有趣的问题,也是一个重要的知识点,它是排查疑难Crash的必备技能。
栈桢的入栈和出栈依赖于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 c) {
}
void func1() {
int c = 7+18;
func2(c);
}
int main(int argc, char * argv[]) {
func1();
}
XCode设置Debug->Debug Workflow->Always Show Disassembly,然后真机调试运行Demo,就可以查看到每一个方法的ARM64汇编指令。
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
第5行到第7行
mov指令是给寄存器赋值,main函数调用func1时会传递3个参数,因此跳转func1前,要先将3个参数存储到寄存器w0,w1,w2.(w寄存器只占32位,也就是4个字节)
第八行
bl 0x102a41fd8 ; func1 at main.m:60
bl是跳转指令,从main函数跳转到下一个函数func1
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
第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之间的内存,主要用来存储寄存器带过来的入参、函数内的局部变量。
函数A有3个入参,每个入参占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的下一行命令。
这里有一个iOS交流圈:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
本文主要介绍调用栈的内存布局,已经ARM汇编如果使用指令进行出栈和入栈。为了方便读者理解,前面还介绍了栈的基础概念,栈在内存中的布局。
作者:Blacktea
链接:https://juejin.cn/post/6897975519048892423
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。