函数栈帧的创建和销毁

目录

引言:

1,函数栈帧的概念

2,函数栈帧的创建与销毁过程

2.1预备知识

2.2main函数栈帧的创建 

2.2.1push  ebp

2.2.2mov    ebp,esp

2.2.3sub    esp,0E4h

2.2.4push     ebx ;push     esi;push     edi

2.2.5lea      edi,[ebp+FFFFFF1Ch]

2.2.6mov   ecx,39h

2.2.7mov         eax,0CCCCCCCCh 

2.2.8rep stos    dword ptr es:[edi]

2.3局部变量的创建

2.3.1mov      dword ptr [ebp-8],0Ah

2.3.2mov      dword ptr [ebp-14h],14h

2.3.3mov      dword ptr [ebp-20h],0

2.4Add函数的调用

2.4.1mov         eax,dword ptr [ebp-14h]

2.4.2push        eax

2.4.3mov         ecx,dword ptr [ebp-8]

2.4.4push        ecx

2.4.5call        00B810E1

2.4.6Add函数栈帧的创建 

2.4.6.1push        ebp

2.4.6.2mov         ebp,esp

2.4.6.3sub         esp,0CCh

2.4.7依次执行push        ebx ;push     esi ;push        edi

2.4.8lea         edi,[ebp+FFFFFF34h]

2.4.9依次执行mov         ecx,33h;mov         eax,0CCCCCCCCh

2.4.10rep stos    dword ptr es:[edi]

2.4.11mov         dword ptr [ebp-8],0

2.4.12mov         eax,dword ptr [ebp+8] 

2.4.13add         eax,dword ptr [ebp+0Ch] 

2.4.14mov         dword ptr [ebp-8],eax 

2.4.15mov         eax,dword ptr [ebp-8]

2.5函数栈帧的销毁

2.5.1依次执行pop         edi;pop         esi;pop         ebx 

2.5.2mov         esp,ebp

2.5.3pop         ebp 

2.5.4ret

2.5.5add         esp,8 esp

2.5.6mov         dword ptr [ebp-20h],eax 

结语:

1.局部变量是怎么创建的?

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

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

4.形参和实参是什么关系?

5.函数调用是怎么做的?

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


引言:

很多伙伴在学习C语言时侯,可能会有很多的困惑?

比如:

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后是怎么返回的?

其实我们只要知道了函数栈帧的创建和销毁上述这些问题自然就迎刃而解了,学习这部分知识就相当于在修炼我们的内功,让自己理解知识的能力更上一层楼。

注:在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现(本文基于VS2013编译器调试)。


1,函数栈帧的概念

函数栈帧是指在函数调用时,为该函数分配的一块内存区域,用于存储函数的局部变量,参数和其它相关信息。每个函数调用都会创建一个新的函数栈帧,函数执行完毕后,函数栈帧会被销毁。

2,函数栈帧的创建与销毁过程

🎈演示代码:

#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\n", c);return 0;
}

2.1预备知识

我们知道,每一个函数调用,都要在栈区创建一个空间,假设我们现在调用main函数,就要在栈区内存上面为main函数分配一块空间,我们就称这块空间是为main函数开辟的一块函数栈帧。那这块空间是由谁来维护的呢?它是由两个寄存器来维护的,一个是ebp(栈底指针),一个是esp(栈顶指针)。那我们现在就可以这样理解,正在调用哪个函数,那esp和ebp维护的就是哪个函数的函数栈帧,esp和ebp之间的空间就是为这个函数调用所分配的空间。

接下来,我们执行main函数,鼠标点击工具栏中的调试,窗口,调用堆栈,我们就可以从函数的调用堆栈里边看见main函数被调用了。

到这儿,大家可能会有点困惑,main函数被谁调用了呢?这时候我们让代码继续往下走,当这个代码执行完的时候,我们就可以看见是__tmainCRTStartup() 这个函数内部调用了我们的main函数,而__tmainCRTStartup() 这个函数又被mainCRTStartup()这个函数调用了。由此,我们可以得知,在VS2013中,main函数也是被其他函数调用的。

我们知道,在使用内存的时候,每一次函数调用都要在栈区上分配空间。那我们接着分析这段代码,开始调用main函数后,就为main函数分配了栈帧,调用完后接着往下走,马上调用了Add函数,这时候也要为Add函数分配栈帧,分配完后esp指针和ebp指针就开始维护Add函数的函数栈帧。我们还应该清楚的一点是,在main函数调用之前它的下面也为__tmainCRTStartup()函数和mainCRTStartup()函数分配了空间。

