函数栈帧的创建与销毁

前言:

本文将围绕如下问题进行详细讨论,使用的编译环境为VS2013

  • 局部变量是如何创建的?
  • 局部变量的值未初始化,为什么局部变量是随机值?
  • 函数是如何传参的?传参的顺序是咋么样的?
  • 形参和实参是什么关系?
  • 函数是如何调用的?
  • 函数调用结束后是如何返回的?

函数栈帧相关的寄存器

1.通用寄存器

  EAX     EBX      ECX     EDX

2.栈寄存器

  EBP-栈底指针寄存器     ESP-栈顶指针寄存器

函数栈帧的创建整体总览

每次函数调用,都要在内存的栈区开辟空间,为该函数所开辟的内存空间称为函数栈帧;

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

main()函数的函数栈帧是由ESP与EBP进行维护,正在调用哪一个函数,ESP与EBP则去维护调用的函数的函数栈帧

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

谁调用了main()函数?

VS2013启动调试, 打开调用堆栈,不难得知

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

main()函数是由__tmainCRTStartup()函数所调用;main()函数调用之后的返回值存放于mainret中;

谁调用了__tmain()CRTStartup()函数?

函数栈帧的创建与销毁_第4张图片

__tmain()CRTStartup()函数是由mainCRTStartup()函数所调用;

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

详细探究函数栈帧的创建与销毁

前文说到当我们调用某个函数,就为该函数开辟函数栈帧,当我们调用main()函数时,main()函数内部调用了Add函数,接下来将从汇编的角度去探索未知的细节

# 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;
//main函数内部调用了Add()函数
	c = Add(a, b);
	printf("%d\n", c);

	return 0;
}

调用main()函数之前,内存已经为__tmainCRTStartup()分配好函数栈帧,同时ESP与EBP维护该函数的函数栈帧,如图

函数栈帧的创建与销毁_第6张图片

栈区的使用规则与基本操作

栈是一种具有特殊的访问方式的存储空间,其特殊性性在于使用方式为后进先出

栈的两个基本操作:

  • 入栈 就是 将一个新的元素放到栈顶
  • 出栈 就是 从栈顶取出一个元素

注:栈操作都是以字为单位进行操作,一个字即2个字节

指令示例:

  • push 寄存器   ;将寄存器中的数据入栈
  • pop   寄存器   ;出栈,用一个寄存器接收出栈的数据

push与pop指令的执行步骤

  • push指令的执行步骤

1. sp=sp-2     2. 向SS:SP指向的字单元中送入数据;

其中ss存放栈段的段地址,而sp存放栈顶的偏移地址

  • pop指令的执行步骤

1.从SS:SP指向的字单元中读取数据 2.SP=SP+2;

注意:任何时刻,SS:SP指向栈顶元素

//上述代码所对应的汇编代码
int main()
{
009D1410  push        ebp   
009D1411  mov         ebp,esp  
009D1413  sub         esp,0E4h  
009D1419  push        ebx  
009D141A  push        esi  
009D141B  push        edi  
009D141C  lea         edi,[ebp+FFFFFF1Ch]  
009D1422  mov         ecx,39h  
009D1427  mov         eax,0CCCCCCCCh  
009D142C  rep stos    dword ptr es:[edi]  
	int a = 10;
009D142E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
009D1435  mov         dword ptr [ebp-14h],14h  
	int c = 0;
009D143C  mov         dword ptr [ebp-20h],0  

	c = Add(a, b);
009D1443  mov         eax,dword ptr [ebp-14h]  
009D1446  push        eax  
009D1447  mov         ecx,dword ptr [ebp-8]  
009D144A  push        ecx  
009D144B  call        009D10E1  
009D1450  add         esp,8  
009D1453  mov         dword ptr [ebp-20h],eax  
	printf("%d\n", c);
009D1456  mov         esi,esp  
009D1458  mov         eax,dword ptr [ebp-20h]  
009D145B  push        eax  
009D145C  push        9D5858h  
009D1461  call        dword ptr ds:[009D9114h]  
009D1467  add         esp,8  
009D146A  cmp         esi,esp  
009D146C  call        009D113B  

	return 0;
009D1471  xor         eax,eax  
}
009D1473  pop         edi  
009D1474  pop         esi  
009D1475  pop         ebx  
009D1476  add         esp,0E4h  
009D147C  cmp         ebp,esp  
009D147E  call        009D113B  
009D1483  mov         esp,ebp  
009D1485  pop         ebp  
009D1486  ret  

逐句精读汇编代码

009D1410  push        ebp
//将寄存器ebp中的数据送入栈顶,由于任意时刻,esp都要指向栈顶,则sp自动递减2

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

mov ebp,esp 含义为将esp中的值送入到ebp中
009D1411  mov         ebp,esp

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

sub esp,0E4h 该指令的含义为寄存器esp中的数据减去0E4h,结果存放于esp当中;
009D1413  sub         esp,0E4h 

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

