【C语言深入】逐汇编详解函数栈帧的创建和销毁过程

news/2024/4/28 14:15:22/文章来源:https://blog.csdn.net/kk702392702/article/details/130024056

【C语言深入】逐汇编详解函数栈帧的创建和销毁过程

  • 一、图解大概过程
  • 二、函数栈帧的创建过程
    • 1、简介一些需要用到的汇编指令和寄存器
    • 2、调用main函数的函数
    • 3、局部变量的初始化
    • 4、形成临时拷贝
    • 5、函数调用
    • 6、形成栈帧
    • 7、提取临时拷贝
    • 8、return返回
  • 三、函数栈帧的销毁过程
    • 1、释放栈帧
    • 2、弹栈
    • 3、回到main函数

一、图解大概过程

一个清晰的流程图解能对我们的理解起到事半功倍的效果,所以我们先不管寄存器和汇编代码,先来大致的理一下函数栈帧的创建和销毁过程。
我所理解的函数栈帧的形成其实是从一个函数被另一个函数调用开始的,我把整个过程分成了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函数也是函数,只要是函数那它们的栈帧的创建和销毁也都是一样的啦。

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

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

相关文章

python:异常处理与文件操作(知识点详解+代码展示)

文章目录一、异常处理1、try...except语句2、finally语句二、断言1、定义2、举例例一&#xff1a;例二&#xff1a;三、文件操作1、写文件操作2、读文件操作&#xff08;当你心情低落时候&#xff0c;记得外面还有美好的风景&#xff01;&#xff09; 学习目标&#xff1a; 1、…

堆相关的面试题

文章目录1. 距离不超过k的推排序2. 最大线段重合问题1. 距离不超过k的推排序 题目&#xff1a;已知一个几乎有序的数组。几乎有序是指&#xff0c;如果把数组排好顺序的话&#xff0c;每个元素移动的距离一定不超过k&#xff0c;并且k相对于数组长度来说是比较小的。 请选择一…

WinRAR压缩解压文件

使用WinRAR压缩管理器压缩解压文件详细步骤如下&#xff1a; ■ 压缩文件 ① 鼠标右键需要压缩的文件&#xff0c;点击“添加到压缩文件”&#xff0c;具体操作步骤如图所示&#xff1a; ② 压缩后的对应文件压缩包会显示在桌面&#xff0c;如图所示&#xff1a; ■ 解压文件 …

如何设计一个高并发系统

目录 如何理解高并发系统 1. 分而治之&#xff0c;横向扩展 2. 微服务拆分&#xff08;系统拆分&#xff09; 3. 分库分表 4. 池化技术 5. 主从分离 6. 使用缓存 7. CDN——加速静态资源访问 8. 消息队列——削锋 9. ElasticSearch 10. 降级熔断 11. 限流 12. 异步…

【OpenLayers】VUE+OpenLayers+ElementUI加载WMS地图服务

【OpenLayers】VUEOpenLayersElementUI加载WMS地图服务准备工作安装vue创建vue项目安装OpenLayers安装ElementUI加载wms地图服务准备工作 需要安装好nodejs&#xff0c;nodejs下载地址&#xff0c;下载对应的版本向导式安装即可。 安装完成后&#xff0c;控制台输入node -v&a…

【CentOS 7安装MySQL 8的教程指南】

CentOS 7安装MySQL 8 添加MySQL官方源 wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm sudo rpm -ivh mysql80-community-release-el7-3.noarch.rpm安装MySQL 8 sudo yum install mysql-community-server安装失败执行下面的命令并再次执行安装…

进程与线程的区别和联系

进程与线程的区别和联系&#x1f50e;进程&#x1f50e;线程&#x1f50e;进程与线程的联系&#x1f50e;进程与线程的区别&#x1f50e;总结&#x1f50e; 结尾&#x1f50e;进程 进程简单来说就是运行着的程序 如果不太理解可以参考一下这篇文章 进程 &#x1f50e;线程 …

【MySQL--01】数据库基础

文章目录1.什么是数据库2.主流数据库2.1 MySQLMySQL架构实例3.基本使用3.1 MySQL的安装3.2 连接服务器3.3服务器管理4.服务器&#xff0c;数据库&#xff0c;表之间的关系5.使用数据库6.SQL分类7.存储引擎查看存储引擎存储引擎对比1.什么是数据库 数据库是用来存储数据的。那么…

Java BigDecimal学习

