【C语言】9.C语言函数栈帧的创建和销毁

C语言函数栈帧的创建和销毁


看完本文你能了解什么?

  1. 局部变量是怎么创建的?
  2. 为什么局部变量的值是随机值?
  3. 函数是怎么传参的?传参的顺序是怎么样的?
  4. 实参和形参是什么关系?
  5. 函数调用是怎么做的?
  6. 函数调用结束后怎么返回的?

寄存器

我们常用的寄存器有eax,ebx,ecx,edx。不过我们这节主要用到的是ebp和esp这两个寄存器。

函数栈帧

ebp和esp这两个寄存器中存放的是地址。这两个地址是用来维护函数栈帧的。

每一个函数调用,都要在栈区创建一个空间。

在这里插入图片描述

假如说要调用main函数就要在栈区创建一个空间

在这里插入图片描述

这个空间是为main函数开辟的,那么我们就称这个空间是为main函数开辟的一块函数栈帧。

这个空间是ebp和esp这两个寄存器来维护的。

在这里插入图片描述

ebp作为寄存器存储的是如图所指向的地址。(寄存器是一块区域,是一个存储空间)

esp作为寄存器存储的是如图所指向的地址。

ebp和esp中间的这块空间是由这两个寄存器来维护的。

一般我们把ebp叫做栈底指针,把esp叫做栈顶指针。

为啥叫这个呢?

实际上我们可以把这个看成一个栈,从高地址向低地址是不是那个地址在被使用消耗啊,如果想使用地址只能往上使用,就像栈一样,只能往栈顶放数据。


在vs2013中,main函数也是被其他函数调用的。

main函数被__tmainCRTStartup函数调用,而__tmainCRTStartup函数被mainCRTStartup函数调用。

int Add(int x, int y) {int z = 0;z = x + y;return z;
}int main() {int a = 10;int b = 20;int c = 0;c = Add(a, b);printf("%c\n", c);return 0;
}

我们调用main函数,会在栈区分配空间。然后往下走,走到c = Add(a, b);又去调用Add函数去了,给Add函数在栈区分配空间。

这个时候ebp和esp这两个指针就去维护Add函数去了。

在这里插入图片描述

那么按照道理,__tmainCRTStartup和mainCRTStartup函数在被调用的时候也会有一个对应的函数栈帧。

在这里插入图片描述

右击鼠标转到反汇编

在这里插入图片描述

在调用main函数前,会创建一个__tmainCRTStartup函数栈帧

在这里插入图片描述

进入main函数第一步就是push,也就是压栈。

在这里插入图片描述

压栈操作后,就像这样:

在这里插入图片描述

__tmainCRTStartup函数栈帧上面出现了一个ebp元素。同时esp指针也指向ebp元素的上面了。

下一步是mov,把esp的值给ebp。

在这里插入图片描述

这个时候esp和ebp指向了同一个地方

在这里插入图片描述

然后下一步是sub,给esp减去0E4h。

在这里插入图片描述

因为下面是高地址,上面是低地址,所以减就意味着esp在图上往上走。esp和ebp之间的那个新的空间就是main函数的栈帧空间。

在这里插入图片描述

然后进行3次push,给顶上压上3个元素。

在这里插入图片描述

然后esp也依次指向ebx上面,esi上面,edi上面。停在edi上面。

在这里插入图片描述

然后进入lea,这里的[]里面的一坨东西看着很费劲

在这里插入图片描述

我们把显示符号名勾上就会变成这样

在这里插入图片描述

把ebp-0E4h放到edi里面去,后面两个mov也是把39h放到ecx里面,把0CCCCCCCCh放到eax里面去。

接下来一步比较重要

在这里插入图片描述

这一步是把刚刚edi往下的39h这么多个dword(一个word两个字节,dword是double word四个字节)个数据改成0CCCCCCCCh。

压栈(push):给栈顶放一个元素。

出栈(pop):从栈顶删除一个元素。

然后进入mov,把0Ah(也就是10)给到ebp-8的位置上。

在这里插入图片描述

我们假设一个紫色框代表4个字节

