速解“函数栈帧的创建与销毁”_压栈今日份通达“函数栈帧”的创建与销毁

前言:

在C语言中所定义的函数都是在栈上进行创建的,进入函数就是向内存的栈空间申请开辟一块空间(压栈),并在此空间里进行该函数相对应的操作,出了该函数就同时销毁释放该函数内所有的空间。

大致的理论认识即使如此,但在内存的视角上”函数栈帧“的创建与销毁是如何的呢?

补充:

这里特别提示一点:该知识点在不同的编译器底下会有一些不同,但是在大致的逻辑思维上是相同的,这里将以VS 2013进行展示。

函数:

此之前首先了解一下寄存器:eax ,ebs,ecx,edx,ebp,esp

主要详解"ebp","esp"这两个寄存器:这两个寄存器中存放的是地址,这2个地址主要是用来维护函数的栈帧。

ebp:维护函数栈底的指针。

esp:维护函数栈顶的指针。

每一个函数调用都会在栈区创建一个空间来实现该函数,main函数同样也是被其它函数调用的。

在vs 2013 中,main函数的调用顺序:由mainCRTStartup()调用👉__tmainCRTStartup()调用👉main()

✨以下面这段代码来演示整个函数栈帧的创建与销毁:

#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 z = Add(a, b);

	printf("%d\n", z);
	return 0;
}

内存中逻辑表示图:

速解“函数栈帧的创建与销毁”_main函数_02

以内存的视角这样的逻辑图是如何实现的呢?

下面一一进行细致的讲解:

①打开编译器,书写这段代码

速解“函数栈帧的创建与销毁”_函数栈_03

②”F11“进入调试

速解“函数栈帧的创建与销毁”_main函数_04

③光标在此位置右击进入反汇编

速解“函数栈帧的创建与销毁”_压栈_05

④便出现以下编译指令

速解“函数栈帧的创建与销毁”_函数栈_06

首先根据以下指令为函数在栈中来开辟空间

速解“函数栈帧的创建与销毁”_压栈_07

函数定义前开辟空间的指令方法都是一样的。

下面将从开辟main函数空间为例:

速解“函数栈帧的创建与销毁”_main函数_08此刻esp和ebp是维护函数"__tmainCRTStartup"。

接下来开辟main函数的空间。

①"push   ebp":将ebp进行压栈

速解“函数栈帧的创建与销毁”_main函数_09"push"压栈的同时,esp指针也相应更新

②”mov   ebp,esp“:将此刻esp的地址赋给ebp

速解“函数栈帧的创建与销毁”_函数栈_10

③”sub   esp ,0E4h“:将esp的地址 减去0E4h为新的esp地址

速解“函数栈帧的创建与销毁”_main函数_11

④”push   ebx“,”push   esi“,”push   edi“:在esp地址的基础上进行压栈

速解“函数栈帧的创建与销毁”_函数栈_12

⑤"lea   edi,[ebp-0E4h]":"lea":load effective addvess 加载。将ebp-0E4h的地址赋给lea。也就是esp”push“之前的地址位置

速解“函数栈帧的创建与销毁”_压栈_13

⑥”mov   ecx,39h“,"mov   eax,0CCCCCCCCh","rep stos   dwors ptr es:[edi]"这段指令表示:从当前edi地址位置开始的前39个”dword“(四字节)的空间全部初始化为”0CCCCCCCCh“。

速解“函数栈帧的创建与销毁”_main函数_14

此刻main函数的空间就开辟好了。

空间开辟好后便对书写的代码进行编译

速解“函数栈帧的创建与销毁”_压栈_15

①”mov   dowrd ptr [ebp-8],0Ah“:0Ah的十进制:10,将ebp-8的地址的值改为10。

同样”mov   dowrd ptr [ebp-14h],14h“:14h的十进制:20,将ebp-20的地址的值改为20。

速解“函数栈帧的创建与销毁”_压栈_16

②”mov   eax,dword ptr[ebp-14h]“,"push   eax"

   ”mov   ecx,dword ptr[ebp-8]“,"push   ecx"

这两段指令相当于函数的传值调用,将函数的实参从右至左进行存值,并压栈保存

速解“函数栈帧的创建与销毁”_main函数_17

③”call   00D511D1“:call 指令为调用函数,并存储下一条指令的地址”add   esp,8“

速解“函数栈帧的创建与销毁”_函数栈_18

Add函数的实参压栈好之后,便进入main函数调用的Add函数中。同main函数首先开辟一块属于Add的函数空间。

速解“函数栈帧的创建与销毁”_函数栈_19

因为同开辟main函数空间一样这里就不再重复演示,将跳过开辟空间的过程速解“函数栈帧的创建与销毁”_压栈_20

①”mov   dword ptr [ebp-8],0“:将ebp-8的地址位置的值赋0速解“函数栈帧的创建与销毁”_压栈_21

②"mov   eax,dword ptr[ebp+8]":将ebp+8的地址位置的值赋给eax速解“函数栈帧的创建与销毁”_函数栈_22

③"add   eax,dword ptr[ebp+0Ch]":将eax的值加上dword ptr[ebp+0Ch]地址的值。速解“函数栈帧的创建与销毁”_函数栈_23

④ “mov         dword ptr [ebp-8],eax”:将eax的值赋给ebp-8地址速解“函数栈帧的创建与销毁”_压栈_24

⑤”mov         eax,dword ptr [ebp-8]“  :将ebp-8地址的值赋给eax,。

这里将z的值计算好之后便开始返回值,此刻将要返回的值再赋值给eax进行保留,以免退出函数就找不到需要的值

00D5176B  pop         edi  

00D5176C  pop         esi  

00D5176D  pop         ebx  

此刻跳出Add函数。并将adi,esi,ebx的值进行出栈释放空间。这时候”call“存的指令地址位置就能够回到之前代码的执行顺序速解“函数栈帧的创建与销毁”_main函数_25

当函数跳出来之后,该栈空间就不需要了

”mov   edp,esp“:将栈顶指针指向栈底指针

速解“函数栈帧的创建与销毁”_函数栈_26

同时"pop   edp":将回到之前edp存的栈底地址(main栈底的地址),且因为”pop“栈顶”esp“便往下挪一位地址

速解“函数栈帧的创建与销毁”_main函数_27

”ret“这条指令,根据之前call指令保存的下一条指令的地址,直接返回到该指令的位置。返回原来的地址esp便也往回跳一个地址速解“函数栈帧的创建与销毁”_压栈_28

”add   esp 8”因为此刻该调用函数以结束且保存形实参的值就也不需要了,直接将esp指针往回跳释放之前保存的地址即可。

速解“函数栈帧的创建与销毁”_压栈_29此刻edp与esp又维护main函数的空间。

“mov         dword ptr [ebp-20h],eax”将eax保存之前函数返回的值,再赋予一个变量地址就完成返回值的保存。

根据以上的解释,思考以下问题:

1,局部变量是怎么创建的?

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

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

4,形参和实参是什么关系?

5,函数调用是怎么做的?

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