函数栈帧的创建和销毁

函数栈帧

  • 一、 概念
  • 二、函数栈帧的创建和销毁
    • 1. 栈
    • 2. 寄存器和汇编指令
    • 3. 认识函数栈帧
      • ①函数的调用堆栈
      • ②环境
    • 4. 解析反汇编
      • ①反汇编
      • ②函数栈帧创建
        • main函数栈帧创建
        • main函数核心代码
        • Add函数
      • ③函数栈帧的销毁

一、 概念

在写c语言程序时,通常会把独立的功能抽象为函数,所以C程序是以函数为基本单位。而函数的调用、返回值和传参等这些问题都和函数栈帧有关。

函数栈帧:函数调用过程中在程序的调用栈所开辟的空间。
这批空间用来存放:

  1. 函数参数和返回值
  2. 临时变量
  3. 保存上下文信息(包括一些寄存器)

二、函数栈帧的创建和销毁

1. 栈

栈:栈被定义为一种特殊的容器,其遵循FIFO规则(也就是先入后出)。栈是一块动态内存区域,并且总是向下增长(由高地址向低地址)的。

2. 寄存器和汇编指令

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

汇编命令:
mov:数据转移指令
push:数据入栈
pop:数据弹出至指定位置
sub:减法命令
add:加法命令
call:函数调用

  1. 压入返回地址
  2. 转入目标函数

jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址

3. 认识函数栈帧

认识:

  1. 每一次函数调用,都要为本次函数调用开辟空间。也就是函数栈帧空间
  2. 这块空间的维护使用了2个寄存器:esp和ebp。

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

①函数的调用堆栈

测试代码:

#include 

int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}


int main()
{
	int a = 3;
	int b = 5;
	int ret = 0;
	ret = Add(a, b);
	printf("%d\n", ret);
	return 0;
}

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

函数调用堆栈是反馈函数调用逻辑的,从上图可以观察到,main函数之前是由invoke_main函数调用的。再之前就不用考虑了。可以确定invoke_main函数也有自己的栈帧,main函数和Add函数也会维护自己的栈帧,每个函数栈帧都有自己的ebp和esp来维护栈帧空间。

②环境

让栈帧的过程更加清晰,去掉编译器附加的代码干扰
函数栈帧的创建和销毁_第3张图片

4. 解析反汇编

①反汇编

测试代码是上面函数调用堆栈的代码

int Add(int x, int y)
{
//函数栈帧创建
00311760  push        ebp  
00311761  mov         ebp,esp  
00311763  sub         esp,0CCh  
00311769  push        ebx  
0031176A  push        esi  
0031176B  push        edi  
	int z = 0;
0031176C  mov         dword ptr [ebp-8],0  
	z = x + y;
00311773  mov         eax,dword ptr [ebp+8]  
00311776  add         eax,dword ptr [ebp+0Ch]  
00311779  mov         dword ptr [ebp-8],eax  
	return z;
0031177C  mov         eax,dword ptr [ebp-8]  
}
0031177F  pop         edi  
00311780  pop         esi  
00311781  pop         ebx  
00311782  mov         esp,ebp  
00311784  pop         ebp  
00311785  ret  


int main()
{
//函数栈帧的创建
00311820  push        ebp  
00311821  mov         ebp,esp  
00311823  sub         esp,0E4h  
00311829  push        ebx  
0031182A  push        esi  
0031182B  push        edi  
0031182C  lea         edi,[ebp-24h]  
0031182F  mov         ecx,9  
00311834  mov         eax,0CCCCCCCCh  
00311839  rep stos    dword ptr es:[edi]  
//main函数核心代码
	int a = 3;
0031183B  mov         dword ptr [ebp-8],3  
	int b = 5;
00311842  mov         dword ptr [ebp-14h],5  
	int ret = 0;
00311849  mov         dword ptr [ebp-20h],0  
	ret = Add(a, b);
00311850  mov         eax,dword ptr [ebp-14h]  
00311853  push        eax  
00311854  mov         ecx,dword ptr [ebp-8]  
00311857  push        ecx  
00311858  call        _Add (03110B4h)  
0031185D  add         esp,8  
00311860  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", ret);
00311863  mov         eax,dword ptr [ebp-20h]  
00311866  push        eax  
00311867  push        offset string "%d\n" (0317B30h)  
0031186C  call        _printf (03110D2h)  
00311871  add         esp,8  
	return 0;
00311874  xor         eax,eax  
}

②函数栈帧创建

main函数栈帧创建

解析代码:

int main()
{
//函数栈帧的创建
00311820  push        ebp                 //把ebp寄存器中的值进行压栈,此时的ebp中存放的是invoke_main函数栈帧的ebp,esp-4
00311821  mov         ebp,esp             //move指令会把esp的值放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp
00311823  sub         esp,0E4h            //sub,让esp中的地址减去一个16进制的数字0xe4,产生新的esp。此时esp是main函数栈帧的esp。————————到此ebp和esp之间维护了一块栈空间,这就是main函数的栈帧空间。这段空间中存储main函数中的局部变量,临时数据和已经调试信息等
00311829  push        ebx                 //将寄存器ebx的值压栈,esp-4 
0031182A  push        esi                 //将寄存器esi的值压栈,esp-4
0031182B  push        edi                 //将寄存器edi的值压栈,esp-4
//上面三条指令保存了3个寄存器的值在栈区,这3个寄存器在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便退出函数后恢复

//下面的代码是在初始化main函数的栈帧空间
//1. 先把ebp-24h的地址,放在edi中。lea:load effection address
//2. 把9放在ecx中
//3. 把0xCCCCCCCC放在eax中
//4. 将从edp-0x24h到ebp这一端的内存的每一个字节都初始化成0xCC
0031182C  lea         edi,[ebp-24h]       
0031182F  mov         ecx,9                
00311834  mov         eax,0CCCCCCCCh       
00311839  rep stos    dword ptr es:[edi]  //dword:四个字节  
//...
}

