C语言中函数的调用与栈帧

概念
栈帧:栈帧在函数中用于声明局部变量,保存函数参数,保存函数返回地址等。
EBP 寄存器又叫栈帧寄存器(作用通过EBP寄存器访问保存在栈中的局部变量,函数参数,函数返回地址等)

栈(stack)是一种数据结构,栈中每个指针(当运行到那个变量时)会指向堆中的某一内存区域或说是空间。它是以先进后出为原则的。

堆(heap)就直接是内存区域了,它是为了栈的引用而开发内存的。通常内置变量就是值类型是被保存在栈中的。其他由.NET框架(Framework)提供的,或者是我们自己定义的对象即引用类型,一般被创建在堆中并将由栈中变量引用。

那么它们运行时的释放是怎样呢?首先刚才说了,栈中会有指针指向堆,其实那叫栈帧(当一个函数被调用时,栈就去堆中借一块内存给它),当函数返回时,栈帧被释放,栈帧中的对象超出作用域而被销毁。这就是一个变量的生命周期了,一只工蜂的生命就是他完成了采蜜工作。而堆中的对象是在没有栈帧指向它的时候,也就是工蜂采完蜜了,蜂王产下卵之后就结束生命了。之后堆中的对象就通过CLR垃圾回收系统销毁。

当一个方法被调用,调用栈会分配一块空间成为栈页,用来保存被调用方法的下一条指令的返回地址。也就是说它会将调用方法之后会产生什么的返回地址发给被调用方法的参数以及被调用方法的所有局部变量。所以“展开调用栈”就是指找到被调用方法的返回地址并强制方法返回,在调用方法的方法中寻找catch语句或其他处理异常的语句来处理异常。在找到异常处理代码之前,栈要“展开”很多被调用的方法。如果最终栈展开到main方法儿还是没找到异常处理代码,默认的异常处理就会被调用,整个程序被终结,栈中的变量和栈帧被销毁,程序终止。

其实如果程序找到了异常处理的代码就会从那段代码开始继续,而不是从抛出异常的地方,或是从调用了抛出异常方法的方法(除非这个方法包含异常处理代码),一定拿栈页被展开,他就被释放。

栈帧对应的汇编代码如下:

push ebp
mov  ebp,esp
...
mov esp,ebp
pop ebp
RETN
原理:
首先将EBP寄存器中的值进栈
将ESP的值赋值给EBP
接下来无论是访问局部变量,还是调用函数都是以EBP 寄存器为基准,这样无论ESP怎么变化都不会影响访问局部变量,函数返回地址,函数等。
最后:
将ebp的值赋值给esp 寄存器,pop 一个数据赋值到ebp寄存器(其实此时pop的就是最初push ebp的值) 
即最后一步骤的操作就是为拉恢复寄存器中为调用函数前的状态

栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。下图为典型的存取器安排,观察栈在其中的位置

 

入栈操作:push eax; 等价于 esp=esp-4,eax->[esp];如下图

出栈操作:pop eax; 等价于 [esp]->eax,esp=esp+4;如下图

我们来看下面这个C程序在执行过程中,栈的变化情况

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

int main()
{
	int a = 2;
	int b = 3;
	int ret = 0;
	ret = func(a, b);
	printf("%d", ret);
	system("pause");
	return 0;
}
这是一个两个数相加的程序,在VS中打开调试中的反汇编,查看汇编代码
int main()
{
002233C0  push        ebp                             
002233C1  mov         ebp,esp  
002233C3  sub         esp,0E4h  
002233C9  push        ebx  
002233CA  push        esi  
002233CB  push        edi  
002233CC  lea         edi,[ebp-0E4h]  
002233D2  mov         ecx,39h  
002233D7  mov         eax,0CCCCCCCCh  
002233DC  rep stos    dword ptr es:[edi]  
	int a = 2;
002233DE  mov         dword ptr [a],2  
	int b = 3;
002233E5  mov         dword ptr [b],3  
	int ret = 0;
002233EC  mov         dword ptr [ret],0  
	ret = func(a, b);
002233F3  mov         eax,dword ptr [b]  
002233F6  push        eax  
002233F7  mov         ecx,dword ptr [a]  
002233FA  push        ecx  
002233FB  call        _Add (02211EFh)  
00223400  add         esp,8  
00223403  mov         dword ptr [ret],eax  
	printf("%d", ret);
00223406  mov         esi,esp  
00223408  mov         eax,dword ptr [ret]  
	printf("%d", ret);
0022340B  push        eax  
0022340C  push        225858h  
00223411  call        dword ptr ds:[22911Ch]  
00223417  add         esp,8  
0022341A  cmp         esi,esp  
0022341C  call        __RTC_CheckEsp (022114Ah)  
	system("pause");
00223421  push        22585Ch  
00223426  call        _system (02210AFh)  
0022342B  add         esp,4  
	return 0;
0022342E  xor         eax,eax  
}
00223430  pop         edi  
00223431  pop         esi  
00223432  pop         ebx  
00223433  add         esp,0E4h  
00223439  cmp         ebp,esp  
0022343B  call        __RTC_CheckEsp (022114Ah)  
00223440  mov         esp,ebp  
00223442  pop         ebp  
00223443  ret  

在main调用func函数前,栈的情况,也就是说main的栈帧:

从低地址esp到高地址ebp的这块区域,就是当前main函数的栈帧。当main中调用func时,写成汇编大致是:

push m

push n; 两个参数压入栈

call func; 调用func,将返回地址填入栈,并跳转到func

当跳转到了func,来看看func的汇编大致的样子:

int func(int x, int y) { 00221C00  push        ebp   00221C01  mov         ebp,esp   00221C03  sub         esp,0CCh   00221C09  push        ebx   00221C0A  push        esi   00221C0B  push        edi   00221C0C  lea         edi,[ebp-0CCh]   00221C12  mov         ecx,33h   00221C17  mov         eax,0CCCCCCCCh   00221C1C  rep stos    dword ptr es:[edi]   int z = 0; 00221C1E  mov         dword ptr [z],0   z = x + y; 00221C25  mov         eax,dword ptr [x]   00221C28  add         eax,dword ptr [y]   00221C2B  mov         dword ptr [z],eax   return z; 00221C2E  mov         eax,dword ptr [z]   } 00221C31  pop         edi   00221C32  pop         esi   00221C33  pop         ebx   00221C34  mov         esp,ebp   00221C36  pop         ebp   00221C37  ret  

push ebp; 这个很重要,因为现在到了一个新的函数,也就是说要有自己的栈帧了,那么,必须把上面的函数main的栈帧底部保存起                        ; 来,栈顶是不用保存的,因为上一个栈帧的顶部讲会是func的栈帧底部。(两栈帧相邻的)

mov ebp, esp; 上一栈帧的顶部,就是这个栈帧的底部

暂时先看现在的栈的情况

到这里,新的栈帧开始了

sub esp, 0CCh  ;  int a, b 这里声明了两个int,所以esp减小8个字节来为a,b分配空间

mov dword ptr [esp+4], [ebp+12];   a=m

mov dword ptr [esp], [ebp+8]; b=n         

这样,栈的情况变为:

ret 8     ;  返回,然后8是什么意思呢,就是参数占用的字节数,当返回后,esp-8,释放参数m,n的空间

通过ebp,能够很容易定位到上面的参数。当从func函数返回时,首先esp移动到栈帧底部(即释放局部变量),然后把上一个函数的栈帧底部指针弹出到ebp,再弹出返回地址到cs:ip上,esp继续移动划过参数,这样,ebp,esp就回到了调用函数前的状态,即现在恢复了原来的main的栈帧。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值