前期学习的时候,我们可能有很多困惑,比如:
局部变量是怎么创建的?
为什么局部变量的值是随机值?
函数是怎么传参的?传参的顺序是怎样的?
形参和实参是什么关系?
函数调用是怎么做的?
函数调用是结束后怎么返回的?
··· ···
知道了函数栈帧的创建和销毁就都会了,这样也能搞懂后面学习的很多知识。
今天,以VS2013为例来学习和观察函数栈帧的创建与销毁。注意是VS013!假如用别的编译器效果可能会有所差异,但重要的是能够通过今天的演示来理解。
我们今天的演示,可能会遇到如下寄存器:eax、ebx、ecx、edx、ebp、esp。
其中,ebp和esp这两个寄存器中存放的地址,用于维护函数栈帧。
每一个函数调用都要在栈区创建一块空间。
现在创建一个新项目,新建.c文件,用函数写一个两数之和的程序:
#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;
}
正在调用的是什么函数,esp和ebp维护的就是哪个函数的栈帧,比如正在调用main函数,就去维护main函数的栈帧,正在调用Add,就要去调用Add的栈帧。esp和ebp之间的空间就是正在维护的函数的空间。
通常,ebp被称为栈底指针,esp被称为栈顶指针。
栈区的使用习惯是先使用高地址,再使用低地址。
当创建新的空间的时候,就会在更低的地址处创建空间:
现在进行调试,并打开调用堆栈:
从调用堆栈里可以看到,main函数被调用了。
是的,main函数也是被别的函数调用的!
那么,main函数是被谁调用的?
我们继续往下调试代码,当代码执行完,可以看到__tmainCRTStartup()
main函数就是被__tmainCRTStartup函数调用的。
进入反汇编,可以看到__tmainCRTStartup函数的汇编代码:
__tmainCRTStratup也有自己的代码。
我们观察到,main函数是被__tmainCRTStartup函数调用的。
也就是说在维护main函数的栈帧之前,ebp和esp在维护__tmainCRTStartup函数的栈帧。
不过这里能够了解即可。重点分析main函数和Add函数。
找到main函数的汇编代码:
int main() {
002718A0 push ebp
002718A1 mov ebp,esp
002718A3 sub esp,0E4h
002718A9 push ebx
002718AA push esi
002718AB push edi
002718AC lea edi,[ebp-24h]
002718AF mov ecx,9
002718B4 mov eax,0CCCCCCCCh
002718B9 rep stos dword ptr es:[edi]
002718BB mov ecx,27C003h
002718C0 call 0027131B
int a = 10;
002718C5 mov dword ptr [ebp-8],0Ah
int b = 20;
002718CC mov dword ptr [ebp-14h],14h
int c = 0;
002718D3 mov dword ptr [ebp-20h],0
c = Add(a, b);
002718DA mov eax,dword ptr [ebp-14h]
002718DD push eax
002718DE mov ecx,dword ptr [ebp-8]
002718E1 push ecx
002718E2 call 002710B4
002718E7 add esp,8
002718EA mov dword ptr [ebp-20h],eax
printf("%d", c);
002718ED mov eax,dword ptr [ebp-20h]
002718F0 push eax
002718F1 push 277B30h
002718F6 call 002710D2
002718FB add esp,8
return 0;
002718FE xor eax,eax
}
00271900 pop edi
00271901 pop esi
00271902 pop ebx
00271903 add esp,0E4h
00271909 cmp ebp,esp
0027190B call 00271244
00271910 mov esp,ebp
00271912 pop ebp
00271913 ret
现在来分析下main函数的汇编代码:
002718A0 push ebp
push是压栈,给栈顶放一个新的元素。
在这里是将ebp放到栈顶,栈顶指针也发生了变化。
第一个图是push之前的,第二个图是push之后的。
002718A1 mov ebp,esp
002718A3 sub esp,0E4h
mov ebp,esp的意思是将esp的值给ebp。
sub esp,0E4h的意思的将esp减去0E4h。
通过监视我们可以看到,在mov之后,ebp和esp的值相同。
mov之后,ebp就到了esp所在的位置:
sub之后esp被减去了0E4h,指向了更低的地址:
减去的0E4h的大小原来是为main函数开辟的空间大小。
而edp和esp也由维护原来的__tmainCRTSstartup函数的栈帧转变为维护main函数的栈帧了。
002718A9 push ebx
002718AA push esi
002718AB push edi
这三个push又是压栈。
002718AC lea edi,[ebp-24h]
002718AF mov ecx,9
002718B4 mov eax,0CCCCCCCCh
002718B9 rep stos dword ptr es:[edi]
这三条汇编指令的意思是将edi向下的39h这么大的空间里全部赋值为CCCCCCCC:
到此为止,main函数就创建完毕。
打开内存可以发现这些地方的值都成了cccccccc
int a = 10;
002718C5 mov dword ptr [ebp-8],0Ah
int b = 20;
002718CC mov dword ptr [ebp-14h],14h
int c = 0;
002718D3 mov dword ptr [ebp-20h],0
c = Add(a, b);
以上代码是将0Ah、14h、0(都是十六进制,其实是a、b、c的值10、20、0)分别放入ebp-8、ebp-14h、ebp-20h中。
假设一个格子是4个字节,那么a、b、c三个变量如下:
打开内存发现相应的值都一存放在内存中:
002718DA mov eax,dword ptr [ebp-14h]
002718DD push eax
002718DE mov ecx,dword ptr [ebp-8]
002718E1 push ecx
这一步就是在传参了。
意思是将ebp-14h的值传给eax,然后压栈,再将ebp-8的值传给ecx,然后压栈。
如图所示:
002718E2 call 002710B4
002718E7 add esp,8
接下来的指令是call,call是调用。
这里需要call的下一条指令压栈,因为函数调用会返回,而返回的地址是call的下一条指令。
此时需要按下F11执行call指令。
再查看内存会发现,此处和刚刚call后边的指令一样:
内存中新出现的值就是call的下一条指令的地址。
当call执行完,会从00C21450继续执行指令。
再按下F11进入Add函数。
这就进入Add函数了,以下为Add函数的汇编代码:
int Add(int x, int y) {
00271770 push ebp
00271771 mov ebp,esp
00271773 sub esp,0CCh
00271779 push ebx
0027177A push esi
0027177B push edi
0027177C lea edi,[ebp-0Ch]
0027177F mov ecx,3
00271784 mov eax,0CCCCCCCCh
00271789 rep stos dword ptr es:[edi]
0027178B mov ecx,27C003h
00271790 call 0027131B
int z = 0;
00271795 mov dword ptr [ebp-8],0
z = x + y;
0027179C mov eax,dword ptr [ebp+8]
0027179F add eax,dword ptr [ebp+0Ch]
002717A2 mov dword ptr [ebp-8],eax
return z;
002717A5 mov eax,dword ptr [ebp-8]
}
002717A8 pop edi
002717A9 pop esi
002717AA pop ebx
002717AB add esp,0CCh
002717B1 cmp ebp,esp
002717B3 call 00271244
002717B8 mov esp,ebp
002717BA pop ebp
002717BB ret
这段指令和先前为main函数开辟栈帧时的指令非常相似:
00271770 push ebp
00271771 mov ebp,esp
00271773 sub esp,0CCh
00271779 push ebx
0027177A push esi
0027177B push edi
0027177C lea edi,[ebp-0Ch]
0027177F mov ecx,3
00271784 mov eax,0CCCCCCCCh
00271789 rep stos dword ptr es:[edi]
push ebp压栈,然后mov将esp的值赋给ebp,sub让esp指向更低地址处,然后将edi下空间全部赋值为CCCCCCCC。
然后又是三次push......
继续看汇编指令,
int z = 0;
00271795 mov dword ptr [ebp-8],0
z = x + y;
0027179C mov eax,dword ptr [ebp+8]
0027179F add eax,dword ptr [ebp+0Ch]
002717A2 mov dword ptr [ebp-8],eax
return z;
002717A5 mov eax,dword ptr [ebp-8]
00271795 mov dword ptr [ebp-8],0
这是先将[ebp-8]位置赋值0然后给变量z
0027179C mov eax,dword ptr [ebp+8]
0027179F add eax,dword ptr [ebp+0Ch]
002717A2 mov dword ptr [ebp-8],eax
这些指令是先mov,将ebp+8位置的值赋值给eax,此时ebp+8位置的值正好是变量a的值为10,现在eax=10。
然后是add,把ebp+0Ch位置的值和eax指向的值相加,此时ebp+0Ch位置的值正好是变量b的值为20。
那么现在,eax的值就是30了。
再mov,把eax的值赋值给ebp-8,而ebp-8又正好是z的地址。
那么z就是30。
这个例子完美印证了“形参是实参的一份临时拷贝”。
我们在传参的时候,并没有去独立开辟新的空间去接收形参,而是通过寄存器去找到先前在主函数里压栈进去的实参。
妙啊~!
继续看:
return z;
002717A5 mov eax,dword ptr [ebp-8]
}
002717A8 pop edi
002717A9 pop esi
002717AA pop ebx
002717B8 mov esp,ebp
002717BA pop ebp
mov eax,dword ptr [ebp-8]
它的意思是让eax保管ebp-8的值。
002717A8 pop edi
002717A9 pop esi
002717AA pop ebx
pop是弹出,那么edi、esi和ebx就被销毁了。
esp的位置也随之发生改变:
002717B8 mov esp,ebp
002717BA pop ebp
把ebp的值给esp,然后pop弹出ebp,那么Add就完全销毁了:
在此之前为什么要让eax保管ebp-8的值?
就是因为ebp-8会随着Add的销毁而销毁。
002717BB ret
ret是返回值。
此时栈顶存放的是call指令的下一条指令的地址,那么现在按下F10,就直接跳到main函数的add指令了。
002718E7 add esp,8
002718EA mov dword ptr [ebp-20h],eax
现在将esp的值加8,esp的位置又发生了改变:
又进行了一次销毁,此时形参x、y的空间就释放了。
这里的mov是将eax的值赋给ebp-20h。
而ebp-20h又是压栈时c的空间的地址,也就是说30赋给了c。
创作不易,码了五千多字,求各位三连支持下!