009D1419  push        ebx  
//该条指令含义为将ebx压入栈顶
009D141A  push        esi  
//该条指令含义为将esi压入栈顶,其中esi为源变址寄存器
009D141B  push        edi 
//该条指令含义为将edi压入栈顶,其中edi为目标变址寄存器

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

009D141C  lea         edi,[ebp+FFFFFF1Ch]  
//lea - load effective address取偏移地址
//lea指令,将源操作数即存储单元的有效地址传送到目的操作数
//左边为目的操作数,操作结果保存于此,操作对象为通用寄存器
//右边为源操作数,源操作数只能为存储单元
//该指令含义为将偏移地址为ebp-0E4h送入寄存器EDI中

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

009D1422  mov         ecx,39h  
//该指令的含义为将39h送入到寄存器ecx之中
009D1427  mov         eax,0CCCCCCCCh 
//该条指令的含义为将0cccccccch送入到寄存器eax之中 
009D142C  rep stos    dword ptr es:[edi] 
//stos-store string串存储指令,将eax中的数据送入到默认目的地址 ES:[EDI]
// rep指令,重复前缀指令,每执行一次,ecx减1,直至ecx减至0为止
//该指令的含义为将eax中的值0cccccccch送入到es:[edi]之中,每次内存填充的的数据大小为双字,循环执行39h次

由edi开始,依次由低地址向高地址填充;

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

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

由于该机器为小端存储模式,通过监视窗口 内存窗口   观察

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

 当局部变量未初始化时,由于cpu为该函数所开辟好的函数栈帧均为ccccccc,所以局部变量未初始化,当编译器不同时,其值有多种可能结果,所以其值为随机值;

int b = 20;
009D1435  mov         dword ptr [ebp-14h],14h
//20所对应的16进制数字为14h,该指令的功能为将14h放于ebp-14h的地址处

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

	int c = 0;
009D143C  mov         dword ptr [ebp-20h],0  
//将0置于ebp-20h的地址处

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

局部变量是如何创建的?

局部变量创建的基本思路是首先为正在调用的函数开辟函数栈帧,在函数栈帧中找到部分空间,将其值存储在函数栈帧之中;

函数调用细节展示

c = Add(a, b);
009D1443  mov         eax,dword ptr [ebp-14h]  
//将地址为ebp-14h中的值即局部变量b的值送入到寄存器eax之中保存,即eax=20
009D1446  push        eax  
//将eax中的中压入栈顶

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

009D1447  mov         ecx,dword ptr [ebp-8]  
//将地址为ebp-8 处的值送入寄存器ecx之中保存
009D144A  push        ecx  
//将ecx压入栈顶

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

 上述俩步过程的本质是为函数调用利用寄存器进行传参;

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

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

当我们进入Add函数内部,由于正在调用Add函数,则如下汇编代码将为Add函数开辟函数栈帧;


009D13C0  push        ebp
//该汇编指令的含义是将来自main函数的ebp压入栈顶,同时由于esp时刻指向栈顶,所以esp自动递减 
009D13C1  mov         ebp,esp
//将寄存器esp中的值送入ebp
009D13C3  sub         esp,0CCh  
//该条汇编指令的含义为esp=esp-0cch
 

函数栈帧的创建与销毁_第21张图片

009D13C9  push        ebx  
009D13CA  push        esi  
009D13CB  push        edi  

函数栈帧的创建与销毁_第22张图片

009D13CC  lea         edi,[ebp+FFFFFF34h]  
//该指令含义为将偏移地址为ebp-0cch送入寄存器EDI中
009D13D2  mov         ecx,33h  
//该指令的含义是将33h送入寄存器ecx中保存
009D13D7  mov         eax,0CCCCCCCCh
//该指令的含义是将0cccccccch送入寄存器eax之中  
009D13DC  rep stos    dword ptr es:[edi]  
//stos-store string串存储指令,将eax中的数据送入到默认目的地址 ES:[EDI]
// rep指令,重复前缀指令,每执行一次,ecx减1,直至ecx减至0为止
//该指令的含义为将eax中的值0cccccccch送入到es:[edi],从edi开始向高地址填充,每次内存填充的的数据大小为双字,循环执行39h次

 函数栈帧的创建与销毁_第23张图片

int z = 0;
009D13DE  mov         dword ptr [ebp-8],0

 函数栈帧的创建与销毁_第24张图片

//形参x,y 并没有创建,形参来自于哪里?
z = x + y;
009D13E5  mov         eax,dword ptr [ebp+8]  
//该汇编指令的含义为将地址为ebp+8处的值送入到eax之中,即ecx中的数字10
//eax=10
009D13E8  add         eax,dword ptr [ebp+0Ch]  
//将地址为ebp+0ch处的值取出并且加到eax之中,该汇编指令的含义为eax=eax+20 
//eax=30
009D13EB  mov         dword ptr [ebp-8],eax  
//将eax中的值即累加结果送入到ebp-8的位置处
//而ebp-8的位置刚好创建了临时变量z,此时ebp-8的位置处存放30

