C语言函数调用的过程图解深入剖析

news2024/12/25 9:33:46

希望是美好的,也许是人间至善,而美好的事物永不消逝。——《肖申克的救赎》


目录

1、什么是函数栈帧?

2、理解函数栈帧能解决什么问题

3、函数栈帧是什么

3.1什么是栈?

3.2认识寄存器和汇编指令

4、函数调用的整个过程

5、main函数的栈帧形成过程

6、自定义函数栈帧形成过程

7、自定义函数返回到主函数


前言:

大家好,我是拳击哥。我们在编写程序时,会自定义一些函数,我们会用它来进行一些功能实现,那它在内存中的样子是什么呢,实参是如何传参给形参的呢?实际上自定义的函数是在栈区创建的一片空间,我们通过一些汇编指令来实现传参回参。今天我给大家带来的博文内容是什么是函数的栈帧,理解函数的栈帧能解决什么问题。并且理解函数栈帧的创建与销毁的过程。下面我就来详细解析。


1、什么是函数栈帧?

我们在编写程序的时候会自定义函数,自定义函数在调用时。会在内存中开辟一道空间那么这个空间就是该函数的栈帧(stack frame)。那么这些空间里面存放函数参数以及函数返回值,临时变量,保存上下文信息。我们来看一个图大致理解一下:


2、理解函数栈帧能解决什么问题

我们在理解函数栈帧后,以下几个问题就理解了:

  • 局部变量的创建过程
  • 为什么局部变量不初始化内容就是随机值
  • 函数调用的时候是如何传参的,顺序是怎样的
  • 函数的形参和实参是怎样实例化的
  • 函数的返回值能带回什么

3、函数栈帧是什么

3.1什么是栈?

栈(stack)是计算语言中最重要的概念之一,我们运行的每一个程序都是用了栈。如果没有栈就没有函数、局部变量、所有的C语言。

栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的书,先叠上去的书在最下面,因此要最后才能取出。

push:压栈,pop:出栈:

那么如果一块函数的栈帧经过push指令后 ,这块函数的栈帧会延申值最后一个push的指令上方。


3.2认识寄存器和汇编指令

1、寄存器

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址

其中ebp,esp是最重要的,这两个寄存器中存放是地址,它们俩是用来维护函数栈帧的。ebp是指向栈底的,esp是指向栈顶的。它们比较重要,如下图所示:


2、汇编指令

  • mov:数据转移指令
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • sub:减法命令
  • add:加法命令
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令

在认识相关的寄存器和汇编指令后,我们来讲解栈帧的创建与销毁


4、函数调用的整个过程

我们来看一组程序:

#include<stdio.h>

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

 输出结果:15

我们都知道,这个程序是自定义了一个Add函数来求a与b的和。那么这个程序在内存中运行的过程是怎样呢?接下来容我步步讲解: 

转入反汇编

注意今天我们是在VS2019环境下来演示,每个编译器。显示的汇编语言有些许差异,但实质上的逻辑的差不多。函数调用的过程和返回的过程都相同,因此不必纠结编译器。

 打开反汇编后,我们就看到了一些汇编指令push、mov、sub、lea等等。

注意以下所有的过程都是拿此程序来讲解。


5、main函数的栈帧形成过程

首先我们要知道main函数也是被调用的,是被_tmainCRTStartup调用的。那么main函数形成的过程,就是通过上述汇编指令一步步完成。


1、第一条指令

首先我们来看第一个条指令push:edp和第二条指令mov:ebp,esp。

push:ebp就是在_tmainCRTStartup的栈顶上压一块空间这个存的就是ebp。注意每次压栈过后,栈底指针会指向最上方。

mov:ebp,esp就是使esp的值给ebp,因此ebp和esp指向的地方是同一方向。


2、第三条指令

我们再来看第三条指令sub:esp,0E4h。

sub:esp,0E4h就是让esp减去0E4h,因此 esp指向到了内存中的上一块区域。此时esp和ebp分别指向了新的空间,而这块新的空间就是main函数的栈帧。


3、第四到第六条指令 

第四到六条指令push:ebx,push:esi,push:edi。

这三个push分别在main函数栈帧上方压三条指令,压栈完后,此时的栈顶指针也是指向了最高层。


4、第七至第十条指令 

第七条至第十条指令,lea:edi,[ebp-24h] ~rep stos    dword ptr es:[edi] 

lea的是意思是load effecitve address(加载有效地址),lea:edi,[ebp-24h]意思是把ebp到ebp后24h个位置里面的值都加载成CCCCCCCC。所以不初始化变量输出的是随机值,是因为CCCCCCCC里面存放都是一些类似于烫烫烫烫这样的乱码。

 此时main函数栈帧的开辟已经完成了。里面的CCCCCCCC都是可以为main函数所用的空间


5、mov创建局部变量a

mov:dword ptr [ebp-8],0Ah,意思是把0Ah也就是10放到ebp-8的位置。此时局部变量a=10已在main函数栈帧里面创建成功,所在的位置是ebp-8。dword是双倍word,word占两个字节。因此dword占四个字节。

 这就是局部变量a=10创建的过程


