今天我们来学习函数栈帧的创建与销毁,让我们一起了解更多的底层原理,看完之后这些问题都迎刃而解了!!!
注:在不同编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节诎诘语编译器的实现
演示环境:Win10+x86+Vs2013
在介绍函数栈帧的创建之前,我们首先要了解一个东西--------------------寄存器
寄存器的种类有很多种,今天主要介绍两种:
ebp和esp 这两个寄存器存放的是地址,用来维护函数栈帧的,简单来说就是维护函数开辟的那一块空间
每一个函数的调用,都要在栈区开辟一块空间,而ebp和esp就是维护这块空间的,如下图:
为了方便演示,我们编写一个加法程序:
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;
}
接下来就是函数栈帧的创建和销毁,这里我们需要打开反汇编代码,逐条分析:
操作步骤:F10----->光标停留在代码块处右击鼠标----->转到反汇编
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函数也是被其他函数所调用的,它是被_tmainCRTStartup这个函数所调用,我们这里主要说明函数栈帧的创建和销毁,所以这里就不带大家介绍这个函数的由来了
,如果感兴趣可以自己去翻阅一下资料------在此之前,ebp和esp两个寄存器都在维护_tmainCRTStartup所分配的空间,接下来我们来分析反汇编代码:
1.
002718A0 push ebp
push ----------------------压栈的意思,这一步我们将ebp压栈
2.
002718A1 mov ebp,esp 002718A3 sub esp,0E4h
mov:移动,将esp的值赋给edp
sub:减,将esp的值减去0E4h大小的空间
通过监视我们可以发现,此时esp和ebp的值已经一模一样了,说明ebp已经移到 _tmainCRTSstartup函数的栈顶了,并且esp的值也发生了变化,如图所示:
由此我们可以发现,减去的0E4h的大小原来是为main函数开辟的空间大小,而edp和esp也由维护原来的_tmainCRTSstartup函数的栈帧转变为维护main函数的栈帧了
3.
002718A9 push ebx 002718AA push esi 002718AB push edi
这里又是压栈了,将ebx,esi,edi从main函数栈顶依次压入:
这里4条汇编指令的意思是将edi向下的39h这么大的空间里全部赋值为cccccccc,如图:
到这里main函数的栈帧就已经创建好了
5.
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);
这里是创建a,b,c三个变量,假设我们用一个格子代表4个字节,那么创建的a,b,c三个变量如下图所示:
通过查看内存可发现,刚好和我们在main函数栈帧里创建的吻合
6.
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,再进行压栈,如图:
大家有没有发现,这一步操作正是我们函数的传参!
7.
002718E2 call 002710B4 002718E7 add esp,8
call指令是调用的意思,这里我们需要将call指令的下一条指令的地址进行压栈,这里因为函数调用会返回,而返回的地址正是call指令的下一条地址
接下来就正是进入我们的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
8.
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]
这里就进入了我们Add函数的汇编指令了,大家有没有发现这串代码和前面main函数开辟函数栈帧的代码很相似:
push:首先是ebp压栈,
mov:移动,将esp的值赋给edp
sub:将esp的值减去0Ch大小的空间
将edi向下的39这么大的空间里全部赋值为cccccccc
9.
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
接下来将[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,一切都是那么的吻合!太美妙了!
我们在学习函数的时候,有一句话叫形参是实参的一份临时拷贝,现在看过来,这句话完全正确,因为我们在传参的时候,并没有独立去开辟新的空间去接收形参,而是通过寄存器去找到我们之前在主函数里压栈进去的实参!
10.
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 将[ebp-8]的值由eax保管
pop 意思是弹出,接下来就是函数栈帧的销毁,此时edi,esi,ebx就被销毁了
mov 将ebp的值赋给esp
pop 弹出ebp
到这里,红线以上的Add函数的栈帧就被销毁了
回过头看,我们为什么要将[ebp-8]的值先由eax保管,原因是[ebp-8] (也就是z的值)会销 毁,如果不由eax保管,那么返回值将带不出来!
10.
002717BB ret
ret 返回值
此时栈顶上存放的就是call指令的下一条指令的地址,此时按F10,就直接跳到main函数的Add指令了
这就是我们为什么要存放call指令的下一条指令的地址,就是为了确保函数销毁时我还能回得来!这一套逻辑真的是太严密了!!!
11.
002718E7 add esp,8 002718EA mov dword ptr [ebp-20h],eax
add 将esp的地址+8,就回到了我们的edi上面了
此时红线以上的部分又被销毁了,此时的形参x,y的空间就释放了
mov 将eax的值赋给[ebp-20h],而此时的[ebp-20h]就是我们之前压栈的c的空间,eax使我们上面带回来的30,赋给了变量c,这一切又是那么的巧妙!!!
当我们真正理通函数栈帧创建和销毁的过程,我们会产生一种敬畏之心(小编是有的),对前辈的敬畏,这么严密的底层逻辑思维,每一步汇编指令都是精心设计,回头来你会发现,原来当我们在写代码的时候,底层的一些东西原来是这样实现的,这个世界真的很奇妙!
如果对上文有意见或者有错误,还请大佬们斧正,觉得有帮助的童鞋们,蟹蟹三连!