2.2main函数栈帧的创建 

接下来我们研究一下这个函数具体是怎么调用的。

第一步,按F10调试,然后右击鼠标点击转到反汇编,这个时候就可以看到C语言所对应的汇编代码了,因为我们一会儿要看具体的地址,内存的布局,而现在显示的是'a','b','c'这样的符号名,不好观察,所以我们可以把显示符号名这个选项卡去掉,去掉后里边就变成了诸如[ebp - 8]这样的地址,方便我们更好的观察。

 那这些代码又该如何去理解呢?其实很简单,接下来,跟着博主的思路继续往下走。

现在我们调用main函数,前面说过,再调用main函数之前,还调用了一个函数__tmainCRTStartup(),而现在我们已经进到main函数,马上要调用main函数了,那另外一个调用main函数的那个函数的函数栈帧肯定已经创建好了,现在进入main函数,进去后的指令执行步骤如下:

2.2.1push  ebp

push叫做压栈操作,就是把ebp压到__tmainCRTStartup()函数的函数栈帧上边,因为esp总是维护的栈顶,所以,当push完后,esp会指向刚才压进去的ebp。

2.2.2mov    ebp,esp

这步操作是将esp的值赋给ebp,那栈底的ebp此时也会指向刚才压进去的ebp。

2.2.3sub    esp,0E4h

这步的操作是给esp的值减去十六进制的0E4h,esp的值就会变小,这意味着此时的esp不再指向刚才压进去的ebp,而是指向了上边的某一块区域,此时,就会为main函数预开辟一块空间。

2.2.4push     ebx ;push     esi;push     edi

这三条指令会将ebx,esi,edi依次压到栈顶,而push操作每执行一次,esp的值就会发生变化,等这三条指令执行完毕后,esp就会指向栈顶,即edi的位置。

2.2.5lea      edi,[ebp+FFFFFF1Ch]

这条指令的意思是将[ebp+FFFFFF1Ch]加载到edi里边去,相当于给edi里边放了一个地址。

2.2.6mov   ecx,39h

这条指令将39h的值放到ecx这个寄存器里边。

2.2.7mov         eax,0CCCCCCCCh 

这一步是将CCCCCCCC这样的值放到eax里边。

2.2.8rep stos    dword ptr es:[edi]

真正产生效果的是这条指令,意思是要把从刚刚edi这个位置开始向下的39h这么多的空间全部都改成CCCCCCCC这样的内容,也就是将刚刚为main函数预开辟的空间里边的内容全部初始化成CCCCCCCC这样的内容。

当我们执行完以上这些指令以后,为main函数栈帧的开辟就已经准备完了。

2.3局部变量的创建

2.3.1mov      dword ptr [ebp-8],0Ah

意思是把0Ah这个十六进制数字放在[ebp-8]的位置,0A就是十进制的10,ebp-8的位置就是为a变量开辟的一块空间,由此我们也知道了为什么定义变量的时候要给它初始化,因为如果不初始化,它里边放的就是CCCCCCCC这样的随机值。

2.3.2mov      dword ptr [ebp-14h],14h

意思是将十六进制的14h放在ebp-14h的位置上,即为b变量开辟了一块空间,里边放的是十进制的20。

2.3.3mov      dword ptr [ebp-20h],0

意思是将0放在ebp-20h的位置上,即为c变量开辟一块空间,里边放的是十进制的0。

2.4Add函数的调用

我们知道函数调用就要传参,那在这儿它是怎么传参的呢?我们一起来看一看。

2.4.1mov         eax,dword ptr [ebp-14h]

意思是把ebp-14h的值放到eax里边去,而在上边我们分析过[ebp-14h]的位置就是b变量的位置,b变量里边存储的是十进制的20,所以就是把十进制的20放到eax里边去。

2.4.2push        eax

push即压栈,就是把eax压到栈顶上去,而eax里边放的是20,即把20压栈到了栈顶上去。此时esp指向了eax的位置。

2.4.3mov         ecx,dword ptr [ebp-8]

意思是把ebp-8的值放到ecx里边去,而上边我们也分析过ebp-8的位置就是a变量的位置,a变量里边存储的是十进制的10,所以就是把十进制的10放到ecx里边去。

2.4.4push        ecx

意思是把ecx里边的值压栈到栈顶上去,即把10压倒了栈顶,此时esp指向了ecx的位置。

2.4.5call        00B810E1