文章目录Java BigDecimal不损失精度的方法Java BigDecimal的几种舍入模式1、UP(BigDecimal.ROUND_UP)2、DOWN(BigDecimal.ROUND_DOWN)3、CEILING(BigDecimal.ROUND_CEILING)4、FLOOR(BigDecimal.ROUND_FLOOR)5、HALF_UP(BigDecimal.ROUND_HALF_UP)6、HALF_DOWN(BigDecimal.ROUN…

QMake宏定义常量和字符串或带空格的字符串(在代码中使用)

答案 宏定义常量 DEFINES EXPIR_TIME123宏定义字符串(不带空格) DEFINES NIHAO\\\"nihao\\\"宏定义字符串(带空格也适用于不带空格的情况) 推荐 DEFINES NIHAO\"\\\"ni" "hao\\\"\"QMAKE宏定义常量 环境: visual studio 2018 …

Java基础之List

文章目录一、List介绍二、List常用方法 List应知应会2.1 调用add()方法增添数据&#xff08;可指定位置添加&#xff09;2.2 调用remove()方法删除指定位置元素并返回被删除元素2.3 调用set()方法修改指定位置元素并返回初始数据2.4 调用get()方法返回指定位置元素三、List可重…

SQL注入写入文件方法(获取webshell)

数据库写入文件条件 1、当前数据库用户为 root 权限2、知道当前网站的绝对路径3、secure_file_priv 的参数必须为空或目录地址4、PHP的 GPC 为 off状态&#xff1b;(魔术引号&#xff0c;GET&#xff0c;POST&#xff0c;Cookie)用 sqli-labs 测试查看当前用户权限Python sqlma…

本机连接Vmware虚拟机中win7的SQLServer数据库

在开发中&#xff0c;可能遇到不同数据库或不同版本的问题&#xff0c;为了避免在本机安装卸载造成后续无法再次安装的情况&#xff0c;我们在虚拟机中安装需要的版本进行测试。 本篇介绍如何在本机连接到虚拟机中的数据库。 解决流程如下&#xff1a; 一&#xff1a;进入虚…

学Vue3这一篇就够了!

目录学习Vue的前提是掌握 HTML,CSS,Js中级知识vue介绍声明式渲染条件与循环处理用户输入组件化应用构建Vue与自定义元素的关系应用和组件实例Vue实例根组件组件实例 property生命周期钩子实例的生命周期图模板语法插值文本原始 HTMLAttribute使用 JavaScript 表达式指令参数动态…

Linux驱动开发——字符设备

目录 Linux设备分类 字符设备驱动基础 字符设备驱动框架 虚拟串口设备 Linux设备分类 Linux系统根据驱动程序实现的模型框架将设备驱动分为下面三种。 (1)字符设备驱动:设备对数据的处理是按照字节流的形式进行的&#xff0c;可以支持随机访问&#xff0c;也可以不支持随…

抽象类,接口

抽象类&#xff1a;当父类的某些方法&#xff0c;需要声明&#xff0c;但是又不确定如何实现时&#xff0c;可以将其声明为抽象方法&#xff0c;那么这个类就是抽象类。 package com.hspedu.abstract_;public class Abstract01 {public static void main(String[] args) {} } a…

Linux 操作系统原理 — PCIe 总线标准

目录 文章目录目录总线系统PCIe 总线PCIe 总线的传输速率PCIe 总线的架构PCIe 外设PCIe 设备的枚举过程PCIe 设备的编址方式BDF&#xff08;Bus-Device-Function&#xff09;编号BAR&#xff08;Base Address Register&#xff09;地址Linux 上的 PCIe 设备查看 PCIe 设备的 BD…

算法强化--两数之和

hi,大家好,今天为大家带来一道题目,求两数之和 题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一…

Python 进阶指南(编程轻松进阶):三、使用 Black 工具来格式化代码

原文&#xff1a;http://inventwithpython.com/beyond/chapter3.html 代码格式化是将一组规则应用于源代码&#xff0c;从而使得代码风格能够简洁统一。虽然代码格式对解析程序的计算机来说不重要&#xff0c;但代码格式对于可读性是至关重要的&#xff0c;这是维护代码所必需的…

【剑指offer|4.从尾到头打印单链表】

0.从尾到头打印单链表 单链表&#xff1a;一般给的都是无头节点的 另外&#xff1a;在面试中&#xff0c;如果我们打算修改输入的数据&#xff0c;则最好问一下面试官是不是允许修改 下面这种先把链表节点的值按链表序放到数组中&#xff0c;然后来一个算法库中的reverse属实有…