上面的代码后四句,等价于下面的伪代码

edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx != 0; --ecx, edi+=4)
{
	*(int*)edi = eax;
}

上面函数栈帧创建的逻辑结构:
函数栈帧的创建和销毁_第4张图片

通过上面,我们就可以知道,为什么以前有些代码会打出“烫”这个字,因为main函数调用的时候,在栈区开辟的空间其中每一个字节都被初始化成0xCC,而两个连续的0xCCCC就是“烫”。

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

main函数核心代码

解析代码:

	int a = 3;
0031183B  mov         dword ptr [ebp-8],3     //将3存储到ebp-8的地址处,ebp-8的位置其实就是a变量
	int b = 5;
00311842  mov         dword ptr [ebp-14h],5   //将5存储到ebp-14h的地址处,也就是b变量
	int ret = 0;
00311849  mov         dword ptr [ebp-20h],0   //将0存储到ebp-20h的地址处,也就是ret变量
//以上汇编,就是三个变量的创建和初始化,这就是局部变量的创建和初始化,在其所在的函数栈帧空间创建的

//调用Add函数
	ret = Add(a, b);                          //函数传参本质就是把参数push到栈帧空间
00311850  mov         eax,dword ptr [ebp-14h] //传递b,将ebp-14h处放的5放到eax寄存器中 
00311853  push        eax                     //将eax的值压栈,esp-4
00311854  mov         ecx,dword ptr [ebp-8]   //传递a,将ebp-8处放的3放在ecx寄存器中
00311857  push        ecx                     //将ecx的值压栈,esp-4
//上面四条语句是函数传参

//跳转调用函数
00311858  call        _Add (03110B4h)  
0031185D  add         esp,8  
00311860  mov         dword ptr [ebp-20],eax  

main函数核心代码的逻辑结构:
函数栈帧的创建和销毁_第6张图片

call指令执行前,还有一个压栈操作:

call指令执行前,把call指令的下一条指令的地址进行压栈操作,这个操作的作用:解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

//跳转调用函数
00311858  call        _Add (03110B4h)  
0031185D  add         esp,80031185D压栈
00311860  mov         dword ptr [ebp-20],eax  

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

Add函数
int Add(int x, int y)
{
//函数栈帧创建
00311760  push        ebp                       //将main函数栈帧的ebp保存,esp-4
00311761  mov         ebp,esp                   //将main函数的esp赋值给新的ebp,此时ebp是Add函数的
00311763  sub         esp,0CCh                  //给esp-0xCC,求出Add函数的esp
00311769  push        ebx                       //将ebx的值压栈,esp-4
0031176A  push        esi                       //将esi的值压栈,esp-4
0031176B  push        edi                       //将edi的值压栈,esp-4
	int z = 0;
0031176C  mov         dword ptr [ebp-8],0       //将0放在ebp-8的地址处。(创建z)
	z = x + y;
00311773  mov         eax,dword ptr [ebp+8]     //将ebp+8地址处的数字存储到eax中
00311776  add         eax,dword ptr [ebp+0Ch]   //将ebp+12地址处的数字加到eax中
00311779  mov         dword ptr [ebp-8],eax     //将eax的结果保存到ebp-8的地址处,放到z中
	return z;
0031177C  mov         eax,dword ptr [ebp-8]     //将ebp-8地址处的值放在eax中,就是把z的值存储到eax中,通过寄存器eax带回计算结果
}
0031177F  pop         edi                      
00311780  pop         esi                      
00311781  pop         ebx                      
00311782  mov         esp,ebp                      
00311784  pop         ebp                      
00311785  ret                                   //回到call指令的下一条指令

当代码开始执行Add函数时,就开始创建Add函数的栈帧空间,基本和main函数的栈帧空间创建大差不差。

Add函数的栈帧逻辑结构:
函数栈帧的创建和销毁_第8张图片

③函数栈帧的销毁

既然有函数栈帧的创建,那无疑也有销毁,接下分析一下函数栈帧销毁的反汇编

0031177F  pop         edi    
00311780  pop         esi  
00311781  pop         ebx  
//前面三个命令弹出三个寄存器,esp+12

00311782  mov         esp,ebp   //再将Add函数ebp的值赋值给main的esp,回收Add函数的栈帧空间
00311784  pop         ebp       //弹出栈顶的ebp,也就是main函数的ebp,esp+4。到这里恢复了main函数的栈帧维护
00311785  ret                   //栈顶弹出一个值,这个值就是call指令下一个指令的地址,esp+4

回到main函数:

0031185D  add         esp,8                   //esp+8,也就是把参数部分跳过
00311860  mov         dword ptr [ebp-20h],eax //eax的值被存到ebp-20的地址处,也就是ret变量中。本次函数的返回值是由eax的寄存器带回来的

注:返回对象是内置类型,一般是寄存器带回返回值,当返回对象较大,一般会在主调函数的栈帧中开辟空间,然后把空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数预留的空间。

你可能感兴趣的:(C,函数栈帧)