函数栈帧的创建与销毁

在这里我的编译器用的是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函数。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值