从函数栈帧的角度带你理解函数

目录

一,我们面对函数的疑惑

二,函数栈帧及问题解决

(一)汇编代码查看调试

(二)函数栈帧的引入

 (三)main函数栈帧的创建

(四)局部变量的创建及初始化

(五)函数的传参

(六)函数返回值的返回

三,总结


一,我们面对函数的疑惑

  做为初学者的我们一定对编程的神奇而感到惊叹!当然我们同样也有许许多多的困惑。我们一点有过:1.局部变量是怎么创建的?
          2.局部变量不初始化为什么是随机值?
          3.函数是怎样传参的,传参的顺序是什么?
          4.函数的实参和形参是什么关系?
          5.函数结束以后是怎么返回的?

这些问题吧,今天我们就走进函数栈帧的世界去解决这些问题吧。

二,函数栈帧及问题解决

我们就以下面这段简单的代码来理解函数栈帧

#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);
	printf("%d", c);
	return 0;
}

(一)汇编代码查看调试

 首先我们要进入我们的代码调试中去看汇编代码方法如下:
1.按F10进入调试

2.右击鼠标,转到反汇编

 3.打开内存窗口和监视窗口(方便观察内存中的变化)

(二)函数栈帧的引入

内存中为我们的每一个函数都分配了空间,我们可以把内存中的这段空间称为函数的栈帧。这段空间是由两个寄存器(esp和ebp)来管理的。esp中放着函数栈帧的低地址,ebp中放着函数的高地址。如图所示:

 (三)main函数栈帧的创建

mian函数是我们c语言程序的入口,当然会有自己的栈帧。要理解main函数栈帧的创建过程,我我们就要明白一个事,就是mian函数,也是被其他函数调用的,只不过c语言的底层已经帮我们调用了。那么下面我们就来看如何去创建我们的main函数的函数栈帧。

首先,使我们的调用main()函数的函数栈帧:

接下来我们就来通过我们的汇编代码来一点一点的看:
00D71880  push        ebp  这条汇编代码意思是压栈,就是把ebp中存放的地址放在栈顶的位置,同时esp移动向栈顶,图解如下:

接着执行下一条汇编代码,00D71881  mov         ebp,esp ,这条代码的意思就是,把esp的值赋给ebp,我们可以通过监视器观察执行这天代码前:

执行这条代码后:

这时esp和ebp中的存放的地址指向了同一地址,图解效果如下(为了方便观察重新画了图):

 接着下一条代码:00D71883  sub         esp,0E4h,这条代码的意思就是,esp减去0E4h,因为我们是从高地址往低地址使用的,所以执行这条代码后我们的esp就会向上移动.监视器中观察如下:

 图解如下:

那么前面我们谈到函数的栈帧是由esp和ebp来管理的,这时我们的esp和ebp所管理的这块空间就是main函数的函数栈帧。

(四)局部变量的创建及初始化

首先执行四条代码:1.00D71889  push        ebx 

                                 2.00D7188A  push        esi  
                                 3.00D7188B  push        edi 

                                 4.00D7188C  lea         edi,[ebp+FFFFFF1Ch]

这三条代码是向栈顶压栈三个元素,具体作用我们在这里不探讨。淡然压栈的同时我们的esp会向上移动,监视器观察如下:

 图解如下:

 接着执行00D71892  mov         ecx,39h  
              00D71897  mov         eax,0CCCCCCCCh  
              00D7189C  rep stos    dword ptr es:[edi] 

这三行汇编代码的意思就是,将esp到ebp直接的所有空间都初始化为0CCCCCCCCh  ,可以通过内存窗口查看:

 图解如下:

接着就来到了主要代码的执行

    int a = 10;
00D718A8  mov         dword ptr [ebp-8],0Ah  
    int b = 20;
00D718AF  mov         dword ptr [ebp-14h],14h    
    int c = 0;
00D718B6  mov         dword ptr [ebp-20h],0 

 根据前面我们知道,ebp处于函数栈帧的最下方,上面的代码,就是将ebp-8,的地方赋值为0Ah (10的十六进制表示形式),将ebp-14h,的地方赋值为14h (20的十六进制表示形式),将ebp-20h,的地方赋值为0 (0的十六进制表示形式),可以通过内存窗口查看:

 图解如下:

此时,我们就解决了两个问题,局部变量是如何创建的呢?就是在函数开辟的空间中为局部变量创建空间。未初始化的局部变量为什么是随机值呢?我们可以看到函数栈帧中的所有空间都被赋值为了随机值,随机变量为初始化时,就会使用原有的随机值。

(五)函数的传参

执行完上述的代码后我们就来到了,函数的调用阶段,也就是到了我们最关心的函数的传参问题上。

执行汇编代码:

00D718BD  mov         eax,dword ptr [ebp-14h]  
00D718C0  push        eax  
00D718C1  mov         ecx,dword ptr [ebp-8]  
00D718C4  push        ecx

这几句汇编代码的意思是:将ebp-14h中的值赋值给eax寄存器,将ebp-8中的值赋值给ecx寄存器,t同时将eax和ecx压栈。根据上文我们知道,ebp-14中放着20也就是b,ebp-8中放着10,也就是a。

图解如下(为方便以后观察我们在这里将main函数的栈帧标记下来):

 其实这一步已经完成了函数的传参,只是现在还有现象表明,在代码继续执行的过程中,我们会的到答案。

下面接着执行代码:

00D718C5  call        00D711CC

这条代码就开始了函数的调用,同时记录下call指令的下一条指令的地址

我现在可以看到esp现在存着10的十六进制:

 当执行call指令时:

我们观察到现在esp中存的内容与call指令的下一条指令的地址相同。

图解如下:

此时我们的代码进入到了Add(int x,int y)的内部,

 不难发现这些代码与mian函数创建栈帧和初始化数据的代码一样,这里就不详细介绍,代码执行的图解如下:

下面执行这条代码

int z = 0;
00D71738  mov         dword ptr [ebp-8],0

同时上文给a,b 赋值一样此时在Add函数的栈帧中给z开辟了空间并赋值为0

图解如下:

执行代码:00D7173F  mov         eax,dword ptr [ebp+8]  

由上文我们知道ebp+8中存放的是10,00D7173F  mov         eax,dword ptr [ebp+8]  这天代码就是将10 存放在eax中。

执行代码:00D71742  add         eax,dword ptr [ebp+0Ch] 

由上文我们知道ebp+0Ch中存放的是10,00D71742  add         eax,dword ptr [ebp+0Ch] ,dword ptr [ebp+0Ch]  这天代码就是将20加在eax上。

再执行代码:00D71745  mov         dword ptr [ebp-8],eax 

这时将eax中的值赋值给ebp-8(也就是z)。

讲到这里我们前面的传参的假设也就得到了证明。但是还有有个问题,就z在结束Add函数后就会被销毁,那么返回值是怎样带回去的呢?下面我们就来探讨这个问题。

(六)函数返回值的返回

为了解决返回值被销毁而不能返回的问题,将得到的结果放在寄存器中带出来是最好的办法。

代码:00D71748  mov         eax,dword ptr [ebp-8] 就是将z的值放在eax寄存器中。
接下来执行代码:

00D7174B  pop         edi  
00D7174C  pop         esi  
00D7174D  pop         ebx  

00D7175B  mov         esp,ebp
就是把栈顶的这些元素进行释放。

图解如下:

 执行代码:00D7175D  pop         ebp 

这时将ebp的内容释放出来,放在ebp中,我们知道只是的ebp中的地址是原来main函数的ebp 的地址,所以现在的ebp又来到了原来main函数的ebp的位置,同时esp下移。

图解如下:

此时执行:00D718F6  ret,这条指令执行时,就要返回到原来进入Add函数的部分,因为此时栈顶放着的就是call指令的下一条指令的地址。

通过上面的执行结果找到了进入Add之前的代码的位置,接着这个位置继续执行

:00D718CA  add         esp,8 

这条代码的意思是给esp加8,通过上文不难发现此时esp来到了原来main函数的栈顶。此时,代码的执行就完美的回到了main函数中。

图解如下:

 

接下来执行:00D718CD  mov         dword ptr [ebp-20h],eax 

从上文中,我们知道ebp-20的位置放着c,而eax放着的就是Add函数所就算出的结果。这样Add函数的返回值就返回给了mian函数。

接下来,main函数的栈帧销毁和Add函数就类似了,这里就不详细解释了。

三,总结

   通过上述的分析,最初提到的问题都得到了解决。

1.局部变量是怎么创建的?

首先创建函数栈帧,然后在函数栈帧的空间中找到空间给局部变量使用。

 2.局部变量不初始化为什么是随机值?

因为,在创建函数栈帧后,就对栈帧的空间进行了赋值,所以,当局部变量没有初始化的时候,就使用了原本赋得值。

3.函数是怎样传参的,传参的顺序是什么?

函数的传参是在函数调用之前进行传递的,通过寄存器传递给函数,从上文不难看出,函数的传递顺序是从右到左传递的。

4.函数的实参和形参是什么关系?

上文的分析中我们并没有创建函数的形参,可见函数的形参就是实参的一份临时拷贝。

5.函数结束以后是怎么返回的?

函数结束后,通过寄存器将函数执行的结果带回到主函数中。通过对进入函数前的指令的下一条指令的记录又回到主函数。

 

 

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值