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

news2024/11/19 19:26:34

目录

1.函数栈帧的含义

概念 

要用到的汇编语言的知识

示例

2.理解栈帧

2.1 main函数栈帧的创建

2.2 局部变量的创建

2.3 函数传参

2.4 调用函数

2.5 函数返回 


        一个.c文件在调用函数的时候(包括main 函数),其内存中的栈区有什么变化?要压栈、出栈哪些寄存器呢?函数的参数是如何进行传递的呢?函数调用结束之后栈区又是如何变化的呢?本文通过使用汇编语言,对这些内容进行了较为详细的剖析。

1.函数栈帧的含义

概念 

        首先,栈的概念想必不需要过多解释,那么什么是栈帧?引用百度百科:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从这句话中,可以提炼以下几点信息:

· 栈帧是一块因函数运行而临时开辟的空间。
· 每调用一次函数便会创建一个独立栈帧。
· 栈帧中存放的是函数中的必要信息,如局部变量、函数传参、返回值等。
· 当函数运行完毕栈帧将会销毁。

        我们知道,C语言的内存区分成了 静态区、栈区、堆区,函数栈帧无疑是在栈区创建和销毁的,所以其要符合栈“后进先出”的特点。

要用到的汇编语言的知识

        在这里使用汇编语言方面知识的原因是:通过它,我们可以深入底层了解一个程序是如何运行的,在何时——什么东西压栈,什么东西出栈,寄存器(汇编语言中一些用来暂时存储数据的东西)如何变化等等。这些都是C语言无法直观体现的,我们可以通过Visual Stdio 的在调试时的反汇编功能,将C语言代码转换成汇编语言代码,以便更好地观察。(另,C语言也是汇编语言编写的。)所以,简单地说,本文主要是在分析汇编语言的执行过程。

        我们首先要了解几个汇编语言方面的东西,其中ESP和EBP时专门维护函数栈帧的,分别指向栈顶和栈底:

寄存器 用途
EAX累加寄存器:用于乘除法、函数返回值
EBX用于存放内存数据指针
ECX计数器
EDX用于乘除法、IO指针
ESP存放栈顶指针(其值是地址)
EBP存放栈底指针(其值是地址)

汇编指令用途
movmov A,B 将数据B移动到A
push压栈
pop出栈
call函数调用
add加法
sub减法
rep重复
lea加载有效地址

示例

        比如,我们写下一个如下的C语言程序,非常容易,只有main() 函数和一个 Add() 函数,主函数里面调用了 Add() 。

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	return 0;
}

        那么,一开始调用主函数的时候,主函数的函数栈帧就压栈;然后在主函数里面,调用了Add() 函数,此时Add() 的函数栈帧也要压栈。那么现在面临一个问题,是维护Add() 函数,还是维护main() 函数,亦或两者都维护?
        其实,从平时使用Visual Stdio 调试的时候就可以看出来,当主函数内部调用一个函数A,按F11分步调试,会进入函数A的内部,函数A调用结束,会返回主函数。同理,实际上从进入函数A,一直到A 函数调用结束,这个过程都在维护函数A。所以,在main() 函数内部调用 Add() 函数之后,会出现如下图所示的情况,ebp和esp来维护Add() 函数的栈帧当Add() 函数调用结束,它的函数栈帧自然就会出栈,此时ebp、esp又会返回维护main() 函数的栈帧

2.理解栈帧

2.1 main函数栈帧的创建

        实际上,main() 函数也是由其他函数调用的,其调用链条如下:

        创建main函数的函数栈帧代码如下,汇编语言的注释是用 ; 所以这里 ; 后面的内容是注释,帮助理解代码。

006117A0  push        ebp                 ; ebp 压栈
006117A1  mov         ebp,esp             ; 将 esp里面的值 赋给ebp
006117A3  sub         esp,0E4h            ; esp减去0E4h(十六进制),得到的结果赋给esp
006117A9  push        ebx                 ; ebx 压栈
006117AA  push        esi                 ; esi 压栈
006117AB  push        edi                 ; edi 压栈
006117AC  mov         edi,[ebp-24h]       ; 将ebp往上数,第24h(16进制)个字节开始,向下4个字节的值赋给edi
006117AF  mov         ecx,9               ; 这里到结束的意思是:
006117B4  mov         eax,0CCCCCCCCh      ; 将0CCCCCCCCh 赋值给某块空间,这块空间从附加段中 edi 指向的位置开始
006117B9  rep stos    dword ptr es:[edi]  ; 一共执行九次(0CCCCCCCCh 是四个字节的内容,每次操作四个字节,所以一共操作了36字节)

