热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

函数运行时在内存中是什么样子?

Python实战社群Java实战社群长按识别下方二维码,按需求添加扫码关注添加客服进Python社群▲扫码关注添加客服进Java社群▲作者丨码农的荒岛求生来源丨码农的

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群

作者丨码农的荒岛求生

来源丨码农的荒岛求生(ID:escape-it)

在开始本篇的内容前,我们先来思考几个问题。

1. 我们先来看一段简单的代码:

void func(int a) {if (a > 100000000) return;int arr[100] = {0};func(a + 1);
}

你能看出这段代码会有什么问题吗?

2. 我们之前提到过一项关键技术——协程,你知道协程的本质是什么吗?有的同学可能会说是用户态线程,那么什么是用户态线程,这是怎么实现的?

3. 函数运行起来后在内存中是什么样子?

这几个问题看似没什么关联,但这背后都指向一样东西,这就是所谓的函数运行时栈,run time stack

接下来我们就好好看看到底什么是函数运行时栈,为什么彻底理解函数运行时栈对程序员来说非常重要。

从进程、线程到函数调用

汽车在高速上行驶时有很多信息,像速度、位置等等,通过这些信息我们可以直观的感受汽车的运行时状态。

同样的,程序在运行时也有很多信息,像有哪些程序正在运行、这些程序执行到了哪里等等,通过这些信息我们可以直观的感受系统中程序运行的状态。

进程和线程的运行体现在函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回。其中函数内部的顺序执行乏善可陈,重点是函数的调用。

因此接下来我们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的。

函数执行的活动轨迹:栈

玩过游戏的同学应该知道,有时你为了完成一项主线任务不得不去打一些支线的任务,支线任务中可能还有支线任务,当一个支线任务完成后退回到前一个支线任务,这是什么意思呢,举个例子你就明白了。

假设主线任务西天取经A依赖支线任务收服孙悟空B和收服猪八戒C,也就是说收服孙悟空B和收服猪八戒C完成后才能继续主线任务西天取经A;

支线任务收服孙悟空B依赖任务拿到紧箍咒D,只有当任务D完成后才能回到任务B;

整个任务的依赖关系如图所示:

现在我们来模拟一下任务完成过程。

首先我们来到任务A,执行主线任务:

执行任务A的过程中我们发现任务A依赖任务B,这时我们暂停任务A去执行任务B:

执行任务B的时候,我们又发现依赖任务D:

执行任务D的时候我们发现该任务不再依赖任何其它任务,因此C完成后我们可以会退到前一个任务,也就是B:

任务B除了依赖任务C外不再依赖其它任务,这样任务B完成后就可以回到任务A:

现在我们回到了主线任务A,依赖的任务B执行完成,接下来是任务C:

和任务D一样,C不依赖任何其它其它任务,任务C完成后就可以再次回到任务A,再之后任务A执行完毕,整个任务执行完成。

让我们来看一下整个任务的活动轨迹:

仔细观察,实际上你会发现这是一个First In Last Out 的顺序,天然适用于栈这种数据结构来处理。

再仔细看一下栈顶的轨迹,也就是A、B、D、B、A、C、A,实际上你会发现这里的轨迹就是任务依赖树的遍历过程,是不是很神奇,这也是为什么树这种数据结构的遍历除了可以用递归也可以用栈来实现的原因。

A Box

函数调用也是同样的道理,你把上面的ABCD换成函数ABCD,本质不变。

因此,现在我们知道了,使用栈这种结构就可以用来保存函数调用信息。

和游戏中的每个任务一样,当函数在运行时每个函数也要有自己的一个“小盒子”,这个小盒子中保存了函数运行时的各种信息,这些小盒子通过栈这种结构组织起来,这个小盒子就被称为栈帧,stack frames,也有的称之为call stack,不管用什么命名方式,总之,就是这里所说的小盒子,这个小盒子就是函数运行起来后占用的内存,这些小盒子构成了我们通常所说的栈区

那么函数调用时都有哪些信息呢?

控制转移

我们知道当函数A调用函数B的时候,控制从A转移到了B,所谓控制其实就是指CPU执行属于哪个函数的机器指令,CPU从开始执行属于函数A的指令切换到执行属于函数B的指令,我们就说控制从函数A转移到了函数B。

控制从函数A转移到函数B,那么我们需要有这样两个信息:

  • 我从哪里来 (返回)

  • 要到去哪里 (跳转)

