【手撕C语言 第八集】函数栈帧的创建与销毁

2024-01-24 16:04

本文主要是介绍【手撕C语言 第八集】函数栈帧的创建与销毁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 一、什么是函数栈帧?
  • 二、函数栈帧能解决什么问题呢?
    • (1)局部变量是如何创建的?
    • (2)为什么局部变量不初始化内容是随机的?
    • (3)函数调用时参数是如何传递的?传参的顺序是什么样?
    • (4)形参和实参的关系?
    • (5)函数的返回值是如何带回的?
  • 三、函数栈帧的创建与销毁解析
    • 1.什么是栈?
    • 2.认识相关寄存器和汇编指令
    • 3.解析函数栈帧的创建与销毁
      • 1.预备知识
      • 2.函数的调用堆栈
    • 4.准备环境
    • 5.转到反汇编
    • 6.函数栈帧的创建
      • 小知识:烫烫烫~
    • 7.函数栈帧的销毁
    • 8.拓展了解:
  • 四、易混乱点


一、什么是函数栈帧?

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。

那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
🎗️函数参数和函数返回值
🎗️临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

二、函数栈帧能解决什么问题呢?

(1)局部变量是如何创建的?

首先要为局部变量所在的函数创建函数栈帧,再在函数栈帧中找到一些空间把局部变量存进去。

(2)为什么局部变量不初始化内容是随机的?

因为随机值是我们放进去的,例如:0ccccccch,初始化意味着覆盖随机值。

(3)函数调用时参数是如何传递的?传参的顺序是什么样?

在函数调用前就已经push push,把两个实参从右向左拷贝过去。在调用的函数栈帧中通过指针偏移量找到形参。
传参的顺序是从右向左。

(4)形参和实参的关系?

形参确实是在压栈时开辟的空间。它和实参的值相同,空间是独立的,所以形参是实参的一份临时拷贝,改变形参不会影响实参。

(5)函数的返回值是如何带回的?

在调用函数之前,就把call指令的下一条指令的地址存进去,把调用该函数的上一个函数栈帧的ebp存进去。当函数调用完返回的时候,将此时的ebp的值赋给esp,弹出ebp就能找到上一个函数的ebp的位置,(这个过程回收了调用的函数空间,esp,ebp此时维护main函数栈帧),由于之前记住了call指令的下一条指令的地址,当call调用完函数后,通过记住的地址找到下一条指令继续往下执行。在函数调用的过程中将函数的返回值存储到寄存器中,再通过后续的指令操作将寄存器的值放到主函数用于接收返回值的变量中。
所以函数的返回值是通过寄存器的方式带回来的。

三、函数栈帧的创建与销毁解析

1.什么是栈?

🎗️栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
🎗️在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
🎗️在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。
🎗️在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。

2.认识相关寄存器和汇编指令

相关寄存器
在这里插入图片描述
相关汇编命令
在这里插入图片描述
🎗️在这里插入图片描述

3.解析函数栈帧的创建与销毁

1.预备知识

首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。

  1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
  2. 这块空间的维护是使用了2个寄存器: esp 和 ebp , ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。
    如图所示:
    在这里插入图片描述

🎗️(1)栈区的使用习惯是先使用高地址,再使用低地址
🎗️(2)空间从高地址向低地址消耗
注意
在不同的编译器下,函数栈帧的创建和销毁是略有差异的,大体逻辑是一致的,具体细节取决于编译器的实现。

2.函数的调用堆栈

演示代码

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

这段代码,如果我们在VS2019编译器上调试,调试进入Add函数后,我们就可以观察到函数的调用堆栈
(右击勾选【显示外部代码】),如下图:
在这里插入图片描述
🎗️函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数。
🎗️在 invoke_main 函数之前的函数调用我们就暂时不考虑了。
🎗️那我们可以确定, invoke_main 函数应该会有自己的栈帧, main 函数和 Add 函数也会维护自己的栈帧,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间。

在VS2013上,我们可以清晰的观察到, main 函数调用之前,是由_ _tmainCRTStartup 函数来调用main函数,__tmainCRTStartup函数调用之前,是由mainCRTStartup 函数来调用__tmainCRTStartup函数的。
在这里插入图片描述
那接下来我们从main函数的栈帧创建开始讲解

4.准备环境

为了让我们研究函数栈帧的过程足够清晰,不要太多干扰,我们可以关闭下面的选项,让汇编代码中排除一些编译器附加的代码:
在这里插入图片描述

5.转到反汇编

调试到main函数开始执行的第一行,右击鼠标转到反汇编。
注意:VS编译器每次调试都会为程序重新分配内存,本次反汇编代码是一次调试代码过程中数据,每次调试略有差异。
在这里插入图片描述

6.函数栈帧的创建

接下来我们就一行行拆解汇编代码:
在这里插入图片描述
在这里插入图片描述
上面的这段代码最后4句,等价于下面的伪代码
在这里插入图片描述
在这里插入图片描述

小知识:烫烫烫~

在这里插入图片描述
之所以上面的程序输出“烫”这么一个奇怪的字,是因为main函数调用时,在栈区开辟的空间的其中每一个字节都被初始化为0xCC,而arr数组是一个未初始化的数组,恰好在这块空间上创建的,0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”。

