函数栈帧的创建和销毁

2024-01-08 10:36:02

函数栈帧的创建和销毁

前言:

你知道函数栈帧是怎样创建的吗?局部变量又是怎样创建的?为什么局部变量的值是随机值?

学习函数栈帧的创建和销毁,解决以上这些问题,让你的水平更上一层。

一.基础

在CPU中,集成有多个寄存器,它们是最直接与CPU进行数据交互的部件。了解一下寄存器是这篇的基础,寄存器有eax、ebx、ecx、edx等,学习这篇最重要两个是ebp和esp。

ebp和esp,它们存放着地址,作用就是共同维护(控制)为函数创建的空间。

在这里插入图片描述

计算机不是在栈区上随便划一块空间,就说:这块空间属于main函数,而是通过esp和ebp指定的这块区域才是main函数空间。

我们可以用esp的s:start(开始)和ebp的b:back(后面)来助记这两个指针。

另外esp也被叫做栈顶指针ebp被叫做栈底指针,学过栈这个数据结构的读者知道,入栈是在栈顶入的。栈区空间的使用习惯是先使用高地址再使用低地址。

在这里插入图片描述

写上学习的例子代码,开启调试,打开调试窗口中的运行时堆栈:显示的逻辑是main函数在调用Add函数,我们发现main函数前面有__tmainCRTStartup()这个函数,说明main函数也是被其它函数调用的。

在调用Add函数之前,main函数的函数栈帧是已经创建好了的。所以在调用main函数之前, __tmainCRTStartup()的函数栈帧也先比main创建,故栈区空间应是这样的:

在这里插入图片描述

? 总结一下:我们知道调用函数需要在栈区上开辟空间,栈使用空间的习惯是,先使用高地址再使用低地址,并且有两个寄存器指针esp和ebp,它们随着程序的执行逻辑在维护着相应函数的栈帧。

二.分析过程

2.1函数栈帧的创建

开启调试,转到反汇编。

在这里插入图片描述

现在运行逻辑在main函数,也就是说__tmainCRTStartup的函数栈帧是已经创建好了的。现在是这样的图:

在这里插入图片描述

第一条汇编指令是push ebp,push是压栈的意思,把__tmianCRTStartup的ebp地址存放到栈上(在栈帧的销毁时有用),esp会向上走4个字节,因为指针大小为4字节(32位平台)。

在这里插入图片描述

第二条汇编指令是mov ebp esp,这里相当于这个函数:
在这里插入图片描述

第一个参数是目的地,第二个参数是源头,把源头拷贝到目的地里去。将esp的地址拷贝到ebp里去,此时ebp指向的位置就和esp一样了。

在这里插入图片描述

第三句汇编指令是sub esp 0E4h,让esp减去0E4h这个值,它是一个十六进制数字。还记得我们前面说过的栈空间往上使用嘛,esp将会往上移动一段距离。ebp和esp形成的这段新空间就是为main函数预开辟的空间。

在这里插入图片描述

补充:尾缀为h表示是十六进制
在这里插入图片描述

接下来是连续的三次push,push ebx,push esi,push edi,变成下面这幅图:

在这里插入图片描述

lea edi,[ebp-0E4h](load effective address)加载有效地址到edi中,这里ebp-0E4h值将存到edi里。

接下来的三句分别是:

mov ecx,39h 将值39h放进ecx中

mov eax,0CCCCCCCCh 将值0CCCCCCCCh值放进eax中

最后的一句非常有意思:rep stos dword ptr es:[edi]

? 把edi值指向的位置,向下ecx(39h)次,每次dword(两字,四个字节),重置内容为eax(0CCCCCCCCh)。

在这里插入图片描述

补充:main函数栈帧里的内容可以被这么完美的重置成0CCCCCCCC,是因为ebp减0E4h(值为228)与39h(值为57)次*(乘)两字(4个字节)= 228。在这里你可以知道,ebp和esp它们可能是字符指针。

2.2有效代码的执行

至此,main函数栈帧开辟工作完成,终于要开始执行main函数里的代码了:

在这里插入图片描述

现在是mov dword ptr [ebp-8],0Ah。将0Ah(值为10)放在ebp往上的8个字节处。

在这里插入图片描述

