调用一个函数时发生了什么?
欢迎来到 Claffic 的博客 💞💞💞
前言:
用C语言写代码,如果一个工程相对复杂时,我们往往会采取封装函数的方式。在主函数中调用函数 这一看似简单的过程,实际上有很多不宜观察的细节,这篇博客我将带大家深入探究函数调用的每个细节。
注:
内容偏向底层原理,可能会比较复杂,但我相信看完后你会对函数调用有一个更加深刻的认识。
目录
💖Part1: 相关问题及概念铺垫
1.几个相关问题
2.寄存器
3.函数栈帧
4.函数调用栈
5.相关汇编指令
💗Part2: 函数栈帧的创建销毁具体过程
1.前期准备
2. main 函数预开辟栈帧
3.实参的创建和初始化
4.Add函数的调用
5.栈帧的销毁
❤️Part3: 问题答案揭晓
Part1: 相关问题及概念铺垫
1.几个相关问题
• 局部变量是怎么创建的?
• 为何局部变量出现屯屯烫烫等随机值?
• 函数是怎么传参的?传参的顺序?
• 实参和形参有何关系?
• 函数调用的过程?
• 函数调用结束,怎么返回?
如果没有进行函数栈帧的学习,我相信你也会像我当初一样懵逼🤣
好在接下来我会带大家逐步分析每一个过程,了解完整个过程后就会豁然开朗~
2.寄存器
寄存器是 CPU 内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。
常见的寄存器有:
eax: 累加(Accumulator)寄存器 , 常用于乘、除法和函数返回值
ebx: 基址(Base)寄存器 , 常做内存数据的指针, 或者说常以它为基址来访问内存
ecx: 计数器(Counter)寄存器 , 常做字符串和循环操作中的计数器
edx: 数据(Data)寄存器 , 常用于乘、除法和 I/O 指针
sbp: 基址指针(Base Point)寄存器 , 只做堆栈指针, 可以访问堆栈内任意地址, 经常用于中转 esp 中的数据
esp: 堆栈指针(Stack Point)寄存器 , 只做堆栈的栈顶指针; 不能用于算术运算与数据传送
有关函数栈帧的是 ebp , esp 这两个寄存器,其中存放的是地址,
这两个寄存器是用来 维护函数栈帧 的。
3.函数栈帧
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
每一个函数调用,都要在 栈区 开辟一段空间。
例如,我写下这一段代码:
#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 函数中调用了 Add 函数。
如图所示,在栈区为 main 函数开辟了一段空间,并且由 ebp 和 esp 这两个寄存器维护。
4.函数调用栈
函数调用栈是一种容器,具有后进先出的特性。在函数调用过程中,我们利用了栈的特性,当调用一个新的函数时,进行压栈Push,这个函数执行完进行出栈Pop。
简单来说,当有函数被调用时,该函数就被添加到栈中,在执行完所有任务后,该栈帧就会被删除。
这时就要问了:main 函数也是函数,难道还有其他函数调用它吗?
是的,main 函数也是其他函数调用的,不过这在 Visual Studio 2013 中有体现。
下面我以 VS2013 演示:
调试 --> 窗口 --> 调用堆栈
此时可以看到 main 函数被调用了:
按 F10 继续调试,直到程序结束:
此时看到了两个陌生的函数:
__tmainCRTStartup 和 mainCRTStartup
通过对 crtexe.c 文件的观察,我们可以得出下列结论:
对应栈帧的开辟:
5.相关汇编指令
我们是在反汇编的模式下观察函数栈帧的动作的,因此需要一些汇编指令:
push:数据压入栈
pop:数据弹出栈mov:数据转移
add:加法命令
sub:减法命令
call:函数调用
jump:转到目标函数,进行调用
ret:恢复返回地址
进行了相关知识的铺垫,
那么接下来就是对具体动作的探究了:
Part2: 函数栈帧的创建销毁具体过程
1.前期准备
F10 调试 --> 鼠标右键 --> 转到汇编
在反汇编下可以清楚地观察函数栈帧的动作
2. main 函数预开辟栈帧
由于 main 函数是由其他函数调用的,所以在调用 main 函数之前就已经开辟好了相关函数的栈帧
00C21410 push ebp //将ebp压入
00C21411 mov ebp,esp //移动esp,让其指向压入的ebp;移动ebp,让其也指向压入的ebp
00C21413 sub esp, 0E4h //esp减去0E4h,指向位置更低的空间,相当于为main函数预开辟空间
执行完三步后的图示
//依次将ebx,esi,edi压入栈帧
00C21419 push ebx
00C2141A push esi
00C2141B push edi//从edi开始,将接下来39h个双字节都改为 OCCCCCCCCh(eax中的内容)
00C2141C lea edi, [ebp+FFFFFF1Ch]
00C21422 mov ecx, 39h
00C21427 mov eax, OCCCCCCCCh
00C2142C rep stos dword ptr es:[edi]
在 main 函数预开辟之后,接下来就要执行有效的代码了:
3.实参的创建和初始化
我们继续:
int a = 10;
//将0A(十进制下是 10)放在 ebp-8 的位置上
00C2142E C7 45 F8 0A 00 00 00 mov dword ptr [ebp-8], 0Ah
int b = 20;
//将14(十进制下是 20)放在 ebp-14 的位置上
00C21435 C7 45 EC 14 00 00 00 mov dword ptr [ebp-14h], 14h
int c = 0;
//将0(十进制下是 0)放在 qbe-20 的位置上
00C2143C C7 45 E0 00 00 00 00 mov dword ptr [qbe 20], 0
执行实参的创建和初始化
4.Add函数的调用
C = Add(a, b);
//创建形参并传值
00C21443 8B 45 EC mov eax, dword ptr [ebp-14h]
00C21446 50 push eax
00C21447 8B 4D F8 mov ecx, dword ptr [ebp-8]
00C2144A 51 push ecx
//调用函数,记录call下一次指令的地址,方便返回
00C2144B E8 91 FC FF FF call 00C210E1
00C21450 83 C4 08 add esp,8
00C21453 89 45 E0 mov dword ptr [ebp- 20h], eax
此时才真正进入Add:
欸?是不是与之前 main 函数的调用有些相似?
对的,还是先压几个寄存器,再填充CCC...
接下来的就是把事先传过来的形参进行运算:
调用了数值之后将要返回的结果放入Add函数的栈帧中。
5.栈帧的销毁
//将 edi,esi,ebx 弹出
00C213F1 5F pop edi
00C213F2 5E pop esi
00C213F3 5B pop ebx
//移动 esp,ebp,找到高地址的寄存器
00C213F4 8B E5 mov esp,ebp
00C213F6 5D pop ebp
//返回值
00C213F7 C3 ret
最终就把Add函数的栈帧销毁了。
Part3: 问题答案揭晓
回到开头的几个问题,在这里做一下回答:
• 局部变量是怎么创建的?
先创建函数的栈帧,在函数栈帧里为局部变量分配空间。
• 为何局部变量出现屯屯烫烫等随机值?
在创建函数栈帧时会事先填充CCC...,打印出来就是 屯屯烫烫等随机值了,所以要养成局部变量初始化的习惯。
• 函数是怎么传参的?
在调用函数之前就把参数压栈了,当函数中使用参数时,再通过指针偏移量找到事先压好的参数
• 实参和形参有何关系?
形参是实参的临时拷贝,两者的空间独立,形参的改变不会改变实参。
• 函数调用的过程?
压栈,创建空间...
• 函数调用结束,怎么返回?
call 事先记录了下一条指令的地址,可以找到此位置,再通过寄存器带回。
总结:
带大家探究了调用函数时的细节,重点是函数栈帧的创建和销毁。
码文不易
如果你觉得这篇文章还不错并且对你有帮助,不妨支持一波哦 💗💗💗
相关文章:

