函数栈帧的创建与销毁(粗略认知)

目录

一.前言:

演示代码展示: 

二.主角粗识&相关了解:

三.正文讲解(正片开始):

1.main函数的预开辟(梦开始的地方):

2.变量空间的创建:

3.自定义函数的函数栈帧的创建和销毁(###重点###):

(1).自定义函数栈帧的创建:

(2).自定义函数栈帧的销毁:

四.结语(补充):


一.前言:

在C语言的学习中,函数是至关重要的,也是我们烂熟于心的,对于一个函数的定义、调用等,大家也都能做到唯手熟尔的程度,那么,在这里,我们就来思考一些新的问题,如,函数是如何被我们调用的,计算机又是怎么为函数开辟空间的,为什么我们说函数的形参是实参的一份临时拷贝,调用函数的时候,参数又是何时销毁的,为什么变量已经销毁了却还是可以得到原本的返回值,等等。这些问题都将在本篇文章中一一解答。

演示代码展示: 

​
#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;
}

​

本段代码将会作为接下来的讲解对象,由于函数栈帧本质过于繁琐,所以我们选择最简单也是最有代表性的一段代码来进行演示。(注:本篇是基于VS2013版本进行的讲解,因为越高等级的编译器其函数栈帧就越复杂且不容易观察,第二点,在不同编译器下观察到的现象可能略有差异,但基本逻辑依旧相同,所以无法做到统一,请理解。)

二.主角粗识&相关了解:

那么,在正式讲解之前,我们先来了解几位接下来会出现的“主角”和一些函数栈帧相关的东西。

1.寄存器:对于很多人,当我们问道电脑的存储道具都有哪些的时候,大家率先想起来的就是硬盘和内存,那么除了这二位还有什么呢,就是我们的主角---寄存器,寄存器是独立于内存,集成到CPU上的另一种存储道具,其种类有很多,而今天,我则只需要了解一种即可,就是---指针寄存器。那么,指针寄存器又是什么呢?它就是用来维护我们的函数栈帧的重要工具,而我们今天的主角便是身为指针寄存器的ebp和esp两位,这两位主角呢,分别指向我们被调用的函数栈底和函数栈顶,他们将被调用的函数覆盖在内,以此来进行维护。

2.栈:我相信大家对栈区应该都是有所了解的,当我们在创建一个临时(局部)变量的时候,我们的编译器就会在栈区中位该变量分配合适的空间,并且,在分配时遵循由高地址向低地址分配的原则,函数也不例外,他们也会在栈区上开辟属于自己的空间,而ebp指向了高地址的一端---函数栈底,esp指向了低地址的一端---函数栈顶,如下图:

三.正文讲解(正片开始):

1.main函数的预开辟(梦开始的地方):

main函数是程序的开始,也是每一位程序员梦开始的地方。在编程时,我们经常把main叫main(梦)函数,所以,顾名思义,main本身就是一个函数,身为一个函数,它必然也就有属于自己的空间(函数栈帧),那么,main函数的空间是如何开辟的呢,它又是被谁调用的呢?

当我们在程序调试时打开我们的调用堆栈,并让main函数内程序执行过return后,在我们的调用堆栈中可以看到,main函数被另一个函数(__tmainCRTStartup())调用了(这里我们不必关心__tmainCRTStaartup()函数又被谁调用,他又是怎么开辟的空间,因为调用逻辑过于复杂,我们只以main函数举例了解即可),所以,根据栈区开辟空间的固定,我们可以知道为main函数所开辟的空间在__tmainCRTStartup()的函数栈帧之上,那么,具体过程是怎样的呢,我们通过图来更好的了解。

看到上述两幅图大家可能会有各种各样的困惑,那么,接下来我会详解上述过程,在这之前,我们再来看一个东西。

 

 此图所显示的过程就是main函数空间开辟的过程,具体步骤如下:

首先我们来看第一行,push ebp,此指令的效果为压栈,将ebp的值压入栈中(可以理解为在栈中开辟一个存储空间存储ebp的值),这也就造成了在__tmainCRTStartup()的函数栈帧上有一个被压入的ebp的值,同时,在进行压栈操作过后,esp的位置会自动向上,依旧指向栈顶。第二句,意思是,将esp的值赋值给ebp,mov是赋值的效果,此时,ebp将会自动指向现在esp的位置。紧接着第三句指令,sub操作,将esp减去一个0E4h,该操作会使esp指向的位置发生改变,上移0E4h个字节。此时,ebp和esp将会覆盖一块新的空间,此空间就是为main函数预开辟的空间。之后,三次push,在main函数预开辟的空间上压入三个值,再通过lea(load effective address)操作将ebp-0E4h所表示的有效地址加载给edi,再通过两次mov赋值,将39h和0CCCCCCCCh分别赋值给ecx和eax。经过上述的一系列操作后,真正发挥效果的,是rep stos操作,该操作的意思是令从edi指向的位置开始,向下的39h个字节以四字节(dword)的形式进行初始化,初始化为0CCCCCCCCh。从第四句指令开始到此为止的一系列操作,目的就是将main函数内的空间进行初始化,这也就是为什么当我们不为变量进行初始化时,变量的值是随机的。那么,到这里为止就是为main函数开辟空间的大致过程了。接下来,就是对变量空间的创建和分配。

2.变量空间的创建:

从此图中的指令开始就是main函数内真正的有效语句了。

三句指令的效果相同,为了节省时间我就只以a变量的创建为例了(绝对不是我懒_(•̀ω•́ 」∠)_)。

该指令的意思是将ebp减某一值所代表的的位置进行赋值,前面我们将a赋值为10,和这里的0Ah是一样的,再重复该指令,以完成所有变量的创建和初始化。

3.自定义函数的函数栈帧的创建和销毁(###重点###):

(1).自定义函数栈帧的创建:

和讲解main函数一样,我们先来看结果图和指令图。

那么,我们先来看第一张指令图,第一句指令意为将ebp-14h位置中的值赋值给eax,然后再压入栈顶,同理再将ecx压入栈顶。接着第五句指令,意味调用Add函数(call指令的效果为调用函数,同时,将call指令的下一条指令的地址压入栈顶),同时将他的下一条指令压入栈顶(这里讲解一下为什么要将call指令的下一条指令压入栈顶:为了可以让函数使用完毕后能够找到call指令下一条指令的位置,使得后续代码可以继续执行)。

 进入Add函数后,先为Add函数开辟空间(开辟空间时的这些操作和为main函数开辟空间时的操作完全类似,所以就不在这里做过多讲解了,如果还是不理解的可以看看前面main函数开辟空间的讲解)。然后,在为函数中的变量开辟空间和初始化。

变量创建之后,开始执行函数的功能,首先将ebp+8位置中的值赋值给eax,再执行add指令,将ebp+0Ch位置中的值加到eax中,最后再执行mov指令,将eax中的值赋值给ebp-8地址指向的空间中。以此实现两数相加和赋值的效果。

(2).自定义函数栈帧的销毁:

上面是最后的结果图,那么,老样子,我们来看看指令图。

在函数开始返回时,第一句指令,将ebp-8位置对应空间中存储的值(也就是z的值),赋值给eax。

然后正式开始自定义函数栈帧的销毁。首先,三个pop指令将压入栈中用于初始化函数空间的三个寄存器进行出栈操作,再将ebp的值赋值给esp,使esp指向Add函数的栈底,再执行pop指令,将原ebp(前面提到过,这个压入栈中的ebp其指向的是的是main函数的栈底)的值出栈弹出到寄存器ebp中,使其重新指向main函数的栈底。最后执行ret(返回)操作,通过前面压入栈中的call指令的下一个指令的地址返回到call指令的下一个指令,从这里开始继续向下执行程序。

回到call指令的下一指令后,开始执行此条指令,通过add指令,将esp的值加8,使其所指向的位置向下移动八个字节,从而达到形参释放的效果。

最后的最后,将存有返回值的寄存器eax中存储的返回值赋值给ebp-20h所指向的空间中,达到返回值变量接受返回值的效果,那么!到这里为止,关于函数栈帧的销毁和形参的释放就彻底结束了!!!而,原本为Add函数开辟的那块空间,也重新被系统收回了。

四.结语(补充):

那么,看到这里,我相信各位对前面提出的一些问题也都有了答案,希望大家看了这篇文章以后也可以有所收获,可以更好的理解函数的相关知识,最后祝给位都能冲出绝望之谷,登上开悟之坡!想着目标冲冲冲!!!(这里补充一下,这些指令图,大家可以在编译器的反汇编中进行查看,不过编译器版本过高可能会过于复杂,所以,查看时建议选择较低版本的编译器,同时,这些地址以及指针寄存器地址值加减多少也都是取决于编译器的,不同编译器大概率会不同,不必奇怪,最后,通过上述讲解,大家也可发现编译器在读取参数时时从右向左读的,并且,栈区从下往上地址是依次减小的。)

(恳求:制作不易,求求各位可怜可怜孩子,给孩子点点赞吧!(இωஇ ))

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值