在这里插入图片描述

在这里插入图片描述

我们把a=10放进了ebp-8里面,原来的CCCCCCCC就被10替换了。

这也可以说明我们没有初始化的时候内存里面放的就是CCCCCCCC ,之前我们有一节里面打印字符数组没有加/0,导致的打印烫烫烫烫烫烫烫烫就是这个原因。

然后进入mov,把14h放到ebp-14h(相当于ebp-20)上。

在这里插入图片描述

在这里插入图片描述

c的创建也类似,然后我们创建好abc之后,我们调用Add函数。

在这里插入图片描述

我们进入mov,把ebp-14h给eax。这个ebp-14h不就是b吗?也就是相当于把20给到eax里面去了。

然后push,eax压栈。eax里面放的是20,实际上把20给放进去了。

在这里插入图片描述

然后mov,把ebp-8(也就是a的值10)给了ecx。

接着push,ecx压栈。ecx里面放10。

在这里插入图片描述

上面两步其实是传参。

接下来call,就是调用,

在这里插入图片描述

执行call指令会把这个00C21450(也就是call指令的下一条指令的地址)给压栈。

在这里插入图片描述

为什么要压栈这个呢?因为我们call指令执行了之后会进入被调用函数的内部,进去了怎么出来呢?这就要靠call指令的下一条指令了。回来的时候就会找到这个地址,然后从这个地址往下执行。

然后就进入Add函数里面了。

在这里插入图片描述

进入push,把ebp压栈。

在这里插入图片描述

然后mov,把esp的值给ebp。

在这里插入图片描述

然后sub,给esp-0CCh。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

然后继续3次push,3次压栈

在这里插入图片描述

在这里插入图片描述

然后lea,把ebp+FFFFFF34h加到edi里面去。

在这里插入图片描述

然后把33h,放到ecx里面,把0CCCCCCCCh放到eax里面去

然后rep,从edi这个位置开始,向下33h的内容初始化成0CCCCCCCCh。

然后进入mov,把0给ebp-8。相当于z=0。

在这里插入图片描述

在这里插入图片描述

然后mov,把ebp+8的值放到eax里面。eax=a=10。

接着add,把ebp+0Ch(也就是ebp+12)的值加到eax里面。eax=10+b=10+20=30。

然后mov,把eax的值放到ebp-8里面。也就是说z从0变成了30。

在这里插入图片描述

在这里插入图片描述

从上面我们可以看到,函数调用的时候没有主动创建x,y这两个形参,而是直接把a,b的参数调用过去了。

传参把a,b的值进行了压栈。

在这里插入图片描述

我们函数传参,还没有调用Add函数的时候我们就先把a,b传过去了,压栈了两个参数。

然后进入Add函数内部进行计算的时候,我们就把压栈的两个参数进行运算。把这两个参数加起来放到z里面去。

所以我们常说,形参是实参的一份拷贝。

然后我们继续mov,把ebp-8的值放到eax(eax是个寄存器)里面去。

我们常说这个z,return z;在函数调用完就销毁了,销毁了怎么传值出来的呢?就是靠的寄存器传递的,放到这样一个全局的寄存器里面就安全了。

在这里插入图片描述

接下来这个3个pop,就相当于出栈3次。

在这里插入图片描述

在这里插入图片描述

然后我们z的值都给寄存器了,那么Add函数的函数栈帧按道理来说也因该被释放掉了。

然后进入mov,把ebp赋值给esp。

在这里插入图片描述

在这里插入图片描述

然后pop,把ebp出栈。这个棕色框就出栈了。然后ebp指针指向的值就是下面的ebp区域了。

在这里插入图片描述

ebp出栈了之后,这个ebp指针指向了下面,因为这个出栈的ebp是为main函数创建的。

在这里插入图片描述

然后esp指针顺势指向了00C21450。

在这里插入图片描述

也就相当于我们回到了main函数的函数栈帧里面。

然后是ret。ret指令会让子程序返回到调用函数的代码处。但问题是它怎么知道回到哪里去呢?

在这里插入图片描述