调用一个函数时发生了什么?
欢迎来到 Claffic 的博客 💞💞💞 前言: 用C语言写代码,如果一个工程相对复杂时,我们往往会采取封装函数的方式。在主函数中调用函数 这一看似简单的过程,实际上有很多不宜观察的细节࿰…...

MindAR的网页端WebAR图片识别功能的图片目标编译器中文离线版本功能(含源码)
前言 之前制作了基于MindAR实现的网页端WebAR图片识别叠加动作模型追踪功能的demo,使用了在线的图像目标编译器对识别图进行了编译,并实现了自制的WebAR效果,大致效果如下: 但是在线的编译器在操作中也不是很方便,我…...

测试经理:“你做了三年测试,连服务端的接口测试都不会?”
服务端的接口测试我们一般从功能开始进行测试,比如请求参数和响应参数的校验,业务逻辑或业务规则的校验,数据库操作的校验。 功能正常后会根据需要进行安全相关的检查、性能测试以及系列扩展测试,比如与历史版本的兼容性测试、接…...
4G AFR到5G应用场景介绍
前面文章介绍过AFR的机制及流程 AFR机制及流程介绍 (qq.com) GSM AFR到LTE流程...
正电源子 IMX6ULL 自学笔记(驱动开发)
一、字符设备驱动开发 1.1 字符设备驱动简介 字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都…...

AM5728(AM5708)开发实战之移植OpenCV-3.4.11
一 概述 OpenCV是一个开源的跨平台计算机视觉库,可以运行在Linux、Windows、Mac OS等操作系统上,它为图像处理、模式识别、三维重建、物体跟踪、机器学习提供了丰富的算法。 由于OpenCV依赖包特别多,尽量不要使用交叉编译,即在什…...

Notepad++ 下载与安装教程
文章目录Notepad 下载与安装教程Notepad 简介一,Notepad 下载二,Notepad 安装Notepad 下载与安装教程 Notepad 简介 Notepad是程序员必备的文本编辑器,Notepad中文版小巧高效,支持27种编程语言,通吃C,C ,Java ,C#, XM…...

