函数栈帧的创建和销毁详解

亲们,今天讲讲函数栈帧的创建与销毁。这里有几个问题,如果你能不加思索的回答出来。

那我只能说,大佬请去别的地方一展拳脚吧,当然你能指出我的不足。我也会深表感激的。

这几个问题如果你能在看完我的文章后搞明白,那么说明我还是讲的不错的撒。

目录

问题

1. 基础知识

2. 以vs2013演示全过程

2.1 观察函数间的调用

2.2 函数栈帧的创建

 2.3 局部变量的创建

2.4 函数的调用

2.4.1实参的拷贝

 2.4.2 Add函数的调用

2.4.3 Add函数的返回


问题

1. 局部变量是如何创建的?

2. 为什么局部变量不初始化内容是随机的?

3. 函数调用时参数时如何传递的?

4. 传参的顺序是怎样的?

5. 函数的形参和实参分别是怎样实例化的?

6. 函数的返回值是如何带回的?

1. 基础知识

函数在调用的时候都会在内存中的栈区上开辟一份空间供函数使用(并非数据结构中的栈哈),我们想要解决如上问题就需要理解函数栈帧的创建和销毁。

我们都知道在CPU中有许多的寄存器(相比内存中的存储单元个数少太多了,不能说很多哈),比如:eax,ebx,ecx,edx······这些寄存器不是咱这片文章的重点。其中有这么两个寄存器:ebp和esp,它们里面存放的是地址,这两个地址是用来维护函数栈帧的。

比如说,当前程序正在main函数里,并未跳转到其他函数。那么ebp和esp这两个地址之间的存储单元就是main函数的栈帧,ebp和esp负责维护它之间的函数栈帧。ebp指向的元素称为栈底元素,

esp指向的元素称为栈顶元素。

上图:

函数栈帧的创建和销毁详解_第1张图片

还有一点:内存上的栈区先使用高地址,再使用低地址 。

2. 以vs2013演示全过程

2.1 观察函数间的调用

我们先看如下程序:

#include
//求和函数,参数x,y表示两个加数
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函数调用了Add函数嘛,是的没错。但是通过vs2013的调试----调用堆栈我能看到更多信息(该功能调试时可以用来显示函数之间的调用逻辑,工程庞大时很好用):

函数栈帧的创建和销毁详解_第2张图片

函数栈帧的创建和销毁详解_第3张图片

我们看到实际上是:mainCRTStartup()这个函数调用了_tmainCRTStratup()函数,然后是 _tmainCRTStratup()函数调用main函数,最后是main函数调用Add函数。是不是有多了一点奇怪的知识呢?

2.2 函数栈帧的创建

上面说到每个函数都有自己的栈帧,上面那两个名字忒长的函数也不例外。咱就来讲讲main函数栈帧的创建。要想理解透,咱就得分析汇编代码。上图:

函数栈帧的创建和销毁详解_第4张图片

函数栈帧的创建与ebpesp这两个寄存器紧密相关,咱要把它监视起来。 函数栈帧的创建和销毁详解_第5张图片

 下面我们一步一步分析汇编代码。

函数栈帧的创建和销毁详解_第6张图片

1):push   ebp  代表将ebp的地址压入栈区,同时esp指向的位置减一:

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

 我们通过内存地址的变化也能看到esp指向位置的变化,以及压入的内容:

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

函数栈帧的创建和销毁详解_第9张图片

 2):mov   ebp, esp  代表将esp的值给ebp,那么他们指向的位置就相同啦:

函数栈帧的创建和销毁详解_第10张图片

 3):sub  esp,0E4h      代表将esp的地址减去0E4h这样一个16进制数(h代表十六进制嘛)

函数栈帧的创建和销毁详解_第11张图片

4),5),6):push  ebx   ,     push   esi   ,  push  edi   同1)压入这三个寄存器的地址  。压入完成esp的地址减 3 (-1代表一个减去四个字节哈,32位的指针大小是4字节嘛)。

函数栈帧的创建和销毁详解_第12张图片

 7):lea  edi , [ ebp - 0E4h ]  :  lea是load effective address 即加载有效地址的意思,表示将

ebp-0E4h这个地址加载到edi里面去。这个地址就是ebx下面的那个地址,上图看ebx的位置,第3)部解释为什么是ebx下面的那个位置。

8):mov   ecx ,39h  表示将39h给到ecx里面。

9):mov   eax ,0CCCCCCCCh  表示将0CCCCCCCCh 给到eax里面去。

10):rep stos    dword ptr es:[edi]   表示从edi这个位置开始向下39h个 dword(double word 双字,即四字节)的数据改为CCCCCCCC。(刚刚好到ebp上面的那个位置嘛,0E4h代表十进制的228, 39h代表十进制的57, 57 * 4字节正好是228,就刚刚好到ebp上面的位置嘛)。

函数栈帧的创建和销毁详解_第13张图片

 2.3 局部变量的创建

函数栈帧的创建和销毁详解_第14张图片

1): mov          dword   ptr  [ebp-8] ,  0Ah   表示将0Ah(就是十进制的10,即a的值)放到dword(四字节)的指针(int) [ebp-8]  里面去 。

2): mov         dword   ptr   [ebp-14h],14h   同上。

3):mov         dword   ptr   [ebp-20h]  ,  0   同上。

函数栈帧的创建和销毁详解_第15张图片

 

到这里大家是不是十分明白为什不能用未初始化的局部变量了撒。因为是随机值嘛。

注意:不同编译器每个变量开辟的空间的间隔可能是不相同的。

2.4 函数的调用

2.4.1实参的拷贝

函数栈帧的创建和销毁详解_第16张图片