call指令是调用的意思,此时,我们按F11进入到它的内部,可以看到它把call指令的下一条指令的地址压到了栈顶,为什么要记住这个地址呢,我们可以想象一下,call指令一调用就会马上去调用add函数,而add函数调用完后它要回到call指令的下一条指令继续往下执行,所以add函数回来的时候就会找到这个地址,再从这个地址继续往下执行。

执行到这的时候我们在按F11来到真正的Add函数内部:

2.4.6Add函数栈帧的创建 

可以看到前面的汇编代码和main函数内部的一模一样,这是在为Add函数创建函数栈帧。

2.4.6.1push        ebp

意思是将main函数栈帧底部的ebp压到栈顶,此时esp指向栈顶的ebp。

2.4.6.2mov         ebp,esp

将esp的值赋给ebp,此时,ebp也指向了和esp同样的位置。

2.4.6.3sub         esp,0CCh

给esp减去0CCh,此时,esp就会指向上边的某一块区域,这时,Add函数的函数栈帧也创建好了。

2.4.7依次执行push        ebx ;push     esi ;push        edi

这三条指令会将ebx ,esi ,edi依次压倒栈顶,此时esp指向了栈顶的edi。

2.4.8lea         edi,[ebp+FFFFFF34h]

将ebp+FFFFFF34h这个地址加载到edi里边去。

2.4.9依次执行mov         ecx,33h;mov         eax,0CCCCCCCCh

分别将33h和0CCCCCCCCh 的值放到ecx和eax里边去。

2.4.10rep stos    dword ptr es:[edi]

将从edi这个位置开始向下到ebp之间这所有的空间的内容初始化成CCCCCCCC。

🍂int z = 0;

2.4.11mov         dword ptr [ebp-8],0

把0放到ebp-8的位置上去,即给z开辟一块空间,ebp-8指向的就是z这块空间。

🍂z = x + y;

2.4.12mov         eax,dword ptr [ebp+8] 

把ebp+8的值放到eax寄存器里边去,而ebp+8指向的这块空间刚好是ecx,它里边放着的是10,所以,eax寄存器里边放着的就是10。

2.4.13add         eax,dword ptr [ebp+0Ch] 

给eax里边加上ebp+0Ch的值,而ebp+0Ch指向的是eax,它里边放着的是20,所以,eax现在就变成了30。

2.4.14mov         dword ptr [ebp-8],eax 

把eax的值放在ebp-8的位置上去,ebp-8即z空间,所以,现在z里边放的就是30。

🍂return z;

2.4.15mov         eax,dword ptr [ebp-8]

把ebp-8的值放在eax里边去(eax是寄存器,它不会因为程序退出就销毁里边的值),ebp-8就是z空间,里边放的是30,所以就是把30放在eax寄存器里边去。

2.5函数栈帧的销毁

2.5.1依次执行pop         edi;pop         esi;pop         ebx 

pop是出栈操作,意思是依次将edi,esi,ebx  弹出栈顶。

2.5.2mov         esp,ebp

把ebp赋给esp,此时esp不在指向栈顶,而是指向和ebp同样的位置。

2.5.3pop         ebp 

此时ebp和esp都指向了栈顶,而这步操作就是把ebp弹出,而ebp弹出后esp也不应该指向这儿,而是向下移一个位置,而移动后esp刚好就指向了前面call指令记住的地址。

2.5.4ret

这条指令执行后刚好就会回到call指令的下一条指令的地方,退出Add函数,回到main函数里边。

回到main函数里边后,main函数的函数栈帧又是由esp和ebp来开始维护。

2.5.5add         esp,8 esp

这是esp指向栈顶,加上8就会向下移动到指向edi的位置,相当于把形参x,y还给操作系统。

2.5.6mov         dword ptr [ebp-20h],eax 

把eax的值放到ebp-20h的位置上,eax寄存器里边放的是30,ebp-20h的位置即c变量的位置,也就是将30赋给c变量。

结语:

现在我们就可以解答文章开头的疑惑了:

1.局部变量是怎么创建的?

局部变量的创建是首先为这个函数分配好栈帧空间,栈帧空间里边初始化好一部分空间之后给局部变量在栈帧里边分配一点空间,这就是局部变量的创建。

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

因为如果不初始化的话,里边的值是我们放进去的,所以就是随机值,如果给局部变量初始化的话,就会把这个随机值给覆盖了。

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

当我们要去调用函数的时候,还没有调用的时候就已经用push指令把这两个参数从右向左开始压栈进去了,当真正进入形参函数的时候,在Add函数栈帧里边通过指针的偏移量找回来找到了我们的形参。

