抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解

2024-02-23 07:20

本文主要是介绍抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

函数栈帧的创建与销毁

  • 导语
  • 问题
  • 寄存器
  • 函数栈帧
    • 函数栈帧是什么?
    • 内存分布
    • 什么是栈?
  • 详细讲解函数栈帧
    • 栈帧的维护
    • 开辟main函数
    • 创建局部变量与初始化
    • 调用Add函数
    • Add函数的内部
  • 返回与销毁
  • 结束

导语

这篇文章是从头贯穿到尾的,让你更加详细的了解函数是什么样在内存里创建,怎么样销毁的,相信家人们读完这篇文章之后能让你眼里的代码变得透明起来(本章不需要过多了解汇编语言,重点是了解函数栈帧怎么创建和销毁的)
注意:这里我们用的是32位平台,用VS2013作为参考。

问题

大家知道这些是为什么吗?
在这里插入图片描述
看完这一篇,这些问题将迎刃而解。

寄存器

寄存器:

eax 通常用来执行加法,函数调用的返回值一般也放在这里面
ebx 通常用来数据存取
ecx 通常用作for循环的计数器
edx 读取I/O端口时,存放端口号
edi 字符串操作时,用于存放目的地址的,和esi两个经常搭配一起使用,执行字符串的复制等操作

今天主要的是:

ebp 栈底指针,指向栈的底部,用ebp+偏移量的形式来定位函数存放在栈中的局部变量
esp 栈顶指针,指向栈的顶部
这两个寄存器用来存放地址用来维护函数栈帧

函数栈帧

函数栈帧是什么?

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。(来自百度百科)。

内存分布

栈区:由高地址往低地址增长,主要用来存放局部变量,函数调用开辟的空间,与堆共享一段空间。(本篇重点)
堆区:由地地址向高地址增长,动态开辟的空间就在这里(malloc,realloc,calloc,free),与栈共享一段空间。
静态区:主要存放全局变量和静态变量。

什么是栈?

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。(来自百度百科)
这里面有更详细的链接: 栈.

详细讲解函数栈帧

栈帧的维护

这里我们用一段代码演示:

