函数栈帧的创建与销毁

本文详细解析C语言中函数栈帧的工作原理,包括栈帧的创建、局部变量的初始化、函数传参和调用过程。通过实例代码分析,揭示了局部变量为何会有随机值,以及函数调用后的返回机制。同时,介绍了eax、ebx、ecx等寄存器在函数调用中的作用,特别是ebp和esp寄存器在维护栈帧中的关键角色。
摘要由CSDN通过智能技术生成

前言:

我们在初学C语言的时候可能会有不少的疑问,例如:

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做?
  • 函数调用结束后怎么返回的?

下面,博主会带领大家一起通过了解函数栈帧,来一一回答上面的问题。

:博主此次用的编译环境:VS2013-X86-Debug
在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。

1.寄存器

要了解函数栈帧的创建,那必然离不开寄存器
寄存器是中央处理器内的组成部分。 寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。 在中央处理器的控制部件中,包含的寄存器有指令寄存器 (IR)和程序计数器 (PC)。

本文不必过多深入了解寄存器,只要知道寄存器集成在CPU之中以及通用寄存器即可

寄存器名称寄存器的功能
eax(针对操作数和结果数据的)累加器,返回函数结果
ebx(常存放存储器地址)基址寄存器
ecx(字符串和循环操作数)计数器
edx(存放数据)数据寄存器
ebp(表示堆栈区域中的基地址)基址指针寄存器
esp(指示堆栈区域的栈顶地址)堆栈指针寄存器
esi(常保存存储单元地址)源变址寄存器
edi(常保存存储单元地址)目的变址寄存器

本篇重点在于要掌握ebpesp 两个寄存器,这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。

2.函数栈帧

2.1什么是栈帧?

栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。
C语言中,每个栈帧对应着一个未运行完的函数

每一个函数调用,都要在栈区创建一个空间,我们称为帧,所有这个函数里面的内部变量都保存在这个帧里面。

所有的帧都存放在Stack,由于帧是一层层叠加的,所以Stack叫做栈。生成新的帧,叫做“入栈”,英文是push;栈的回收叫做“出栈”,英文是 pop。

栈的特点是,最晚入栈的帧最早出栈。

寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。
栈区的使用习惯

2.2函数栈帧的创建和销毁

下面的代码是博主对于本次了解函数栈帧的实验对象

#include<stdio.h>
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;
}

2.2.1 观察main函数的函数栈帧的准备

对于main函数的函数栈帧并不是直接就创立的,而是建立在编译器的隐藏条件上的,我们进入调试打开调用堆栈
在这里插入图片描述

从这个调用堆栈里看出main函数也是被调用了的,那么它是被谁调用了呢?

我们往上面翻,最后发现,他是被__tmainCRTStartup()函数调用了,而__tmainCRTStartup()又被mainCRTStartup 函数调用。

在VS2013中,main函数也是被其他函数调用的。

也就是说在main()函数的栈帧被创建出来之前,编译器就已经创建好了__tmainCRTStartup()的函数栈帧,
在这里插入图片描述
然后在这个基础上,在来创建我们的main()函数的函数栈帧
让我们进入代码界面感受一下吧,首先进入调试界面,然后在调试开始处的箭头位置不要动,单击鼠标右键,点击转到反汇编
在这里插入图片描述
然后就可以看到这个反汇编语言这个界面:
反汇编
然后在这个界面再次单击右键,如果显示符号名前面被勾选,那就取消它在这里插入图片描述
因为如果用符号名,不利于我们理解观察
下面是取消符号名和未取消符号名的对比:
在这里插入图片描述
在这里插入图片描述
我们可以发现,取消符号名之后,我们对于编译器的执行观察得更加清楚明白,看到它是如何将值存在栈里面的,存在栈里哪个位置的,都是一目了然。

2.2.2 main函数的函数栈帧的创建

刚开始的时候,ebp和esp是 __tmainCRTStartup()函数的函数栈帧里,对 __tmainCRTStartup()的函数栈帧进行维护,但是经过1三步走之后,ebp和esp 跑到 __tmainCRTStartup()函数的函数栈帧上面去开辟了main函数的函数栈帧,并对main函数进行维护。
最直观的表现就是他们的地址:
打开监视和内存,让我们直观感受三步走之后的变化。
这是未变化之前的地址:
在这里插入图片描述
ebp:
在这里插入图片描述
esp:
在这里插入图片描述

走完第一步压栈后:

在这里插入图片描述
我们可以对比前后发现,此时的esp的地址大小减少了4个字节,然后压栈之后,此时 esp存放的值是ebp 的地址
在这里插入图片描述
注意:栈的使用习惯是先用高地址,后用低地址,所以这里的存放ebp的地址的时候,是倒着存储的。

走完第二步mov后
mov			ebp,esp
 //就是将esp的值存放在ebp里面

这里大家可能多少看的有点懵,我们还是在内存和监视里面一起来直观感受下。

此时的ebp:
在这里插入图片描述
在这里插入图片描述
mov之后:
ebp:
在这里插入图片描述
在这里插入图片描述
ebp的地址发生了变化,它的地址比之前小,可见它在栈中在向前移,因为是将esp的值movebp,所以ebp这个栈底指针来到了esp这个栈顶指针的位置,那么,既然ebp顶替了esp,那么esp呢?
别急,马上就知道了。

最后一步:sub开辟main空间
sub				esp,0E4h
//sub 就是减法的意思,用esp - 0E4h

这里的esp的地址减去0E4h,这就解释了上面esp的去处,那么espebp之间的空间就是 main函数的函数栈帧
此时的esp:
在这里插入图片描述
在这里插入图片描述

示意图:

