函数栈帧的创建与销毁【此一篇,足以让卿彻底扫盲】

发布时间:2024年01月24日

一、函数栈帧要解决的问题

? 刚接触编程语言的的我们,想必都会存在这些问题:

Ⅰ? 局部变量不初始化,为什么是随机值;

Ⅱ? 形参与实参的关系;

Ⅲ? 函数是如何被调用,以及开辟相对应的空间的;

Ⅳ? 函数是如何传参的;

Ⅴ? 函数的返回值;....等等

? 通过对函数栈帧的学习,我相信:只要你理解了函数栈帧是什么?函数栈帧如何创建?函数栈帧如何销毁?你对这些问题,都能达到自我的攻略的地步。下面我们将通过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的销毁相似,故不在深入探讨。感觉大家的观看。

文章来源:https://blog.csdn.net/FSRMWK/article/details/135825209
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。