目录
一,我们面对函数的疑惑
做为初学者的我们一定对编程的神奇而感到惊叹!当然我们同样也有许许多多的困惑。我们一点有过: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.函数结束以后是怎么返回的?
函数结束后,通过寄存器将函数执行的结果带回到主函数中。通过对进入函数前的指令的下一条指令的记录又回到主函数。