是不是很简单,就好比你出去旅游,你需要知道去哪里,还需要记住回家的路。

函数调用也是同样的道理。

当函数A调用函数B时,我们只要知道:

  • 函数A对于的机器指令执行到了哪里 (我从哪里来,返回)

  • 函数B第一条机器指令所在的地址 (要到哪里去,跳转)

有这两条信息就足以让CPU开始执行函数B对应的机器指令,当函数B执行完毕后跳转回函数A。

那么这些信息是怎么获取并保持的呢?

现在我们就可以打开这个小盒子,看看是怎么使用的了。

假设函数A调用函数B,如图所示:

当前,CPU执行函数A的机器指令,该指令的地址为0x400564,接下来CPU将执行下一条机器指令也就是:

call 0x400540

这条机器指令是什么意思呢?

这条机器指令对应的就是我们在代码中所写的函数调用,注意call后有一条机器指令地址,注意观察上图你会看到,该地址就是函数B的第一条机器指令,从这条机器指令后CPU将跳转到函数B。

现在我们已经解决了控制跳转的“要到哪里去”问题,当函数B执行完毕后怎么跳转回来呢?

原来,call指令除了给出跳转地址之外还有这样一个作用,也就是把call指令的下一条指令的地址,也就是0x40056a push到函数A的栈帧中,如图所示:

现在,函数A的小盒子变大了一些,因为装入了返回地址:

现在CPU开始执行函数B对应的机器指令,注意观察,函数B也有一个属于自己的小盒子(栈帧),可以往里面扔一些必要的信息。

如果函数B中又调用了其它函数呢?

道理和函数A调用函数B是一样的。

让我们来看一下函数B最后一条机器指令ret,这条机器指令的作用是告诉CPU跳转到函数A保存在栈帧上的返回地址,这样当函数B执行完毕后就可以跳转到函数A继续执行了。

至此,我们解决了控制转移中“我从哪里来”的问题。

传递参数与获取返回值

函数调用与返回使得我们可以编写函数,进行函数调用。但调用函数除了提供函数名称之外还需要传递参数以及获取返回值,那么这又是怎样实现的呢?

在x86-64中,多数情况下参数的传递与获取返回值是通过寄存器来实现的。

假设函数A调用了函数B,函数A将一些参数写入相应的寄存器,当CPU执行函数B时就可以从这些寄存器中获取参数了。

同样的,函数B也可以将返回值写入寄存器,当函数B执行结束后函数A从该寄存器中就可以读取到返回值了。

我们知道寄存器的数量是有限的,当传递的参数个数多于寄存器的数量该怎么办呢?

这时那个属于函数的小盒子也就是栈帧又能发挥作用了。

原来,当参数个数多于寄存器数量时剩下的参数直接放到栈帧中,这样被调函数就可以从前一个函数的栈帧中获取到参数了

现在栈帧的样子又可以进一步丰富了,如图所示:

从图中我们可以看到,调用函数B时有部分参数放到了函数A的栈帧中,同时函数A栈帧的顶部依然保存的是返回地址。

局部变量

我们知道在函数内部定义的变量被称为局部变量,这些变量在函数运行时被放在了哪里呢?

原来,这些变量同样可以放在寄存器中,但是当局部变量的数量超过寄存器的时候这些变量就必须放到栈帧中了。

因此,我们的栈帧内容又一步丰富了。

细心的同学可能会有这样的疑问,我们知道寄存器是共享资源可以被所有函数使用,既然可以将函数A的局部变量写入寄存器,那么当函数A调用函数B时,函数B的局部变量也可以写到寄存器,这样的话当函数B执行完毕回到函数A时寄存器的值已经被函数B修改过了,这样会有问题吧。

这样的确会有问题,因此我们在向寄存器中写入局部变量之前,一定要先将寄存器中开始的值保存起来,当寄存器使用完毕后再恢复原值就可以了。

那么我们要将寄存器中的原始值保存在哪里呢?

有的同学可能已经猜到了,没错,依然是函数的栈帧中。

最终,我们的小盒子就变成了如图所示的样子,当寄存器使用完毕后根据栈帧中保存的初始值恢复其内容就可以了。

现在你应该知道函数在运行时到底是什么样子了吧,以上就是问题3的答案。

Big Picture