接下来我们再分析main函数中的核心代码
在这里插入图片描述
在这里插入图片描述
Add函数的传参
在这里插入图片描述
在这里插入图片描述
函数调用过程
在这里插入图片描述
call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。
这里记住call指令的下一条指令的地址,当call调用完函数之后,通过记住的地址找到下一条指令继续执行
在这里插入图片描述
当我们跳转到Add函数,就要开始观察Add函数的反汇编代码了。
在这里插入图片描述
代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

  1. 将main函数的 ebp 压栈
  2. 计算新的 ebp 和 esp
  3. 将 ebx , esi , edi 寄存器的值保存
  4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的
    参数,这就是形参访问。
  5. 将求出的和放在 eax 寄存器尊准备带回

在这里插入图片描述
图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

7.函数栈帧的销毁

当函数调用要结束返回的时候,前面创建的函数栈帧也开始销毁。
那具体是怎么销毁的呢?我们看一下反汇编代码。
在这里插入图片描述
回到了call指令的下一条指令的地方:
在这里插入图片描述
但调用完Add函数,回到main函数的时候,继续往下执行,可以看到:
在这里插入图片描述

8.拓展了解:

其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。

四、易混乱点

(1)函数内部创建的静态变量是在全局开辟的;函数内部创建的局部变量是在栈区上创建的。
(2)每个函数开辟的空间不是一样大的,这取决于编译器(编译器会计算需要多大空间)
(3)寄存器独立于内存,不在内存上,是集成到CPU上的

电脑上的存储(相互独立):
硬盘
内存
寄存器

(4)为什么局部变量a和b创建的位置不是相邻的,中间隔了一块空间?
这完全取决于编译器,中间空多大空间也取决于编译器。
(5)计算机删除数据是直接允许后续操作直接覆盖的。
(6)事实上,在函数开辟空间上进行的是z=x+y操作,x,y形参空间的开辟是在main函数和add函数之间的一块空间,也可以认为是在main函数的栈帧空间里,但是不在ad函数的栈帧空间里。

这篇关于【手撕C语言 第八集】函数栈帧的创建与销毁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/640206

相关文章

C语言中%zu的用法解读

《C语言中%zu的用法解读》size_t是无符号整数类型,用于表示对象大小或内存操作结果,%zu是C99标准中专为size_t设计的printf占位符,避免因类型不匹配导致错误,使用%u或%d可能引发... 目录size_t 类型与 %zu 占位符%zu 的用途替代占位符的风险兼容性说明其他相关占位符验证示

MySQL常用字符串函数示例和场景介绍

《MySQL常用字符串函数示例和场景介绍》MySQL提供了丰富的字符串函数帮助我们高效地对字符串进行处理、转换和分析,本文我将全面且深入地介绍MySQL常用的字符串函数,并结合具体示例和场景,帮你熟练... 目录一、字符串函数概述1.1 字符串函数的作用1.2 字符串函数分类二、字符串长度与统计函数2.1

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所

C语言进阶(预处理命令详解)

《C语言进阶(预处理命令详解)》文章讲解了宏定义规范、头文件包含方式及条件编译应用,强调带参宏需加括号避免计算错误,头文件应声明函数原型以便主函数调用,条件编译通过宏定义控制代码编译,适用于测试与模块... 目录1.宏定义1.1不带参宏1.2带参宏2.头文件的包含2.1头文件中的内容2.2工程结构3.条件编

Go语言并发之通知退出机制的实现

《Go语言并发之通知退出机制的实现》本文主要介绍了Go语言并发之通知退出机制的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、通知退出机制1.1 进程/main函数退出1.2 通过channel退出1.3 通过cont

Go语言编译环境设置教程

《Go语言编译环境设置教程》Go语言支持高并发(goroutine)、自动垃圾回收,编译为跨平台二进制文件,云原生兼容且社区活跃,开发便捷,内置测试与vet工具辅助检测错误,依赖模块化管理,提升开发效... 目录Go语言优势下载 Go  配置编译环境配置 GOPROXYIDE 设置(VS Code)一些基本

postgresql使用UUID函数的方法

《postgresql使用UUID函数的方法》本文给大家介绍postgresql使用UUID函数的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录PostgreSQL有两种生成uuid的方法。可以先通过sql查看是否已安装扩展函数,和可以安装的扩展函数

MySQL字符串常用函数详解

《MySQL字符串常用函数详解》本文给大家介绍MySQL字符串常用函数,本文结合实例代码给大家介绍的非常详细,对大家学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql字符串常用函数一、获取二、大小写转换三、拼接四、截取五、比较、反转、替换六、去空白、填充MySQL字符串常用函数一、

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

MySql基本查询之表的增删查改+聚合函数案例详解

《MySql基本查询之表的增删查改+聚合函数案例详解》本文详解SQL的CURD操作INSERT用于数据插入(单行/多行及冲突处理),SELECT实现数据检索(列选择、条件过滤、排序分页),UPDATE... 目录一、Create1.1 单行数据 + 全列插入1.2 多行数据 + 指定列插入1.3 插入否则更