我是靠谱客的博主 典雅豌豆,最近开发中收集的这篇文章主要介绍函数栈帧的创建与销毁,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在这里我的编译器用的是VS2022,x86环境下运行

一.main函数的函数栈帧

在这里我们先了解两个寄存器:esp,ebp。
这两个寄存器存放的是地址,是用来维护函数栈帧的

还有几个其他寄存器eax,ebx,ecx,edx

首先,我们要知道函数的创建是在内存里的栈区创建的。就是每调用一个函数都要在栈区开辟一块空间。
栈区空间的使用特点:先使用高地址,在使用低地址。
在这里插入图片描述

在我们使用main函数的时候,就会在栈区为main函数开辟一块空间,寄存器edp,esp分别存的是main函数栈底和栈顶的两个地址。所以此时这两个指针就开始维护main函数的函数栈帧
因为sdp,esp存的也是地址,所以我们也可以把他们叫成指针。edp是栈底指针,esp是栈顶指针。

我们先做一个关于main函数的一点补充
我们可能再写main函数的时候,一般都会在最后写return 0;那我们有没有想过,我们返回的值去了哪里呢?
我们在使用编译器VS2013时,在进行调用堆栈调试的时候。会发现:
在这里插入图片描述

main函数其实是被__tmainCRTStartup()函数调用的。
在这里插入图片描述
然后 __tmainCRTStartup()又是被mainCRTStartup函数调用的

在这里插入图片描述

所以说在栈区空间main函数下面其实还有两块空间:
在这里插入图片描述

对main函数的调用了解一些即可
mainCRTStartup() 调用 __tmainCRTStartup() 调用 main().

二.main函数栈帧的开辟

我们刚才说过了main函数是被__tmainCRTStartup()函数调用的。我们先不管函数mainCRTStartup(),假设我们已经把__tmainCRTStartup()的空间开辟好了:
在这里插入图片描述

然后我们通过反汇编代码,一步一步的分析

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d", c);
	return 0;
}

我们先随便写个这样的函数,点击调试,然后鼠标放在我们写的代码那里右击鼠标,会发现有个反汇编
在这里插入图片描述
我们就看到我们的反汇编代码:
在这里插入图片描述
我们要把显示符号名去掉,要不我们不容易看到运行的细节
在这里插入图片描述

你们不知道这些是什么意思不要紧,现在跟着我一步一步的分析就行:

007318A0 push ebp
push就是压栈的意思,就是把ebp这个寄存器的地址压到我们开辟的函数栈帧上去,因为ebp也是一个指针,也就是压了一个四个字节的空间,这里还要注意一个问题:每增加一点空间,指针esp都会增加到最新空间的顶部(因为esp,ebp维护的是一个函数的函数栈帧,新压进去的元素也属于这个函数栈帧里面的):
在这里插入图片描述

007318A1 mov ebp,esp
mov,是将esp的值赋给ebp,换句话就是ebp指向的地方和esp一样
在这里插入图片描述

007318A3 sub esp,0E4h
sub,是将esp的值减去0E4h。我们刚才讲个,栈区空间的使用特点是先使用高地址,在使用低地址,esp-0E4h是不是说明,esp指向的地址变低了,也就要向上移动
在这里插入图片描述
这么大的空间是用来干啥的呢?
其实这些就是为main函数预开辟的空间
在这里插入图片描述

esp,ebp之前确实差了0E4h。不相信的可以自己根据地址算。

007318A9 push ebx
007318AA push esi
007318AB push edi
这三个步骤就是在新开辟的函数栈帧上面再压三个元素,就是ebx,esi,edi存的地址。具体用来干啥,我们现在先不用管。记住每压一个元素,esp就要移动到当前最顶端的位置。

再把这三个元素压进去的时候我们看edp,esp的地址
压ebx:
在这里插入图片描述

压esi:
在这里插入图片描述

压edi:
在这里插入图片描述

我们发现esp一直在减,且一下减4个字节,而ebp没有变化

我们看压完这三个元素后的空间:
在这里插入图片描述

007318AC lea edi,[ebp-24h]
lea:load effective address(加载有效地址)
这里就是把ebp-24h的值放到edi里面去
在这里插入图片描述

007318AF mov ecx,9
007318B4 mov eax,0CCCCCCCCh
两个move是分别将9赋值给ecx,0CCCCCCCCh赋给eax
在这里插入图片描述

