一、函数栈帧要解决的问题
刚接触编程语言的的我们,想必都会存在这些问题:
Ⅰ 局部变量不初始化,为什么是随机值;
Ⅱ 形参与实参的关系;
Ⅲ 函数是如何被调用,以及开辟相对应的空间的;
Ⅳ 函数是如何传参的;
Ⅴ 函数的返回值;....等等
通过对函数栈帧的学习,我相信:只要你理解了函数栈帧是什么?函数栈帧如何创建?函数栈帧如何销毁?你对这些问题,都能达到自我的攻略的地步。下面我们将通过C语言的一些简单的代码,去一块分析函数栈帧的全过程;
一、函数栈帧的引入
1.认识栈
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。
2.认识栈帧
我们都知道,程序的运行,是在内存上的。而函数的栈帧,即指的是为该函数所开辟的空间,而为该函数开辟的空间是由两个寄存器esp、ebp共同维护的。而esp存储的该函数栈顶空间,而ebp存储的是栈底空间。
3.了解main函数也是被调用的
其次我们还要知道的时候,程序的入口:main函数也是被另一个函数调用的——__tmaincrtstartup,而__tmaincrtstartup又被tmaincrtstartup调用的。
最后函数的空间是在栈上开辟的,先使用高地址,在使用低地址。
4.环境准备:
建议用32平台的,其次在属性,C/C++中,将仅可支持我的代码由否改为是。
二、函数栈帧的创建与销毁的全过程
这里我用下列的代码,下面转到反汇编来模拟一下函数栈帧创建与销毁的全过程(因为我刚开始忘记准备环境,为了能够让我们研究函数栈帧的创建与销毁过程足够清晰,所以我中途中断过程序,但这不影响你理解函数栈帧的过程!!!)
int main()
{
00881840 push ebp
00881841 mov ebp,esp
00881843 sub esp,0E4h
00881849 push ebx
0088184A push esi
0088184B push edi
0088184C lea edi,[ebp-24h]
0088184F mov ecx,9
00881854 mov eax,0CCCCCCCCh
00881859 rep stos dword ptr es:[edi]
int a = 1328, b = 14,c = 0;
0088185B mov dword ptr [ebp-8],530h
00881862 mov dword ptr [ebp-14h],0Eh
00881869 mov dword ptr [ebp-20h],0
c = Sub(a,b);
00881870 mov eax,dword ptr [ebp-14h]
00881873 push eax
00881874 mov ecx,dword ptr [ebp-8]
00881877 push ecx
00881878 call 008811EA
0088187D add esp,8
00881880 mov dword ptr [ebp-20h],eax
printf("%d\n", c);
00881883 mov eax,dword ptr [ebp-20h]
00881886 push eax
00881887 push 887B30h
0088188C call 008810D2
00881891 add esp,8
return 0;
00881894 xor eax,eax
}
00881896 pop edi
00881897 pop esi
00881898 pop ebx
00881899 add esp,0E4h
0088189F cmp ebp,esp
008818A1 call 00881253
008818A6 mov esp,ebp
008818A8 pop ebp
008818A9 ret
模拟函数栈帧创建(汇编指令片段一)
指令1:将ebp的值压进去,esp向上走4个字节。这是__tmaincrtstartup函数栈帧的栈底地址,目的是为了,main函数栈帧销毁后,能找到__tmaincrtstartup的函数栈帧的栈底地址。
指令2:将esp的值赋给ebp。此时ebp的则真正开始维护main函数的栈底地址。
指令3:将esp的值减去0x0E4。这是为main函数预开辟的空间,我们可以类比于局部变量的创建去理解。
指令4:将寄存器ebx,esi,edi的值压入栈中。因为后面可能会使用这些寄存器,而造成当前值的改变,所以,这步指令的目的是为了,之后能够方便找到原先寄存器中的值。
完成图如上所示。
模拟函数栈帧创建(汇编指令片段二)
指令1:lea,将ebp-0x24的地址的值,加载给edi,
指令2:exc,将9的值给ecx,
指令3:eax,将0xCCCCCCCC的值存给eax,
指令4:将ebp-0x24与ebx之间的空间,全部初始化为0CCCCCCCC
这四个指令的作用,可以类比于局部变量的初始化来进行理解,为了我们更深一步的理解,写下了下面的伪代码:
for(edi = ebp - 0x24;edi < ebp;edi+4)
{
*(int*)edi = 0xCCCCCCC;
}
模拟函数栈帧创建后变量创建与初始化
指令1:mov,将ebp-8的地址,划定为a的内存空间,并将前4个字节赋值为1328
指令2:mov,将ebp-14的地址,划定为b的内存空间,同理前4个字节赋值为14
指令3:mov,将ebp-20的地址,划定为c的内存空间,同理前4个字节赋值为0
模拟函数栈帧创建后Sub函数的调用与传参
指令1,指令2:mov,push,将b的值给eax,将eax的值压栈,改变esp的位置,
指令3:指令4:mov,push,将a的值给ecx,将exc的值压栈,改变esp的位置
指令5:call(1.压入下一指令的地址,类似于push 00881870,2.调入子程序,点Fn+F11来到Sub函数)
模拟函数栈帧创建后Sub函数的创建
因为Sub函数栈帧空间的创建与main函数一致,故直接展示进行过后的抽象图:
模拟函数栈帧创建后Sub函数的局部变量
指令1:mov,将ebp-8的位置为z开辟,并将前4个字节赋值为0;
指令2,3,4:将a,b的值给eax寄存器,完成整数的减法,并将exa的值赋给z;
指令5,将z的值交给eax,目的是为了,在函数栈帧销毁后,能带回z的值;
模拟函数栈帧创建后Sub函数栈帧的销毁
指令1,2,3,弹出edi,esi,ebx,得到之前的值
指令4,ebp的值给esp,此时Sub函数栈帧真正销毁
指令5,弹出main函数ebp的值
指令6,恢复返回地址,压入eip,类似于pop eip
模拟函数栈帧销毁
因为main函数栈帧的销毁,与Sub的销毁相似,故不在深入探讨。感觉大家的观看。