谈谈C语言函数栈帧创建和销毁(细)

前言

我们都知道函数是在栈区开辟空间的,但你是否知道:

  • 函数栈帧是什么?
  • 函数是如何在栈区开辟空间的呢?
  • 函数的实参是如何传参的?传参的顺序如何?
  • 为什么函数形参无法改变外部的变量?
  • 为什么说实参是形参的一份临时拷贝? 形参和实参的关系又是什么?
  • 函数是怎么调用的?
  • 函数调用是如何返回值的?
  • 局部变量是如何在栈开辟空间的?
  • 为什么说局部变量的值是随机的?

这篇文章都已给你解答这些问题,接下来跟着我一起走下去,让我们一起进入函数是如何在栈区中玩耍的旅途。我们慢慢来回倒这个问题

🦻🦻🦻注意:测试环境是在vs2013编译器下的,其他编译器和本编译器可能会有略微数据或者界面等差异,但是大体逻辑上是没有变的



简单的小常识:
👀:eax,ebx,ecx,edx,这些寄存器都是用来存放数据的;可以存放地址,可以存放值。

👀:esp 是存放栈顶指针,ebp是存放栈底指针。

👀:栈区的使用特点是:从高地址向低地址使用,并且当压栈时候,寄存器essp–;弹栈时候,esp++;


main函数也是被调用的

我们写的程序都是从main函数入口的,程序的开始也是从main函数入口进去,但是main函数其实也是被别的函数调用起来的,我们来看看具体代码,是谁调用了main函数;

操作步骤:

  1. 在vs2013按下 F10,开始逐步调试,进入主函数,然后,按下图点击进入调用堆栈的选项:
    在这里插入图片描述
    然后观察那个黄色的小箭头,就是调试的按钮,一直按F10,直到黄色小箭头到达右花括号 },这说明,main函数执行完毕,也就是说明,main函数被调用完毕,但是被谁调用的呢?
    在这里插入图片描述
    我们再继续按F10,此时后,我们就会跳转到这个页面:这个页面就显示着是main函数被调用了,在调用堆栈我们可以看到,这个main函数是被一个名为:__tmainCRTStartup() ;的函数调用。
    在这里插入图片描述
    其实继续下去,还可以继续看到__tmainCRTStartup()函数也是被调用的,但是我么那就看下去了,这里我是想要说明什么问题呢?

在main函数被调用时候,我们是在栈空间开辟一段空间,并且用,esp指针和ebp指针,指向这段空间给main函数去使用

函数栈帧

👀:什么是函数栈帧呢?

在上面我们说到,main函数被调用时候,会在栈开辟一段空间,并且这段空间是由两个指针,esp和ebp来维护的,被维护的这段空间,我们就叫做main函数的栈帧;就是可以给main函数去自由使用的空间。

如下图:
在这里插入图片描述
esp 和 ebp两个指针,是哪个函数正在被调用,就用来维护哪一段空间

👀并且发现了吗? 栈顶指针esp是在低地址,栈底指针ebp是在高地址,当我们压栈时候,也是从高地址往低地址压栈。


函数的创建调用过程

接下来我以一段简单的加法函数来讲解函数的创建过程:

# 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(10, 20);
	return 0;
}

我们知道,当我们开始调用代码时候,从main函数入口进去,一旦进入main函数,就为main函数开辟了一段空间,用栈顶和栈顶指针去维护这段main函数的空间,一旦当程序走到,int c = Add(10,20);时候,Add函数被调用起来,一旦进入到Add函数,就会在栈区就用栈顶着栈底指针去指向这一段空间,用来维护Add函数。
大概是这个过程:

维护的细节到底是怎么样的?让我们来看看吧。
首先先按F10开始调试,然后,在一个空白的界面右击鼠标,点击
转到反汇编

在这里插入图片描述
然后就会弹出一个反汇编的界面,这个界面是程序对应的汇编代码:
如下图:
在这里插入图片描述
不要被汇编代码吓走了,你看到这里,我可以保证你看的懂,就很简单的汇编指令,不过你现在包鼠标放到反汇编界面,右击鼠标,把那个显示符号名的选项去掉,这是为了看到偏移地址的位置,不看变量名。
在这里插入图片描述
去掉之后,带你简单熟悉以下汇编的界面:
大概布局就是这个模样,至于指令是什么功能接下来一步一步的分析;
在这里插入图片描述
接下来我把机器码的选项勾走不要,因为和接下来讲的内容关系不大;


