【C语言】函数栈帧的创建与销毁

目录

1.寄存器

2.函数空间的分配

3.函数栈帧的开辟

4.函数空间的初始化及局部变量的创建

5.函数的调用

6.函数的销毁


在学习C语言的前期,我们可能会有很多困惑,例如:局部变量是如何创建的?为什么局部变量的值是随机的?函数是怎样传参的?传参的顺序是怎样的?形参好实参是怎样的关系?函数的调用是怎样做的?函数调用结束后是怎样返回的?

这些问题都是C语言底层的原理,在这篇文章中将会解答这些问题。

我们用一段简单的代码来观察一下函数栈帧的创建与销毁。

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
int Add(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d", c);
	return 0;
}

1.寄存器

我们先了解两个寄存器ebp栈底指针esp栈顶指针,这两个寄存器是用来维护函数栈帧的,在调用哪个函数时,esp和ebp两个寄存器就会跑去维护哪个函数栈帧。此外,常见的寄存器还有eax,ebx,ecx,edx。

2.函数空间的分配

接下来,我们观察到Add函数是被main函数调用的,而事实上main函数也是被其他函数调用的,main函数是被_tmianCRTStartup函数调用的,而_tmianCRTStartup函数又是被其他函数调用的。就像俄罗斯套娃一样是一环套一环的,函数的调用也是一个函数调用另一个函数,并且esp和ebp两个寄存器会跑去维护被调用的函数。

3.函数栈帧的开辟

接下来,我们进入这段代码的反汇编来观察一下函数在栈区里是如何运作的。

前面讲过,main函数也是被其他函数所调用的,所以我们从main函数被调用的前一步来说起。

从这张图片里面我们可以看出esp和ebp正在维护_tmainCRTStartup函数,同时要注意,在栈区中下面是高地址,上面是低地址,栈区的使用是由高地址向低地址使用的。

当进入到main函数的时候,第一步叫做push,push的意思是压栈,作用是ebp这个元素的值压在栈顶,同时将esp栈顶指针的地址会指向刚刚压栈的ebp上面。(push:压栈,给栈顶放一个元素;pop:出栈,从栈顶删除一个元素)

第二步叫做mov,作用是将后面的值赋给前面,也就是说将esp的值赋给ebp,在这种情况下,ebp将不在指向栈底,而是指向esp的地址。

第三步叫做sub,意思是减,所以esp就会减去0E4h,意味着esp的值会变小,esp就会指向上面的某一个位置。做到这一步我们会发现esp和ebp指向了一段新的空间,这段空间实际上就是为main函数预开辟好的空间。

第四,五,六步,我们可以看见push了三次,分别将ebx,esi,edi三个元素压栈压进了栈顶空间中。同时每压完一步,esp栈顶指针就会移动到栈顶。

lea是加载有效地址的意思,也就是将[ebp-24h](也就是main函数预开辟的空间与ebx之间的地址)的地址加载到edi中,在将9的值赋给ecx,在将cccccccc的值赋给eax,最后一段的作用是将与开辟的main函数的空间初始化全部初始化为cccccccc的值,一共初始化9个dword长度(一个word是两个字节,dword就是四个字节),而cccccccc这样的值其实就是一个随机值。

4.函数空间的初始化及局部变量的创建

我们可以进入内存查看一下edi地址之下有9行的空间被初始化了。

接下来的步骤就是给定局部变量a,b,c,并且将它们初始化

这里,我们可以看出如果不给局部变量初始化的话,那么局部变量的值将会是一个随机值。所以当我们创建好一个局部变量的时候,将它们初始化是一个很好的习惯。

5.函数的调用

这段的意思是将ebp-14的值赋给exa寄存器中,在进行压栈,把exa元素压到栈顶。再将ebp-8的值赋给eca寄存器中,在进行压栈,把eca元素压到栈顶。

下一步是call,意思是调用函数,我认为它就像是一个通道一样,会直接通到Add函数中。在执行call指令的同时会将call指令下一个指令的地址压栈到栈顶,这样做的意义是当执行完Add函数时返回来的时候有一个媒介。

在运行到call 指令时,我们按F11进入Add函数,我们会看到Add函数的反汇编:

在进入Add函数之后,我们发现Add函数前半段的反汇编与main函数的原理基本一致。

首先,将ebp元素push压栈到栈顶(这里的ebp记录的是main函数的地址),再将esp的值mov赋给edp,esp再sub减去0CCh。接下来再将ebx,esi,edi三个元素分别压栈到栈顶,然后经过了一系列的操作将新开辟的Add函数的空间初始化,再将局部变量z创建并赋值为0。将这些步骤全部完成将会是下列的状态:

接下来到了计算z=x+y的步骤,这一步要将[ebp-8]的值赋给寄存器eax中,我们可以发现[ebp-8]的值正好是存在ecx寄存器中的值,这就是我们刚刚传参的意义,到这里发现x这个参数并没有出现在Add函数的栈帧中,而是通过参数的形式来对x进行使用。下一步是一个add指令,是将exa寄存器中的值加上[ebp+0ch]的值在赋给exa寄存器当中。再将exa寄存器中的值赋给[ebp-8]中,也就是赋给z,最后z的值赋给寄存器exa中(因为在后续过程中,Add函数会被销毁,z也会被销毁,所以要将z的值先存起来)。

6.函数的销毁

在return z之后,就开始销毁Add函数。首先,将edi这个元素pop出栈,就会将edi这个元素弹出来删除,同时esp向下移动4个字节;然后在将esi和ebx分别出栈,esp也同样下移。再将esp加0CCh,esp就指向了ebp的位置,然后将ebp的值赋给esp,这一步之后Add函数开辟的空间就不会被维护了,所以就会销毁将它返回给系统。最后将ebp元素pop出去(因为ebp记录的是main函数中ebp的地址,所以ebp被弹出后,ebp栈底指针会指向main函数的栈底,esp好ebp两个寄存器就重新开始维护maib函数)。

最后一步ret指令会返回main函数,那么ret指令是如何返回main函数的呢?还记得进入Add函数的那个call指令吗,call指令除了会进入Add函数之外,还将call指令下一步指令的地址压到了栈顶。而ret指令就会找到call指令的下一个地址,然后返回。将call指令下一步指令的地址压到栈顶就是为了在Add函数销毁之后还可以找回main函数。

再返回来之后是一个add指令,将esp加8,相当于将esp栈顶指针指向edi,所以ecx和eax两个寄存器就会被销毁,两个参数就会还给操作系统。再将exa的值(之前Add函数将z的值存在了寄存器eax中)放到[ebp-20h](局部变量c)中。

后面又会调用print函数,基本的函数的创建与销毁的原理是一样的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值