对函数栈帧创建与销毁的探讨

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

前言

在初期学习语言过程中,相信大家会遇到许多不理解又找不到理论去支撑自己理解的一些问题。经过长久的学习再回头去看后发现,其中有许多类似如下问题:

局部变量是怎么创建的?其为什么是随机的?

函数到底是如何传参的?传参的顺序又是怎样?

形参实参到底有什么关系?

函数是如何调用的?

调用的结果又是如何返回的?

相信细心的读者一定遇到过此类困惑。其实上述问题本质上是对函数栈帧的创建与销毁理解不够深刻,这篇文章就是通过查看函数调用反汇编的方式来深刻研究函数栈帧创建和销毁的过程进而解决上述问题。相信不论是计算机学习的新手还是未研究过此类问题的大佬,都能让你今后更高效的处理此类问题。


提示:不同编译器其调试结果可能不完全相同,但总体思路是一样的。这里我使用的是VS2013。

#include<stdio.h>
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);
	return 0;
}

 在开始调试之前先要对寄存器有个大概的了解,寄存器就可以简单的理解为是在CPU上用来存储特定数据的。其中esp和ebp在此研究中起到重要作用,他们分别用来表示当前所运行函数的栈顶和栈底,用来维护栈空间。此外eax,ecx,edi等均为寄存器,只需知道他们是临时存取数据的即可。

接下来我们找出反汇编代码,进行逐行解析!

int main()
{
01001410  push        ebp  
01001411  mov         ebp,esp  
01001413  sub         esp,0E4h  
01001419  push        ebx  
0100141A  push        esi  
0100141B  push        edi  
0100141C  lea         edi,[ebp+FFFFFF1Ch]  
01001422  mov         ecx,39h  
01001427  mov         eax,0CCCCCCCCh  
0100142C  rep stos    dword ptr es:[edi]  
	int a = 10;
0100142E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
01001435  mov         dword ptr [ebp-14h],14h  
	int c = 0;
0100143C  mov         dword ptr [ebp-20h],0  
	c = Add(a, b);
01001443  mov         eax,dword ptr [ebp-14h]  
01001446  push        eax  
01001447  mov         ecx,dword ptr [ebp-8]  
0100144A  push        ecx  
0100144B  call        010010E1  
01001450  add         esp,8  
01001453  mov         dword ptr [ebp-20h],eax  
	return 0;
01001456  xor         eax,eax  
}

一、main函数栈帧的创建                                                                                                          

其实main函数也是通过其他函数调用的,这个函数叫做__tmainCRTStartup,所以main函数刚开始调用时,堆的内容是这样的:

push    ebp

将ebp的内容添加到栈顶,并将esp向上移动到添加的值之上。

  mov         ebp,esp

esp将esp的值给ebp及将ebp所指向的位置变为现在esp所指向的位置。

注意:之前的ebp(下方的ebp)实际上已经不存在了,但经过上一步储存在栈上。

sub         esp,0E4h

sub是减的意思,这段话的整体意思是将esp减0E4h也就是指向低地址移动一定距离。

 push        ebx  
 push        esi  
 push        edi

这就是简单的向栈顶压入三个寄存器的值。

lea         edi,[ebp-0E4h]  
mov         ecx,39h  
mov         eax,0CCCCCCCCh

lea的意思是load effictive address,也就是加载有效地址,将ebp-0E4h也就是刚才扩充空间时,ebp-0E4h所在相同的地址放入edi中,下面两个也是把对应的值放入对应的寄存器中,到这一步还看不出是为了要干什么。那我们接着往下看。

rep stos    dword ptr es:[edi]

从刚才edi中的地址开始,39h个双字(dword一个双字是4字节)的值变成0CCCCCCCCh(除了刚才压入的ebx,esi,edi全变成CCCCCCCC)

mov         dword ptr [ebp-8],0Ah
mov         dword ptr [ebp-14h],14h
mov         dword ptr [ebp-20h],0

这三个分别对应a,b,c的定义和初始化,拿第一个举例,其实就是将ebp-8哪个位置的空间看成是a然后给他赋值0Ah(也就是10)。其他两个也一样。

二、Add函数栈帧创建的准备

接下来开始为调用Add函数做准备

mov         eax,dword ptr [ebp-14h]
push        eax

将ebp-14位置的值(main函数中变量b的值)放入eax寄存器并将eax压入栈顶。

