函数栈帧的创建和销毁

“总有人间一两风,

填我十万八千梦”

作者:小赛毛

文章初次日期:2022/11/21


目录

函数栈帧解决了什么问题?

什么是栈?

什么是寄存器?

  函数栈帧的创建和销毁

预热知识准备:

 函数的调用堆栈

转到反汇编

函数栈帧的创建

补充说明:烫烫烫~

调用Add函数:

函数栈帧的销毁


大家学了这么久C语言有没有产生一些问题与疑惑哩?

好家伙,你问俺啥问题,那我只能说6哈哈哈哈

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

比如噻

函数栈帧解决了什么问题?

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用是结束后怎么返回的?

老铁这会儿可能又要说,你小子,哪来这么多问题?

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

 其实哩,老铁们呢只要知道函数栈帧的创建和销毁,学会了就明白这些问题了,内功呢自然也就修炼了~

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。
那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。
函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:函数参数和函数返回值
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

什么是栈?

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函
数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可
以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出
栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。

在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据
从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的


要给大家讲明白这一点呢,就要给大家讲一下寄存器

什么是寄存器?

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址

ebp

esp

这两个呢是今天的重点,要理解函数栈帧,就必须理解这两个寄存器。

ebp,esp中存放的是地址,这2个地址是用来维护函数栈帧的。

同时我们也要知道相关汇编命令

  • mov:数据转移指令
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • sub:减法命令
  • add:加法命令
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令

  函数栈帧的创建和销毁

预热知识准备:

在开始之前呢,再给大家补充一下知识:

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

 每一个函数调用,都要在栈区创建一个空间每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间

  • push压栈:给栈顶放一个元素
  • pop出栈:从栈顶删除一个元素

我们知道每一个函数调用都会在栈区上开辟一块空间,

如果下面是高地址,上面是低地址

函数栈帧的创建和销毁_第4张图片 这块空间由ebp esp所维护,在正在调用哪个函数,ebp和esp就去维护哪块函数空间的函数栈帧,比如说此时调用Add函数,那esp ebp就去维护Add函数空间的栈帧,通常我们把ebp叫做栈底指针,esp叫做栈顶指针。 

 函数的调用堆栈

 在vs2013中,main函数也是被其他函数调用的

__tmainCRTStartup

mainCRTStartup

函数栈帧的创建和销毁_第5张图片


函数栈帧的创建和销毁_第6张图片 接下来我们继续:

转到反汇编

首先f10调试到main函数开始执行的第一行,右击鼠标转到反汇编。

#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;
}
int main()
{
00C418B0  push        ebp  //把ebp寄存器中的值进行压栈
00C418B1  mov         ebp,esp   //move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp    //sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp
00C418B3  sub         esp,0E4h  
00C418B9  push        ebx  
00C418BA  push        esi  
00C418BB  push        edi  
//下面的代码是在初始化main函数的栈帧空间。
//1. 先把ebp-24h的地址,放在edi中
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从edp-0x2h到ebp这一段的内存的每个字节都改为cccccccc
00C418BC  lea         edi,[ebp-24h]  
00C418BF  mov         ecx,9  
00C418C4  mov         eax,0CCCCCCCCh  
00C418C9  rep stos    dword ptr es:[edi]  
00C418CB  mov         ecx,0C4C008h  
00C418D0  call        00C4131B  
	int a = 10;
00C418D5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00C418DC  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00C418E3  mov         dword ptr [ebp-20h],0  

	c = Add(a, b);
00C418EA  mov         eax,dword ptr [ebp-14h]  
00C418ED  push        eax  
00C418EE  mov         ecx,dword ptr [ebp-8]  
00C418F1  push        ecx  
00C418F2  call        00C410B4  
00C418F7  add         esp,8  
00C418FA  mov         dword ptr [ebp-20h],eax  

	printf("%d\n", c);
00C418FD  mov         eax,dword ptr [ebp-20h]  
00C41900  push        eax  
00C41901  push        0C47B30h  
00C41906  call        00C410D2  
00C4190B  add         esp,8  
	return 0;
00C4190E  xor         eax,eax  
}

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


函数栈帧的创建

 我们先来push压栈

操作前:

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

 我们看执行完:

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

这就是给大家讲的先把ebp压进去

ebp压进去以后再怎么办呢?


接下里是move

move是把esp给ebp

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

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


 下一步sub,sub是解码嘛

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

 我们注意到esp变了,这意味着什么呢?

esp的地址往上走了~

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


紧接着下来三次push ,其实是给顶上压进去了三个元素,这三个元素呢,我们不需要去管

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


 接下来lea

lea:load effective address

给edi里面放了一个地址

将从edp-0x2h到ebp这一段的内存的每个字节都改为cccccccc


