【C语言】—— 函数栈帧的创建与销毁(详解)

 

目录

一、 基础知识掌握(针对本文)

1.寄存器的种类及功能

2. 汇编指令

二、简单了解内存管理和函数栈帧

三、初步了解main函数的函数栈帧创建

1.创建一个简单的代码

2.main函数的创建过程

​3.main函数的调用图解

四、main函数的栈前准备 

1.push压栈操作

2.mov指令

3.sub指令

4.push - ebx、esi、edi指令

5.led、mov、mov、rep stos指令

五、正式执行有效代码

1.局部变量的创建以及值的问题

2.函数是如何传参的

3.Add函数的函数栈帧创建

4.形参与实参之间的关系

5.函数是如何返回值的

1.pop出栈指令

2.函数值的返回

 六、总结

一、 基础知识掌握(针对本文)

1.寄存器的种类及功能

1.eax:累加寄存器,相对于其他寄存器,在运算方面比较常用。

2.ebx:基地址寄存器,作为内存偏移指针使用。

3.ecx:计数器,用于特定的技术。

4.edx:作为EAX的溢出寄存器,(除法产生的余数)。

5.esp:指针的寄存器,用于堆栈操作。又称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,esp也就越来越小。

                在32位平台上,esp每次减少4字节。
6.ebp:基址指针,指栈的栈底指针。

7.esi:在内存操作指令中作为“源地址指针”使用。

8.edi:在内存操作指令中作为“目的地址”使用。

2. 汇编指令

1.push 指令:入栈指令,将源操作数指定的字数据压入堆栈栈顶,在32位平台上,esp每次减少4字节。

2.mov 指令:将源操作数送至目的操作数。

3.pop 指令:出栈指令,将源操作数指定的字数据压入堆栈栈顶,在32位平台上,esp每次增加4字节。

4.jmp 指令:跳转至指定地址执行指令。

5.lea 指令:全称(Load effective address),加载有效地址(偏移地址)至寄存器(如edi)。

6.call 指令:将程序的执行交给其他代码段,执行结束后会返回到call指令的下一行指令。

7.ret 指令:子程序的返回指令。

8.add 指令:加法指令。

9.sub 指令:减法指令。

10.rep 指令:重复前缀指令,不可独立使用

        形式:位于stos、lod、ins、outs等传送指令之前,如 rep stos dword ptr es:[edi] 

11.stos 指令:传输指令,可独立使用,它是将eax(寄存器)中的值传送到指定单元;

        例如:rep stos dword ptr es:[edi] 将eax中的值拷贝到es:[edi]指向的地址

二、简单了解内存管理和函数栈帧

【C语言】—— 函数栈帧的创建与销毁(详解)_第1张图片

本次我们主要探讨栈区,函数在调用的过程中都是在栈区上开辟一块属于自己的空间的,这块空间是由两个寄存器esp和ebp来维护的,这块空间我们称为函数栈帧;

本篇文章主要探究以下几个问题:

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

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

3.函数是如何传参的?

4.形参与实参之间的关系?

5.函数调用是如何进行的?

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

编译器的选择: 尽量选低版本的编译器,高版本的编译器封装性比较好,难以看到效果;我选用的是VS2013

三、初步了解main函数的函数栈帧创建

1.创建一个简单的代码

在VS2013下创建如下代码,来对函数栈帧的创建和销毁进行演示

#define _CRT_SECURE_NO_WARNINGS 1
#include 
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\n", c);
	return 0;
}

 2.main函数的创建过程

 按F10进行代码调试,在菜单栏(调试)中找到调用堆栈,逐步走完mian函数(不需要进入Add函数),就会出现以下代码,虽然大部分我们都看不懂,但是我们对于函数的调用关系还是能够找出来的,看下图的具体分析;

【C语言】—— 函数栈帧的创建与销毁(详解)_第2张图片 3.main函数的调用图解

 此时此刻栈区已经创建好__tmainCRTStartup函数的函数栈帧,为调用main函数做好准备了,同时有两个指针来维护这块空间(ebp-栈底指针 和 esp-栈顶指针);

