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

news2024/9/29 7:20:16

前期学习的时候,我们可能有很多困惑,比如:

局部变量是怎么创建的?

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

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

形参和实参是什么关系?

函数调用是怎么做的?

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

··· ···

知道了函数栈帧的创建和销毁就都会了,这样也能搞懂后面学习的很多知识。

今天,以VS2013为例来学习和观察函数栈帧的创建与销毁。注意是VS013!假如用别的编译器效果可能会有所差异,但重要的是能够通过今天的演示来理解。

我们今天的演示,可能会遇到如下寄存器:eax、ebx、ecx、edx、ebp、esp。

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

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

 

现在创建一个新项目,新建.c文件,用函数写一个两数之和的程序:

#include <stdio.h>
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("%d", c);
	return 0;
}

正在调用的是什么函数,esp和ebp维护的就是哪个函数的栈帧,比如正在调用main函数,就去维护main函数的栈帧,正在调用Add,就要去调用Add的栈帧。esp和ebp之间的空间就是正在维护的函数的空间。

通常,ebp被称为栈底指针,esp被称为栈顶指针。

栈区的使用习惯是先使用高地址,再使用低地址。

当创建新的空间的时候,就会在更低的地址处创建空间:

现在进行调试,并打开调用堆栈:

 从调用堆栈里可以看到,main函数被调用了。

是的,main函数也是被别的函数调用的!

 那么,main函数是被谁调用的?

我们继续往下调试代码,当代码执行完,可以看到__tmainCRTStartup()

main函数就是被__tmainCRTStartup函数调用的。

进入反汇编,可以看到__tmainCRTStartup函数的汇编代码:

__tmainCRTStratup也有自己的代码。

我们观察到,main函数是被__tmainCRTStartup函数调用的。

也就是说在维护main函数的栈帧之前,ebp和esp在维护__tmainCRTStartup函数的栈帧。

不过这里能够了解即可。重点分析main函数和Add函数。

找到main函数的汇编代码:

int main() {
002718A0  push        ebp  
002718A1  mov         ebp,esp  
002718A3  sub         esp,0E4h  
002718A9  push        ebx  
002718AA  push        esi  
002718AB  push        edi  
002718AC  lea         edi,[ebp-24h]  
002718AF  mov         ecx,9  
002718B4  mov         eax,0CCCCCCCCh  
002718B9  rep stos    dword ptr es:[edi]  
002718BB  mov         ecx,27C003h  
002718C0  call        0027131B  
	int a = 10;
002718C5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
002718CC  mov         dword ptr [ebp-14h],14h  
	int c = 0;
002718D3  mov         dword ptr [ebp-20h],0  
	c = Add(a, b);
002718DA  mov         eax,dword ptr [ebp-14h]  
002718DD  push        eax  
002718DE  mov         ecx,dword ptr [ebp-8]  
002718E1  push        ecx  
002718E2  call        002710B4  
002718E7  add         esp,8  
002718EA  mov         dword ptr [ebp-20h],eax  
	printf("%d", c);
002718ED  mov         eax,dword ptr [ebp-20h]  
002718F0  push        eax  
002718F1  push        277B30h  
002718F6  call        002710D2  
002718FB  add         esp,8  
	return 0;
002718FE  xor         eax,eax  
}
00271900  pop         edi  
00271901  pop         esi  
00271902  pop         ebx  
00271903  add         esp,0E4h  
00271909  cmp         ebp,esp  
0027190B  call        00271244  
00271910  mov         esp,ebp  
00271912  pop         ebp  
00271913  ret  

现在来分析下main函数的汇编代码:

002718A0  push        ebp 

push是压栈,给栈顶放一个新的元素。

在这里是将ebp放到栈顶,栈顶指针也发生了变化。

第一个图是push之前的,第二个图是push之后的。

 

002718A1  mov         ebp,esp  
002718A3  sub         esp,0E4h  

mov ebp,esp的意思是将esp的值给ebp。

sub esp,0E4h的意思的将esp减去0E4h。

通过监视我们可以看到,在mov之后,ebp和esp的值相同。

 mov之后,ebp就到了esp所在的位置:

sub之后esp被减去了0E4h,指向了更低的地址:

减去的0E4h的大小原来是为main函数开辟的空间大小。

而edp和esp也由维护原来的__tmainCRTSstartup函数的栈帧转变为维护main函数的栈帧了。

​​​​002718A9  push        ebx  
002718AA  push        esi  
002718AB  push        edi 

这三个push又是压栈。

002718AC  lea         edi,[ebp-24h]  
002718AF  mov         ecx,9  
002718B4  mov         eax,0CCCCCCCCh  
002718B9  rep stos    dword ptr es:[edi]  

这三条汇编指令的意思是将edi向下的39h这么大的空间里全部赋值为CCCCCCCC:

到此为止,main函数就创建完毕。

打开内存可以发现这些地方的值都成了cccccccc

	int a = 10;
002718C5  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
002718CC  mov         dword ptr [ebp-14h],14h  
	int c = 0;
002718D3  mov         dword ptr [ebp-20h],0  
	c = Add(a, b);

以上代码是将0Ah、14h、0(都是十六进制,其实是a、b、c的值10、20、0)分别放入ebp-8、ebp-14h、ebp-20h中。

假设一个格子是4个字节,那么a、b、c三个变量如下:

打开内存发现相应的值都一存放在内存中:

002718DA  mov         eax,dword ptr [ebp-14h]  
002718DD  push        eax  
002718DE  mov         ecx,dword ptr [ebp-8]  
002718E1  push        ecx  

这一步就是在传参了。

意思是将ebp-14h的值传给eax,然后压栈,再将ebp-8的值传给ecx,然后压栈。

如图所示:

002718E2  call        002710B4  
002718E7  add         esp,8  

接下来的指令是call,call是调用。

这里需要call的下一条指令压栈,因为函数调用会返回,而返回的地址是call的下一条指令。

此时需要按下F11执行call指令。

再查看内存会发现,此处和刚刚call后边的指令一样:

内存中新出现的值就是call的下一条指令的地址。

当call执行完,会从00C21450继续执行指令。

再按下F11进入Add函数。

这就进入Add函数了,以下为Add函数的汇编代码:

 
int Add(int x, int y) {
00271770  push        ebp  
00271771  mov         ebp,esp  
00271773  sub         esp,0CCh  
00271779  push        ebx  
0027177A  push        esi  
0027177B  push        edi  
0027177C  lea         edi,[ebp-0Ch]  
0027177F  mov         ecx,3  
00271784  mov         eax,0CCCCCCCCh  
00271789  rep stos    dword ptr es:[edi]  
0027178B  mov         ecx,27C003h  
00271790  call        0027131B  
	int z = 0;
00271795  mov         dword ptr [ebp-8],0  
	z = x + y;
0027179C  mov         eax,dword ptr [ebp+8]  
0027179F  add         eax,dword ptr [ebp+0Ch]  
002717A2  mov         dword ptr [ebp-8],eax  
	return z;
002717A5  mov         eax,dword ptr [ebp-8]  
}
002717A8  pop         edi  
002717A9  pop         esi  
002717AA  pop         ebx  
002717AB  add         esp,0CCh  
002717B1  cmp         ebp,esp  
002717B3  call        00271244  
002717B8  mov         esp,ebp  
002717BA  pop         ebp  
002717BB  ret  

这段指令和先前为main函数开辟栈帧时的指令非常相似:

00271770  push        ebp  
00271771  mov         ebp,esp  
00271773  sub         esp,0CCh  
00271779  push        ebx  
0027177A  push        esi  
0027177B  push        edi  
0027177C  lea         edi,[ebp-0Ch]  
0027177F  mov         ecx,3  
00271784  mov         eax,0CCCCCCCCh  
00271789  rep stos    dword ptr es:[edi]  

push ebp压栈,然后mov将esp的值赋给ebp,sub让esp指向更低地址处,然后将edi下空间全部赋值为CCCCCCCC。

然后又是三次push......

继续看汇编指令,

int z = 0;
00271795  mov         dword ptr [ebp-8],0  
	z = x + y;
0027179C  mov         eax,dword ptr [ebp+8]  
0027179F  add         eax,dword ptr [ebp+0Ch]  
002717A2  mov         dword ptr [ebp-8],eax  
	return z;
002717A5  mov         eax,dword ptr [ebp-8]  
00271795  mov         dword ptr [ebp-8],0  

这是先将[ebp-8]位置赋值0然后给变量z

0027179C  mov         eax,dword ptr [ebp+8]  
0027179F  add         eax,dword ptr [ebp+0Ch]  
002717A2  mov         dword ptr [ebp-8],eax  

这些指令是先mov,将ebp+8位置的值赋值给eax,此时ebp+8位置的值正好是变量a的值为10,现在eax=10。

然后是add,把ebp+0Ch位置的值和eax指向的值相加,此时ebp+0Ch位置的值正好是变量b的值为20。

那么现在,eax的值就是30了。

再mov,把eax的值赋值给ebp-8,而ebp-8又正好是z的地址。

那么z就是30。

这个例子完美印证了“形参是实参的一份临时拷贝”。

我们在传参的时候,并没有去独立开辟新的空间去接收形参,而是通过寄存器去找到先前在主函数里压栈进去的实参。

妙啊~!

 继续看:

	return z;
