浅谈函数的调用,栈帧的创建和销毁

      今天用一段简单的程序详细的看看函数在调用过程中的栈帧情况。(栈帧表示程序的函数调用记录

      代码如下:

int Add(int a, int b)
{
	int ret = 0;
	ret = a + b;
	return ret;
}

int main()
{
	int a = 10;
	int b = 20;
	int ret = 0;
	ret = Add(a,b);
	
	return 0;
}

这个的函数的功能是求两数之和。

转到反汇编代码:

首先我们要明白的一点是,在程序跑起来之前,会有一个入口函数mainCRTStartup,然后该函数会调用main函数;因此,开始,我们会在内存中(栈上)为函数mainCRTStartup开辟一块空间。如图:

esp表示栈顶,ebp表示栈底,并且栈是由高地址向低地址延伸



接下来看汇编代码:

int main()
{
010F1410  push        ebp                      
010F1411  mov         ebp,esp  
010F1413  sub         esp,0E4h  
010F1419  push        ebx  
010F141A  push        esi  
010F141B  push        edi  
010F141C  lea         edi,[ebp-0E4h]  
010F1422  mov         ecx,39h  
010F1427  mov         eax,0CCCCCCCCh  
010F142C  rep stos    dword ptr es:[edi]

一步步分析:

010F1410  push        ebp 
push进行压栈操作,将ebp压入栈中,esp指向新的栈顶,如图:


010F1411  mov         ebp,esp
mov移动指令,将esp放入ebp中,也就是说ebp指向了esp处的位置,esp暂时不动,如图:


010F1413  sub         esp,0E4h
sub表示减指令,将esp挪向esp-0E4h的位置处,同时,这段空间就是为main函数所开辟的空间,即main函数栈帧

如图:


010F1419  push        ebx  
010F141A  push        esi  
010F141B  push        edi  

连续三次push



010F141C  lea         edi,[ebp-0E4h]  
010F1422  mov         ecx,39h  
010F1427  mov         eax,0CCCCCCCCh  
010F142C  rep stos    dword ptr es:[edi]  

lea表示加载有效地址,rep表示重复执行;这四行代码表示:从ebp-0E4h这个位置向ebp处循环拷贝ecx次(即39h次)eax (0cccccccch),如图:


我们可以看一下内存:


一共57次(即039h次)


继续看:

int a = 10;
010F142E  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
010F1435  mov         dword ptr [ebp-14h],14h  
	int ret = 0;
010F143C  mov         dword ptr [ebp-20h],0  
	ret = Add(a,b);
010F1443  mov         eax,dword ptr [ebp-14h]  
010F1446  push        eax  
010F1447  mov         ecx,dword ptr [ebp-8]  
010F144A  push        ecx  
010F144B  call        010F10E6  
010F1450  add         esp,8  
010F1453  mov         dword ptr [ebp-20h],eax  
	
	return 0;
010F1456  xor         eax,eax  

继续一步步分析:

int a = 10;
010F142E  mov         dword ptr [ebp-8],0Ah  

这句代码表示:将0Ah存入ebp-8处的这个位置,我们可以看内存:

ebp的位置:


ebp-8的位置:


我们可以再看看0Ah是否存入了ebp-8处:


通过内存窗口可以看到0A已经存入ebp-8处;

再看b:

int b = 20;
010F1435  mov         dword ptr [ebp-14h],14h

还是同上:


14h也成功存入;

再看ret:

int ret = 0;
010F143C  mov         dword ptr [ebp-20h],0 

如图:


ret=0也成功存入;


继续看:

ret = Add(a,b);
010F1443  mov         eax,dword ptr [ebp-14h]  
010F1446  push        eax  
010F1447  mov         ecx,dword ptr [ebp-8]  
010F144A  push        ecx  

这几句代码表示:将ebp-14h的值( 也就是b的值20)存入eax中,再将他压入栈中;

                             将ebp-8h的值(也就是a的值10)存入ecx中,再将其压入栈;

注意,这就是函数传参为什么是从右向左。

如图:



继续往下看:

010F144B  call        010F10E6  
010F1450  add         esp,8  

call表示要调用函数了,此刻注意call指令的下一个地址:010F1450;

按一下F11,进入Add函数,此时,已经将010F1450这个地址压入栈中,如图:



现在进入Add函数内部

int Add(int a, int b)
{
010F13C0  push        ebp  
010F13C1  mov         ebp,esp  
010F13C3  sub         esp,0CCh  
010F13C9  push        ebx  
010F13CA  push        esi  
010F13CB  push        edi  
010F13CC  lea         edi,[ebp+FFFFFF34h]  
010F13D2  mov         ecx,33h  
010F13D7  mov         eax,0CCCCCCCCh  
010F13DC  rep stos    dword ptr es:[edi]  
	int ret = 0;
010F13DE  mov         dword ptr [ebp-8],0  
	ret = a + b;
010F13E5  mov         eax,dword ptr [ebp+8]  
010F13E8  add         eax,dword ptr [ebp+0Ch]  
010F13EB  mov         dword ptr [ebp-8],eax  
	return ret;
010F13EE  mov         eax,dword ptr [ebp-8]  

继续分析:

010F13C0  push        ebp
这句代码表示:将main函数的ebp压入栈中,如图:



继续看:

010F13C1  mov         ebp,esp  
010F13C3  sub         esp,0CCh  
010F13C9  push        ebx  
010F13CA  push        esi  
010F13CB  push        edi  
010F13CC  lea         edi,[ebp+FFFFFF34h]  
010F13D2  mov         ecx,33h  
010F13D7  mov         eax,0CCCCCCCCh  
010F13DC  rep stos    dword ptr es:[edi] 

这段代码和第一个一样,在栈上开辟一块属于Add函数的空间:



int ret = 0;
010F13DE  mov         dword ptr [ebp-8],0  
	ret = a + b;
010F13E5  mov         eax,dword ptr [ebp+8]  
010F13E8  add         eax,dword ptr [ebp+0Ch]  
010F13EB  mov         dword ptr [ebp-8],eax  

这段代码表示:将0存入ebp-8的位置处,然后再将ebp+8的值,也就是10存入eax中,将ebp+0ch处的值也就是20和eax中的值(10)相加并存入ebp-8的位置处(即ret值由0变为30),看图;





return ret;
010F13EE  mov         eax,dword ptr [ebp-8]
再将ebp-8处的值,也就是30存入eax中;

到这里,Add函数已经调完。


继续往下看:

010F13F1  pop         edi  
010F13F2  pop         esi  
010F13F3  pop         ebx  
010F13F4  mov         esp,ebp  
010F13F6  pop         ebp  
010F13F7  ret  

在这里,三次pop表示弹出栈,如图:



010F13F4  mov         esp,ebp 
将esp指向ebp的位置:



010F13F6  pop         ebp 
弹出ebp,此时,ebp回到了main函数的栈底位置处, 因为ebp在弹出之前指向的位置处保存着main函数栈底的位置:



010F13F7  ret  
ret返回指令后,我们可以发现:


返回到了call指令的下一条指令位置处,特别注意刚刚我们前面在栈上存储的call指令的下一个指令位置:010F1450

因此,直接返回该位置处;

注意esp的位置变化:



010F144B  call        010F10E6  
010F1450  add         esp,8  
010F1453  mov         dword ptr [ebp-20h],eax  
	
	return 0;
010F1456  xor         eax,eax  

紧接着:

010F1450  add         esp,8
注意esp:


两个形参(10,20)分别销毁,还给了操作系统;

再看:

v010F1453  mov         dword ptr [ebp-20h],eax 

刚刚我们eax中存的是30这个返回值,现在将他存入ebp-20h处(main函数中ret的位置处):


此刻,实参ret的值为30;


到此,整个程序回到main函数中,当main函数调用完后,整个程序也结束了。


这就是函数调用过程中,函数栈帧的创建和销毁情况,有错误的地方欢迎大家指正,谢谢大家!


猜你喜欢

转载自blog.csdn.net/l__xing/article/details/78377432