005+limou+HTML——(5)HTML图片和HTML超链接
1、图片标签<img> (1)图片标签属性 [src]:用于指定这个图片所在的路径,常使用相对路径,比较少使用绝对路劲。如果图片路径有错误的话,就会发生图片显示错误[alt]:用于指定图片的提示文字…...
ES6 Generator
Generator Generator是es6引入的,主要用于异步编程。 最大特点是可以交出函数的执行权(即暂停执行)。 它和普通的函数写法有点不同 function关键字与函数名之间有一个*号,以与普通函数进行区别。 它不同于普通函数,是可以暂停执行的。 Gen…...

SCI期刊写作必备(二):代码|手把手绘制目标检测领域YOLO论文常见的性能对比折线图,一键生成YOLOv7等主流论文同款图表,包含多种不同功能风格对比图表
绘制一个原创属于自己的YOLO模型性能对比图表 具体绘制操作参考:(附Python代码,直接一键生成,精度对比图表代码 ) 只需要改动为自己的mAP、Params、FPS、GFlops等数值即可,一键生成 多种图表风格📈,可以按需挑选 文章目录 绘制一个原创属于自己的YOLO模型性能对比图…...
linux cpu飙高排查
linux定位cpu飙高原因 jpstop 定位应用进程 pidtop -Hp {pid}找到线程 tid将 tid 转换成十六进制 printf “%x\n” {tid}jstack 打印堆栈信息过滤出我们想要的 jpstop 定位应用进程 pid jps或ps -ef | grep java查看java进程id jps结果: 57152 abc.jar 83383 e…...
2023实习面试公司【二】
2023实习面试第二家公司 文章目录2023实习面试第二家公司前言一、面试官所问的问题?二、总结1.公司待遇2.推荐指数3.自己的感受前言 某岸科技,这家公司是我从拉钩上找的第二家面试公司,也是北京本地的一家公司。 提示:以下是本篇…...
C++ thread_local 存储类
目录标题概述实现场景总结概述 thread_local指示对象拥有线程存储期。也就是对象的存储在线程开始时分配,而在线程结束时解分配。每个线程拥有其自身的对象实例。唯有声明为 thread_local 的对象拥有此存储期。 thread_local 能与 static 或 extern 结合一同出现&am…...
冥想第七百二十三天
1.周日早上跑了5公里,很舒服精力满满的,感谢老婆给我做的饭,鱿鱼面筋腐竹。都非常的好吃。 2.下午13:19分送我到了地铁口,这个点卡的真好,以至于离高铁开车只剩5分钟,14:41发车。到上…...

zookeeper 集群配置
文章目录zookeeper 集群配置1、集群安装zookeeper 集群配置 1、集群安装 1) 集群安装 在 hadoop102、hadoop103 和 hadoop104 三个节点上都部署 Zookeeper。 2) 解压安装 在 hadoop102 解压 Zookeeper 安装包到/opt目录下 输入命令:tar -zxvf apache-zookeeper-3.…...

怎么用消息队列实现分布式事务?
当消息队列和事务联系在一起时,它指的是消息生产者和消息消费者之间如何保持数据一致性。 什么是分布式事务? 事务是指当我们进行若干项数据更新操作时,为了保证数据的完整性和一致性,我们希望这些更新操作要么都成功࿰…...

什么蓝牙耳机佩戴舒适?2023长时间佩戴最舒适的蓝牙耳机
现如今,很多蓝牙耳机的产品都在不断地更新,市面上的耳机也是越来越普及,可以说是成为我们日常生活中不可或缺的一类电子设备,下面介绍一些佩戴舒适性好的蓝牙耳机。 一、南卡小音舱蓝牙耳机 音质推荐指数:★★★★★…...
刮刮乐--课后程序(Python程序开发案例教程-黑马程序员编著-第4章-课后作业)
实例1:刮刮乐 刮刮乐的玩法多种多样,彩民只要刮去刮刮乐上的银色油墨即可查看是否中奖。每张刮刮乐都有多个兑奖区,每个兑奖区对应着不同的获奖信息,包括“一等奖”、“二等奖”、“三等奖”和“谢谢惠顾”。假设现在有一张刮刮乐…...
LeetCode 全题解笔记:两数相加(02)
两数相加(medium) 题目描述 给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储 一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数…...

网络工程师面试题(面试必看)(1)
作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭:低头赶路,敬事如仪 个人主页:网络豆的主页 目录 前言 一.正题 1.TCP UDP协议的区别...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...

AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...

STM32---外部32.768K晶振(LSE)无法起振问题
晶振是否起振主要就检查两个1、晶振与MCU是否兼容;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容(CL)与匹配电容(CL1、CL2)的关系 2. 如何选择 CL1 和 CL…...