在调用main函数之前,也就是我们即将进去main函数的时候,我们是不是说了,有一个函数会调用main函数呀,同时这个__tmainCRTStartup()函数,也会在内存开辟一段空间,用esp和ebp维护:
在内存的布局如下:
在这里插入图片描述
接下来我们看main函数的汇编代码:
push ebp,这是什么什么意思呢?就是把ebp的值压入栈中,压到哪儿呢?压到栈顶。同时,esp指针会向上移动一位。
同时注意:不要以为压栈了,就把ebp的指向给改变了,只是把一个ebp的值压栈而已;
如下图:
在这里插入图片描述
我们监视看看还没有执行push ebp时候 esp 和 ebp的值是多少,这个值,也就是维护__tmainCRTStartup()函数的栈指针。
在这里插入图片描述
一旦我们按F10也就是执行了push ebp操作,你看esp的值发生了变化,并且,这个值是变小的,这说明压栈进去了,且小了4个字节
在这里插入图片描述
如果你还是觉得不像压栈了,那我们从内存角度看看,esp指针指向的位置,里面存放的值是否和ebp相等,如果相等这就说明了一个问题:我们执行push ebp操作确实是把 ebp压栈进去了。
在这里插入图片描述
好,理解了上面的push ebp,接下来就是 mov ebp,esp这是什么意思呢?就是mov 指令把 esp的值移动到,存放到ebp寄存器里面,那我们进一步思考 把esp值给ebp,而且espebp又是栈指针,这类操作不就是像 ebp = esp吗,这说明什么问题?这就说明 ebp指针指向了esp指针的内存空间啊.
在这里插入图片描述
所以我们从监视看,按下F10,即一旦执行 mov ebp,esp操作,就会看到ebp的值和esp值相等,这里的值是地址。
在这里插入图片描述
好的,我们继续,这时候接下来的指令是 sub esp,0E4h ,这又是怎么理解sub这个指令?就是用esp寄存器的值减去0E4h,之后赋值给esp ,等价这条语句:esp = esp - 0E4h; 这个0E4h就是表示十六进制的数字;
那这条指令的作用在内存发生了什么?esp是一个指针,当指针减去一个数时候,就会发生偏移到这个esp-0E4h地址,也就是说,在内存中,这个栈底指针esp指向了 esp-0E4h这个内存地址;由于地址是减小了,所以esp指向了更低的地址,在内存的大概图如下:
在这里插入图片描述

我们回头看一看,是不是突然发现ebp和esp不再维护__tmainCRTStartup()函数的空间地址了?
使得就是不在维护了,那维护了谁?还记得是我们刚刚是从什么函数入口的吗?对,就是main函数入口的,这就是esp和ebp维护的新空间,即main函数的栈帧;
这就是传说中的当调用函数时候,就会在栈空间开辟一段内存空间


当我们按了F10,观察监视esp的值,会变小,变小了多少呢?就是小了0E4h的值;
在这里插入图片描述
我们从内存角度看看,esp指向的地方和ebp指向的地方之间就是main函数栈帧空间,那么在内存中到底有多少呢?只要我们找到esp和ebp对应的指针地址,就可以知道它们之间的地址了。
在这里插入图片描述
你有没有发现,哇哦,好长啊,居然可以为一个main函数开辟那么多空间。嗯,确实,我也觉得好长。
接下来就继续指向压栈了,看汇编指令push ebx push esi push edi,就是往栈顶压栈,先不用管这是什么意思,,只需知道,在栈空间,ebx esi edi这个三个寄存器的值依次被压入栈了,同时不要忘记栈顶指针esp也会跟着指到栈顶。
在这里插入图片描述
继续按F10,按3下,把刚刚三条指令指向完,观察esp栈顶指针的值,变化了12个字节:
在这里插入图片描述
接下到了 lea edi,[ebp+FFFFF1Ch]这个操作,lea是加载有效地址的意思,把 ebp+FFFFFF1Ch的地址加载到edi寄存器里面,为什么说是加载地址呢?因为这里有个 中括号[ ],他的意思就是把中括号[ ]里面值当作地址;那ebp+FFFFFF1Ch到底是什么地址?我们打开反汇编的加载符号选项,可以看到,ebp+FFFFFF1Ch就是 ebp-0e4h的地址.
在这里插入图片描述
那接下来来到了这几条指令 mov ecx,39h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] ,这里有三条指令,我们不需要理解具体是干什么的,我们只需要理解这三条指令合起来就是做一件事情:
ebp-0E4h的地址到ebp的地址 ,这个范围地址空间里面的每4个字节的值赋值为0CCCCCCCCh,为什么是4个字节,因为在rep stos dword ptr es:[edi]这条指令中,有个dword表示double word 就是4个字节的意思,一个 word就是两个字节;
在内存布局的模样就是大概这个样子:
在这里插入图片描述
下面开始指向,main函数里面代码了,来到int a= 10;即来到汇编指令: mov dword ptr [ebp-8],0Ah,意识就是把 0Ah放到ebp-8的地址空间,看看我们的内存布局 ebp在栈底,那么很容易就知道ebp-8在哪里了:
注意内存布局图:里卖弄 ebp-8的位置值修改成为了 0Ah
在这里插入图片描述
继续接下来的指令,一模一样也是 指向int b = 20; mov dword ptr [ebp-14h],14h int c= 0; mov dword ptr [ebp-20h],0都是把 值放入对应的位置,在内存布局如下:
找到 ebp-14h ebp-20h这两个内存地址就行:
到这里,你思考一下:你有没有发现无形之中,你好像看到了局部变量在内存是如何存放在栈空间的了


