函数栈帧的创建和销毁

函数栈帧的创建和销毁在所有编译器中都是大同小异的,不同的编译器会有不同的方式,但是了解到了简单的底层的这些方法后,其他的编译器都是在此基础上修饰,不必深究。

1、寄存器
ebp,esp 这两个寄存器中存放的是地址,用来维护函数栈帧
2、编译器的选择
最好使用visual 6.0来观察,它更加简洁,我们用到的是vs2013,因为越早的编译器观察到的过程越不复杂。

我们来给到一个简单的加法函数

#include
int Add(int x, int y)
{
    int z = 0;
    z=x+y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c =Add(a, b);
	printf("%d",c);
	return 0;
}

最粗略的整体的逻辑

我们知道每一个函数调用都要在栈区创建一块空间,一般是由高地址向低地址使用,main函数的使用也要开辟栈帧
函数栈帧的创建和销毁_第1张图片
esp存入函数低位置的地址,叫做栈顶指针,ebp存入函数高位置的地址,叫做栈底指针。
我们用调试的方法来观察过程。
函数栈帧的创建和销毁_第2张图片
main函数也是被调用的,被 __tmainCRTStartup() 这个函数调用,而这个函数又被下面的 mainCRTStartup() 调用,这样也就是能够解释为什么我们的main方法要return 0 了,它返回到了调用它的函数 __tmainCRTStartup()里面
函数栈帧的创建和销毁_第3张图片

当然在一开始的时候我们也会为这两个函数创建空间,在main函数之前
函数栈帧的创建和销毁_第4张图片
调用Add函数时再创建空间
函数栈帧的创建和销毁_第5张图片
汇编语言的指令
打开反汇编,我们可以看到汇编语言对程序的操作,这里push叫压栈,push ebp就是将一个叫做ebp的量压到栈顶上边(这里涉及到监视窗口可以监视到ebp确实是地址小于的正好在 __tmainCRTStartup() 函数的上方,有兴趣的大家可以打开监视窗口查看一下,这里我们为了缩短篇幅只讲结果)(与push相对的叫做pop,出栈,从栈顶删除一个元素)
函数栈帧的创建和销毁_第6张图片

在我们创建 __tmainCRTStartup() 这个函数时,接着push ebp 如下两图所示
(因为esp维护的是栈顶,所以push之后esp要跟着变化)

函数栈帧的创建和销毁_第7张图片
函数栈帧的创建和销毁_第8张图片

然后mov ebp,esp 是把esp的值给ebp,此时两个值相等,同时指向上图esp所指的地方。

第三条指令sub是减法,就是让esp-0E4h,改变栈顶的地址,esp指向了上边的某一区域(这里不会越出界限)
函数栈帧的创建和销毁_第9张图片

紫色即main函数的栈帧

然后把ebx,esi,edi 三个push上去
函数栈帧的创建和销毁_第10张图片

lea 即 load effective address 加载有效地址
第四条指令即将后边的ebp+FFFFFF1Ch加载到edi里边去,即ebp-0E4h
然后将39h和0CCCCCCCCh这个数字分别给到ecx和eax。(这里不需要管这两个数字是什么)

word是两个两个字节,dword 就是 double word 四个字节
然后将这从edi开始向下39h个数字全部变成0CCCCCCCCh,即下图区域
函数栈帧的创建和销毁_第11张图片
以上就是为了正式的代码做铺垫

然后就是赋值a赋值b
然后进入到Add函数中

函数栈帧的创建和销毁_第12张图片
传参过程
然后mov push 给到eax和ecx
函数栈帧的创建和销毁_第13张图片
函数栈帧的创建和销毁_第14张图片
call是调用函数,它会压栈一个00C21450,这是call指令的下一条指令,以便call返回时继续使用
函数栈帧的创建和销毁_第15张图片
这里的汇编语言指令在前面都说到过,我们跳过继续说
函数栈帧的创建和销毁_第16张图片
函数栈帧的创建和销毁_第17张图片
注意这里先传b再传a,传参的顺序是从右往左的,在汇编指令中我们可以很明显的发现,传参的方式,就是调用实参出来给到形参,而不是形参的单独创建,这有利于我们了解形参和实参的关系。
形参是实参的一份临时拷贝(中肯的一针见血的)
这里我们会有一个疑问,为什么return z 之后这个函数不是销毁了吗?那值不是也会随之销毁吗?
其实这里看到return z 后面的这个汇编指令,它把z的值(z的地址就是ebp-8)赋值给了eax,销毁之后再把eax的值传出去
三个pop将他们逐出去了
函数栈帧的创建和销毁_第18张图片
它们就被回收了
然后把ebp赋值给ebp,此时两个指针指向相同的地方,上面的函数所占的空间被系统回收了,即Add函数被回收了
函数栈帧的创建和销毁_第19张图片
此时再pop最上面的ebp,这里面存放的是最开始main函数的栈底指针,这样我们就能很容易的找到main函数栈底的位置,此时ebp指针回到这个位置,然后esp指向00C21450的这个位置,并且此时两指针之间的这一区域就是main函数所占的区域。
然后ret返回到call指令的下一条指令,即00C21450,然后将此地址也弹出,指针指向下一位(标黄)
函数栈帧的创建和销毁_第20张图片

函数栈帧的创建和销毁_第21张图片
然后下一步将形参x,y所占的空间释放esp+8,往下走,,然后就把eax的值给到ebp-20h了,也就是z的值给了c:z在销毁前把值传给eax,eax在00C21453这一步时将值传给ebp-20h,在这个位置的值就是c。

到现在,我把函数栈帧的创建和销毁的过程大致梳理了一遍,我在学完之后有一种恍然大悟的感觉,希望这篇能够帮到大家。

你可能感兴趣的:(小有用处的底层逻辑,算法,c语言,学习方法,程序人生,visual,studio)