6、mov创建局部变量b

mov:dword ptr [ebp-14h],5。意思是把5放到 edp-14h(20d)的位置。此时局部变量b=5已经在main函数栈帧创建成功,所在位置为edp-14h。

 这就是局部变量b=5的创建过程。


7、mov创建局部变量c

mov:dword ptr[edp-20h]把0赋值给edp-20h的位置。

这就是变量c的创建过程


6、自定义函数栈帧形成过程

函数传参的过程,5中我们通过汇编指令了解到了局部变量在内存里面的创建步骤。下面我们就来看函数调用过程。首先我们得进入Add函数,会出现以下界面:


1、局部变量压栈1

mov:eax,dword ptr [ebp-14h]意思是把ebp-14h里面的值5存到寄存器eax里面

push:eax意思是把eax压在最顶部,也就是b的值5放在了最顶部。

这就是形参b的创建过程


2、局部变量压栈2

mov:eax,dword ptr [ebp-8]意思是把ebp-14h里面的值10存到寄存器ecx里面

push:ecx意思是把eax压在最顶部,也就是a的值10放在了最顶部。


3、call指令

call指令作用是函数调用1. 压入返回地址 2. 转入目标函数,也就是把call下一条指令压入栈顶。方便函数返回时找到来时路。注意每次重新调试时 ,地址都会不一样。但功能是一样的。

4、使用形参

因为函数栈帧创建的过程在main函数栈帧创建中已经讲到了,此时我们省略Add创建过程,直接来到使用形参。

上面我们说到了,ecx和eax这两个寄存器放的是两个局部变量的值,实质上此时的ecx和eax就是形参x和y的值。我们可以让栈底指针依次向下访问这两个寄存器的值(形参的值)这样就可以进行安全的操作。我们常说对形参进行修改不会影响实参,就是这个道理。因为形参是在寄存器里面存储的。


7、自定义函数返回到主函数

当函数返回主函数时会出现以下界面。我们不用管上一部分,我们来看下半部分。

 下半部分就是Add函数的销毁过程,分别pop栈底的三个指令,然后通过cmp、mov指令回到main函数主体。


最后main函数回到了最初的样子


最后输出c的值

注意,我们先把z的值放到了寄存器eax里面。 不然Add函数的栈帧结束后z变量里面存的值都会被销毁。

 

mov:dword ptr[ebp-20h],eax。此时把eax的值放到了ebp-20h的位置。因此c的值就变为了z的值。这就是返回值。


 总结:

局部变量创建过程是通过栈底指针ebp来执行的

函数创建后的内存里面默认值是CCCCCCCC因此会造成不初始化变量输出乱码

形参放在是eax和ecx或ebx里面的


本期博客到这里就结束了,相信大家已经理解了开头的理解函数栈帧能解决什么问题。感谢您的观看

Never Give Up

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

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

相关文章

LBA逻辑区块地址

现在很多硬盘采用同密度盘片&#xff0c;意味着内外磁道上的扇区数量不同&#xff0c;扇区数量增加&#xff0c;容量增加&#xff0c;3D很难定位寻址&#xff0c;出现了新的寻址模式&#xff1a;LBA(Logical Block Addressing)。在LBA地址中&#xff0c;地址不再表示实际硬盘的…

Mysql 并发多版本控制MVCC

什么是MVCC MVCC&#xff0c;全称Multi-Version Concurrency Control&#xff0c;即多版本并发控制。MVCC是一种并发控制的方法&#xff0c;一般在数据库管理系统中&#xff0c;实现对数据库的并发访问&#xff0c;在编程语言中实现事务内存。 一般情况下我们使用mysql数据库的…

Prometheus 采集vCenter7监控数据

前提要求 安装docker&#xff0c;docker-compose创建vmware_exporter目录 mkdir -p /vmware_exporter 创建基于docker部署vmware_exporter脚本 VSPHERE_HOST: "vCenter 管理地址" VSPHERE_IGNORE_SSL: "True" VSPHERE_USER: "administratorvsphere…

[附源码]计算机毕业设计springboot求职招聘网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

信创平台:查询CPU,内存等命令

信创平台&#xff1a;海光、鲲鹏服务器查询CPU,内存等命令 #1、查看操作系统(统信操作系统) 海光查询操作系统&#xff1a; cat /etc/os-release查看操作系统 cat /etc/os-version查看操作系统版本 鲲鹏查询操作系统&#xff1a;cat /etc/system-release Kylin Linux Adva…

Python图像处理【3】Python图像处理库应用

Python图像处理库应用0. 前言1. 将 RGB 图像转换为灰度图像算法1.1 算法原理3.2 算法实现2. 使用 PIL 库计算图像差异2.1 算法原理2.2 算法实现3. 使用 Scikit-image 转换图像色彩空间3.1 将 RGB 图像转换至 HSV 色彩空间3.2 将 RGB 图像转换至 YUV 色彩空间4. 用 OpenCV 调整图…

html学习笔记

1.在idea里右键创建html文件 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body></body> </html>在谷歌浏览器中输入chrome://version可以看…

【学习笔记】深度学习入门:基于Python的理论与实现-误差反向传播法