在这里插入图片描述
然后接下来又是三次push压栈:
在这里插入图片描述
又给栈顶压入三个元素,这三个元素我们暂不作讨论,就知道它压入进去就行了

在这里插入图片描述

2.2.3局部变量随机值

压入,三个元素之后,esp又往上走了3步,然后我们再进入到下一步:
在这里插入图片描述

lea -> load effective address//加载有效地址
就是将 [ebp+FFFFFF1Ch]存到edi里面

我们就这样是看不出来它到底加上了多少,但是我们在将它变成在显示符号的情况下,我们就能看到了
在这里插入图片描述
它是一个八进制数:
在这里插入图片描述
这样我们就清楚edi里面存放的多少

mov ecx,39h
// 39h->0x00000039
//ecx是计数器寄存器,这里存放0x00000039次
mov eax,0CCCCCCCCh
//将0CCCCCCCCh 存放到累加寄存器eax里
rep stos dword ptr es:[edi]
//意思是从ebp-0E4h及其以下,全部转换成CCCCCCCC
在这里插入图片描述

这也就解释了为什么局部变量在没有初始化的前提下是随机值的问题。

2.2.4局部变量的创建

在这里插入图片描述
[ebp-8] //a
[ebp-14h] //b
[ebp-20h] //c
这里就很明显的诠释了栈区的使用规则,是先用高地址,再用低地址
局部变量的创建,就是建立在函数栈帧的前提下,再寻找合适的空间将其放进去。
在这里插入图片描述

2.2.5函数传参和函数调用

在这里插入图片描述
这里是先存放b的值将它的值放入eax(y)里面,然后eax压栈压入进去,再将a的值放入ecx里面,然后压栈,将ecx(x)压入进去。
传参
这里这个过程就叫做传参。
传参传完之后,来到了

call 003B10E1

这条指令,这里的003B10E1 就是call指令的下一个指令的地址,
在这里插入图片描述
call指令这里可以正式进入到Add函数里面去,再进去之前,会先将003B10E1压栈,这里有什么用一会我们再来讨论。
在这里插入图片描述

Add函数的函数栈帧的创建

在这里插入图片描述
这里的Add 的函数创建和main函数一样

先将ebp的地址压入栈中,然后将esp的地址传给ebp,

再用esp的地址减去0CCh 从而得到Add函数的函数栈帧,

然后再次压入ebx,esi,edi。

然后再将[ebp-0CCh]传给edi

将33h存入ecx

eax中存放,0CCCCCCCCh

rep stos dword ptr es:[edi]
给Add函数自[ebp-0CCh]起及其以下,全部填入CCCCCCCC

mov dword ptr [ebp-8],0//创建局部变量z

终于来到了Add 函数的核心z=x+y;
这里它的函数里面似乎并没有直接接受想,传参,那么它是如何调用,函数的呢?

mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
//这里将x和y的值都存放到eax里面,在eax里面完成加法运算。
从这里也可以看出,我们在创建这个函数的时候,根本没有创建形参
形参是在我们刚开始调用这个函数的时候就已经创建好了

在这里插入图片描述
我们提前将 a 和 b 拷贝了一份放在 ecx 和 eax 里的,后面即便我们对 ecx 和 eax 做出修改,也不会影响到原来的 a 和 b 。
从这里也可以看出,实参传给形参时,形参只是实参的一份临时拷贝。

mov dword ptr [ebp-8],eax //把算术结果传给z
mov eax,dword ptr [ebp-8]//将[ebp-8]存放到寄存器eax里。
存放到寄存器里面,即便空间被销毁,数据也不会丢失

2.2.6函数栈帧的销毁

Add函数的函数栈帧的销毁

在这里插入图片描述
这里的三次pop,就是三次释放空间,
第一次:
在这里插入图片描述

第二次pop:
在这里插入图片描述
第三次pop:
在这里插入图片描述
执行到了这里的时候,怎么释放Add函数的函数栈帧呢?
这时候,编译器将 ebp 的值赋给了 esp,让esp 直接来到
Add函数的栈底,直接释放了Add函数的空间

最后pop掉 原先压栈压进来的ebp , 让 ebp 直接返回到main函数的栈底。
结束掉了Add函数的生命

那么结束掉了 Add函数的生命之后,main 函数还要继续运行啊,怎么办呢,这时候,我们之前压入的 call 指令的下一个指令的地址,就起到作用了
在这里插入图片描述

在pop完了之后,指令
在这里插入图片描述
利用上面的地址,直接让函数回到了之前运行的地方。
在这里插入图片描述
这里这条语句释放掉了形参
在这里插入图片描述
最后这里的 eax 寄存器里面的值传给了c
在这里插入图片描述

总结

局部变量是怎么创建的?

在函数栈帧被分配一定空间后,在函数栈帧里面分配一定的空间存放局部变量

为什么局部变量的值是随机值?

在函数的函数栈帧分配完之后,函数栈帧的所有空间会被赋值有意义的地址,在划分完地址之后,前面的文章里说过,会被赋值CC CC CC CC这样的值

函数是怎么传参的?传参的顺序是怎样的?

函数传参是在原来的主函数上面压栈,压入空间,来存放形参,传参顺序满足栈的先进后出规律,如前文提到的实参a
和实参b,因为实参a先被存入函数栈帧中,所以后传参。

形参和实参是什么关系?

形参是实参的一份临时拷贝

函数调用是怎么做?

在调用函数之前,编译器汇通过call 指令向栈中压入一个地址,这个地址是函数调用指令的下一条指令,在创建好地址之后,开辟需要调用的函数的函数栈帧,再在函数栈帧里面通过寄存器来完成一系列计算存储操作,从而完成函数调用。

函数调用结束后怎么返回的?

详情请看 2.2.6 函数栈帧的销毁




  1. 在这里插入图片描述 ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值