mov         ecx,dword ptr [ebp-8]  
push        ecx

同理将ebp-8位置的值(main函数中变量a的值)放入ecx寄存器中,并将ecx压入栈顶。
上述两步也就是将局部变量放在寄存器中,供Add函数使用。并且要注意是先压后面的参数,再压前面的参数。

call  010010E1
调用Add函数,并将call指令的下一条指令的地址01001450压入栈顶:

三、Add函数栈帧的创建

运行到此后按F11进入Add函数的反汇编 

int Add(int x, int y)
{
010013C0  push        ebp  
010013C1  mov         ebp,esp  
010013C3  sub         esp,0CCh  
010013C9  push        ebx  
010013CA  push        esi  
010013CB  push        edi  
010013CC  lea         edi,[ebp+FFFFFF34h]  
010013D2  mov         ecx,33h  
010013D7  mov         eax,0CCCCCCCCh  
010013DC  rep stos    dword ptr es:[edi]  
	int z = 0;
010013DE  mov         dword ptr [ebp-8],0  
	z = x + y;
010013E5  mov         eax,dword ptr [ebp+8]  
010013E8  add         eax,dword ptr [ebp+0Ch]  
010013EB  mov         dword ptr [ebp-8],eax  
	return z;
010013EE  mov         eax,dword ptr [ebp-8]  
}


下面继续逐步解析:

push        ebp
将ebp压入栈顶,此时的ebp为main函数的ebp为了用于Add销毁后重新回到main。后面销毁时会用到 。

接下来的几步操作与main函数栈帧创建时完全一直,不额外赘述。
010013C1  mov         ebp,esp  
010013C3  sub         esp,0CCh  
010013C9  push        ebx  
010013CA  push        esi  
010013CB  push        edi  
010013CC  lea         edi,[ebp+FFFFFF34h]  
010013D2  mov         ecx,33h  
010013D7  mov         eax,0CCCCCCCCh  
010013DC  rep stos    dword ptr es:[edi]
    int z = 0;
010013DE  mov         dword ptr [ebp-8],0  
 
    z = x + y;
010013E5  mov         eax,dword ptr [ebp+8]  
010013E8  add         eax,dword ptr [ebp+0Ch]  
010013EB  mov         dword ptr [ebp-8],eax 

这几步实现对参数的使用,并z进行赋值。第一步将[ebp+8]位置的a'先放入寄存器eax中,第二步将eax中的值与[ebp+0Ch]位置的b'的值相加放在eax中。  最后一步将寄存器eax中的值,赋值给[ebp-8]位置,及z。至此,参数的传递和使用已经完全完成,
总结一下就是,传参的时候直接在调用函数之前从后向前压入参数,形成参数的一份临时拷贝。使用时再从对应位置(ebp+8,ebp+0C)直接提取相应参数的值。
 
return z;
010013EE  mov         eax,dword ptr [ebp-8] 

返回这一步也及其重要,他采用的方式是将[ebp-8]也就是z的值,直接放入eax寄存器中。待调用者使用。

四、Add函数栈帧的销毁

在Add函数使用完毕后,接下来进行栈帧销毁操作:

010013F1  pop         edi  
010013F2  pop         esi  
010013F3  pop         ebx  
010013F4  mov         esp,ebp  
010013F6  pop         ebp  
010013F7  ret

pop         edi  
pop         esi  
pop         ebx

将edi,esi,ebx三个寄存器从栈顶弹出。

mov         esp,ebp
将ebp的值给esp,这个操作直接为esp找回了main函数的栈顶!

栈顶知道了,那么栈底呢?我们接着往下看。

pop        ebp

从栈顶弹出一个值,并赋给ebp现在栈顶的值刚好记录的是main函数的ebp所以直接找到了main的栈底。

此刻栈顶存在的是应该运行的下一条指令的地址,直接通过该地址找到main中调用完Add后的执行语句,继续执行。

add         esp,8

之后将esp加8刚好从栈中移除调用Add前传入的参数的临时拷贝。至此Add栈帧完全销毁!

    c = Add(a, b);
mov         dword ptr [ebp-20h],eax

最后将[ebp-20h]位置的值,即c用之前存取Add返回值的eax寄存器赋值。


总结

至此上述问题就当作练习题,认真看完本文章,然后完成吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值