c语言深入逐汇编详解函数栈帧的创建和销毁过程(代码片段)

林先生-1 林先生-1     2023-04-08     123

关键词:

【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函数也是函数,只要是函数那它们的栈帧的创建和销毁也都是一样的啦。

c语言进阶顶级神功!函数栈帧的创建和销毁(代码片段)

本章目录温馨提示开篇介绍学前疑惑学前准备1.环境选择2.知识铺垫正文开始1.大致轮廓了解(源代码及反汇编)2.函数栈帧的创建和销毁总流程3.函数栈帧的创建3.1main函数的创建(分解)3.2Add函数的创建(分解... 查看详情

函数栈帧的创建和销毁(代码片段)

目录各种寄存器的作用main()函数的调用通过汇编观察函数调用过程main()函数栈帧开辟过程Add()函数栈帧开辟过程Add()函数栈帧销毁过程各种寄存器的作用eax是“累加器”(accumulator),它是很多加法乘法指令的缺省寄存器ebx是“... 查看详情

c语言学习--函数栈帧的创建和销毁(代码片段)

前言在学习C语言的过程中,大家是否会存在一些困惑?比如:局部变量是如何创建的?为什么说局部变量未初始化时,其中存储的时随机值?函数到底时如何传参的?实参传递的顺序又是怎样的?... 查看详情

c语言函数栈帧的创建和销毁,以简单函数的调用来进行详细刨析(代码片段)

...个寄存器和函数栈帧的创建和销毁密切相关。3、以汇编语言为例进行刨析我们以如下一个简单的加法实现代码的汇编语言为例来对问题展开分析,为使过程更加的清晰,我将代码的每一步都写的非常的细腻。代码如下... 查看详情

函数栈帧的创建与销毁(代码片段)

文章目录1.函数栈帧的概念2.函数栈帧的创建2.1main函数函数栈帧的创建过程2.2main函数中创建变量2.3Add函数函数栈帧的创建2.4Add函数栈帧的销毁1.函数栈帧的概念函数栈帧:使用每一个函数都要在栈区开辟一块空间.栈帧也叫过程活... 查看详情

函数栈帧的创建和销毁(待写)(代码片段)

函数栈帧的创建和销毁main函数被调用的过程:具体过程main函数被调用的过程:mainCRTStartup()调用_tmainCRTStartup()再调用main()寄存器:ebp(栈底指针),esp(栈顶指针)(sp是esp的低16位,esp是rsp的低32位,ss是16位堆栈... 查看详情

函数栈帧的创建与销毁(代码片段)

目录写在前面函数栈帧的创建与销毁了解两个寄存器ebp和esp函数栈帧创建与销毁的具体过程main函数的函数栈帧变量的创建Add函数栈帧的创建与销毁回到main函数总结写在前面在我们前期的学习编程的过程中,我们会遇到许多... 查看详情

函数栈帧的创建和销毁(代码片段)

...的说明二、对于创建和销毁的全过程1.对于_mainCRTstarup的函数的创建2.对于main函数的创建(1).为什么有时候会打印出烫烫烫3.对于Add的函数的创建(2).为什么说形参不在函数中(3).函数中return值如何放回的>(4).ebp-main出栈后ebp寄存器... 查看详情

函数的调用过程,栈帧的创建和销毁。

一.函数调用1.函数调用过程涉及到的寄存器: (1)esp:栈指针寄存器(extendedstackpointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。 (2)ebp:基址指针寄存器(extendedbasepointer),其内存放着一个... 查看详情

函数栈帧的创建与销毁(代码片段)

栈帧的创建与销毁什么是栈帧C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一... 查看详情

函数栈帧的创建与销毁(代码片段)

文章目录1.函数栈帧的概念2.函数栈帧的创建2.1main函数函数栈帧的创建过程2.2main函数中创建变量2.3Add函数函数栈帧的创建2.4Add函数栈帧的销毁1.函数栈帧的概念函数栈帧:使用每一个函数都要在栈区开辟一块空间.栈帧也叫过程活... 查看详情

图解c/c++底层:函数栈帧的创建和销毁(下篇)

函数栈帧的创建和销毁(下篇)上篇原文链接根据上篇的函数栈帧过程的学习,我们了解到:什么是寄存器?计算机的速度最快的存储单元,因为寄存器是集成在CPU之上的,与内存是不同的独立的存储空间。什么... 查看详情

图解c/c++底层:函数栈帧的创建和销毁(下篇)

函数栈帧的创建和销毁(下篇)上篇原文链接根据上篇的函数栈帧过程的学习,我们了解到:什么是寄存器?计算机的速度最快的存储单元,因为寄存器是集成在CPU之上的,与内存是不同的独立的存储空间。什么... 查看详情

函数栈帧的创建与销毁,带你了解代码底层原理(代码片段)

在学习C语言时,我们难免有许多疑问例如:(1)局部变量是怎么创建的?(2)为什么局部变量的值是随机的?(3)函数是怎么传参的?传参的顺序如何?(4)形参和实参是什么关系?(5)函数调用是怎么做的࿱... 查看详情

函数栈帧的创建和销毁——“c”(代码片段)

...你们好呀,今天小雅兰来为大家介绍一个知识点——函数栈帧的创建和销毁。其实这个知识点,我们很早之前就要讲,但是因为我的一系列原因,才一直拖到了现在,那么,话不多说,让我们一起进入... 查看详情

c语言从入门到入土(进阶篇)函数栈帧的创建和销毁讲解(不看必后悔系列)(超详细)

在文章开始之前,先给大家补充两点,第一个是文章里面好像有一个我说的8进制,其实是16进制哈,就那么一处地方,当然也可能改了咳咳。第二点就是里面16进制数后面有0开头h结尾,那是计算机那么写... 查看详情

学好c语言,还需要掌握这个内功——函数栈帧的创建与销毁(代码片段)

学习本篇文章之前,你或许还有这些疑问:局部变量是怎么创建的?为什么局部变量的值是随机值?函数是怎么传参的?传参的顺序是什么?形参和实参是什么关系?函数调用结束后怎么返回?看... 查看详情

图解函数栈帧-函数的创建与销毁(代码片段)

函数栈帧🎂前言🌹栈帧的概念💖准备工作😀main函数栈帧的创建及初始化😁main函数的被调用😂main函数栈帧的开辟🤣main函数栈帧的初始化👩临时变量的创建。👨Add函数栈帧的创建🧑Add... 查看详情