好了执行完后,你会来到这个 c = Add(10,20); 这是什么?这是函数调用,我们知道函数调用要传参,来看看是如何如何传参的。你看我为啥先说传参?因为汇编指令就是这么玩的,为什么我不说函数调用为函数开辟栈空间?因为汇编就是先传参。
如何传参?
首先,push 14h 和 push 0Ah,就是压栈,同时esp指针往上移动,先传了20,在传10,你有没有发现,传参的顺序是从括号()的右边往左边传
在这里插入图片描述

接下来到了 call 004E10E1 这个操作,这个操作是干什么的?call指令就是开始真正的调用 Add函数了,并且这个call指令还会把下条指令的地址进行压栈;
在这里插入图片描述

在内存中,我们只要观察 esp指针指向的值是否为 call指令的下一条指令即可。
在这里插入图片描述
调用函数时候,我们按F11;此时就是执行了call 004E10E1;此时就进入了Add函数,你仔细观察一下,前面的汇编指令
004E13C0 push ebp
004E13C1 mov ebp,esp
004E13C3 sub esp,0CCh
004E13C9 push ebx
004E13CA push esi
004E13CB push edi

这操作是不是和main函数调用时候,很像?对就是很像。那你说这几段汇编代码做了什么?哎,就是给我们的Add函数开辟栈空间,开辟我们Add的栈帧
下面那几条就是在Add函数的空间内,存放随机值。
在这里插入图片描述
在内存布长这个模样
在这里插入图片描述

此时,到了 int z = 0;这个代码对应汇编就是 mov dword ptr [ebp-8],0,也就是在 ebp-8的位置存放 0 值,在内存布局中:

在这里插入图片描述
接下来到 z = x +y;这段代码对应汇编:
z = x + y;
004E13E5 mov eax,dword ptr [ebp+8]

004E13E8 add eax,dword ptr [ebp+0Ch]

004E13EB mov dword ptr [ebp-8],eax


先看: mov eax,dword ptr [ebp+8] :
把ebp+8的地址指向的值放入 eax寄存器中,你看, ebp+8在哪儿?
是不是存放的刚好时我们在main函数栈帧里面值 014h;也就是20
add eax,dword ptr [ebp+0Ch]
这个也是 add指令把 ebp+och地址里面的值,也在main函数的栈帧中,也就是10,加到eax里面,并赋值给eax,那么 eax就变成了 30;
mov dword ptr [ebp-8],eax
最后把eax里面的30 放入到 ebp-8的位置,那ebp-8的位置就是变量 z的位置啊;
到此这个z = x +y;就是执行完了;
在内存布局时这样的
在这里插入图片描述
到这里,你思考一下:是否:在 Add函数使用的变量 不是在 自己Add函数里面的,而是在main函数的栈帧里面的压进去的值,也就是说在调用Add函数的时候,我们先把实参压栈,此时,这个实参就是形参的一份拷贝了,你往内存布局下面看,你就发现,实参还是在函数的栈帧里面,根本没有和形参发生真正的使用,这就是说实参为什么说就是形参的一份拷贝了


接下来就到return z;这个语句了, 那对应的汇编: mov eax,dword ptr [ebp-8]
把 ebp -8的值放入到eax中,这个时候,并没有发生什么函数调用完了就返回的操作,只是把值放入到eax寄存器中,但是函数调用完后,就返回到主函数把return语句的值带回去是有错的吗?并没有。还有的是,函数返回了,不是说,局部变量都销毁了嘛,那怎么还可以把局部变量的值带回去,汇编代码告诉你,在函数返回的时候,是先把return z中的局部变量先保存起来,到一个寄存器先,等函数执行结束后,才开始返回


函数开始销毁的过程