CONTENTS五、误差反向传播法5.1 计算图5.2 链式法则5.3 反向传播5.4 简单层的实现5.5 激活函数层的实现5.6 Affine/Softmax层的实现5.7 误差反向传播法的实现五、误差反向传播法 5.1 计算图 先引入一个很简单的问题&#xff1a;在超市买了222个100100100元一个的苹果&#xf…

[附源码]JAVA毕业设计个人饮食营养管理信息系统(系统+LW)

[附源码]JAVA毕业设计个人饮食营养管理信息系统&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 …

云原生|kubernetes|kubernetes集群使用私有镜像仓库拉取镜像(harbor或者官方的registry私有镜像仓库)

前言&#xff1a; 在实际的生产中&#xff0c;我们可能会有许多的由开发制作的docker镜像&#xff0c;这也就造成使用这些镜像需要打包成tar文件&#xff0c;然后上传到服务器内然后在导入并使用&#xff0c;但&#xff0c;kubernetes节点很多&#xff0c;有时候并不是明确的要…

13.前端笔记-CSS-盒子样式应用(圆角、阴影)

1、圆角边框 border-radius属性&#xff0c;用于设置元素的外边框圆角 原理&#xff1a;(椭)圆和矩形的两条边相切&#xff08;圆的半径就是length&#xff09;&#xff0c;形成圆角效果 属性&#xff1a; border-top-left-radius;左上 border-top-right-radius:右上 border…

B-神经网络模型复杂度分析

B-神经网络模型复杂度分析 前言一&#xff0c;模型计算量分析 卷积层 FLOPs 计算全连接层的 FLOPs 计算二&#xff0c;模型参数量分析 卷积层参数量BN 层参数量全连接层参数量三&#xff0c;模型内存访问代价计算 卷积层 MAC 计算四&#xff0c;一些概念 双精度、单精度和半精…

数苹果-第12届蓝桥杯Scratch选拔赛真题精选

[导读]&#xff1a;超平老师计划推出Scratch蓝桥杯真题解析100讲&#xff0c;这是超平老师解读Scratch蓝桥真题系列的第91讲。 蓝桥杯选拔赛每一届都要举行4~5次&#xff0c;和省赛、国赛相比&#xff0c;题目要简单不少&#xff0c;再加上篇幅有限&#xff0c;因此我精挑细选…

【Android】Fragment使用

使用Fragment 我们可以把页面结构划分成几块&#xff0c;每块使用一个Fragment来管理。这样我们可以更加方便的在运行过程中动态地更新Activity中的用户界面&#xff0c;日后迭代更新、维护也是更加方便。 Fragment并不能单独使用&#xff0c;他需要嵌套在Activity 中使用&…

Redis最佳实践(上)

引言 尽管 redis 是一款非常优秀的 NoSQL 数据库&#xff0c;但更重要的是&#xff0c;作为使用者我们应该学会在不同的场景中如何更好的使用它&#xff0c;更大的发挥它的价值。主要可以从这四个方面进行优化&#xff1a;Redis键值设计、批处理优化、服务端优化、集群配置优化…

某些设置由你的组织来管理

今天莫名其妙Windows更新出现&#xff1a;*某些设置由你的组织来管理 我们来看看如何恢复吧。 根据上面的图片我们可以知道&#xff0c; 可查看配置的更新策略&#xff1a; 可以看到设备设置的策略有下面几个&#xff1a; 解决方案 这个时候我们就要进入设置更改那些策略即…

Java企业微信对接(二)微信端回调到企业端

准备工作 先下载demo 下载完成后的目录,把这些类之间copy到工程里面就行,都是封装好的加密算法 回调配置 什么时候需要回调 在集成企业微信与内部系统时,我们往往需要搭建一个回调服务。回调服务,可以实现: 自定义丰富的服务行为。比如,用户向应用发消息时,识别消…

RNA-seq 详细教程:count 数据探索(4)

学习目标 了解 RNA-seq count 数据的特征比较 count 数据的不同数学模型确定最适合 RNA-seq count 数据的模型了解设置生物学重复对于鉴定样本间差异的好处1. 计数矩阵 当开始差异表达基因分析时&#xff0c;先从一个矩阵开始&#xff0c;该矩阵总结了数据集每个样本中的基因水…

ZMQ请求应答模式之无中间件的可靠性--自由者模式

一、引言 我们讲了那么多关于中间件的示例&#xff0c;好像有些违背“ZMQ是无中间件”的说法。但要知道在现实生活中&#xff0c;中间件一直是让人又爱又恨的东西。实践中的很多消息架构能都在使用中间件进行分布式架构的搭建&#xff0c;所以说最终的决定还是需要你自己去权衡…

3.8、集线器与交换机的区别

1、早期总线型以太网 最初使用粗同轴电缆作为传输媒体&#xff0c;后来是用相对便宜的细同轴电缆 普遍认为有源器件不可靠&#xff0c;无缘的电缆线最可靠&#xff08;并没有那么可靠&#xff09; 2、只用双绞线和集线器 HUB 的星型以太网 主机中的以太网卡及集线器个接口使…