007318AC lea edi,[ebp-24h]
007318AF mov ecx,9
007318B4 mov eax,0CCCCCCCCh
007318B9 rep stos dword ptr es:[edi]
这四句话是连在一起的,最后一句的意思是:
从edi所存的那个位置开始往下的9的空间全部改成0CCCCCCCCh。dword == double word一个word是两个字节,double word是四个字节。
换句话说,从ebp所指向的位置ebp-24h指向的位置之间的空间全部替换成0CCCCCCCCh** 。
在这里插入图片描述
也就是说这一块总共9行,每行四个字节全部初始化成c:
在这里插入图片描述

这一块如果是在VS2013的编译器运行的话,是整个main函数的空间都被赋值成了C。可能我这个VS2022优化的好一点

三.main函数内部变量空间的开辟

我们继续之前那个代码:

int a = 10;
007318C5 mov dword ptr [ebp-8],0Ah
int b = 20;
007318CC mov dword ptr [ebp-14h],14h
int c = 0;
007318D3 mov dword ptr [ebp-20h],0
就是将0Ah(换成10进制就是10),从地址为ebp-8开始存,之前说过dword相当于4个字节,所以将10放到地址ebp-8的地方
同理,14h(十进制是20)放在地址为ebp-14h里面。0放到ebp-20h里面。
在这里插入图片描述
我们看在内存中存储的情况是否一致:
在这里插入图片描述

我们可能会发现如果我么的变量没有初始化,它里面的值就默认为全C,不小心打印的话就会出现乱码,比如;烫烫烫烫。

四.函数是如何传参的

007318DA mov eax,dword ptr [ebp-14h]
007318DD push eax
007318DE mov ecx,dword ptr [ebp-8]
007318E1 push ecx

首先,我们将ebp-14h地址里的元素放到eax里面,ebp-14h是不是很熟悉,里面的值不就是b的值吗?
放到里面之后在把eax的地址压栈压上去
同理,我们把a的值放到ecx里面去,并且在把a压上去。
注意看这个图,我们是把ecx,eax存的地址压进去了,不是把这两个寄存器压进去
在这里插入图片描述
这两步有没有感觉到什么?这是不是就是在传参啊。

在这里插入图片描述
在内存中也可以看到确实放进来了。虽然是传参,其实这两个值还是在我们main函数的函数栈帧中

接下来我们继续看:

007318E2 call 007310B4
007318E7 add esp,8
执行到这里我们按F11,看接下来会发生什么
在这里插入图片描述
在这里插入图片描述

我们发现在栈区上面又压了一块地址e7 18 73 00, 我们再看007318E7 add,发现add的地址就是这个(我这个编译器是小端字节序存储,所以看的时候是反着的)。这代表什么呢?

我们摁下F11后,发现地址压过去了,然后我们在摁一下F10:
在这里插入图片描述

我们是不是已经进到add函数里面去了?但是这个函数总要返回吧,所以我们把call指令下一条指令的地址先存起来,这样在返回的时候知道返回到哪里去

至此,我们把main函数里面的内容都了解清楚了。记住,我们看到我们在原有的空间上压了很多元素,这些元素都属于main函数的函数栈帧,因为ebp,esp始终维护的是main函数的函数栈帧。也就是说每次压栈的时候,main函数的函数栈帧一直在增长。
在这里插入图片描述

五.函数是如何开辟和使用的

我们之间写的代码是:

int Add(int x, int y)
{
	return x + y;
}

这个虽然说简介,但是我们转到反汇编代码那一块的时候,不容易看到过程。所以我们改一下:

int Add(int x, int y)
{
	int c = 0;
	c = x + y;
	return c;
}

我们看反汇编代码:

00CD1770 push ebp
00CD1771 mov ebp,esp
00CD1773 sub esp,0CCh
00CD1779 push ebx
00CD177A push esi
00CD177B push edi
00CD177C lea edi,[ebp-0Ch]
00CD177F mov ecx,3
00CD1784 mov eax,0CCCCCCCCh
00CD1789 rep stos dword ptr es:[edi]

我们发现这里和main函数开辟那块很像。这就是开辟add函数的函数栈帧做的准备工作。基本一致我就不细讲了:
在这里插入图片描述
这样我们就把函数的栈空间准备好了。

我们继续往下看:

int c = 0;
00CD1795 mov dword ptr [ebp-8],0
这一块也不用多说了吧,将0的值存到ebp-8地址的空间里
在这里插入图片描述
在这里插入图片描述

现在我们再来看函数是如何实现运算的