注意噢:我们从edi指向的那个位置重置到ebp,ebp没有包含在里面,如果这里有疑惑的读者,可以再理顺一遍,其中总共修改了0E4h个(或者说(两字、39h次)个)字节。

到这里,我们就清楚了,如果我们创建的局部变量不初始化,它所分配到的空间里的内容是0CCCCCCCC,因此它就是一个随机值。

创建b变量和c变量大同小异,我们快速pash掉。

在这里插入图片描述

现在main函数里到调用Add函数了,让我们来学习函数的调用逻辑:

在这里插入图片描述

在调用函数的时候有传参,如何实现传参?传参的顺序是什么?

看到第一条指令:mov eax,dword ptr [ebp-14h],0Ah是把[ebp-14h]里的值拷贝给eax。

第二条指令:push eax是把eax压栈,表面上看压的是eax,实际上是压入了参数b,这样就传了参数b。

第三四条指令相当于把参数a传参了。

在这里插入图片描述

在这里插入图片描述

现在要执行call指令了,调用的时候,会把call指令的下一条指令(00451450)的地址给保存起来,在调用完add回来后要继续执行main函数里的代码。

在这里插入图片描述

进入函数内部使用f11,这里还有一条跳转指令,即将跳到004513C0,那里才是函数代码保存的地方。

在这里插入图片描述

我们看到内存里确确实实存进了call的下一条指令(内存中要反着读,请看整型在内存中的存储)。Add函数里开始的汇编指令和main函数开始的汇编指令一样,都是在开辟函数使用的空间。

在这里插入图片描述

让我们再看一遍Add函数栈帧创建的过程锻炼一下:

先是Push ebp保存上一个运行逻辑的栈底指针。

在这里插入图片描述

第二步是mov ebp esp,将esp的值拷贝给ebp,这样就是下面这幅图:

在这里插入图片描述

此时ebp和esp指向同一个位置,接下来是sub esp ,0CCh 让esp减去0CCh值,为Add函数创建栈帧:

在这里插入图片描述

接下来连续三次push,push ebx,push esi,push edi。

在这里插入图片描述

然后再是以下三步:

在这里插入图片描述

加载有效地址到edi中,将33h拷贝给ecx,将0CCCCCCCCh拷贝给eax,从edi这个地址往下ecx次,每次两字赋值为eax。

在这里插入图片描述

完成这一步后,Add函数的栈帧就开辟好啦,开始执行Add里的代码:

在这里插入图片描述

将ebp-8的地址处开辟给z变量、然后把ebp+8的值拷贝给eax、再把ebp+0Ch的值加给eax,最后再把eax的值拷贝给z。

在这里插入图片描述

接着准备把z的值返回:这里学习的是栈帧的销毁过程

先mov eax,dword ptr [ebp-8],把z的值放到eax寄存器中;

在这里插入图片描述

然后pop edi、pop esi、pop ebx将这三个变量弹出栈,pop是出栈,弹栈的意思:

pop完edi的示意图:

在这里插入图片描述

其它两个同理,pop的时候只能从栈顶一个一个按顺序pop,这也是栈(数据结构)的特性。

在这里插入图片描述

接着mov esp sbp 把ebp的值拷贝给esp,这样esp就下来了,为Add开辟的空间还给了操作系统。

在这里插入图片描述

再pop ebp:

在这里插入图片描述

pop完ebp的时候,注意ebp回到了上一个存档ebp所在的位置,可以理解pop ebp的时候,把ebp(main)的值拷给了ebp。

最后再走一步ret,Add函数调用结束:

在这里插入图片描述

调用回来时,调用函数之前压栈call下一条指令也被出掉,相当于一次性用品,在回来那会用于使运行逻辑回到call指令下一条的位置。

在这里插入图片描述

在这里插入图片描述

此时esp+8,将传递给Add函数的参数b,参数a还给操作系统:

在这里插入图片描述

在这里插入图片描述

这里在pop归还的时候,注意地址是变大的,因为在使用的时候往小的方向使用,归还自然往大的走!

到这里,参数归还后,函数的调用就完整的结束了,包括传参数、函数栈帧的开辟、局部变量的创建、传递返回值等。

希望读者看完这篇文章能有所收获~

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