【手撕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

相关文章

Python中help()和dir()函数的使用

《Python中help()和dir()函数的使用》我们经常需要查看某个对象(如模块、类、函数等)的属性和方法,Python提供了两个内置函数help()和dir(),它们可以帮助我们快速了解代... 目录1. 引言2. help() 函数2.1 作用2.2 使用方法2.3 示例(1) 查看内置函数的帮助(

Python中使用uv创建环境及原理举例详解

《Python中使用uv创建环境及原理举例详解》uv是Astral团队开发的高性能Python工具,整合包管理、虚拟环境、Python版本控制等功能,:本文主要介绍Python中使用uv创建环境及... 目录一、uv工具简介核心特点:二、安装uv1. 通过pip安装2. 通过脚本安装验证安装:配置镜像源(可

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

Java中实现线程的创建和启动的方法

《Java中实现线程的创建和启动的方法》在Java中,实现线程的创建和启动是两个不同但紧密相关的概念,理解为什么要启动线程(调用start()方法)而非直接调用run()方法,是掌握多线程编程的关键,... 目录1. 线程的生命周期2. start() vs run() 的本质区别3. 为什么必须通过 st

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Go语言如何判断两张图片的相似度

《Go语言如何判断两张图片的相似度》这篇文章主要为大家详细介绍了Go语言如何中实现判断两张图片的相似度的两种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 在介绍技术细节前,我们先来看看图片对比在哪些场景下可以用得到:图片去重:自动删除重复图片,为存储空间"瘦身"。想象你是一个

Go语言中Recover机制的使用

《Go语言中Recover机制的使用》Go语言的recover机制通过defer函数捕获panic,实现异常恢复与程序稳定性,具有一定的参考价值,感兴趣的可以了解一下... 目录引言Recover 的基本概念基本代码示例简单的 Recover 示例嵌套函数中的 Recover项目场景中的应用Web 服务器中

Python中bisect_left 函数实现高效插入与有序列表管理

《Python中bisect_left函数实现高效插入与有序列表管理》Python的bisect_left函数通过二分查找高效定位有序列表插入位置,与bisect_right的区别在于处理重复元素时... 目录一、bisect_left 基本介绍1.1 函数定义1.2 核心功能二、bisect_left 与

java中BigDecimal里面的subtract函数介绍及实现方法

《java中BigDecimal里面的subtract函数介绍及实现方法》在Java中实现减法操作需要根据数据类型选择不同方法,主要分为数值型减法和字符串减法两种场景,本文给大家介绍java中BigD... 目录Java中BigDecimal里面的subtract函数的意思?一、数值型减法(高精度计算)1.

Macos创建python虚拟环境的详细步骤教学

《Macos创建python虚拟环境的详细步骤教学》在macOS上创建Python虚拟环境主要通过Python内置的venv模块实现,也可使用第三方工具如virtualenv,下面小编来和大家简单聊聊... 目录一、使用 python 内置 venv 模块(推荐)二、使用 virtualenv(兼容旧版 P