函数栈帧的创建和销毁

我们用一个简单的求和函数来探索函数的调用过程

#include<stdio.h>
#include <stdlib.h>

int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 20;
    int sum = Add(a, b);
    printf("sum=%d\n", sum);
    system("pause");
    return 0;
}

按F10进入调试,并转到反汇编。


首先是main函数调用的过程。

1.ebp和esp是用来维护函数的栈底指针和栈顶指针,push ebp,即压栈,将__mainCRTStarup函数的ebp压栈,在它的栈顶开辟一块空间放入它的ebp;

2.将esp给ebp,此时ebp与esp同时指向刚刚开辟空间的顶端;(创建栈帧的过程)

3.esp减去4Ch大小的值,我们知道栈空间中元素存放顺序是由高地址到低地址,则该步骤在ebp的上面开辟了4Ch大小的内存空间;

4.ebx,esi,edi三块空间进行压栈,随着压栈的进行,esp指向edi的顶端;

5.将ebp-4Ch这个地址放到edi中(方便之后ret步骤的使用);

6.lea 加载有效地址初始化与开辟空间为0Xcccccccc。

7.创建变量a,b,ret。


8.“ebp-0Ch”放入了0,执行int ret =0这一步骤。

9.“ebp-8”存放着b的值,将它mov给eax寄存器并且压栈;同理“ebp-4”存放着a的值,将它mov给ecx寄存器并且压栈。

10.接着调用call指令。注意,在调用call指令的时候,在ecx的上方又开辟了一块空间用于存放call指令下一条指令的地址(这个地址的作用是在call指令调用add函数结束的的时候jump指令能够找到call指令下一条指令的地址,从而回到main函数中),很关键。

11.由call指令进入add函数之后,第一步就是进行压栈,将main函数的ebp压栈保存在上面开辟的空间中。

12.接下来函数开辟空间和初始化跟main函数是一样的。

13.创建z空间,地址为abp-4,放进去内容为0;

14.“ebp+8”指向x=10的存储空间,将10放到寄存器eax中;同理“ebp+0Ch”指向y=10的存储空间,将20add到eax中,此时eax中的内容为30;

15.将eax给“ebp-4”,即z这块空间,意思为将z的内容由零改为30;

16.将z的值放回到eax中,这一步骤代表着返回机制,z的值将由寄存器eax带回到main函数中。

17.之后执行pop出栈操作,edi,esi,ebx三块空间出栈后,esp向下移动,这三块空间已经不属于add函数,但是依然存在。

18.将ebp给esp,此时esp回退到当前ebp的位置。

19.之后对下一块空间(即保存着main函数ebp的空间由)执行出栈操作, 由于保存的是main函数的ebp,栈底指针ebp得以回到main函数ebp的位置。

20.执行ret指令,相当于继续pop下一块空间,我们知道存放main函数ebp空间的下一块空间,存放的是call指令下一条指令的地址,那么在ret执行完成之后,程序返回到main函数中call指令的下一条指令。

21.call指令的下一条指令就是“add  esp,8”,即将esp向下移动8个字节,将刚刚调用的两个实参10,20也弹出去。

22.把eax的值放到“ebp-0Ch”(即c的空间)中,一致eax中存着30,至此c的值已被赋值为30。

23.eax寄存器已经没有用处,需要归零,我们执行xor(异或)这一步骤,两个相同的eax异或归零。

24.接下来就是栈的销毁。


猜你喜欢

转载自blog.csdn.net/zhangtianqiang1314/article/details/79998023