函数调用过程的真实情况(栈帧)

在函数的学习过程中,我们简单的了解函数之间是如歌调用并返回是不够的,这时候我们就需要进入内存中来看看对应的栈帧到底是怎么创建并完成整个操作的。

先给一段非常非常简单的代码,以便我们进行分析

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>

int Add(int x, int y)
{
	int sum = 0;
	sum = x + y;
	return sum;
}
int main()
{
	int a = 2;
	int b = 3;
	int ret = 0;
	ret = Add(a, b);
	return 0;
}


这段代码十分简单,在main函数中定义并初始化了三个变量,接下来将a,b两个参数传给Add函数,并完成求和操作后将结果传给主函数,接下来我们就进入反汇编来看看到底是怎么完成这一过程的!

要想看懂汇编代码先得掌握一些最基本的指令(先简单介绍下等下对照汇编代码详细解释每一步):

PUSH:压栈;

POP:出栈;

MOV:传送字或字节;

LEA:装入有效地址;

REP:重复;

RET:栈顶字单元出栈,实现一个程序的转移;

还有两个最关键的寄存器:ESP和EBP。

ESP始终指向调用函数栈帧的栈顶;

EBP则是指向栈底。

main函数的汇编代码:

    10: int main()                          //初始化main函数的栈帧空间
    11: {
011D16E0  push        ebp  
011D16E1  mov         ebp,esp  
011D16E3  sub         esp,0E4h  
011D16E9  push        ebx  
011D16EA  push        esi  
011D16EB  push        edi  
011D16EC  lea         edi,[ebp+FFFFFF1Ch]  
011D16F2  mov         ecx,39h  
011D16F7  mov         eax,0CCCCCCCCh  
    10: int main()                          // 执行我们的代码进行操作
    11: {
011D16FC  rep stos    dword ptr es:[edi]  
    12: 	int a = 2;
011D16FE  mov         dword ptr [ebp-8],2  
    13: 	int b = 3;
011D1705  mov         dword ptr [ebp-14h],3  
    14: 	int ret = 0;
011D170C  mov         dword ptr [ebp-20h],0  
    15: 	ret = Add(a, b);
011D1713  mov         eax,dword ptr [ebp-14h]  
011D1716  push        eax  
011D1717  mov         ecx,dword ptr [ebp-8]  
011D171A  push        ecx  
011D171B  call        011D10F0             //按F11,调到Add函数内部
011D1720  add         esp,8  
011D1723  mov         dword ptr [ebp-20h],eax  
    16: 	return 0;
011D1726  xor         eax,eax  
    17: }


Add函数的汇编代码:

     4: int Add(int x, int y)
     5: {                                    //同样先初始化Add函数栈帧的空间
011D1690  push        ebp  
011D1691  mov         ebp,esp  
011D1693  sub         esp,0CCh  
011D1699  push        ebx  
011D169A  push        esi  
011D169B  push        edi  
011D169C  lea         edi,[ebp+FFFFFF34h]  
011D16A2  mov         ecx,33h  
011D16A7  mov         eax,0CCCCCCCCh  
011D16AC  rep stos    dword ptr es:[edi]  
     6: 	int sum = 0;                //开始我们的代码!
011D16AE  mov         dword ptr [ebp-8],0  
     7: 	sum = x + y;
011D16B5  mov         eax,dword ptr [ebp+8]  
011D16B8  add         eax,dword ptr [ebp+0Ch]  
011D16BB  mov         dword ptr [ebp-8],eax  
     8: 	return sum;                 //计算完成将值放入eax中传回
011D16BE  mov         eax,dword ptr [ebp-8]  
     9: }
011D16C1  pop         edi  
011D16C2  pop         esi  
011D16C3  pop         ebx  
011D16C4  mov         esp,ebp  
011D16C6  pop         ebp  
011D16C7  ret                     

看汇编不是很形象,所以接下来就拿画好的图来对应代码谈谈(假设上为栈的低地址,下为栈高地址


一:在整个代码的开始之前:先由ESP和EBP共同维护一块_tmainCRTStarup函数的栈帧,通过它我们才可以调用main函数。


二:调用main函数,初始化这片空间


对应汇编代码如下(对着图看着注释能更快懂):


    10: int main()
    11: {
011D16E0  push        ebp                //在第一幅图的基础上先将ebp压入栈中
011D16E1  mov         ebp,esp            //把第一幅图中ebp挪至esp的位置
011D16E3  sub         esp,0E4h           //将esp地址减0E4h,相当于将esp向上挪了0E4h个位置
011D16E9  push        ebx                //将ebx压栈
011D16EA  push        esi                //将esi压栈 

011D16EB  push        edi                //将edi压栈
011D16EC  lea         edi,[ebp+FFFFFF1Ch] 

011D16F2  mov         ecx,39h            

011D16F7 mov eax,0CCCCCCCCh 
011D16FC rep stos dword ptr es:[edi] //以上四行我们可以先简单理解将刚才开辟的空间全部初始化为\
                                          // 0CCCCCCCCh


三:进入到我们的逻辑了

 12: 	int a = 2;
011D16FE  mov         dword ptr [ebp-8],2     //把2放入[ebp-8]地址中 (相当于把a = 2放入栈中)
    13: 	int b = 3
011D1705  mov         dword ptr [ebp-14h],3   //把3放入[ebp-14h]地址中(将b = 3放入)
    14: 	int ret = 0;
011D170C  mov         dword ptr [ebp-20h],0   //把0放入[ebp-20h]地址中(将ret = 0放入)
    15: 	ret = Add(a, b);
011D1713  mov         eax,dword ptr [ebp-14h]  //把[ebp-14h]地址的内容放入eax寄存器并压栈
011D1716  push        eax  
011D1717  mov         ecx,dword ptr [ebp-8]    //把[ebp-8]地址中的内容放入ecx寄存器并压栈(传递参数)
011D171A  push        ecx  
011D171B  call        011D10F0                 //把call下一条指令的地址压栈,要不一会回不来了!
                                               //在看汇编时走到这时按F11调到函数内部




四:进入Add函数创建并初始化一片空间完成操作(在调用每个函数后都会先创建并初始化一片空间,调用完后即被回收)


 4: int Add(int x, int y)
     5: {
011D1690  push        ebp  
011D1691  mov         ebp,esp  
011D1693  sub         esp,0CCh  
011D1699  push        ebx  
011D169A  push        esi  
011D169B  push        edi  
011D169C  lea         edi,[ebp+FFFFFF34h]  
011D16A2  mov         ecx,33h  
011D16A7  mov         eax,0CCCCCCCCh  
011D16AC  rep stos    dword ptr es:[edi]  
     6:     int sum = 0;
011D16AE  mov         dword ptr [ebp-8],0    
     7:     sum = x + y;
011D16B5  mov         eax,dword ptr [ebp+8]     //取到形参a的值,放到寄存器eax中
011D16B8  add         eax,dword ptr [ebp+0Ch]   //将eax与形参b的值相加
011D16BB  mov         dword ptr [ebp-8],eax    //将相加的结果重新拷贝回[ebp-8]中,此时sum=5
     8:     return sum;
011D16BE  mov         eax,dword ptr [ebp-8]    //再次将此时[ebp-8]的内容放在eax中  
     9: } 
从011D16AE开始一直到011D16BE就完成了调用参数,将结果放回sum,再将sum的值放入寄存器,等待传回结果。



五:返回主函数


011D16C1  pop         edi  
011D16C2  pop         esi  
011D16C3  pop         ebx   //将三个寄存器出栈
011D16C4  mov         esp,ebp  
011D16C6  pop         ebp  
011D16C7  ret 



上述过程简单面描述就是先将该函数顶部三个寄存器出栈,再回收空间,由于之前的call指令,我们记住了call指令下一条指令的地址,ret操作后,将这个地址也再出栈,找到我们调用之前函数的内部。


六,传回参数


011D1720  add         esp,8                       //将esp向下挪8,跳过实参
011D1723  mov         dword ptr [ebp-20h],eax      //将我们之前计算的结果放入[ebp-20h]中
    16: 	return 0;
011D1726  xor         eax,eax  
    17: }

七,回收main的空间,完成整个函数过程

 

总结:我们在每次调用一个函数的过程中,就相当于再专门为这个函数开辟一片内存空间,等待函数完成内部操作后,再返回调用之前的函数内部,在这过程中,我们需要使用call指令来记住调用前call指令下一条指令的地址,要不回都回不来,每当一个函数完成对应操作后,自动释放空间。但是在函数递归调用时,我们可以设想下如果多次调用自己本身,空间又不能及时的被释放,会占用很大的内存空间,每次在使用时又要初始化,传参,在整体代码上也会受影响,可是如果我们不了解栈帧的话,就可能不会这么容易就知道递归的缺点!

 

 新手上路,如发现错误请及时告知我,谢谢!

 

 




评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值