函数栈桢的创建与销毁@内功修炼

2023-12-26 20:20

本文主要是介绍函数栈桢的创建与销毁@内功修炼,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引:

本文将解决你可能遇到的如下困惑:

  • 局部变量是怎样创建的?
  • 为什么局部变量不初始化时的值是随机值?
  • 函数是怎样传参的?传参顺序如何?
  • 形参与实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用后怎样返回的?

学会了函数栈桢的创建与销毁,其实就是修炼了自己的内功,也能搞懂后期很多知识。

本文时使用的环境是vs2013,注意不要使用太高级的编译器,越高级的编译器越不容易学习和观察。同时,不同的编译器下,函数调用中栈桢的创建也是略有差异的,具体细节取决于编译器的实现。

🍓嘿嘿:初次修炼内功在四个月之前,如上文所说,最近学习的东西又需要好好理解这部分内容,然而我功力渐退,于是有了这篇文章,作为再次修炼的结果。整个“过程”让人不禁感叹精妙!

正文开始

目录

  • 1. 知识铺垫
  • 2. 函数栈桢的创建
  • 3. 函数栈桢的销毁

1. 知识铺垫

为了观察函数栈桢的创建与销毁,这里采用最最简单的代码,并将其拆分的足够详细,便于观察:

#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;
}

知识铺垫与了解函数栈桢的大致轮廓:(细节后面聊)
在这里插入图片描述
我们知道,调用一个函数,都要在栈区上为它分配空间。
对于上面的一段代码 ——
在这里插入图片描述
这里我们需要知道,在vs2013中,main函数也是被其他函数调用的 ——可以看到,*main*函数是在

2. 函数栈桢的创建

下面将研究这个调用过程,大家跟着思路,逐步解答文章开头的疑问。
建议自己动手,F10调试起来,右击转到反汇编,把监视和内存窗口都打开,其实很有意思,因为真的是太精妙了!

来吧!

一上来,我们可以看到espebp还在维护调用main函数的__tmainCRTStartup这个函数的栈桢,这就是为什么刚刚提了一嘴main函数也是被其他函数调用的——在这里插入图片描述
那么接下来的绿色框框这几条汇编指令,就是在main函数预开辟栈桢的过程——
(注:栈区空间是高地址向低地址使用的,在插图中即是从下向上使用)
在这里插入图片描述
我们来配合画图和监视窗口一条条看吧!

可以看到ebp的确压到栈顶了,并且esp也随之而动了——
在这里插入图片描述
下面是在为main函数预开辟了一段空间 ——
在这里插入图片描述
接下来,我们push,push,push压栈,这是为了什么,我们不用管,继续 ——
在这里插入图片描述
这个绿框框中的汇编指令意思是,edi开始向下把39h(ecx)这么多个doubleword(dword)的数据全部赋为CCCCCCCC(eax).

在内存监视窗口,我们也可以看到,从edi开始好大一块空间都被改成了cccccccc ——
在这里插入图片描述
以上就是为main函数建立栈桢的全过程,至此我们才开始执行C语言代码 ——
在这里插入图片描述
我们来看,是怎样为局部变量a分配空间的 ——
在这里插入图片描述
🍓 这就解释了为什么变量没有初始化时默认的是随机值,如果我创建变量a没有初始化为10,那么默认就是cccccccc。事实上,我们之前经常不小心打印出来的"烫烫烫烫"就是内存中的cccccccc。这就是为什么创建变量时最好同时初始化。

我们继续bc的创建 ——
在这里插入图片描述
至此,我们明白了局部变量是如何创建的——建立栈桢,分配空间,初始化的话会赋值。

我们继续阅读汇编指令,接下来,我们要调用函数 —— 在call调用函数之前,绿色框框里发生了什么?
在这里插入图片描述
对的,实际上这就是在“传参” ,有趣的是,我们还没有调用Add函数,就已经传参过去了,而且是先传的b后传的a,从右向左传的 ——
在这里插入图片描述
做好准备工作了,接下来按F11我们调用Add函数,继续读指令,

