Bootstrap

超详细C语言函数栈帧的创建和销毁的过程讲解,细节满满

目录

一、ebp和esp

二、main函数的函数栈帧(解释反汇编)

(1)、push操作

(2)、mov操作

(3)、sub操作

(4)、然后又是三个push操作

(5)、lea操作:

(6)、接着四步操作:

三、如何创建的变量(创建变量的反汇编)

(1)、第一个int a = 10:

此时如果没有给a变量赋值,那么该位置的值就是CCCCCCCC,这就是为什么未初始化的局部变量默认为随机值的原因。

(2)、变量b、c创建的过程同理:

四、函数调用、传参过程

(1)、对变量b操作

(2)、对变量a操作

执行call指令后,call指令还会将它下一条指令的地址压栈:原因是因为,当我们把调用的函数执行完后,我们需要回到main函数,就会通过这个地址回来。

五、被调用函数的栈帧

(1)、变量的相加

此时我们就可以理解:形参是实参的临时拷贝

六、函数返回值的过程

此时我们就可以理解为什么函数调用结束后,会把局部变量给销毁,但也可以把值传出来,因为值是放在了eax寄存器里面保存起来了。

(2)、pop:弹出出栈 

(3)、回收函数空间:

此时执行这条指令就会返回到刚开始call指令的下一条指令 :


(1)、首先要注意函数栈帧在不同的编译器下面都有所差异

(2)、首先我们需要认识几个寄存器eax、ebx、ecx、edx、ebp、esp。这些会在后续讲解中出现,其中最重要的就是ebp和esp。

(2)、ebp、esp里面存放的是地址,esp存放低地址,ebp存放高地址,,这两个地址是用来维护函数栈帧的。

一、ebp和esp

下面用一段简单的代码来介绍:

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);
	return 0;
}

注意每个函数调用,都要在栈区创建一个空间:

当前我们在调用main函数,所以在栈区开辟了一块空间,esp就指向这块空间的低地址,ebp指向这块空间的高地址用于维护这块空间,在调用哪个函数,这两个寄存器就去维护哪块空间。又因为栈区的空间是从高地址向低地址使用的,所以esp被成为栈顶指针,ebp被称为栈低指针。

二、main函数的函数栈帧(解释反汇编)

首先我们看到要知道在vs2013中main函数还会被其他函数调用:

然后我们来看main函数的反汇编:

刚开始还在是调用main函数那个函数的栈帧:

(1)、push操作

即压栈,意思是把ebp压入栈顶。

(2)、mov操作

可以理解成赋值,即将esp赋值给ebp指针,这样ebp也指向esp栈顶

(3)、sub操作

即减操作,意思是将esp减去0E4h(这是十六进制数字)地址,因为栈区是高地址向低地址使用,所以esp会往上走指向某个地址。

此时紫色这块空间其实就是为main函数申请的空间(main的栈帧)

(4)、然后又是三个push操作

注意push有个动作,push后esp指针会跟着动,所以push完后如下:

(5)、lea操作:

加载有效地址,即将后面那个地址放进edi里面去

(6)、接着四步操作:

更改相应指针的值后,将edi指向的地址向下39h个位置,每dword为一个单位(dword叫双字,即四个字节)的值全部改为0CCCCCCCCh的值。(刚好等于刚刚给main函数开辟的栈帧空间)

此时给main函数开辟栈帧这个准备就准备完了。

三、如何创建的变量(创建变量的反汇编)

main函数栈帧准备完后就开始创建变量了:

(1)、第一个int a = 10:

是将0Ah(十进制就是数字10),mov(移动或赋值)到[ebp-8]的位置,即main函数栈帧中的一个位置:

此时如果没有给a变量赋值,那么该位置的值就是CCCCCCCC,这就是为什么未初始化的局部变量默认为随机值的原因。

(2)、变量b、c创建的过程同理:

四、函数调用、传参过程

(1)、对变量b操作

首先把ebp-14h里面的值放进eax,即把变量b的值20放进eax,然后将eax压栈,即将20压栈,因为push操作,注意esp也会做出相应移动:

(2)、对变量a操作

这两条指令与上面同理:

(3)、call操作,即调用,此时按快捷键f11进入

执行call指令后,call指令还会将它下一条指令的地址压栈:原因是因为,当我们把调用的函数执行完后,我们需要回到main函数,就会通过这个地址回来。

接着再按下f11就会进入被调用函数的栈帧

五、被调用函数的栈帧

通过上述步骤后就来到了被调用函数的栈帧:

我们会发现前面准备工作的几条指令和main函数栈帧都差不多,在为该函数分配内存开辟空间:

(1)、变量的相加

因为前面操作都Main一样,所以直接跳到变量相加的地方,此时形参x,y就出现了

结合上面我们知道ebp+8就是变量a传参的位置:

同理ebp+0ch(ebp+12)就是变量b传参的位置:

然后将相加的值(add操作)放到ebp-8的位置,即赋值给变量z。

此时我们就可以理解:形参是实参的临时拷贝

六、函数返回值的过程

 (1)、第一个操作,把ebp-8(即z的值(返回值))转移到eax寄存器。

此时我们就可以理解为什么函数调用结束后,会把局部变量给销毁,但也可以把值传出来,因为值是放在了eax寄存器里面保存起来了。

(2)、pop:弹出出栈 

 出栈后,esp指针会跟着向下移动,此时把三个寄存器给出栈了

(3)、回收函数空间:

到这就开始回收空间了,把ebp的值给esp:

根据上述,我们知道ebp在Add函数栈帧的下面:

然后又执行pop指令:

把ebp给pop了,而这个位置我们放的是main的ebp地址,所以把这个ebp弹出后,ebp指针就会找到main函数的ebp地址去,然后有pop操作,esp也会移动。

此时就彻底回到main函数的栈帧了

(4)、ret操作:

此时执行这条指令就会返回到刚开始call指令的下一条指令 :

在继续执行add指令,此时形参就销毁了

;