【C语言】—— 函数栈帧的创建与销毁(详解)_第3张图片

 接下来main函数开始调用,就需要在此基础上开辟空间,称为压栈;因为栈区的使用特点就是先使用高地址,再使用低地址;就好比盖楼房,先打地基,再一步一步的往上盖;

四、main函数的栈前准备 

不要眨眼,请仔细看接下来的操作: 

1.push压栈操作

00DE1410  push        ebp

重新调试,进入C语言的反汇编代码(在代码区直接右键鼠标就可以找到),此时是还没执行任何操的情况,从内存中我们获取到esp(栈顶指针)和ebp(栈底指针)的地址;

【C语言】—— 函数栈帧的创建与销毁(详解)_第4张图片

  

【C语言】—— 函数栈帧的创建与销毁(详解)_第5张图片

 执行push操作:push  ebp --- 将ebp压栈(把ebp放到已经创建好的函数栈帧上),此时esp会自动的去维护栈顶,那么相应的其地址也会发变小,上图中esp的地址是0x00cffb34,执行push后地址变为0x00cffb30。由下表可以看出ebp确实被压入栈中了,esp现在所指向的就是ebp;

 【C语言】—— 函数栈帧的创建与销毁(详解)_第6张图片

 2.mov指令

00DE1411  mov         ebp,esp

move指令:是将源操作数复制到目的操作数中

此时ebp就指向了栈顶所指的位置

【C语言】—— 函数栈帧的创建与销毁(详解)_第7张图片

3.sub指令

00DE1413  sub         esp,0E4h 

sub指令---就是减法(将esp - 0E4h(16进制数))

当esp减去0E4h之后,地址变小了,将会指向上面的空间,此时esp和ebp维护的是紫色的空间,也就是为main函数预开辟的空间,main函数终于等到老弟(__tmainCRTStartup)的消息了,如图所示;

 ​​​​【C语言】—— 函数栈帧的创建与销毁(详解)_第8张图片

 4.push - ebx、esi、edi指令

00DE1419  push        ebx  
00DE141A  push        esi  
00DE141B  push        edi  

这里对于ebx、esi、edi不做详细的解释,在本文最后会加以注释;

压栈压入3个元素后,esp的地址又变小了,也确实从下图可以看出这三个元素的的确确被压进去了,也许从刚才的几张图来看,不是很能理解为什么esp会自动,但是结合下图和前几张图来看,就很容易的理解了,本人的理解就是压一个元素就要esp来维护;

这三个元素在函数返回的时候会自动弹出去,切不要在这三个寄存器上多考虑;

【C语言】—— 函数栈帧的创建与销毁(详解)_第9张图片

 5.led、mov、mov、rep stos指令

00DE141C  lea         edi,[ebp-0E4h]  
00DE1422  mov         ecx,39h  
00DE1427  mov         eax,0CCCCCCCCh  
00DE142C  rep stos    dword ptr es:[edi] 

1.lea           edi,[ebp-0E4h] --- 加载有效地址:edi可以指向ebp - 0E4h这个位置

2. mov        ecx,39h --- 把39h这个值存放到ecx寄存器中

3.mov         eax,0CCCCCCCCh --- 把0CCCCCCCCh这个值存放到eax寄存器中

4.rep stos    dword ptr es:[edi] --- 从edi这个位置开始向下39h次个内容初始化为eax的内容(0CCCCCCCCh)

具体如下图所示:

【C语言】—— 函数栈帧的创建与销毁(详解)_第10张图片

五、正式执行有效代码

1.局部变量的创建以及值的问题

        局部变量的创建就是建立在栈帧已经创建好的基础上,去找到一些空间来存放这些局部变量,当局部变量未初始化时,里面默认的是0CCCCCCCCh这些值,所以就有了我们在编写代码时未初始化变量,导致打印出来是随机值;

        下图是局部变量在栈区中存放的位置,不是每个编译器都按照这种方式存储,有可能是紧挨着存储,也有可能随机存,但一定在已经开辟好的栈区空间中,这里数据的存储是小端存储,后续会给大家介绍数据的存储;

	int a = 10;