call指令,让我们跳转到它后面的地址。有趣的是,与此同时栈顶压入了call指令的下一条汇编指令的地址。这是做什么用的?

我们能想到,再按F11我们jmpAdd函数之后,会执行Add函数中的一系列汇编指令,然而,调用完之后,我们还要回来接续它的下一条指令执行,因此要记住它的地址——
在这里插入图片描述
我们进入Add函数 :
那么最开始这几条指令就是在为Add函数开辟栈桢,可以看到,这的汇编指令和main函数一模儿一样,我们快进一下吧 。
在这里插入图片描述

但还是要注意,第一句指令push的是,此时正在维护main函数的栈底寄存器ebp。这个位置的记录,又为我们后面神奇事情的发生埋下了伏笔。(没关系,待会儿我们开始销毁的时候就能实实在在的感受它的作用了)

来吧,看快进结果 ——
在这里插入图片描述
建立好Add函数栈桢就是这样滴 ——
在这里插入图片描述
接下来,在Add函数栈桢中,我们同样为局部变量z分配了空间并初始化,再接下来我们就要计算啦 ——
在这里插入图片描述
那么z = x + y;是怎样计算的呢?
很有意思的是,我并没有在Add函数中创建形参,而是在调用Add函数之前就进行了压栈,在需要用它们的时候又通过指针的偏移回去找了压栈这段空间 ——
在这里插入图片描述
🍓形参是实参的一份临时拷贝这句话也是千真万确,它们的空间是独立的,形参的改变不会影响实参。
and呃 ——
在这里插入图片描述
🍓计算之后,返回值是如何带回来的呢?
从汇编指令可以看到Add函数栈桢马上就要开始销毁了,我们先把返回值放在寄存器中,等到回到调用它的这个函数,再把它赋给局部变量 ,这样函数销毁了也没有关系 ——
在这里插入图片描述
好嘞~ 我们接着看销毁过程吧!

3. 函数栈桢的销毁

上来就是三连pop,栈顶指针esp随之而动 ——
在这里插入图片描述
and then继续执行指令, Add函数栈桢被回收了,like this 哈哈——
在这里插入图片描述
接下来,神奇的事情就要发生了!
读下一条指令的意思是,pop弹出栈顶数据存入ebp,而此时栈顶元素是什么?

就是我们当初记录的main函数的栈底寄存器ebp呀!看呐!一切是如此的精妙、顺理成章!(完了,我写激动了)这样做是因为,随着函数栈桢的销毁,我们要找到它的栈顶是容易的,而它的栈底我却不记得了,因此要记录栈底,此时esp,ebp继续维护main函数栈桢

继续读指令,ret让我们返回栈顶的地址,即当初call的下一条指令的位置 。
一切是如此的精妙,这就再次解释了上文我们为什么要记录这个地址了,就是为了调用完函数之后,还能找回来 ——
在这里插入图片描述
调用完函数,回来,执行下一步指令,它在销毁形参
🍓在这里形参是如何销毁的?什么时候销毁的?我们也就清楚了 ——
在这里插入图片描述
就是这样 ——
在这里插入图片描述
继续读下一条指令,🍓返回值是怎样带回来的?
上文提到我们在函数栈桢销毁之前,把返回值存在eax寄存器中,在这条指令里,我们把eax的值赋给了局部变量——
在这里插入图片描述
就是这样——
在这里插入图片描述
至此,我们已经解答了开篇的所有问题,你还记得本文有几个草莓🍓吗?哈哈

我们再来回过头来看,这些问题已经在行文过程中都有了答案,在此再总结一下——

🍓局部变量是怎样创建的?
为函数分配好栈桢空间,栈桢空间初始化好,然后为局部变量分配空间。