我们注意,现在的栈顶是00C21450,也就是上面说的call指令的下一条指令的地址。ret指令返回的时候其实就是在栈顶返回了call指令的下一条指令的地址。这样才能返回之前的地方。ret后00C21450相当于也pop出去了。

在这里插入图片描述

在这里插入图片描述

然后add,esp+8。

在这里插入图片描述

在这里插入图片描述

然后mov,把eax的值给ebp-20h。

在这里插入图片描述

也就是把之前Add函数存在eax寄存器里面的返回值给c。

在这里插入图片描述

在这里插入图片描述

c的值从0变成了30。


答案:

  1. 局部变量是怎么创建的?

    在函数栈帧里面划定一块区域给局部变量。

  2. 为什么局部变量的值是随机值?

    因为局部变量不初始化的时候,值是我们随机放进去的。就像这里面的CCCCCCCC。

  3. 函数是怎么传参的?传参的顺序是怎么样的?

    还没调用函数的时候就push,push,把这两个要用的实参压栈压进去。然后调用的时候通过指针偏量来调用这两个压栈压进去的值。

  4. 实参和形参是什么关系?

    形参是实参的一份拷贝。但是形参不会影响实参。

  5. 函数调用是怎么做的?

    上面讲过。

  6. 函数调用结束后怎么返回的?

    我们在调用函数之前就把call指令的下一条指令的地址压栈压进去了。当我们调用完函数后,弹出ebp,和call指令下一条指令的地址后,esp指针往下走的时候就可以跳转到call指令下一条指令的地址。返回值是通过寄存器来传递回来的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.tpcf.cn/bicheng/18791.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode hot100-57-G

17. 电话号码的字母组合 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。不会,放IDEA里执行了一下大概理解了流程 …

『大模型笔记』KV缓存:Transformer中的内存使用!

『大模型笔记』KV缓存:Transformer中的内存使用! 文章目录 一. KV缓存:Transformer中的内存使用!1.1. 介绍1.2. 自注意力机制回顾1.3. KV 缓存的工作原理1.4. 内存使用和示例1.4.1. 存储键值缓存需要多少内存1.4.2. Example: OPT-30B(300亿参数)四. 参考文献进一步阅读:…

深圳比创达电子|EMC与EMI滤波器:电子设备的“电磁防护罩”

在电子科技日新月异的今天,电磁兼容性(EMC)问题越来越受到工程师和技术人员的关注。其中,电磁干扰(EMI)和电磁干扰抑制(即EMI滤波器)是实现良好EMC性能的关键技术之一。 一、EMC与E…

KineFX —— 简介

KineFX是绑定和动画的框架和工具集,可在SOP级别创建和编辑角色;可从头创建自己的KineFX角色,或使用特定的KineFX SOP和常规的SOP去编辑导入的角色和动画; 程序化绑定 KineFX构建与程序化绑定的原则上,可快速非破坏性迭…

AI绘画Stable Diffusion【艺术写真】:蒙版法图生图,局部重绘实现AI艺术写真

大家好,我是设计师阿威 之前我分享过几篇使用SD插件换脸方式实现AI写真的教程,主要存在2个大的问题。 (1)人脸相似度 (2)生成的图片整体色调有时候会比较怪异 对于上面的问题,在对图片质量要…

一些图形界面的工具可以帮助你模拟点击和进行其他浏览器自动化操作

1. Selenium IDE Selenium IDE 是一个用于记录、编辑和调试测试的集成开发环境。它有一个图形界面,允许你通过点击和录制来创建测试用例。它支持Chrome和Firefox浏览器。 安装和使用步骤: 在Chrome或Firefox的扩展商店中搜索“Selenium IDE”并安装。打开Selenium IDE扩展。…

每天写两道(二)LRU缓存、数组中最大的第k个元素

146.LRU 缓存 . - 力扣(LeetCode) 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存…

类中使用QtConcurrent::run

在QtConcurrent::run中调用类的成员函数时,你需要注意几个关键点: 对象生命周期:你需要确保在QtConcurrent::run调用的整个期间,类对象都是有效的。如果对象在成员函数执行期间被销毁,将会导致未定义行为。成员函数访…