第一行 

        我们来开始逐句剖析上方代码,首先执行第一行(图中红色圆圈圈出来的黄色箭头,表示已经执行完其上一行,按F10调试就执行当前行),由于是压栈操作,所以esp的值会有所变化,如下图右边监视窗口,esp的值(十六进制显示的)相较之前改变了,所以变成红色:

        如下,ebp压栈,同时esp上移:

第二行

         该行是将esp的值赋给ebp,效果也如下图,右边监视窗口的红色部分所示。

        如下,将esp的值赋给ebp之后,ebp和esp指向同一块地方:
 

第三行

        该行是将esp的值减去0E4h(十六进制),得到的结果赋给esp,如下图。

         如图所示,由于图中从下往上是地址高处到地址低处,所以esp值变小,实际上图中是上移。并且,现在ebp和esp维护的空间,就是main函数的函数栈帧:

第四行

        压栈,压入ebx,改变栈顶指针esp的值。

        如下,压栈,esp上移:

第五行

        压入esi,改变esp的值。

        如下,和上一步类似:
 

第六行

        压入edi,改变esp的值。 

        和上一步也类似:

第七行

        将[ebp-24h] 表示的地址赋值给edi。

        这里就是把 edi 里面的值改变,从函数栈帧看不出什么,看上面的监视图就可以直到确实是改变了。

最后三行

        如之前代码里的注释所说。

        如下,两个箭头指示的值是一样的,其代表的是edi所表示的地址,从该地址开始,往后9个dw(double word 双字,一个双字等于四个字节)的内容,都赋值为cccccccc (十六进制)。

        这三行代码效果如下:

        整个过程可以用一张动图生动形象地展示:

2.2 局部变量的创建

        接下来,我们在汇编代码中鼠标右击,然后将下图红色箭头所指示的"显示符号名" 的勾去掉。

        发生改变的是下图中红色圆圈圈出来的,可以看出,原本所有的变量名,都变成了寄存器减去某个十六进制数字。他们实际上是等价的,即变量的地址就等于替换后的地址

        接下来分析局部变量创建过程。
        首先,创建变量a,代码如下,其含义就是,将0Ah 这个十六进制数字,从ebp的地址低八位处开始放,占四个字节

002C17C5  mov         dword ptr [ebp-8],0Ah  

        如下图,可以通过两个红色箭头看到,右边监视的ebp的值就是左边 地址处的箭头指向的地址,说明这就是ebp的地址,然后减去八位,再根据栈从下往上使用以及Visual Stdio小端存储的特点,就成了内存区里面红色方框框出来的内容。(注意,比如 cc cc cc cc cc占据的是一个字节的空间,四个cc 就占据四个字节,而汇编语言中,地址-1,只跳过一个c,所以ebp-8是跳过8个c,即四个字节)

        如下,图中一个小格子代表四个字节,不难看出,变量a存储的位置,在栈底指针往上跳过四个字节的地方。

        然后创建局部变量b,通过内存图可以看出,变量b和变量a是间隔八个字节的:

        如下图:

2.3 函数传参

        代码如下:

002C17D3  mov         eax,dword ptr [ebp-14h]   ; 将变量b的值赋给eax
002C17D6  push        eax                       ; eax压栈
002C17D7  mov         ecx,dword ptr [ebp-8]     ; 将变量a的值赋给ecx
002C17DA  push        ecx                       ; ecx压栈

        执行前两行代码,确实将变量b的值赋给了eax,然后eax引起的压栈导致了esp改变:

        如下图,不要忘了eax里面的值和变量b是一样的哦:

        执行后两行代码:

        其效果和前两行类似,同时ecx里面存的是变量a的值:

2.4 调用函数

        上面的内容执行完之后,要执行如下语句,其意思是,执行 002C10B4 地址处的内容:

002C17DB  call        002C10B4  

        然后我们将其滑倒该地,发现是这样的,意思是跳到002C1740地址处:

         又找到该地址,发现如下,所以,通过这两步调用Add() 函数,如下红色部分,和创建main函数的函数栈帧类似,实际上就是创建了Add() 的函数栈帧

        效果如下,建立了Add函数的函数栈帧:

        红色个方框后面两行代码不是很重要,是用来检查bug的,如下代码和图片:

002C1757  mov         ecx,2CC003h  
002C175C  call        002C130C  

 

2.5 函数返回 

        函数返回

002C1761  mov         eax,dword ptr [ebp+8]   
002C1764  add         eax,dword ptr [ebp+0Ch]  

        第一行代码: 将ebp+8 地址处的数据放到eax 。

        第二行代码:将ebp+0Ch 地址处的数据和eax相加,结果存到eax里面。

        如下图中,由于图片从下往上是地址从高到低,所以图片中ebp+8是在ebp下方。实际上就是ecx和eax的值相加,然后存到eax里面。eax里面存储变量b的值,ecx里面存储变量a的值,最后eax的值就是变量a、b之和。并且eax是不会随着Add() 函数的函数栈帧销毁而改变值。

        通过监视也可以看出,eax的值变成0x0000001e,转换成十进制就是30。
 
        此时已经拿到返回值,存储在eax里面,还要执行以下几行代码:

