今天用一段简单的程序详细的看看函数在调用过程中的栈帧情况。(栈帧表示程序的函数调用记录)
代码如下:
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 ebppush进行压栈操作,将ebp压入栈中,esp指向新的栈顶,如图:
010F1411 mov ebp,espmov移动指令,将esp放入ebp中,也就是说ebp指向了esp处的位置,esp暂时不动,如图:
010F1413 sub esp,0E4hsub表示减指令,将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 retret返回指令后,我们可以发现:
返回到了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函数调用完后,整个程序也结束了。
这就是函数调用过程中,函数栈帧的创建和销毁情况,有错误的地方欢迎大家指正,谢谢大家!