00DE142E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00DE1435  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00DE143C  mov         dword ptr [ebp-20h],0 

【C语言】—— 函数栈帧的创建与销毁(详解)_第11张图片

 2.函数是如何传参的

 下图中这几段代码是函数传参;可以看出,在传参时,先传b再传a;

 【C语言】—— 函数栈帧的创建与销毁(详解)_第12张图片

 3.Add函数的函数栈帧创建

00DE13C0  push        ebp  
00DE13C1  mov         ebp,esp  
00DE13C3  sub         esp,0CCh  
00DE13C9  push        ebx  
00DE13CA  push        esi  
00DE13CB  push        edi  
00DE13CC  lea         edi,[ebp-0CCh]  
00DE13D2  mov         ecx,33h  
00DE13D7  mov         eax,0CCCCCCCCh  
00DE13DC  rep stos    dword ptr es:[edi]

这部分的代码和刚刚main函数创建的函数栈帧是一样的,这里就不一步一步的给大家解释了,仿照main函数的函数栈帧创建过程也能够画出下面的图解;

【C语言】—— 函数栈帧的创建与销毁(详解)_第13张图片

 4.形参与实参之间的关系​​​​​​​

        仔细观察下面的图,就可以发现,函数在传参的过程中是先将b的值存到eax这个寄存器里,然后压栈,把a的值存到exc这个寄存器里,然后压栈,在调用这些值进行运算的时候,是通过ebp加减的操作来获取这些值,并保存到eax寄存器里;而且变量x、y也没有在Add函数的栈帧中创建,通过ebp+8找到的值(可以理解为形参x)、ebp+12找到的值(可以理解为形参y),所以形参就是实参的一份临时拷贝,改变形参并不会影响实参;

【C语言】—— 函数栈帧的创建与销毁(详解)_第14张图片

 5.函数是如何返回值的

1.pop出栈指令

pop --- 出栈:当执行这个指令时,会将栈顶的元素弹出,并且esp(栈顶指针)会变大,也就是向高地址走  

00DE13F1  pop         edi  
00DE13F2  pop         esi  
00DE13F3  pop         ebx

【C语言】—— 函数栈帧的创建与销毁(详解)_第15张图片

 ret这个代码,是用来返回值的,刚才计算好的z是存放在eax这个寄存器中的,它是由寄存器带回给主函数的,虽然Add函数栈帧销毁了,但是算好的值已经被保存了下来;

当走到这一步,此时的esp和ebp就开始维护main函数的函数栈帧了

00DE13F4  mov         esp,ebp  
00DE13F6  pop         ebp  
00DE13F7  ret  

【C语言】—— 函数栈帧的创建与销毁(详解)_第16张图片

 上面的操作都是在执行call的指令(就是调用Add),因为对Add的地址进行压栈的,弹出之后也就能找到了Add的地址(存这个地址就是为了函数在返回时能够找到原来的位置)也就能够继续执行下面的程序了,如下图;

【C语言】—— 函数栈帧的创建与销毁(详解)_第17张图片

 2.函数值的返回

 下图是Add函数栈帧销毁后,返回值返回的操作

00DE144B  call        00DE10E1  
00DE1450  add         esp,8  
00DE1453  mov         dword ptr [ebp-20h],eax 

【C语言】—— 函数栈帧的创建与销毁(详解)_第18张图片

 六、总结

        以上是main函数创建及Add函数的创建和销毁,main函数的销毁就不往下画了,大致的步骤都是一样的,笔者也是花了一点时间一步一步的操作,注释。希望可以给大家带来帮助,不足之处希望大家指出。

        总的来说,细细的去感受函数栈帧,确实是挺快乐的,你会发现很多奇妙的事情。

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