00AA13F1  pop         edi  
00AA13F2  pop         esi  
00AA13F3  pop         ebx  
00AA13F4  mov         esp,ebp  
00AA13F6  pop         ebp  
00AA13F7  ret  

         就是出栈、赋值等等,结果如下,回到了调用Add() 函数之前的状态:

        然后执行main() 函数后续代码代码,如下图红色框出:

        第一行:esp加8,即esp在途中向下移动四个字节。

        第二行,将eax的值赋给ebp-20h 地址处。

        执行完之后,调试图如下,通过对比两个红色方框的内容,左边红色方框的地址,和右边&c 的值一样,说明那就是变量c 存储的地方,其值也是变量c 的值:

        示意图如下:

        通过对函数栈帧创建、销毁过程的剖析使我们不仅了解计算机做了什么,还了解了它是如何做的。通过函数栈帧尝试解析递归等问题相信也会更加直观。由于本人水平有限,不足之处还请大家多多指教。

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

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

相关文章

Qt之实现工具箱界面程序

最近终于有点空闲时间了&#xff0c;就写写博客&#xff0c;就把上次给客户实现的一个程序开发过程写出来&#xff1b;客户要求的是在主界面上能有几个很好看的按钮&#xff0c;单击各个按钮能弹出不同的应用窗口&#xff0c;如游戏窗口&#xff0c;显示图像窗口等等&#xff0…

pcl 基本操作汇总

目录 PCLVisualizer简单的点云可视化 createViewPort创建视窗 代码 效果 点云视窗上打印文本信息 使用addText 合并多个点云 xyzxyz xyz nxnynz 新建自己的Point类型 点云的刚体变换&#xff08;旋转平移&#xff09; 以下是pcl点云基本操作&#xff0c;后面会慢慢…

C++--数据结构--最小生成树-- Kruskal--Prim--高阶0713

注&#xff1a;本次修改了添加边的一些其他情况可以采用坐标来添加边 void _AddEdge(size_t srci, size_t dsti, const W& w) {_matrix[srci][dsti] w;// 无向图if (Direction false){_matrix[dsti][srci] w;} }void AddEdge(const V& src, const V& dst, const…

【学习笔记03】vue的组件

目录一、组件二、组件的分类&#xff08;一&#xff09;全局组件&#xff08;二&#xff09;局部组件1、为什么vue组件 data函数返回一个对象2、bootstrap的使用三、父组件传值给子组件1、父传子实现进度条2、 props的属性四、子组件传值给父组件五、兄弟组件传值一、组件 可以…

【Javassist】快速入门系列04 使用Javassist更改整个方法体

系列文章目录 01 在方法体的开头或结尾插入代码 02 使用Javassist实现方法执行时间统计 03 使用Javassist实现方法异常处理 04 使用Javassist更改整个方法体 文章目录系列文章目录前言引入Javassist jar包使用Javassist更改整个方法体总结说明前言 上一章我们介绍了使用Javas…

2022全年度白酒十大热门品牌销量榜单

白酒为中国特有的一种蒸馏酒&#xff0c;是世界六大蒸馏酒之一&#xff0c;中国是全球最大的蒸馏酒市场&#xff0c;中国的白酒消费也位列世界烈酒行业领先地位。近几年来&#xff0c;由于市场需求的不断提升及居民的消费升级&#xff0c;高档白酒价格也不断增长&#xff0c;从…

会员管理系统可行性研究

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 1.引言 1.1编写目的 1.2项目背景 1.3定义 1.4参考资料 2.可行性研究前提 2.1要求 2.2目标 2.3条件、假定和限制 2.4决定可行性的因素 3.现有小程序分析 3.1经…

进程-计算机是如何工作的

文章目录冯诺依曼计算机体系组成寄存器和内存编译型语言vs解释型语言进程进程管理进程的状态虚拟地址空间虚拟内存冯诺依曼计算机体系 组成 (1) 输入设备:键盘&#xff0c;鼠标 (2) 输出设备:显示器&#xff0c;打印机 其中硬盘(可做输入、输出) (3) 存储器:内存 (4) CPU 运…

Composing Programs(SICP python版) chap1 笔记

《Composing Programs》(SICP python版) chap1 笔记 持续更新中 在学习 CS61A 2022fall的时候配着看的 文章目录《Composing Programs》(SICP python版) chap1 笔记Chapter 1: Building Abstractions with Functions1.1 Getting Started1.1.1 Programming in Python1.1.2 Insta…

