函数栈帧的创建和销毁

函数栈帧的创建和销毁

在这里插入图片描述

引子

无论是正在学习C语言、数据结构还是已经学到C++的朋友,我们很容易发现,在我们学习的过程中总是绕不开的一个东西就是函数栈帧,可以说这是必不可少、非常重要的基础,所以本文我们就来学习函数栈帧的创建和销毁。


前言

在掌握函数栈帧的创建和销毁之前我们会遇到一些难以理解的问题比如:

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后是怎么返回的?

在学习函数栈帧的创建和销毁之后我们就能解决这些疑惑。

(注意:在不同编译器下,函数栈帧的创建和销毁略有差异)


正文

一、 寄存器

寄存器:eax ebx ecx edx ebp esp

要理解函数栈帧,就必须理解ebp和esp两个寄存器,这两个寄存器中存放的是地址,而这两个地址是用来维护函数栈帧的。

我们知道,每一个函数调用都要在栈区创建一块空间。而正在调用哪个函数,ebp和esp就维护哪块空间。ebp通常叫为栈底指针,esp为栈顶指针。(栈区的使用习惯是先使用高地址再用低地址)

假设我们现在有一个这样的程序:

#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\n", c);

	return 0;
}
  • 值得注意的一点是,main函数其实也是被其他函数调用的(从我们需要return 0这一点就可以看出)。main函数被__tmainCRTStartup()调用,而这个函数又被mainCRTStartup()调用。

在main函数一开始转到反汇编后我们会看到这样的第一条指令

push ebp

也就是将我们的ebp压栈,push完后esp维护新的栈顶:

  • 通过监视窗口我们也能看到执行完这一条指令后,esp的地址变小了(栈是先使用高地址再使用低地址);

  • 通过内存窗口,我们则可以看到esp此时存放的就是ebp的地址,可见确实压入栈了。

下一条指令mov是将esp的值赋给ebp,这一条指令的效果就是让ebp不再指向原来的位置而是指向esp指向的位置。

再下一条指令sub是让esp减去0E4h,效果是让esp往上走了一段。这样我们就给main函数预开辟了空间:

然后的三条指令中,我们push了三个元素。

再然后是lea指令。意思是加载有效地址。这条指令的作用是将原本main函数的函数栈帧栈顶的地址给了edi。

然后后面的三条指令,前两条是赋值,第三句dword代表double word,双字,而一个word是双字节,dword就是四个字节。效果就是从edi的位置开始向下的9次dword大小的数据全部初始化为0CCCCCCCCh。

也就是为main函数预开辟的空间全部初始化为了0CCCCCCCCh。

接下来执行的才是有效的代码:int a=10;对应的汇编代码的意思是将0A(十六进制,也就是10)放到ebp-8的位置。可见,当我们没有初始化时,内存里存放的就是cccccccc,随机值(不同编译器可能不同)。这就是为什么之前我们会打印出烫烫烫…。

再下面两句int b=20;和int c=0;也是相同的做法,从它们间隔的位置可以看出,两个变量之间空了两个整型的空间。

我们梳理一下创建abc变量的过程:首先为这次函数调用创建函数栈帧并初始化为随机值,然后在这个栈帧中找到一些空间,将变量abc存进去。

(Add函数调用部分的汇编代码)

第一句,ebp-14h我们已经知道是b,这句就是把b的值即20放到eax中去。

第二句,push也就是将eax即20压栈。

第三句,mov,将ebp-8也就是a的值放到ecx中

第四句,push也就是将ecx即10压栈。

以上的动作其实就是在“传参”。

下面的一句call,也就是调用函数。然后call指令的下一条指令的地址就被压栈了。这样从函数执行回来我们就能从下一条指令开始往下执行。

现在我们来到Add函数里,前面的一大部分都是和main一样,在为Add函数准备栈帧。

首先的三条指令是在为Add函数分配栈帧空间。

然后的三条push也和main中一样,相当于在顶上放了三个元素。(esp要跟着往上走)

lea将逗号后面的地址加载到edi中;mov将33h放到ecx中;再把0CCCCCCCCh放到eax中。

rep让从edi开始向下到ebp中所有空间的内容初始化成cccccccc,次数为ecx次。(也跟前面main函数一样)

后面的z=x+y;对应的汇编代码中可以看到,跑去找到了参数。mov将x对应的值10放入eax;add将y对应的值也就是20加给了eax;mov将此时得到的和eax,放入z的地址中。

(可以看到先压的是b,再压的a)

在我们调用Add函数之前就已经把参数压栈了,而在我们真正进入Add函数要用到两个参数时我们又去找回了这两个参数,计算好了放入z。

  • 所以我们也不难看出,所谓“形参只是实参的临时拷贝,对形参的改变不影响实参”这句话是真的。

接下来我们看看 函数是怎么返回的。

我们注意return z;下来的第一句汇编指令,将[ebp-8]的值**放到eax寄存器中,**这也就是为什么我们的z除了函数,函数栈帧销毁后这个值还保存着,回到主函数后就能使用eax的值了。

接下来的三句pop让edi、esi、ebx出栈。

因为我们已经return了结果,Add函数栈帧已不需要存在,所以接下来就是这块空间的销毁,那么是怎么销毁的呢?

首先将ebp赋给esp,这意味着esp往下来到了ebp的位置;然后是pop,就是将main函数的ebp的地址出栈,值赋给ebp(这就是为了能找到main函数的栈底)。此时esp也pop后往下走一步,此时ebp和esp维护的就是main函数的栈帧

接下来是ret指令,而此时我们esp指向的是刚才call指令的下一条指令的位置,所以我们能回到call指令的下一条指令继续往下执行。(设计严谨)

然后此时栈顶的10和20是已经用完的参数,所以call的下一条就是add,让esp往下走8。

再下一句的mov,把eax(就是我们出Add函数时计算的结果)放到ebp-20h,也就是之前存放c的位置。所以现在我们的c就从寄存器里得到了我们的计算结果

而main函数的销毁和Add函数的销毁也是差不多的。


后记

这就是函数栈帧创建和销毁的过程,可以看到是设计非常严谨的。对汇编指令的解释虽然不够完善,函数栈帧的创建和销毁能理解到这样的程度已经算较为深刻。

所以我们现在也能够解答开头抛出来的问题:

  • 局部变量的创建,也就是先给我们的函数分配栈帧空间,然后再在其中给局部变量分配空间。

  • 为什么局部变量的值是随机值?原来这是我们在开辟函数栈帧后放进去的。

  • 函数是怎么传参的?其实我们在调用函数之前就已经从右向左将两个参数压栈了。

  • 形参和实参的关系我们也发现二者只是值是相同的,形参有自己的空间,改变形参不会影响实参。

    ……

补充:寄存器是集成到CPU上的。还有一些细节是取决于编译器的。

本文到此结束=_=

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值