4.形参和实参是什么关系?

形参是在压栈的时候开辟的空间,它和实参只是值是相同的,但空间是独立的,所以形参是实参的一份零时拷贝,改变形参不会影响实参。

5.函数调用是怎么做的?

见上文

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

函数在调用之前就把call指令的下一条指令的地址存进去了(即把ebp调用这个函数的上一个函数的栈帧的ebp存进去),当函数调用完要返回的时候,弹出ebp就能够找到上一个函数调用的ebp,然后指针往下走的时候就能够找到esp的地址,回到我们的栈帧空间,返回值是通过寄存器的方式带回来的。

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

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

相关文章

【Windows】Edge浏览器自动更新服务启用选禁用被拒绝访问的解决方案

Windows系统的服务窗口里&#xff0c;把一些服务的启动类型选择禁用有可能会提示拒绝访问&#xff0c;怎么弄呢&#xff0c;这里讲一讲怎样禁用这个服务。 举一个类似禁用服务的例子&#xff1a;怎样关闭Edge浏览器的自动更新服务&#xff0c; 关闭服务 已知&#xff0c;Win…

Javascript命令模式

Javascript命令模式 1 什么是命令模式2 命令模式的例子—菜单程序3 JavaScript 中的命令模式4 撤销命令5 宏命令 1 什么是命令模式 在一个餐厅中&#xff0c;当客人现场点餐或者打电话订餐时&#xff0c;老板会把客人的需求写在清单上&#xff0c;厨师会按照清单的顺序给客人炒…

C++STL的迭代器(iterator)

一、定义 迭代器是一种检查容器内元素并且遍历容器内元素的数据类型。 【引用自&#xff1a;C迭代器&#xff08;iterator&#xff09;_c iterator_NiUoW的博客-CSDN博客】迭代器是一个变量&#xff0c;相当于容器和操纵容器的算法之间的中介。C更趋向于使用迭代器而不是数组下…

makesense在线yolov5标注

文章目录 一、创建图片文件夹和label.txt二、在线标注数据 参考文章博主&#xff1a;风吹落叶花飘荡 一、创建图片文件夹和label.txt 创建一个放置图片的文件夹images&#xff0c;存放需要标注的图片&#xff08;图片最好重命名为1,2,3…避免后面混淆&#xff09; 创建label.t…

在软件测试行业这种情况下,凭什么他能拿25k?我却约面试都难?

在当今竞争激烈的软件测试行业中&#xff0c;近期的招聘市场确实面临一些挑战。大量的求职者争相涌入岗位&#xff0c;许多热衷于功能测试的人士甚至难以找到理想的工作机会。更不幸的是&#xff0c;连自动化测试和性能测试这些专业领域也受到了测试开发人员的竞争压力。然而&a…

整理MongoDB文档:身份验证

整理MongoDB文档:身份验证 个人博客&#xff0c;求关注。 文章概叙 本文主要讲MongoDB在单机状态下的账户配置。理解了MongoDB的语法&#xff0c;对于如何配置用户权限会知道怎么配置&#xff0c;但是请注意给谁配置什么权限才是最重要的。 最小权限原则 系统的每个程序或者…

kubernetes(2)

pod管理 应用部署 上传测试镜像 [rootk8s1 docker]# docker push reg.westos.org/library/myapp:v1 [rootk8s1 docker]# docker push reg.westos.org/library/myapp:v2创建自助式pod&#xff08;生产不推荐&#xff09; [rootk8s2 ~]# kubectl run demo --imagemyapp:v1[ro…

uniapp 安装 u-view 组件库

u-view 组件库安装教程&#xff1a;https://uviewui.com/components/install.html 注&#xff1a;以下使用 HBuilderx 安装 u-view 2.0 版本&#xff0c;不适用于其它版本。 1.安装 u-view 组件库 2、注册并登录 HBuilderx 账号&#xff0c;点击下载 u-view 组件库。 3、点击…

如何开发一个 Safari 插件

本文字数&#xff1a;2493字 预计阅读时间&#xff1a;15分钟 由于常用浏览器是Safari&#xff0c;而Safari浏览器的插件比不上Chrome&#xff0c;所以就有了自己开发常用的Safari插件的想法。 打算开发当前页面生成二维码的Extension&#xff0c;因为网络原因&#xff0c;AirD…

真实感受:是智能家居在选择合适的技术!

