【C语言深入】逐汇编详解函数栈帧的创建和销毁过程

一、图解大概过程

一个清晰的流程图解能对我们的理解起到事半功倍的效果,所以我们先不管寄存器和汇编代码,先来大致的理一下函数栈帧的创建和销毁过程。
我所理解的函数栈帧的形成其实是从一个函数被另一个函数调用开始的,我把整个过程分成了6部分:
1、压入临时拷贝及返回信息
我们最开始先假设main函数的栈帧已经形成,并且在main函数内调用了函数。那么在正式进入被调用函数之前,就要先形成传入参数的临时拷贝,以及一些辅助返回的信息:
在这里插入图片描述
而在这个过程中一有两个指针来维护我们的栈,一个是栈顶指针top一个是栈底指针base,刚开始时,top和base分别指向main函数栈帧的栈顶和战底:
在这里插入图片描述
而随着我们元素的压入,栈顶指针top需要一直往上移动,直到信息压完:
在这里插入图片描述
2、形成栈帧
前面的准备工作做完后,就可以形成被调用函数的栈帧了,形成栈帧其实就是使base和top指向一块新的空间:
在这里插入图片描述
3、初始化局部变量
当我们形成了func函数的栈帧后,就需要对func内的局部变量进行初始化,初始化包括分配空间并赋值:
在这里插入图片描述
4、计算并返回
在func函数内初始化了一些局部变量后,我们就可以计算返回值了。
计算返回值时,出了用到func函数内的局部变量之外,还需要用用到我们传进来的参数。但当我们在func内计算的时候,并不会再次产生参数的拷贝,而是直接提取我们已压入栈的临时拷贝:
在这里插入图片描述
5、销毁栈帧
销毁栈帧的过程其实就是将top指向base,当top和base的指向相同时,也就说明base上面的空间被回收了:
在这里插入图片描述
6、弹栈
在弹栈之前要做的是,让base指向会原来的main函数的栈底,这个操作是通过之前压入的返回信息完成的,再返回信息中其实就包含的原本main函数栈底地址的信息:
在这里插入图片描述
然后就可以通过弹栈操作,将之前压入栈中的临时拷贝和返回信息都弹出栈去:
在这里插入图片描述
这样我就恢复到了main函数的栈帧。

至此,就大致地介绍完了函数栈帧的创建和销毁的主要过程。
接下来,就给大家逐语句的分析整个过程。

二、函数栈帧的创建过程

1、简介一些需要用到的汇编指令和寄存器

在正式开始之前,必须先要对我们将要用到的一些汇编指令和寄存器做一些了解。不需要深入理解,只需要了解一下它们的作用和含义即可。
相关汇编指令

mov: 数据转移指令,开辟空间并将数据写入空间
push: 数据入栈。同时esp栈顶寄存器也要发生改变
pop: 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub: 减法命令
add : 加法命令
call: 函数调用,1.压入返回地址2.转入目标函数
jump: 通过修改eip,转入目标函数,进行调用
ret: 恢复返回地址,压入eip,类似pop eip命令

相关寄存器

eax: 通用寄存器,保留临时数据。常用于返回值ebx;通用寄存器,保留临时数据
ebp: 栈底寄存器
esp: 栈顶寄存器
eip: 指令尚存器,保存当前指令的下一条指令的地址

当然啦,大家如果是初次见的话,肯定还会有很多地方不理解的,但是一回生二回熟,在后面具体遇到的时候在对它们的功能做详细的介绍也不迟。

2、调用main函数的函数

我们先写上一个简单的测试代码:

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

一个函数要想起作用,那就必须时被调用。
那当然我们的main函数也是被别的函数调用的,为了看到调用main函数的函数,我们可以在编译器vs2013的调试中看到:
在这里插入图片描述
当我们进入main函数内时,就会发现main函数也是被一个名为__tmainCRTStartup的函数调用的。
而至于__tmainCRTStartup这个函数又是被那个函数调用的我们不用管,我们这里只需要直到main函数也是被别的函数所调用的即可。

3、局部变量的初始化

知道了main函数也是被别的函数所调用的,我们就可以理解为什么可以假设main函数的栈帧已被创建好的了。
当我们的main函数的栈帧创建好之后,上面所说到的寄存器esp和ebp就会默认指向main函数的栈顶和栈底:
在这里插入图片描述
在这里插入图片描述
当创建好了main函数的栈帧后,紧接着就需要对main函数内定义的局部变量进行初始化了,我们可以看看这部分的汇编代码:
在这里插入图片描述
我们可以看到,三个局部变量的初始化对应的就是三条mov指令:
在这里插入图片描述
所以这三条语句所做的工作就是在地址为ebp - 8、ebp - 14和ebp - 20的地址处放上a、b、c三个变量:
在这里插入图片描述

4、形成临时拷贝

当我们完成局部变量的初始化工作之后,程序就直接来到了函数调用语句:
在这里插入图片描述
而我们知道进行函数调用的汇编语句是call,但上面的结果显示并没有直接执行call指令,而是执行了其他的4条语句。
其实在call指令之前的这4条语句完成的就是形成参数的临时拷贝。
这四条语句所执行的就是将ebp - 14的内容放入到寄存器eax中并把eax的值压入栈中,然后把ebp - 8的内容放入到寄存器ecx中并把ecx的值压入栈中。
而由上文我们知道,ebp - 14和ebp - 8中放的不就是变量b和变量a的内容吗?
所以成这个过程为形成临时拷贝:
在这里插入图片描述

而且通过以上过程,我们也可以得到两个结论:

1、参数的临时拷贝(形参)是在函数调用之前就完成的。
2、函数形参实例化的顺序是从右向左的。