🍓为什么局部变量不初始化时的值是随机值?
随机值是我放进去的,初始化即覆盖。

🍓函数是怎样传参的?传参顺序如何?
实际上我还没有调用时,我已经把这两个值从右向左传过去压栈,要使用时通过指针偏移量再找回。

🍓形参与实参是什么关系?
形参确实是实参的一份临时拷贝,是我在压栈时开辟的空间,值是相同的,但是空间是独立的。

🍓函数调用是怎么做的?
调用之前,我们就把call指令的下一条指令的地址压进去了,弹出ebp就能找到上一个函数的ebp

🍓 函数调用后怎样返回的?
返回值是通过寄存器帮我们带回来的,函数栈桢销毁了并没有影响。

文末碎碎念:回想上次自己调试不熟练,导致文章只开始了一点点就被其他事情冲走了,没有亲自上手把所发生的一切都记录下来的勇气。最近数据结构的练习,让我强迫自己调,忍住,别去找老师,几次调下来就自信了很多,于是本文的整个过程的记录就顺顺利利的完成了。

本文完

这篇关于函数栈桢的创建与销毁@内功修炼的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/540667

相关文章

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

顺序表之创建,判满,插入,输出

文章目录 🍊自我介绍🍊创建一个空的顺序表,为结构体在堆区分配空间🍊插入数据🍊输出数据🍊判断顺序表是否满了,满了返回值1,否则返回0🍊main函数 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以:点赞+关注+评论+收藏(一键四连)哦~ 🍊自我介绍   Hello,大家好,我是小珑也要变强(也是小珑),我是易编程·终身成长社群的一名“创始团队·嘉宾”

C++操作符重载实例(独立函数)

C++操作符重载实例,我们把坐标值CVector的加法进行重载,计算c3=c1+c2时,也就是计算x3=x1+x2,y3=y1+y2,今天我们以独立函数的方式重载操作符+(加号),以下是C++代码: c1802.cpp源代码: D:\YcjWork\CppTour>vim c1802.cpp #include <iostream>using namespace std;/*** 以独立函数

Maven创建项目中的groupId, artifactId, 和 version的意思

文章目录 groupIdartifactIdversionname groupId 定义:groupId 是 Maven 项目坐标的第一个部分,它通常表示项目的组织或公司的域名反转写法。例如,如果你为公司 example.com 开发软件,groupId 可能是 com.example。作用:groupId 被用来组织和分组相关的 Maven artifacts,这样可以避免

函数式编程思想

我们经常会用到各种各样的编程思想,例如面向过程、面向对象。不过笔者在该博客简单介绍一下函数式编程思想. 如果对函数式编程思想进行概括,就是f(x) = na(x) , y=uf(x)…至于其他的编程思想,可能是y=a(x)+b(x)+c(x)…,也有可能是y=f(x)=f(x)/a + f(x)/b+f(x)/c… 面向过程的指令式编程 面向过程,简单理解就是y=a(x)+b(x)+c(x)

批处理以当前时间为文件名创建文件

批处理以当前时间为文件名创建文件 批处理创建空文件 有时候,需要创建以当前时间命名的文件,手动输入当然可以,但是有更省心的方法吗? 假设我是 windows 操作系统,打开命令行。 输入以下命令试试: echo %date:~0,4%_%date:~5,2%_%date:~8,2%_%time:~0,2%_%time:~3,2%_%time:~6,2% 输出类似: 2019_06

ORACLE 11g 创建数据库时 Enterprise Manager配置失败的解决办法 无法打开OEM的解决办法

在win7 64位系统下安装oracle11g,在使用Database configuration Assistant创建数据库时,在创建到85%的时候报错,错误如下: 解决办法: 在listener.ora中增加对BlueAeri-PC或ip地址的侦听,具体步骤如下: 1.启动Net Manager,在“监听程序”--Listener下添加一个地址,主机名写计