科技从来都是为了让我们的生活更加的简单、舒适&#xff0c;而智能家居的智能&#xff0c;体现在如何更更更方便的使用我需要控制的家居。 例如&#xff1a;下班躺在床上想休息&#xff0c;房间和大厅的灯还开着&#xff0c;这时你会选择什么产品躺着解决问题&#xff1f; 红外…

Hadoop3教程(二十八):(生产调优篇)NN、DN的多目录配置及磁盘间数据均衡

文章目录 &#xff08;148&#xff09;NN多目录配置&#xff08;149&#xff09;DataNode多目录配置及磁盘间数据平衡磁盘间数据均衡 参考文献 &#xff08;148&#xff09;NN多目录配置 NN多目录的意思是&#xff0c;本地目录可以配置成多个&#xff0c;且每个目录存放内容相…

20款VS Code实用插件推荐

前言&#xff1a; VS Code是一个轻量级但功能强大的源代码编辑器&#xff0c;轻量级指的是下载下来的VS Code其实就是一个简单的编辑器&#xff0c;强大指的是支持多种语言的环境插件拓展&#xff0c;也正是因为这种支持插件式安装环境开发让VS Code成为了开发语言工具中的霸主…

【数据结构】八大排序

目录 1. 排序的概念及其作用 1.1 排序的概念 1.2 排序运用 1.3 常见的排序算法 2. 常见排序算法的实现 2.1 插入排序 2.1.1 基本思想 2.1.2 直接插入排序 2.1.3 希尔排序&#xff08;缩小增量排序&#xff09; 2.2 选择排序 2.2.1 基本思想 2.2.2 直接选择排序 2.2…

Linux下Samba服务安装及启用全攻略

Linux下Samba服务安装及启用全攻略 前言一、安装SSH Server二、安装Samba Server1.安装net-tool2.建立账号的samba3.windows通过Samba与linux共享文件4.使用远程工具登录Linux 总结 前言 提示&#xff1a;本文详解了在Linux系统下如何安装和启用Samba服务&#xff0c;涵盖了从…

【2023年11月第四版教材】软考高项极限冲刺篇笔记(2)

1 我们要知道的事 1、考试的选择题不会出假大空的管理,一般较为具体 2.3 信息系统治理 首先治理的目标是什么 治理的管理层分为三层 原则:简单透明适合 COBIT IT审计范围:总体、组织、物理、逻辑、其他 IT审计风险:固有、控制、检查、总体审计 IT审计方法:访谈、调查、…

【微服务 SpringCloudAlibaba】实用篇 · Nacos注册中心

微服务&#xff08;5&#xff09; 文章目录 微服务&#xff08;5&#xff09;1. 认识和安装Nacos2. 服务注册到nacos和拉取服务1&#xff09;引入依赖2&#xff09;配置nacos地址3&#xff09;重启 3. 服务分级存储模型3.1 给user-service配置集群3.2 同集群优先的负载均衡 4. …

vue el-dialog弹出框自定义指令实现拖拽改变位置-宽度-高度

前言 在实际开发中我们经常使用el-dialog弹出框做表单&#xff0c;一般情况都是居中。遮挡到了一部分数据 当我们想要查看弹出框下面的数据时&#xff0c;就只能先把弹出框关闭&#xff0c;查看完数据之后在打开弹框 我们通过动态样式&#xff0c;和鼠标事件就可以实现。但自…

effective c++学习笔记(后四章)

六 继承与面向对象设计 红色字 \color{FF0000}{红色字} 红色字 32 确定你的public继承塑模出 is-a关系 如果你令class D (“Derived”)以public形式继承class B (“Base”)&#xff0c;你便是告诉C编译器&#xff08;以及你的代码读者&#xff09;说&#xff0c;每一个类型为…

Origami Studio for Mac:塑造未来,掌握原型设计之巅

在当今高度竞争的设计领域&#xff0c;原型设计的重要性不言而喻。它不仅是沟通想法&#xff0c;也是测试和改进设计的关键环节。而现在&#xff0c;一款强大的原型设计工具——Origami Studio for Mac&#xff0c;正在席卷设计界&#xff0c;以其独特的功能和卓越的性能&#…

shell变量

shell变量之学习笔记 Shell变量概念1 shell变量分类&#xff1a;2 变量的赋值3 变量赋值格式&#xff1a;4 变量命名方式5 变量声明6 变量引用7 变量清除8 变量只读9 内部参数变量10 位置参数变量11 退出和返回状态12 命令替换13 read命令14 字符串长度与截取15 字符串替换16 变…