需要再次强调的一点就是,上述讨论的栈帧就位于我们常说的栈区。

栈区,属于进程地址空间的一部分,如图所示,我们将栈区放大就是图左边的样子。

最后,让我们回到文章开始的这段简单代码:

void func(int a) {if (a > 100000000) return;int arr[100] = {0};func(a + 1);
}void main(){func(0);
}

想一想这段代码会有什么问题?

原来,栈区是有大小限制的,当超过限制后就会出现著名的栈溢出问题,显然上述代码会导致这一问题的出现。

因此:

  1. 不要创建过大的局部变量

  2. 函数栈帧,也就是调用层次不能太多

总结

本章我们从几个看似没什么关联的问题出发,详细讲解了函数运行时栈是怎么一回事,为什么我们不能创建过多的局部变量。细心的同学会发现第2个问题我们没有解答,这个问题的讲解放到下一篇,也就是协程中讲解。

希望这篇文章能对大家理解函数运行时栈有所帮助。

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 为何说IT科技公司应该留住35岁员工?

 工友们!大家好,今天你摸鱼了吗?

 缓存穿透,雪崩,击穿以及解决方案分析

 图文详解:如何给女朋友解释什么是微服务?


在看点这里好文分享给更多人↓↓


推荐阅读
  • FinOps 与 Serverless 的结合:破解云成本难题
    本文探讨了如何通过 FinOps 实践优化 Serverless 应用的成本管理,提出了首个 Serverless 函数总成本估计模型,并分享了多种有效的成本优化策略。 ... [详细]
  • 选择适合生产环境的Docker存储驱动
    本文旨在探讨如何在生产环境中选择合适的Docker存储驱动,并详细介绍不同Linux发行版下的配置方法。通过参考官方文档和兼容性矩阵,提供实用的操作指南。 ... [详细]
  • 主板IO用W83627THG,用VC如何取得CPU温度,系统温度,CPU风扇转速,VBat的电压. ... [详细]
  • 本题旨在通过给定的评级信息,利用拓扑排序和并查集算法来确定全球 Tetris 高手排行榜。题目要求判断是否可以根据提供的信息生成一个明确的排名表,或者是否存在冲突或信息不足的情况。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
  • 在本教程中,我们将深入探讨如何使用 Python 构建游戏的主程序模块。通过逐步实现各个关键组件,最终完成一个功能完善的游戏界面。 ... [详细]
  • 使用Python计算文件的CRC32校验值
    本文记录了一次对路由器固件分析时,如何利用Python计算文件的CRC32校验值。文中提供了完整的代码示例,并详细解释了实现过程。 ... [详细]
  • 采用IKE方式建立IPsec安全隧道
    一、【组网和实验环境】按如上的接口ip先作配置,再作ipsec的相关配置,配置文本见文章最后本文实验采用的交换机是H3C模拟器,下载地址如 ... [详细]
  • 目录一、salt-job管理#job存放数据目录#缓存时间设置#Others二、returns模块配置job数据入库#配置returns返回值信息#mysql安全设置#创建模块相关 ... [详细]
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
  • 本文详细介绍了Grand Central Dispatch (GCD) 的核心概念和使用方法,探讨了任务队列、同步与异步执行以及常见的死锁问题。通过具体示例和代码片段,帮助开发者更好地理解和应用GCD进行多线程开发。 ... [详细]
  • ElasticSearch 集群监控与优化
    本文详细介绍了如何有效地监控 ElasticSearch 集群,涵盖了关键性能指标、集群健康状况、统计信息以及内存和垃圾回收的监控方法。 ... [详细]
  • 访问一个网页的全过程
    准备:DHCPUDPIP和以太网启动主机,用一根以太网电缆连接到学校的以太网交换机,交换机又与学校的路由器相连.学校的这台路由器与一个ISP链接,此ISP(Intern ... [详细]
  • 深入理解Java多线程并发处理:基础与实践
    本文探讨了Java中的多线程并发处理机制,从基本概念到实际应用,帮助读者全面理解并掌握多线程编程技巧。通过实例解析和理论阐述,确保初学者也能轻松入门。 ... [详细]
  • 嵌入式系统开发:外部中断详解
    本文深入探讨了外部中断的原理和应用,详细介绍了如何配置和使用外部中断,包括硬件设计、编程实现及调试技巧。 ... [详细]
author-avatar
伤心脑残猪_940
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有