作者:syx______
链接:https://juejin.cn/post/6917823435905662983
本文使用的代码如下
void func3(int a, int b){
int c = a + b;
printf("--%d---", c);
}
void func2(){
int a = 1;
int b = 2;
func3(a, b);
int c = b * 2;
printf("---%d----",c);
}
void func1(){
func2();
}
对函数调用栈的认识
- 函数调用过程中,局部变量,lr(x30)函数返回值,fp(x29)寄存器不断的进栈出栈的过程.
- 函数的调用栈是从高地址往低地址分配的,是一块连续的内存.
- 函数的调用栈是高度平衡的
arm汇编的基础知识(64位系统)
1. fp(x29)寄存器
指向当前的函数调用栈的栈底(高地址)
2. sp寄存器
指向当前函数调用栈的栈顶(低地址)
3. lr(x30)寄存器
存储函数返回之后下一条指令的地址。
4. bl指令
函数调用指令
5. stp指令
将寄存器的值存储到内存
6. ldp指令
将内存的值存储到寄存器
7.sub指令
做减法
8.add指令
做加法
其它(重要)
函数a调用b,b调用c,那么b函数调用的时候,lr里面会存储a里面调用b的下一条指令地址;然后b调用c,此时lr里面又会存b调用c的下一条指令地址。此时的lr被修改了,在b执行完成之后回不到正确的地址.所以在进入函数b调用后的最开始,要将lr寄存器的值存储在内存.同理调用c的时候lr也会存储在内存,防止在后续的调用过程修改了lr的值。在b执行完返回的之前,要从内存取出下一条指令的地址存储在lr寄存器.
例如我在func2里面调用func3,func3执行完成之后,要返回到func2继续执行.在调用func3的时候,下一条执行的地址会存储在lr寄存器。
- 在调用函数func3的地方下个断点,进入汇编,输入si单步执行,执行到bl函数调用这个地方,如下图.发现下一条指令的地址为0x102fe9744.
2.输入si单步执行,会进入函数func3内如,如下图。此时查看lr寄存器的值为0x102fe9744,就是函数func3下一条指令的地址.
具体调用过程
1.调用func1
- 在调用func1的地方下个断点,进入func1内部,汇编代码如下图
执行stp之前,此时
sp=0x000000016d3877a0,
lr=0x0000000102a7d794,
fp=0x000000016d387860,
2. 移动sp向下移动16个字节,然后将x29和x30的值存储在sp上面各八个字节的位置。 此时的调用栈如下图
stp x29, x30, [sp, #-0x10]! -> 这个[sp, #-0x10]!后面有感叹号,表示sp的值会改变
这里有一个iOS交流圈:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
sp向下移动了0x10(16)个字节,同时下一条mov x29,sp将x29移动到了sp的位置,这里直接标注出来。stp将x29存储在sp开始的前8个字节,将x30存储在后8个字节,如图。同样可以使用lldb读取一下sp寄存器对应内存地址的内如,如下图.读取了16个字节,内容正好是x29和x30的值.
2.调用func2
1.开辟func2的调用栈,保存现场
1.查看一下当前fp,sp,lr的值,如下图。此时的lr已经是func1里面下一条指令的地址,sp,fp依旧是进入func1之前的地址。2. 第一条指令将sp向下移动
sub sp, sp, #0x30 -> sp = sp-0x30,就是sp像下移动0x30(3*16)个字节
- 第二条指令将x29,x30的值存储在sp+0x20开始像上16个字节的位置.
stp x29, x30, [sp, #0x20] -> [sp, #0x20]后面没有!,表示sp的值不改变
- 第三条指令将x29移动到sp+0x20的位置.这三条指令执行完成之后,此时的调用栈如下图。图中的1,2,3分别表示每一条指令执行之后的状态.
add x29, sp, #0x20
2.局部变量入栈
- 第一条指令将1存储在w8(x8的低32位)寄存器里面,第二条指令将w8寄存器的值存储在x29-0x4的位置。
mov w8, #0x1
stur w8, [x29, #-0x4]
- 同样第三条第四条指令是将2的值存储在x29-0x8的位置.这四条指令执行完后的调用栈如图,红色框为这四条指令改变调用栈状态
3.调用func3
1.开辟栈空间,保存现场
1.此时func3的汇编代码如下,并且x29 x30 sp的值如图,都是func2执行后当前寄存器的状态。2.局部变量入栈
1.后面的两条stur指令是将w0,w1也就是参数入栈。入栈之后的调用栈如下,红色框为这两条指令改变的调用栈状态.3.函数返回,栈平衡
1.再看一下func3运行完成后,调用栈的状态如何.我们知道printf函数执行完成之后,func3就执行完成了,中间那一部分就是计算并且打印。我们直接看最后3条指令。
ldp表示将sp+0x20位置的值赋值给x29和x30,从func开始的汇编我们知道,sp+0x20开始8个字节存的就是x29(func2调用栈fp栈底)的值,sp+0x20后8个字节存的就是x30(func2下一条指令的地址)的值。 add 表示将sp向上增长0x30个字节,前两条指令执行完成之后。调用栈的状态如下图。可以看到调用ret之前。sp,fp已经指向了func2的调用栈。这就是栈平衡,函数执行后会恢复执行之前的状态。ldp x29, x30, [sp, #0x20]
add sp, sp, #0x30
ret
func2调用func3后面的内容
后面的内容是一样的,就不分析了,也是局部变量入栈啊,栈回收啊这样的。
总结
函数调用栈其实就是栈顶寄存器fp(x29)、调用结束下一条指令的地址lr(x30)寄存器和函数参数、局部变量入栈,函数调用结束栈又恢复平衡的过程。调用栈是一段连续的地址,从高地址往低地址延伸。
栈溢出
理解了函数的调用栈,就不难理解栈溢出了,例如一个没有出口的递归调用,栈空间会不断的从高地址往地地址延伸,最终将栈空间消耗完了抛出异常。
该文章是笔者的从通过上面的示例代码对调用栈的理解,如果有错误的地方欢迎指正。
文章到这里就结束了,你也可以私信我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言。