c = x + y;
00CD179C mov eax,dword ptr [ebp+8]
00CD179F add eax,dword ptr [ebp+0Ch]
00CD17A2 mov dword ptr [ebp-8],eax

我们有没有想过,变量z我们已经创建好了,但是x,y呢?其实,x,y我们早就写好了:我们在之前main函数的函数栈帧空间里已经将10,20的数放到了寄存器ecx,eax里面了:
在这里插入图片描述

但是我们这三条指令是干什么的呢?

mov eax,dword ptr [ebp+8]
首先我们先把10放到eax里面去
在这里插入图片描述
a的十进制就是数字10,我们这里确实把10放进去了

eax,dword ptr [ebp+0Ch] ,C换成10进制就是12,这里不就是ebp+12的地方吗?这一句就是将eax的内容+ebp+0Ch里的内容,不就是10+20吗?
在这里插入图片描述
eax里面的内容就变成了30,所以说,在这里我们已经要把函数需要计算的内容计算好了。

dword ptr [ebp-8],eax
还记得ebp-8是什么的地址吗?
在这里插入图片描述
我们看到这就是我们之前在Add函数里创建的变量c.也就是说我们把计算好的值放到c里面了。

既然我们已经计算好了,我们是不是该把计算的值返回了。

return c;
00CD17A5 mov eax,dword ptr [ebp-8]
这里就是把c里面的值存到寄存器eax里面去。因为寄存器是集成在CPU上面的,不会因为你Add结束运行而销毁。

好了,现在我们的Add函数已经用完了,用完之后如何呢?

00CD17A8 pop edi
00CD17A9 pop esi
00CD17AA pop ebx
00CD17AB add esp,0CCh
00CD17B1 cmp ebp,esp
00CD17B3 call 00CD1244
00CD17B8 mov esp,ebp
00CD17BA pop ebp
00CD17BB ret
我们之前学过push就是压栈,那pop就是出栈的意思。
刚开始先pop三遍,把寄存器edi,esi,ebx的地址全部弹出去,当然esp也会跟着动
在这里插入图片描述
然后:00CD17AB add esp,0CCh
esp的地址加上0CCh,我们要知道,当初是减0CCh,从而把Add函数的栈帧空间创建好了。这次我们是加不就代表,这一块空间也没了吗?
在这里插入图片描述
此时esp,ebp指向同一块地方。

我们接下来直接看:

00CD17BA pop ebp
00CD17BB ret
我们直接pop寄存器ebp的地址,这是什么意思呢?
在这里插入图片描述
我们注意到我们在创建Add函数的栈帧那些指令,第一行是00CD1770 push ebp 。这就是把指向main栈底的那个ebp的地址压栈压上去了。
现在我们pop一下,就是把里面的值弹出去并赋给ebp寄存器。
这时候我们是不是发现,ebp又指会原来的位置了。
在这里插入图片描述

最后ret返回,但是返回到哪里去了呢?此时我们发现esp指向的位置是:在这里插入图片描述
发现是call指令下一条指令的地址,此时我们摁F10:
在这里插入图片描述
发现直接回到call指令下面这里来了。也就是说

00CD18E2 call 00CD10B4 这个指令就是为了能让你在返回的时候找到回家的路

此时内存是这个样子的:
在这里插入图片描述

我们继续往下走:

00CD18E7 add esp,8
我们既然已经不用Add函数了,那ecx,eax里的值也没用了吧,所以esp+8,esp往下移动:
在这里插入图片描述
这样ecx,eax也走了,不归我们管了

继续看:

00CD18EA mov dword ptr [ebp-20h],eax
这又是什么呢?eax虽然走了,但是里面的值我们还是可以用的,里面的值就是我们之前算的30,然后把30放到ebp-20h里面。在这里插入图片描述
ebp-20h刚好就是变量c的地址啊。这样我们是不是就把Add返回的值存到变量c里面了。

好了,到这里我们就已经基本结束了,因为main函数的销毁和Add的差不多。

六.结尾

其实中间我有几个地方没有讲:

00CD18BB mov ecx,0CDC008h
00CD18C0 call 00CD131B

就是这两行,如果你用的是VS2013可能没有这两行,如果有的话也不用管,这个是新的vs版本做的优化,调了一些debug函数。

最后的最后,希望可以跟着我的思路自己对着代码画一遍,光看是很容易弄混的。

最后

以上就是典雅豌豆为你收集整理的函数栈帧的创建与销毁的全部内容,希望文章能够帮你解决函数栈帧的创建与销毁所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部