002717A5  mov         eax,dword ptr [ebp-8]  
}
002717A8  pop         edi  
002717A9  pop         esi  
002717AA  pop         ebx  
002717B8  mov         esp,ebp  
002717BA  pop         ebp  
mov eax,dword ptr [ebp-8]

它的意思是让eax保管ebp-8的值。

002717A8  pop         edi  
002717A9  pop         esi  
002717AA  pop         ebx  

pop是弹出,那么edi、esi和ebx就被销毁了。

esp的位置也随之发生改变:

 

002717B8  mov         esp,ebp  
002717BA  pop         ebp  

把ebp的值给esp,然后pop弹出ebp,那么Add就完全销毁了:

在此之前为什么要让eax保管ebp-8的值?

就是因为ebp-8会随着Add的销毁而销毁。

002717BB  ret  

ret是返回值。

此时栈顶存放的是call指令的下一条指令的地址,那么现在按下F10,就直接跳到main函数的add指令了。

002718E7  add         esp,8  
002718EA  mov         dword ptr [ebp-20h],eax 

现在将esp的值加8,esp的位置又发生了改变:

又进行了一次销毁,此时形参x、y的空间就释放了。

这里的mov是将eax的值赋给ebp-20h。

而ebp-20h又是压栈时c的空间的地址,也就是说30赋给了c。

创作不易,码了五千多字,求各位三连支持下!

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

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

相关文章

分享丨品牌零售行业三大实践解读,全面搭建并落地数字化运营体系

以下内容来自神策数据创始人 & CEO 桑文锋关于数据驱动零售品牌业务增长的相关分享。点击文末“阅读原文”&#xff0c;了解神策品牌零售行业完整版解决方案。神策从 2020 年深入品牌零售领域&#xff0c;一路走来跌跌撞撞&#xff0c;不过思路越来越清晰&#xff0c;对该领…

Echarts:饼图默认高亮和选中(突出并高亮)/添加单击事件/X轴字符标签换行处理

饼图默认高亮: // 我这里实现的是饼图// 获取 this.sysfbChart this.$echarts.init(document.getElementById(sysfb))// 默认高亮第一个 // highlight:突出 this.sysfbChart.dispatchAction({type: highlight,seriesIndex: 0,dataIndex: 0}); 效果图: 饼图选中(突出并高亮):…

为什么自学 Python 看不进去?

如果刚开始学习python&#xff0c;没看懂&#xff0c;那就多看几次就会了。看不进去很正常&#xff0c;这个过程会很乏味&#xff0c;但是对于你建立牢固的编程思维是必须的。会有看不进去的这个阶段 python学习的秘诀在于对技术了解的广度和对数据结构研究的深度&#xff0c;不…

2022“易观之星”奖项公布,聚焦数字经济,助力数智创新

12月29日&#xff0c;“易观之星”颁奖典礼线上举办&#xff0c;重磅颁出2022“易观之星”奖项。 2022“易观之星”设置数字产品、数字企业2个系列共7大奖项&#xff0c;覆盖金融、品牌零售、汽车出行、文化消费等领域&#xff0c;挖掘和表彰各领域表现卓越的数字产品、数字企业…

【go语言之http模型reactor】

go语言之http模型reactor示例gnet.ServeinitListenerserveactivateEventLoopspolling前面说了go自带的原生netpoll模型&#xff0c;大致的流程就是每一个新的连接都会开启一个goroutine去处理&#xff0c;这样的处理的过程简单&#xff0c;高效&#xff0c;充分利用了go的底层的…

在 K8S 中测试环境中搭建 mongodb

1.可在服务器上面创建 https://kubernetes.io/docs/home/ #k8s官网 搜索Deployment 第一个就是例子 vi rs-mongodb.yaml apiVersion: apps/v1 kind: Deployment #资源类型 metadata:namespace: klvchenname: mongodblabels:app: mongodb spec:replicas: 1selector:mat…

STM32CubeMX学习笔记(50)——USB接口使用(DFU固件升级)

一、USB简介 USB&#xff08;Universal Serial BUS&#xff09;通用串行总线&#xff0c;是一个外部总线标准&#xff0c;用于规范电脑与外部设备的连接和通讯。是应用在 PC 领域的接口技术。USB 接口支持设备的即插即用和热插拔功能。USB 是在 1994 年底由英特尔、康柏、IBM、…

量子+智慧交通!玻色量子中标北京城市轨道交通项目

​2022年11月&#xff0c;玻色量子与北京交通大学国家重点实验室组成联合体&#xff0c;接受北京城市轨道交通咨询有限公司委托&#xff0c;三方共同探索和实施量子计算在智慧地铁中的应用场景研究工作。 北京城市轨道交通咨询有限公司 此次科研合作&#xff0c;玻色量子将充分…

IB学生申请牛津剑桥需要注意什么?

