提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
在初期学习语言过程中,相信大家会遇到许多不理解又找不到理论去支撑自己理解的一些问题。经过长久的学习再回头去看后发现,其中有许多类似如下问题:
局部变量是怎么创建的?其为什么是随机的?
函数到底是如何传参的?传参的顺序又是怎样?
形参实参到底有什么关系?
函数是如何调用的?
调用的结果又是如何返回的?
相信细心的读者一定遇到过此类困惑。其实上述问题本质上是对函数栈帧的创建与销毁理解不够深刻,这篇文章就是通过查看函数调用反汇编的方式来深刻研究函数栈帧创建和销毁的过程进而解决上述问题。相信不论是计算机学习的新手还是未研究过此类问题的大佬,都能让你今后更高效的处理此类问题。
提示:不同编译器其调试结果可能不完全相同,但总体思路是一样的。这里我使用的是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寄存器赋值。
总结
至此上述问题就当作练习题,认真看完本文章,然后完成吧。