在table表格中如何给tr的每一个子元素加haver效果

效果图: 核心代码: tbody tr :hover {background-color: #d5d5d5; } 改变子元素 tbody tr:hover {background-color: #d5d5d5; } 改变父元素 两段代码看起来一样,其实不一样,其中差了一个空格字符 希望可以帮到大家

多线程新手村3--多线程代码案例

1.1 单例模式 单例模式是设计模式中非常经典的一种。那么有同学肯定就会好奇了,什么是设计模式呢? 设计模式简单的说就是程序员的“棋谱”,我们下象棋时肯定或多或少都背过棋谱,例如当头炮、马后炮等,设计模式也是这…

接口性能测试复盘:解决JMeter超时问题的实践

在优化接口并重新投入市场后,我们面临着一项关键任务:确保其在高压环境下稳定运行。于是,我们启动了一轮针对该接口的性能压力测试,利用JMeter工具模拟高负载场景。然而,在测试进行约一分钟之后,频繁出现了…

新人学习笔记之(函数2)

一、函数的参数 1.形参和实参 (1)在声明函数时,可以在函数名称后面的小括号中添加一些参数,这些参数被称为形参,而在调用该函数时,同样也需要传递相应的参数,这些参数被称为实参 参数说明形参形…

【前端之npm镜像地址】

npm镜像地址 淘宝镜像地址华为镜像地址腾讯云镜像地址 淘宝镜像地址 npm config set registry https://registry.npmmirror.com查看镜像设置: npm config get registry 华为镜像地址 npm config set registry https://mirrors.huaweicloud.com/repository/npm/ 腾讯云镜像地…

【机器学习】分值融合方法

举例假设现有图片的预测分数文本的预测分数。为了合理地融合图片和文本的预测分数,可以采取多种方法,包括加权平均、直接相加或相乘等,但需要注意两者是否在同一空间。以下是一些常见的方法和考虑因素: FROM GPT4 1. 确定预测分…

Mysql数据库创建自增序列

创建序列表 CREATE TABLE sequence (name varchar(50) NOT NULL,current_value bigint(30) NOT NULL,increment int(11) NOT NULL DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8 ROW_FORMATDYNAMIC COMMENT序列表;创建函数 查询当前序列名的序列值 CREATE DEFINERroot% FUNC…

Lambda表达式及Stream的使用

前言: 函数式编程是一种编程范式,它将计算过程视为函数应用的连续组合。函数式编程强调使用纯函数(Pure Function),避免使用可变状态和副作用,倡导将计算过程抽象为函数,便于代码的理解、测试和…

Pytorch训练LeNet模型MNIST数据集

如何用torch框架训练深度学习模型(详解) 0. 需要的包 import torch from torch.nn import CrossEntropyLoss from torch.optim import SGD from torch.utils.data import DataLoader from torchvision import datasets, transforms1. 数据加载和导入 …

Python图形界面(GUI)Tkinter笔记(九):用【Button()】功能按钮实现人机交互

在Tkinter库中,功能按钮(Button)是实现人机交互的一个非常重要的组件: 【一】主要可实现功能及意义: (1)响应用户交互: Button组件允许用户通过点击来触发某个事件或动作。当用户点击按钮时,可以执行一个指定的函数或方法。 (2)提供用户输入: Button组件是图形用户界面(G…

持续总结中!2024年面试必问 20 道 Rocket MQ面试题(三)

上一篇地址:持续总结中!2024年面试必问 20 道 Rocket MQ面试题(二)-CSDN博客 五、什么是生产者(Producer)和消费者(Consumer)在RocketMQ中? RocketMQ是一个高性能、高吞…

Linux完整版命令大全(二十五)

pine 功能说明&#xff1a;收发电子邮件&#xff0c;浏览新闻组。语  法&#xff1a;pine [-ahikorz][-attach<附件>][-attach_and_delete<附件>][-attachlist<附件清单>][-c<邮件编号>][-conf][-create_lu<地址薄><排序法>][-f<收件…