补充说明:烫烫烫~

之所以程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一
个字节都被初始化为cccccccc,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,cccccccc的汉字编码就是“烫”,所以cccccccc被当作文本就是“烫”。


a,b,c创建:

	int a = 10;
00C418D5  mov         dword ptr [ebp-8],0Ah  //将10存储到ebp-8的地址处,ebp-8的位置其实就是a变量
	int b = 20;
00C418DC  mov         dword ptr [ebp-14h],14h  //将20存储到ebp-14h的地址处,ebp-14h的位置其实是b变量
	int c = 0;
00C418E3  mov         dword ptr [ebp-20h],0  //将0存储到ebp-20h的地址处,ebp-20h的位置其实是c变量

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

 以上汇编代码表示的变量a,b,c的创建和初始化,这就是局部的变量的创建和初始化 ,其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的


当a,b,c创建好后,我们就该:

调用Add函数:

c = Add(a, b);
00C418EA  mov         eax,dword ptr [ebp-14h]  //把b的值20放入eax
00C418ED  push        eax                      //栈顶压入
00C418EE  mov         ecx,dword ptr [ebp-8]    //把a的值10放入ecx
00C418F1  push        ecx                      //栈顶压入
00C418F2  call        00C410B4                 //调用
00C418F7  add         esp,8  
00C418FA  mov         dword ptr [ebp-20h],eax  

	printf("%d\n", c);
00C418FD  mov         eax,dword ptr [ebp-20h]  
00C41900  push        eax  
00C41901  push        0C47B30h  
00C41906  call        00C410D2  
00C4190B  add         esp,8  
	return 0;
00C4190E  xor         eax,eax  
}
00C41910  pop         edi  
00C41911  pop         esi  
00C41912  pop         ebx  
00C41913  add         esp,0E4h  
00C41919  cmp         ebp,esp  
00C4191B  call        00C41244  
00C41920  mov         esp,ebp  
00C41922  pop         ebp  
00C41923  ret  
00C418EA  mov         eax,dword ptr [ebp-14h]  //把b的值20放入eax
00C418ED  push        eax                      //栈顶压入
00C418EE  mov         ecx,dword ptr [ebp-8]  
00C418F1  push        ecx 

以上两个动作就是在进行传参

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

int Add(int x, int y)
{
00C41770  push        ebp  //将main函数栈帧的ebp保存,esp-4
00C41771  mov         ebp,esp  //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00C41773  sub         esp,0CCh  //给esp-0xCC,求出Add函数的esp
00C41779  push        ebx  //将ebx的值压栈,esp-4
00C4177A  push        esi  //将esi的值压栈,esp-4
00C4177B  push        edi  //将edi的值压栈,esp-4
00C4177C  lea         edi,[ebp-0Ch]  
00C4177F  mov         ecx,3  
00C41784  mov         eax,0CCCCCCCCh  
00C41789  rep stos    dword ptr es:[edi]  
00C4178B  mov         ecx,0C4C008h  
00C41790  call        00C4131B  
	int z = 0;
00C41795  mov         dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z 
	z = x + y;//接下来计算的是x+y,结果保存到z中
00C4179C  mov         eax,dword ptr [ebp+8]  //将ebp+8地址处的数字存储到eax中
00C4179F  add         eax,dword ptr [ebp+0Ch]  //将ebp+12地址处的数字加到eax寄存中
00C417A2  mov         dword ptr [ebp-8],eax  //将eax的结果保存到ebp-8的地址处,其实就是放到z中
	return z;
00C417A5  mov         eax,dword ptr [ebp-8]   //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}
00C417A8  pop         edi  
00C417A9  pop         esi  
00C417AA  pop         ebx  
00C417AB  add         esp,0CCh  
00C417B1  cmp         ebp,esp  
00C417B3  call        00C41244  
00C417B8  mov         esp,ebp  
00C417BA  pop         ebp  
00C417BB  ret

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

 图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。 


函数栈帧的销毁

函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。
那具体是怎么销毁的呢?我们看一下反汇编代码:

00C417A8  pop         edi  //在栈顶弹出一个值,存放到edi中,esp+4
00C417A9  pop         esi  //在栈顶弹出一个值,存放到esi中,esp+4
00C417AA  pop         ebx  //在栈顶弹出一个值,存放到ebx中,esp+4
00C417AB  add         esp,0CCh  //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈帧空间

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

 各位老铁们,本章知识到此就要说再见啦,我们接下来的知识,下一篇见,小赛毛与你不见不散!

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

加油啦,小比特~

记得一键三连嗷!三连!!三连!!!

你可能感兴趣的:(笔记,开发语言,c语言,算法,visual,studio)