函数栈帧的创建与销毁_第25张图片

函数调用时,形参并不会主动创建,当我们调用Add函数之前,就将相关参数存放到寄存器中,以压栈的形式保留在栈中,而且参数传递从右向左传递,压栈时先压入最右边的参数,其次一直向左,直到最左边;

形参根本不会在函数内部创建,而是回头找到在Add函数调用之前由于压栈操作保留再栈中的参数;

return z;
009D13EE  mov         eax,dword ptr [ebp-8] 
//函数调用的结果存放于ebp-8的地址处,该汇编指令的含义是将ebp-8位置处的值送入到eax之中
//z既然是局部变量,出作用域销毁,如何将返回值带回去?
//函数调用的结果存放于寄存器中,寄存器中的值并不会销毁
 
// pop 寄存器 出栈,用寄存器接收出栈的数据
009D13F1  pop         edi  
//该条汇编指令的含义为用edi接收出栈的数据,同时esp自动递增
009D13F2  pop         esi  
//该条汇编指令的含义为用esi接收出栈的数据,同时esp自动递增
009D13F3  pop         ebx  
//该条汇编指令的含义为用ebx接收出栈的数据,同时esp自动递增
//回收Add函数所开辟好的函数栈帧
009D13F4  mov         esp,ebp 
//该汇编指令的含义是将ebp赋给esp
009D13F6  pop         ebp
//此时当前栈顶为来自main函数的ebp,进行出栈操作,将main函数的ebp的值赋给ebp,ebp重新指向main函数的函数栈底

//当前栈顶元素为call指令下一条指令的地址,Add函数调用结束后,回到main函数的函数栈帧,应该从函数调用结束后的下一条指令开始执行
009D13F7  ret  
//该汇编指令的含义为从当前栈顶弹出main函数函数栈帧的下一条指令的地址
//跳到call指令的下一条指令
//ret指令为转移指令,实现近转移,修改IP中的内容,程序是从cs:ip处执行

 Add函数的汇编代码整体概览

int Add(int x, int y)
{
009D13C0  push        ebp  
009D13C1  mov         ebp,esp  
009D13C3  sub         esp,0CCh  
009D13C9  push        ebx  
009D13CA  push        esi  
009D13CB  push        edi  
009D13CC  lea         edi,[ebp+FFFFFF34h]  
009D13D2  mov         ecx,33h  
009D13D7  mov         eax,0CCCCCCCCh  
009D13DC  rep stos    dword ptr es:[edi]  
	int z = 0;
009D13DE  mov         dword ptr [ebp-8],0  
	z = x + y;
009D13E5  mov         eax,dword ptr [ebp+8]  
009D13E8  add         eax,dword ptr [ebp+0Ch]  
009D13EB  mov         dword ptr [ebp-8],eax  
	return z;
009D13EE  mov         eax,dword ptr [ebp-8]  
}
009D13F1  pop         edi  
009D13F2  pop         esi  
009D13F3  pop         ebx  
009D13F4  mov         esp,ebp  
009D13F6  pop         ebp  
009D13F7  ret  

接下来,回到main函数的函数栈帧

009D1450  add         esp,8
//esp=esp+8,该条指令用于销毁形参
009D1453  mov         dword ptr [ebp-20h],eax  
//将寄存器eax中Add函数调用结束之后的返回值送到main函数函数栈帧中c的位置
//而且c正好是main函数中接受返回值的局部变量

接下里,只是main函数函数栈帧的销毁,不再一一赘述,汇编代码如下

	printf("%d\n", c);
009D1456  mov         esi,esp  
009D1458  mov         eax,dword ptr [ebp-20h]  
009D145B  push        eax  
009D145C  push        9D5858h  
009D1461  call        dword ptr ds:[009D9114h]  
009D1467  add         esp,8  
009D146A  cmp         esi,esp  
009D146C  call        009D113B  

	return 0;
009D1471  xor         eax,eax  

 总结

  • 局部变量是如何创建的?

首先为函数分配好函数栈帧空间,初始化一部分空间之后,然后在函数栈帧中为局部变量分配空间;

  • 局部变量的值未初始化,为什么局部变量是随机值?

随机值取决于不同的编译器,编译器不同,初始化的值不相同;

  • 函数是如何传参的?传参的顺序是咋么样的?

函数在调用之前,通过压栈操作将参数从右向左依次压栈,参数保留在栈空间之中,函数调用时,通过指针的偏移量找到形参;

  • 形参和实参是什么关系?

形参和实参只是值相同,但是空间上是独立的,所以形参是实参的一份临时拷贝,改变形参不影响实参;

  • 函数是如何调用的?

详细过程见上文函数调用细节展示

  • 函数调用结束后是如何返回的?

函数调用之前,就将call指令的下一条指令的地址压入栈中,并且将主函数的ebp压入栈中,函数调用结束之后,弹出ebp,就能找到上一个函数调用的ebp,找到esp,回到原来函数的栈帧空间,返回值通过寄存器的方式带回来的;

 

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