python安装face_recognition

本人使用系统为windows10,python的版本是3.8&#xff0c;在安装face_recognition之前需要安装以下内容&#xff1a; 1.cmake 2.dlib&#xff0c;dlib的安装依赖于cmake 1 安装CMake 1.1 官网下载&#xff1a;CMake 1.2 开始安装CMake: 1.3 验证是否安装成功&#xff1a; 打开…

10 Mysql中各种锁

概述 MySQL中的也存在一些类型的锁&#xff0c;用来保证多个连接同时操作数据时的安全即数据的一致性问题&#xff1b;同时&#xff0c;虽然锁能够解决一些数据的一致性和有效性&#xff0c;但是我们还是要选择合适的锁来降低锁对于并发问题的影响 1. 全局锁 全局锁就是对整…

傻白探索Chiplet,互连技术研究现状(七)

目录 一、串行互连 二、并行互连 三、串行与并行互连的比较 四、互连标准接口 &#xff08;1&#xff09;背景 &#xff08;2&#xff09;UCIe Chiplet的可行性常常受到片间互连的性能、可用性以及功耗和成本问题的限制&#xff0c;各种异构芯片的互连接口和标准的设计在技…

Web3中文|恐惧vs伦理:AI艺术评论家错在哪里?

本周&#xff0c;人工智能引发众怒。随着“AI艺术”在网络的流行&#xff0c;一群艺术家正在知名艺术家平台Art Station上掀起一场反AI艺术的抗议活动&#xff0c;而人工智能技术的拥趸者也及时回击了这波反对热潮。 这种充斥着反对意见的热潮是迟早会出现的。现在这些人认为“…

简单说手什么是JWT?

JSON Web Token&#xff08;缩写 JWT&#xff09;是目前最流行的跨域认证解决方案。 传统的session认证 http协议本身是一种无状态的协议&#xff0c;而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证&#xff0c;那么下一次请求时&#xff0c;用户还要再一…

【Lingo】【MATLAB】【求解运筹学问题模板题】

文章目录一、线性规划模型&#xff08;Lingo&#xff09;1.线性规划问题&#xff08;模板&#xff09;2.求解最优化问题3.包装箱平板车问题4.职员时序安排问题5.运输问题6.排菜单问题7.工地施工问题8.生产计划优化研究&#xff08;柴油机生产&#xff09;二、线性规划问题&…

机器学习算法基础——逻辑回归

01逻辑回归可以用来解决简单的二分类问题。 逻辑回归的预测函数为hθ(x)g(θTx)h_\theta (x)g(\theta^Tx)hθ​(x)g(θTx)&#xff0c;其中g(x)g(x)g(x)为sigmoidsigmoidsigmoid函数&#xff0c;用于将数值映射到区间[0,1][0,1][0,1]中&#xff0c;然后再取对数值用于刻画损失函…

51单片机实训day3——点亮LED灯、闪烁LED灯(一)理论

内 容&#xff1a;编写代码实现LED灯的点亮功能 学 时&#xff1a;2学时 知识点&#xff1a;分析原理图、LED灯控制原理 重点&#xff1a;GPIO参数配置、LED原理图分析 难点&#xff1a;编写 GPIO参数配置函数、LED点亮函数 时间&#xff1a;2022年12月21日 9:00&#xff5e;…

实验1 数据库定义与操作语言实验

前言&#xff1a;实验本身并不是很难&#xff0c;照着实验指导书抄就行&#xff0c;不过注意有些sql语句和mysql语句是不相同的&#xff0c;需要进行一定的修改 数据集链接 实验1 数据库定义与操作语言实验 实验1.1 数据库定义实验 1.实验目的 理解和掌握数据库DDL语言&am…

指挥中心显示大屏类型简介

因工作需要&#xff0c;现在需要不断补充指挥中心建设过程中各种设备知识&#xff0c;怕被别人忽悠了也不知道&#xff0c;抓紧学习了解。今天学习大屏部分&#xff0c;目前来说&#xff0c;常见的显示大屏主要分为DLP拼接屏、LCD拼接屏和LED小间距大屏几种类型。 1、DLP大屏 …

【maven工程的pom.xml文件内部结构详解+maven工程的多层次依赖管理】

目录pom文件内部【结构详解】pom文件内部【依赖管理】1、依赖传递&#xff1a;2、依赖传递过程中&#xff0c;版本冲突&#xff1a;3、依赖传递过程中&#xff0c;对外隐藏主动断开&#xff1a;pom文件内部【依赖的作用范围】pom文件内部【结构详解】 <?xml version"…