HL要选修3门颇具挑战性的课程&#xff0c;SL还要再选3门课程&#xff0c;写好IA&#xff0c;通过包括EE\CAS\TOK在内的3门核心科目&#xff0c;IB课程的学习压力确实挺大。。 对于志在牛剑这样顶尖高校的IB学生而言&#xff0c;压力更大&#xff0c;因为牛剑等顶尖高校对于IB小…

【JavaEE】在Linux上搭建一个Java部署环境

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录在Linux上搭建一个Java部署环境1. 安装jdk2. 安装tomcat3. 安装mysql小结普通小孩也要热爱生活&#xff01; 在Linux上搭建一个Java部署环境 为了部署java web程序&#xff0c;需要安装&#xff1a;jdk、tomcat、m…

Pandas处理大数据的性能优化技巧

Pandas是Python中最著名的数据分析工具。在处理数据集时&#xff0c;每个人都会使用到它。但是随着数据大小的增加&#xff0c;执行某些操作的某些方法会比其他方法花费更长的时间。所以了解和使用更快的方法非常重要&#xff0c;特别是在大型数据集中&#xff0c;本文将介绍一…

百趣代谢组学文献分享:《Food Function》发表过的封面文章

百趣代谢组学今日分享西北大学食品科学与工程学院曹炜教授团队在食品科学领域国际著名学术期刊《Food& Function》上发表封面文章&#xff1a;Effects of honey-extracted polyphenols on serum antioxidant capacity and metabolic phenotype in rats。该研究在国际上首次…

Springboot定时调度任务动态管理

前言 现在智能手表、手环&#xff0c;相信很多人都使用过&#xff0c;其中有一个功能&#xff0c;就是会有各种的提醒&#xff0c;如喝水提醒、运动提醒、远眺提醒&#xff0c;本质上根据用户的设置&#xff0c;间隔一定时间执行一个调度任务&#xff0c;提醒用户做某件事情。这…

myBaits Expert Human Affinities Kit ——捕获人类样本中的 SNPs 信息,化石、法医样本和降解 DNA 样本均适用

myBaits Expert Human Affinities Kit用于人类基因组SNPs富集&#xff0c;该试剂盒与古人类种群遗传学领域的专家合作设计和开发&#xff0c;针对古代和现代人类种群中已知的2M以上多态位点设计&#xff0c;特异性捕获人类基因组文库中标志性的有效信息&#xff0c;提高人类种群…

ardupilot EKF3核心算法《可以使用的传感器》

目录 文章目录 目录摘要1.可以使用的传感器2.可以使用的传感器如果从代码中提供原始数据2.1 IMU传感器提供的有用数据2.2地磁传感器提供的有用数据2.3 GPS传感器提供的有用数据2.4 气压计传感器提供的有用数据摘要 本节主要讲解Ardupilot EKF3核心算法《可以使用的传感器》。 …

java租房app房东直租系统租房网站

简介 本系统是前后端分离的项目&#xff0c;前端使用mui开发科打包成为安卓的apk。后端采用springboot开发。主要是房东登录注册后&#xff0c;可以发布房源信息&#xff0c;可以上架下架编辑删除房源信息等&#xff0c;租房者可以搜索自己需要的房子&#xff0c;然后进入详情…

Maven基础知识

第1章 Maven 介绍 1.1 什么是 Maven 1.1.1 什么是 Maven Maven 的正确发音是[ˈmevən]&#xff0c;而不是“马瘟”以及其他什么瘟。Maven 在美国是一个口语化的词 语&#xff0c;代表专家、内行的意思。 一个对 Maven 比较正式的定义是这么说的&#xff1a;Maven 是一个项…

代码随想录算法训练营第二天 java : 977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II ,

文章目录Leecode977.有序数组的平方题目链接题目暴力解法双指针解法小结Leecode209.长度最小的子数组。题目连接题目难点 &#xff1a;滑动窗口Leecode59.螺旋矩阵II题目链接题目思路用到的变量代码总结今日收获Leecode977.有序数组的平方 题目链接 题目 给你一个按 非递减顺…

攻防世界-shrine

题目 访问题目场景 这个界面很乱奥&#xff0c;我们访问网页源代码&#xff0c;我们就能看见我们需要审计的源码了 我们把python源码复制下来&#xff0c;进行分析 import flask import osapp flask.Flask(__name__)app.config[FLAG] os.environ.pop(FLAG)app.route(/) def…

如何对企业内部进行知识内容有效性管理

企业内部文档管理版本的场景&#xff0c;如果日常没有比较好的工具进行内容控制&#xff0c;经常出现意外&#xff0c;有些意外很小、但是有些意外可能就伤筋动骨&#xff0c;例如标书里边的文件放错了&#xff0c;如果定性为串标&#xff0c;那么就非常严重了。JVS 企业文档管…