作者: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();
}

对函数调用栈的认识

  1. 函数调用过程中,局部变量,lr(x30)函数返回值,fp(x29)寄存器不断的进栈出栈的过程.
  2. 函数的调用栈是从高地址往低地址分配的,是一块连续的内存.
  3. 函数的调用栈是高度平衡的

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寄存器。

  1. 在调用函数func3的地方下个断点,进入汇编,输入si单步执行,执行到bl函数调用这个地方,如下图.发现下一条指令的地址为0x102fe9744.

2.输入si单步执行,会进入函数func3内如,如下图。此时查看lr寄存器的值为0x102fe9744,就是函数func3下一条指令的地址.

具体调用过程

1.调用func1

  1. 在调用func1的地方下个断点,进入func1内部,汇编代码如下图

执行stp之前,此时
sp=0x000000016d3877a0,
lr=0x0000000102a7d794,
fp=0x000000016d387860,

这几个值保存的是调用func1函数的时候,当前寄存器的值。我们对此时的调用栈画一个图如下。

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的值.

3.执行一下si.调用func2之前,我们此时我们看一下寄存器的值,如下图,此时的调用fp,sp的值就是我们上面调用栈所指向的位置。 并且下一条指令的地址为0x102a7d770,我们可以在进入func2之后,查看一下lr寄存器的内容

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)个字节

  1. 第二条指令将x29,x30的值存储在sp+0x20开始像上16个字节的位置.

stp x29, x30, [sp, #0x20] -> [sp, #0x20]后面没有!,表示sp的值不改变

  1. 第三条指令将x29移动到sp+0x20的位置.这三条指令执行完成之后,此时的调用栈如下图。图中的1,2,3分别表示每一条指令执行之后的状态.

add x29, sp, #0x20

5. 此时读取一下x29 x39 sp寄存器的值如下.是和我们的调用栈对应的
6. 读取一下sp+0x20存储的值,因为我们在第3步第二条指令将x29,x30存储在该位置了。可以看到存储的值就是第1步,func1执行之前fp,lr的值
2.局部变量入栈
  1. 第一条指令将1存储在w8(x8的低32位)寄存器里面,第二条指令将w8寄存器的值存储在x29-0x4的位置。

mov w8, #0x1
stur w8, [x29, #-0x4]

  1. 同样第三条第四条指令是将2的值存储在x29-0x8的位置.这四条指令执行完后的调用栈如图,红色框为这四条指令改变调用栈状态
3. 此时查看一下x29-0x4和x29-0x8位置的值。int占4个字节,前面4个字节存储的是1,后面4个字节存储的是2
4.然后通过ldur指令将1存储在w0里面,将2存储在w1里面。接着调用func3.此时查看一下x29 x30 sp的值如下。此时的值和我们的调用栈是对上的

3.调用func3

1.开辟栈空间,保存现场
1.此时func3的汇编代码如下,并且x29 x30 sp的值如图,都是func2执行后当前寄存器的状态。
2.开始的三条指令同样和func2的三条指令是一样的,sp向下移动0x30个字节,x29,x30入栈,移动x29.执行完之后调用栈如下图.红色框为func3的调用栈
3.执行完前三条指令之后x29 x30 sp的值为.和我们分析的调用栈是一致的。
2.局部变量入栈
1.后面的两条stur指令是将w0,w1也就是参数入栈。入栈之后的调用栈如下,红色框为这两条指令改变的调用栈状态.
2.读取一下fp-0x8位置的内容,刚好就是存的值为1和2,就是我们的调用栈标出来的值。
3.函数返回,栈平衡

1.再看一下func3运行完成后,调用栈的状态如何.我们知道printf函数执行完成之后,func3就执行完成了,中间那一部分就是计算并且打印。我们直接看最后3条指令。

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

ldp表示将sp+0x20位置的值赋值给x29和x30,从func开始的汇编我们知道,sp+0x20开始8个字节存的就是x29(func2调用栈fp栈底)的值,sp+0x20后8个字节存的就是x30(func2下一条指令的地址)的值。 add 表示将sp向上增长0x30个字节,前两条指令执行完成之后。调用栈的状态如下图。可以看到调用ret之前。sp,fp已经指向了func2的调用栈。这就是栈平衡,函数执行后会恢复执行之前的状态。
2. 看一下前两条指令执行完成之后,x29, x30 , sp的值。和我们分析的调用栈是吻合的。
3. ret表示返回,该指令执行后pc(程序执行的当前指令的地址)寄存器会从lr寄存器开始执行。我们发现lr的值为0x102a7d730.我们输入si单步执行,看一下下一条执行的指令的地址.如下图。跳转到了func2里面调用func3之后的下一条指令的地址执行。就是lr里面的内容

func2调用func3后面的内容

后面的内容是一样的,就不分析了,也是局部变量入栈啊,栈回收啊这样的。

总结

函数调用栈其实就是栈顶寄存器fp(x29)、调用结束下一条指令的地址lr(x30)寄存器和函数参数、局部变量入栈,函数调用结束栈又恢复平衡的过程。调用栈是一段连续的地址,从高地址往低地址延伸。

栈溢出

理解了函数的调用栈,就不难理解栈溢出了,例如一个没有出口的递归调用,栈空间会不断的从高地址往地地址延伸,最终将栈空间消耗完了抛出异常。

该文章是笔者的从通过上面的示例代码对调用栈的理解,如果有错误的地方欢迎指正。

文章到这里就结束了,你也可以私信我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言。