#include <stdio.h>
int Add(int x,int y)
{int 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函数的地址,栈帧使用是先从高地址再到低地址。
在这里插入图片描述
假设我们main函数是存入在这个地方。
当然,这个地址没有申请之前不是你的,我们需要两个寄存器维护main的这块空间。
它们就是我们之前介绍的esp和ebp:
在这里插入图片描述

esp和ebp中间的空间就是main函数的空间,它俩是维护函数栈帧的,调用哪个就去维护哪个函数栈帧。
这时我们在编译器里按下F10,点击调试,移动到窗口,然后点击监视,内存,反汇编。
这时我们接下来需要的三个窗口,有助于理解。
反汇编那里我们逐步分析:
首先我们要知道main函数也是被调用的(不做过多了解),调用main函数的函数一开始是被esp和ebp维护的。
在这里插入图片描述

开辟main函数

然后我们去看反汇编那里的指令:
在这里插入图片描述

push 的指令是压栈,也就是给栈放了一个元素进去,这里是把epb的值放了进去。
在这里插入图片描述
结果就是这个样子,我们发现,esp调到上面去了,这是因为esp的性质。
我们用调试里面的内存和监视看一看:

这是原来esp的地址:
在这里插入图片描述
这是第一行汇编运行后的:
在这里插入图片描述
我们知道地址是从高到低使用,esp向上面移动了,也就是代表地址要变小,这里减少了4。
我们再看看内存里:
在这里插入图片描述
确实压进去了。
第二行的指令是什么意思呢?

把esp的值给ebp(注意,esp和ebp为指针,它们里面储存的是地址)

也就是说ebp不会指向原来的位置了,和esp指向相同的位置。
在这里插入图片描述
在这里插入图片描述
变成了这个样子。
第三行指令是做什么呢?

这里只给esp减去0E4h这个值,这个值是十六进制的数字,转换十进制为228。(至于后面的h我们不做详细的讲解)

也就是说我们的esp移到了上面的某一个位置去了。
在这里插入图片描述
也就是说我们的esp和ebp再一次的维护了一块空间,这块空间就是我们main函数的空间。
在这里插入图片描述
然后下面的三行汇编指令就是压栈:
在这里插入图片描述
值如下:
在这里插入图片描述

然后是后面的指令:
在这里插入图片描述

第一行是把[ebp-24h]这个值给edi。
第二行把39h放在ecx里面。
第三行把0CCCCCCCCh的内容放在eax里面。
第四行是从edi开始往下的ecx空间里面放eax的值。(dword是四个字节的意思)

我们发现,edi是esp原来指向的位置,也就是这个位置:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
最后面的地址我们发现是ebp的地址:
在这里插入图片描述
至于edi为什么地址变了,这个我们不做深究,只需要看[ebp-0E4h]的地址就可以了,因为这是没变之前edi的地址。
这里我们也发现,main函数里面都放满了0CCCCCCCCh这个值。
(这也能解释我们在打印字符数组的时候没有\0会打出来一堆乱码,因为里面都是随机值,也就是你放进去的0CCCCCCCCh。)
在这里插入图片描述
也就是说我们在main函数的区域里初始化了上面的蓝色值。
上面只是为main函数栈帧的开辟。

创建局部变量与初始化

现在初始化三个值:
在这里插入图片描述
我们看第一条指令,最后面的0Ah是十六进制,代表10的意思,把10放进[ebp-8]这个地址里面:
在这里插入图片描述

看,我们里面CCCCCCCC的值被改成, 十六进制a也就是十进制的10。
那么下面的两条汇编指令也就容易易理解了,最后变成这个样子:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里顺便说一句,因为esp的上移,main函数的栈区已经变成了这样。
这里就是局部变量的创建与初始化。

调用Add函数

现在我们应该调用Add函数了。
在这里插入图片描述
首先分析第一条指令,我们要把[ebp-14h]的地址存进eax的寄存器里,然后往下看,再进行压栈。
在这里插入图片描述
其实也就是把我们的20存进eax,10存进了ecx而已。
在这里插入图片描述
我们又把eax和ecx进行压栈,其实也就是把20和10放再了上面。
这个动作其实就是传参
然后看下一行的指令call,这是准备调用Add函数,这是到了call这一行时,按F11进入这个函数内部,在进入内部之前我们发现这么一个问题:
在这里插入图片描述
红色是变化的一行,这里存入的地址是不是很眼熟?没错就是call下面add的地址。
这个位置是在我们传参上面的位置,也就是说再一次进行了压栈。
在这里插入图片描述

为什么要把地址放再这个地方呢?因为我们都知道,函数调用之后都是需要返回的,在这里记住地址就好从这里返回,然后继续执行指令。

Add函数的内部

这是进入Add函数里面的汇编指令:
在这里插入图片描述
是不是看着似曾相识?没错就是给Add函数分配空间并且维护。
在正式说这段代码我要说一句,现在维护代码的两个寄存器已经移动很多次了,也就是说现在main函数的栈帧已经这么大了:
在这里插入图片描述
我们再看现在需要的指令:让我们把ebp进行压栈,这里的ebp其实是main函数的ebp地址。
这些指令就和之前开辟main函数一样的逻辑:
在这里插入图片描述
这就是Add函数的栈帧。
下面进行局部变量的创建和计算加法还有返回值:
在这里插入图片描述
第一行指令先创建整型变量Z初始化为0:
在这里插入图片描述
然后看第二行指令,把[ebp+8]放进eax里面。
第三行指令,把[ebp+8]的值和[ebp+0Ch]加起来放在eax里面。
在这里插入图片描述
这时,eax等于30。
然后看第四行指令,意思是把eax的值放在[ebp-8]这个地址里面。
这里我们就明白了,是这样调用参数然后把他们放进了Z里面。
到了这里我们也明白了一件事,之前说的形参和实参问题,其实ecx和eax里面是10和20这个数值,并不像之前的ebp一样存的是地址,也就是说这里的ecx和eax有单独的空间,通过这个空间也只能找到10和20这个数值而已。
并且我们接收的值是int x和int y都没显示怎么运作。
Add的返回:
现在到返回了,我们也有一个疑惑,局部变量Z出了Add函数不就已经销毁了吗?其实是这样的,看最后一行,我们把[ebp-8]这个地址的值暂时存在了eax这个寄存器里,虽然变量Z销毁了,但是寄存器eax是不会销毁的,它是集成在CUP的硬件,所以说寄存器带着Z的值就走了。

返回与销毁

让我们看接下来的指令:
在这里插入图片描述
这里的pop是什么意思呢?是弹出的意思,连续三个弹出,把edi,esi,ebx都弹出去了:
在这里插入图片描述
变成了这个样子,esp因为这三个元素的弹出从而变化。
然后继续看第四行的指令,把ebp的地址赋给esp,也就是说esp拿到了现在的ebp的地址,和ebp同时指向了一个地方:
在这里插入图片描述
第五行的指令是弹出ebp(也就是main函数的ebp原来的地址),把弹出的结果弹到指向这里的ebp里面,就等于把原来再main函数的ebp地址赋给了现在指向这里的ebp,然后esp增加了一个4个字节的地址:
在这里插入图片描述
现在我们就发现,已经都回到了main函数的栈帧里面,esp和ebp又开始维护main函数了。
我们还有一个是ret指令,这个指令是返回的意思,弹出当前这个函数从栈顶返回。
因为之前我们存了00C21450这个地址,那么落脚点就是这个地址。
在这里插入图片描述
左边黄色箭头的地方就是落脚点。(当时存这个地址就是为了能让我们返回main函数里面的这一行)
这里注意,ret完事之后会pop一下,也就是说把这个main栈帧的最上面的元素给弹出了(也就是00C21450这个地址)。
add这一行的意思是给esp+8,就等于弹出了这两个元素(ecx和eax),形参也就销毁了:
在这里插入图片描述
这是图解。
我们继续往下看:
在这里插入图片描述
黄色箭头指向的地方指令是什么意思?把eax的值赋给[ebp-20h](这个地址就是局部变量C的地址)eax是个寄存器,之前我们把变量Z的值放了进来,也就是说我们最后把变量Z放进了变量C里面。
这样我们就把返回值给带回来了。
在这里插入图片描述
至于printf打印这些指令我们不做讲解了。
剩下的main函数的销毁和之前add的销毁一样。

结束

到这里函数栈帧与销毁就讲完了,我相信大家对于上面的疑惑都有了答案。
请路过的家人们点个赞,大佬们纠正错误和指点不足,谢谢!!!

这篇关于抽丝剥茧C语言(中阶)函数栈帧的创建与销毁——图解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Kotlin 作用域函数apply、let、run、with、also使用指南

《Kotlin作用域函数apply、let、run、with、also使用指南》在Kotlin开发中,作用域函数(ScopeFunctions)是一组能让代码更简洁、更函数式的高阶函数,本文将... 目录一、引言:为什么需要作用域函数?二、作用域函China编程数详解1. apply:对象配置的 “流式构建器”最

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C语言实现两个变量值交换的三种方式

《C语言实现两个变量值交换的三种方式》两个变量值的交换是编程中最常见的问题之一,以下将介绍三种变量的交换方式,其中第一种方式是最常用也是最实用的,后两种方式一般只在特殊限制下使用,需要的朋友可以参考下... 目录1.使用临时变量(推荐)2.相加和相减的方式(值较大时可能丢失数据)3.按位异或运算1.使用临时

使用C语言实现交换整数的奇数位和偶数位

《使用C语言实现交换整数的奇数位和偶数位》在C语言中,要交换一个整数的二进制位中的奇数位和偶数位,重点需要理解位操作,当我们谈论二进制位的奇数位和偶数位时,我们是指从右到左数的位置,本文给大家介绍了使... 目录一、问题描述二、解决思路三、函数实现四、宏实现五、总结一、问题描述使用C语言代码实现:将一个整

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++中函数模板与类模板的简单使用及区别介绍

《C++中函数模板与类模板的简单使用及区别介绍》这篇文章介绍了C++中的模板机制,包括函数模板和类模板的概念、语法和实际应用,函数模板通过类型参数实现泛型操作,而类模板允许创建可处理多种数据类型的类,... 目录一、函数模板定义语法真实示例二、类模板三、关键区别四、注意事项 ‌在C++中,模板是实现泛型编程

kotlin的函数forEach示例详解

《kotlin的函数forEach示例详解》在Kotlin中,forEach是一个高阶函数,用于遍历集合中的每个元素并对其执行指定的操作,它的核心特点是简洁、函数式,适用于需要遍历集合且无需返回值的场... 目录一、基本用法1️⃣ 遍历集合2️⃣ 遍历数组3️⃣ 遍历 Map二、与 for 循环的区别三、高