5、函数调用

完成了临时拷贝我们就来到了,调用函数的call语句,而这一条语句所做的工作就不一般了,我们需要特别来看看:
在这里插入图片描述
这条语句一共做了两件事,我们想来看第一件事:压入返回地址;
压入返回地址的目的其实是为了在函数调用完后,返回到call命令的下一条指令:
在这里插入图片描述
所以我们要压入栈的就是这里的add这条命令的地址:
在这里插入图片描述
因为call指令完成了两个工作,所以我们应该按F11来观察更细节,当我们按下F11后就会发现esp的值变成了002E18F7:
在这里插入图片描述
而跳转目标函数其实是通过修改eip寄存器的内容达成的,修改的地址其实就是call指令后面跟的那个地址:
在这里插入图片描述
在这里插入图片描述
而通过上图我们也可以观察到此时的eip已经被修改成了对应内容:
在这里插入图片描述
而进入到call指令内部我们看到其实002E10B4其实对应的是一条jmp指令:
在这里插入图片描述
而jmp指令的功能是,通过修改eip,转入目标函数,进行调用:
在这里插入图片描述
所以我们是通过jmp指令转入被调用函数内的。

至此,我们完成了返回地址的压入和函数的调用:
在这里插入图片描述

6、形成栈帧

在进入函数后,其实就可以形成栈帧了,我们先看汇编:
在这里插入图片描述
第一条指令为push ebp,意思是将ebp的内容入栈,而我们知道ebp此时孩纸想的就是main函数的栈底:
在这里插入图片描述
所以这个指令所做的工作就是将main函数的栈底地址压入栈中:
在这里插入图片描述
下一条指令mov ebp,esp所做的工作就是将esp中的内容移入ebp,也就是使ebp与esp指向同一位置:
在这里插入图片描述
接下来的一条语句是sub esp,0CCh,其意思就是将esp里的内容减去CCh(十六进制),结果相当于让esp往上移动:
在这里插入图片描述
而此时,由esp和ebp所制定的这一段空间其实就是Add函数的栈帧:
在这里插入图片描述
至此,一个函数栈帧从无到有的创建过程也就演示完了。
而接下来的这一部分汇编指令其实就是在对我们新创建好的这块空间进行初始化,就像将一个数组元素全都初始化为0一样:
在这里插入图片描述
而这对于我们理解栈帧的创建和销毁的过程并没有什么实际的意义,所以这一部分我们可以忽略。

7、提取临时拷贝

而接下来的这条语句就是对局部变量进行初始化操作,这和前面是一样的,所以就不用多说:
在这里插入图片描述
在这里插入图片描述
现在我们执行到下一条语句:
在这里插入图片描述
其意思就是将ebp + 8中的内容移入eax中,那么ebp+8中又是哪里的内容呢?
因为我们现在所在的平台是32位的,所以每个地址是4个字节,所以ebp + 8就相当于跳过了两个指针类型变量的空间,所以此时的ebp+8指向的就应该是a`,也就是实参a的临时拷贝:
在这里插入图片描述
接下来的一条语句是:
在这里插入图片描述
其实就是将ebp - 12(0C为十六进制,转为十进制就是12)中的值与eax中的只进行相加,其结果依然保存到eax当中。而这里的ebp+12指向的就是实参b的临时拷贝:
在这里插入图片描述
所以我们此时就完成了两个临时变量的相加,其结果保存在eax当中。
而接下来的这条指令所做的就是将eax中的值写入到ebp - 8中:
在这里插入图片描述
而此时我们的ebp - 8不就是我们变量z的地址吗?
所以我们就将计算结果赋值给了变量z。
而此时我们Add的逻辑也就完成了。

8、return返回

此时,我们就来到了我们的return。
在这里插入图片描述
return对应各这条汇编指令所做的就是将ebp - 8的值移动到eax当中,也就是将计算得到的结果移动到eax当中。

三、函数栈帧的销毁过程

1、释放栈帧

而下面的指令中其实我们也只需要关心最后三条:
在这里插入图片描述
因为其他的都是别的一些设置(相当于把这块空间再次初始化成默认值),我们这里可以不管。
而我们这里的前两条其实和前面是类似的,我们先要做的就是再次让esp与ebp指向同一个位置:
在这里插入图片描述
而到此时,原本Add的栈帧空间也就被释放了。

2、弹栈

接下来的这条指令就是弹栈了:
在这里插入图片描述
在这里插入图片描述
所以我们这里的这条指令所做的就是将栈顶的数据pop到ebp当中,而我们当前的栈顶数据其实就是之前压入栈中的main函数的栈底地址:
在这里插入图片描述
所以,当我们这条指令执行结束后,ebp也就恢复到了main函数的栈底,并且esp也往回走了:
在这里插入图片描述
最后我们就到一条很重要的汇编指令ret:
在这里插入图片描述
在这里插入图片描述
这条语句其实做的就是将我们之前压入栈中的返回地址移入eip中,这样我们下一条指令就是从返回值地址处的指令开始执行了,也就是跳转回了我们用于调用Add函数的call命令的下一条指令add:
在这里插入图片描述
并且执行完后esp也相应的要往会走:
在这里插入图片描述

3、回到main函数

接下来我们就回到了main函数:
在这里插入图片描述
我们发现回到main函数后首先执行的就是将esp加8,其所对应的效果就是将两个临时拷贝的空间也给释放了:
在这里插入图片描述
至此我们也就完全回到了main函数当中。也就是Add函数栈帧的创建和销毁也都全完成了。至于后面的指令也就和之前的一样了,毕竟main函数也是函数,只要是函数那它们的栈帧的创建和销毁也都是一样的啦。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林先生-1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值