这时候当Add函数执行完后,就开始销毁空间了,看看接下来的弹栈的汇编代码
pop edi
pop esi
pop ebx
一旦执行了这三句汇编,就会从栈顶开始弹栈,此时esp++;如下图;
在这里插入图片描述
之后来到这句汇编:
mov esp,ebp
这个意思就是esp指向了ebp,此时就发生了一个重大的事情,一旦esp指向了ebp,这就说明,这段空间的栈帧没了,也就是说Add函数被销毁了,你发现了吗?在函数被销毁的时候,我们的return z 还是没有返回啊,不用担心,因为z已经被保存到寄存器eax里面了,及时函数的空间没有了,eax里面的值还是存在的;那什么时候返回,别急,往下看;
在这里插入图片描述
继续看 pop ebp:这个汇编指令就很厉害了,当我们 pop ebp时候,
ebp就不指向刚刚的空间了,而是回到了main函数原来是ebp所指向的空间,我们要知道 ebp指向的那段空间里面存放的就是 main函数的地址,并且,esp也会++;
所以内存布局是这个样子
在这里插入图片描述
之后来到 ret 这条指令,这条指令的意思是 返回到栈顶元素存放值地址指令那里,同时弹出栈顶元素:
此时esp++的位置
在这里插入图片描述

那我们看现在栈顶就是 call指令得下一条指令,还记得是什么嘛?就是执行完函数得时候时候,即Add(int x,int y);j就会回到主调函数 c = Add(10,20);此时,我们并没有执行完这条语句
所以你就可以思考:为什么刚刚我要把 call指令下一条指令压栈呢?就是为了还能回到主调函数函数;
之后就来到了 add esp 8;这个意思就是esp+8 赋值给esp,所以 esp指向了下面8个字节的位置,这也说明 形参 x, y 空间销毁;
esp的指向如下图
在这里插入图片描述

接下来到 :mov dword ptr [ebp-20h],eax ,这个就强了,到这里才是把 eax的值 放到 ebp-20h的地方去,ebp_20h就是 变量c的值,此时,这算终于把Add函数返回的 z给带回来了。
在这里插入图片描述
啊,这里终于解释完了,add函数的创建和销毁了。那么main函数的销毁机制也是一样的我就不过多解释了,你们可以试一试。


回答开头的问题

接下来我们终于看完了,来回答一下开头的问题吧:测一测自己。

  • 函数栈帧是什么?

函数栈帧就是esp和ebp维护的空间,这段空间可以共供给函数使用,一旦esp和ebp发生变化,指向另一段新的空间时候,这个就发生了函数调用,此时 esp 和ebp又会重新维护那一段空间,总的来说:函数栈帧就是函数能够利用的空间范围,并且由esp和ebp指针维护;

  • 函数是如何在栈区开辟空间的呢?

函数在栈区开辟空间,一旦进入函数体,首先做的事情是给函数设置栈指针,用来维护栈空间,同时会给栈空间赋值 0CCCCCCCCh,即给维护的空间赋值为随机值;并且我们知道栈的使用是从高地址向低地址使用的;

  • 函数的实参是如何传参的?传参的顺序如何?

函数传参是先碰到调用函数,先给形实参压栈,压栈的时候,还是在主调函数体里面,压栈的顺序是从实参列表的右边到左边依次入栈;
当进入到函数体内部时候,形参是使用主调函数体里面的实参数据;这就是实参传参给形参,这也说明了,实参是形参的一份临时拷贝。

  • 为什么函数形参无法改变外部的变量?

因为我们在调用函数的时候,是给实参压栈一份数据进去栈顶的,形参使用的数据就是在调用函数中实参的数据,所以说,形参改变不了外面函数的变量

  • 为什么说实参是形参的一份临时拷贝? 形参和实参的关系又是什么?

这个参考上面的两个问题;

  • 函数是怎么调用的?

函数的调用,首先,会给实参压栈,其次会保存调用函数体执行结束后的下一条指令的地址,然后给函数设置栈指针,用来维护函数的空间,同时,为函数的赋值为随机值,这个时候,执行函数体的代码,如果函数执行有返回值,先把返回值放到一个寄存器临时保存,然后函数体执行结束,销毁了函数的栈空间,回到主调函数的调用函数中,此时销毁形参,同时把返回值带回来。

  • 函数调用是如何返回值的?

参考上面的回答

  • 局部变量是如何在栈开辟空间的?

局部变量是以压栈的形式创建的,并且,创建时从高地址向低地址使用空间,在创建局部变量的时候,函数栈空间的地址已经时随机值了,此时当你创建局部变量时候,得到的也是随值

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

参考上面的回答;


一个还没开始销毁函数空间的内存布局图

在这里插入图片描述

  • 29
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呋喃吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值