1):mov         eax , dword   ptr   [ebp-14h]   把 [ebp-14h]  里面的内容给到eax,[ebp-14h] 里面的内容就是b的值呀。

2):push        eax  压栈,压入eax 。

3):mov         ecx,dword ptr [ebp-8]  ,同上将b的值给到ecx。

4):push        ecx  压栈,压入ecx 。

进行一次push  esp地址减1,  1),2),3),4)结束后esp减了2(这个 - 2代表减去八个字节):

函数栈帧的创建和销毁详解_第17张图片

 

函数栈帧的创建和销毁详解_第18张图片

5):call        003C10E1  表示调用Add函数,并且把call指令下一条指令的地址压栈。为什么呢?因为call指令表示调用Add函数,调用函数完了,肯定得继续执行call指令下面的指令撒,调用call的时候记住下一条指令的地址,调用完了才回得来嘛。举个不恰当的比喻:你去朋友家里玩,玩完了,肯定的回自己的家撒,如果你去的时候都记不住你家的地址了,等你玩完了,还怎么回来嘞。

函数栈帧的创建和销毁详解_第19张图片

 

函数栈帧的创建和销毁详解_第20张图片 

 2.4.2 Add函数的调用

函数栈帧的创建和销毁详解_第21张图片

进入Add函数后我们看到它的汇编代码的前面和main函数创建栈帧的时候是差不多的。简单分析一下即可。也可以来检验一下亲们,刚刚main函数栈帧的创建搞懂没有哦。画一下图呗!

1):push        ebp    压栈,压入ebp的值(存的是地址嘛)

2):mov         ebp , esp  将esp里面的值给到ebp。

3):sub         esp , 0CCh    esp - 0CCh 改变esp指向的位置。

4):push        ebx   压栈,压入ebx的值。

4):push        esi   压栈,压入esi的值。

5):push        edi   压栈,压入edi的值。

6):lea         edi , [ ebp-0CCh ]   加载有效地址,将ebp - 0CCh的地址给到edi 。

7):mov         ecx,33h  将33h的值给到ecx。

8):mov         eax,0CCCCCCCCh  将0CCCCCCCCh的值给到eax里面。

9):rep stos    dword ptr es:[edi]   表示从edi这个位置开始向下33h个 dword(double word 双字,即四字节)的数据改为CCCCCCCC。(刚刚好到ebp上面的那个位置嘛。原因上面创建main函数栈帧讲过了)

函数栈帧的创建和销毁详解_第22张图片

 

 

 函数栈帧的创建和销毁详解_第23张图片

 10):mov           dword   ptr   [ebp-8]   ,  0    将0,给到ebp-8指向的位置。就是给局部变量z开辟了空间。

函数栈帧的创建和销毁详解_第24张图片

 

11):mov         eax , dword ptr [ebp+8]   把ebp + 8 里面的值放到eax里面去。

12):add         eax , dword  ptr  [ebp+0Ch]  把ebp+0Ch(ebp+12)里面的值加到eax里面去。

到这里,eax的值变成了30.(你可能会想为什么  11)不会把下图中eax--20 的值覆盖呢?eax是一个在CPU里面的寄存器,这里的eax--20只是说明eax 上次 eax用了过后,里面存留的是20 ,11)步是将eax的值改成了10,上次的push eax是将eax的值压入了内存的栈区 )

13):mov         dword ptr [ebp-8] , eax   将eax里面的值放到ebp - 8 的位置里面去。

函数栈帧的创建和销毁详解_第25张图片

 

到此我们可以看出形参并没有创建,而是在 调用Add函数前,拷贝了一份实参,压入到内存的栈区,调用Add函数再找到这个地址。由此可以得出结论:形参是实参的临时拷贝。所以改变形参不会影响实参。

我们还可以看出b的值20,是先压栈的,故传参的时候是先传b的。

2.4.3 Add函数的返回

函数栈帧的创建和销毁详解_第26张图片

 1):mov         eax,dword ptr [ebp-8]   把ebp - 8 的值给到eax里面,即是z的值30。这样就能解释为什么z销毁了还能返回z的值。

2): pop         edi     弹栈, 弹出一个栈顶元素,将弹出的值放到edi里面去,esp加一。
3): pop         esi      弹栈, 弹出一个栈顶元素,将弹出的值放到esi里面去,esp加一。
4): pop         ebx      弹栈, 弹出一个栈顶元素,将弹出的值放到ebx里面去,esp加一。

5):mov         esp , ebp  将ebp的值给到 esp。执行完这条指令,esp, esp指向同一个地方。即存放原来main函数的ebp的位置。

6):pop         ebp   弹出一个栈顶元素,将弹出的值给到ebp里面去,这个栈顶元素就是,存放原来main函数的ebp的位置。这么搞完又回到了main的栈帧了。

函数栈帧的创建和销毁详解_第27张图片

 

7) ret  弹出栈顶元素(call指令下一条指令地址),并跳到那个地址处。这就是存call指令下一条指令地址的原因。跳到call下一条地址:

函数栈帧的创建和销毁详解_第28张图片

 8):add         esp,8  把esp存的地址加8。esp指向了图中edi的位置(ret弹出的时候加了1个字节),此时形参销毁,注意形参销毁的时间

函数栈帧的创建和销毁详解_第29张图片 

 9):mov         dword ptr [ebp-20h],eax    将eax中的值给到 ebp -20h中去 , 看上图,就是为c开辟的那一块空间。即把求得的和给到c里面去。

好啦,今天的函数栈帧的创建与销毁就到这,看完了可以看看前面的问题,看能否回答上,以检验学习成果。加油,亲们。

你可能感兴趣的:(开发语言,c语言)