您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

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

时间:2022-06-09 11:03:58  来源:  作者:linux上的码农

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

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


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

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

你知道协程的本质是什么吗?有的同学可能会说是用户态线程,那么什么是用户态线程,这是怎么实现的?3. 函数运行起来后在内存中是什么样子?这几个问题看似没什么关联,但这背后都指向一样东西,这就是所谓的函数运行时栈run time stack。接下来我们就好好看看到底什么是函数运行时栈,为什么彻底理解函数运行时栈对程序员来说非常重要。

 

从进程、线程到函数调用

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

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

其中,我们创造了进程、线程这样的概念来记录有哪些程序正在运行,进程和线程的运行体现在函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回。其中函数内部的顺序执行乏善可陈,重点是函数的调用。因此接下来我们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的。

 

更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

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

 


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

 

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

函数执行的活动轨迹:栈

玩过游戏的同学应该知道,有时你为了完成一项主线任务不得不去打一些支线的任务,支线任务中可能还有支线任务,当一个支线任务完成后退回到前一个支线任务,这是什么意思呢,举个例子你就明白了。假设主线任务西天取经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个问题我们没有解答,这个问题的讲解放到下一篇,也就是协程中讲解。



Tags:函数   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
一篇文章教会你使用Python中三种简单的函数
所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。一、函数简介所谓函数,就是指:把某些特定功能的代码组成为一个整体,这个整体就叫做函数。二、函数定义...【详细内容】
2024-04-11  Search: 函数  点击:(12)  评论:(0)  加入收藏
原来 SQL 函数是可以内联的!
介绍在某些情况下,SQL 函数(即指定LANGUAGE SQL)会将其函数体内联到调用它的查询中,而不是直接调用。这可以带来显著的性能提升,因为函数体可以暴露给调用查询的规划器,从而规划器...【详细内容】
2024-04-03  Search: 函数  点击:(7)  评论:(0)  加入收藏
十个前端冷门但好用的前端工具函数库
本文推荐十个冷门但好用的前端工具函数仓库,它们可能没有很高的知名度,但却能为你解决实际问题,提高开发效率。在前端开发中,有时候我们会遇到一些常见的功能需求,但却不知道从哪...【详细内容】
2024-02-27  Search: 函数  点击:(23)  评论:(0)  加入收藏
Go函数指针是如何让你的程序变慢的?
导读Go 语言的常规优化手段无需赘述,相信大家也能找到大量的经典教程。但基于 Go 的函数值问题,业界还没有太多深度讨论的内容分享。本文作者根据自己对 Go 代码的使用与调优...【详细内容】
2024-01-15  Search: 函数  点击:(92)  评论:(0)  加入收藏
C++中使用宏定义一个函数:灵活性与风险并存
在C++编程中,宏是一种强大的预处理指令,可以用于定义函数。本文将探讨如何使用宏定义函数,并分析其优势和潜在风险。通过理解这些内容,程序员可以更加明智地决定是否使用宏来定...【详细内容】
2023-12-25  Search: 函数  点击:(165)  评论:(0)  加入收藏
JavaScript的作用域、闭包、高阶函数、柯里化、函数缓存和纯函数
作用域就是当前执行环境的上下文,它限制了变量、函数的有效范围。在当前作用域下声明的变量、函数只能在当前作用域内以及它嵌套的子作用域内有效。这样避免变量和函数的命名...【详细内容】
2023-12-15  Search: 函数  点击:(148)  评论:(0)  加入收藏
深度解读 JS 构造函数、原型、类与继承
本文深入浅出地讨论了 JavaScript 构造函数、原型、类、继承的特性和用法,以及它们之间的关系。希望看完本文,能帮助大家对它们有更加清晰通透的认识和掌握!01、前言众所周知,Ja...【详细内容】
2023-12-07  Search: 函数  点击:(249)  评论:(0)  加入收藏
一篇搞懂 toString()函数与valueOf()函数
tostring 和 valueOf 函数是解决值的显示和运算的问题。所有的 Object 类型的数据都自带 toString 和 valueOf 函数。比如我们定一个 Object 类型数据:const obj = { name...【详细内容】
2023-12-06  Search: 函数  点击:(210)  评论:(0)  加入收藏
Python的函数递归与调用,你会吗?
Python中的函数递归是一种函数调用自身的编程技术。递归可以用来解决问题,特别是那些可以分解为更小、相似子问题的问题。一、函数递归的基本概念1、什么是函数递归?函数递归...【详细内容】
2023-12-04  Search: 函数  点击:(124)  评论:(0)  加入收藏
MySQL的常用函数,你学会几个?
在开发中,数据库的种类千奇百怪,有各种,比如早期的 SQLServer,Mysql,Oracle,现在还有许多国产的数据库,但是有不少开发还是使用的 Mysql,但是对于 Mysql 中的各种各样的函数,用的却是...【详细内容】
2023-11-30  Search: 函数  点击:(128)  评论:(0)  加入收藏
▌简易百科推荐
Meta如何将缓存一致性提高到99.99999999%
介绍缓存是一种强大的技术,广泛应用于计算机系统的各个方面,从硬件缓存到操作系统、网络浏览器,尤其是后端开发。对于Meta这样的公司来说,缓存尤为重要,因为它有助于减少延迟、扩...【详细内容】
2024-04-15    dbaplus社群  Tags:Meta   点击:(2)  评论:(0)  加入收藏
SELECT COUNT(*) 会造成全表扫描?回去等通知吧
前言SELECT COUNT(*)会不会导致全表扫描引起慢查询呢?SELECT COUNT(*) FROM SomeTable网上有一种说法,针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小...【详细内容】
2024-04-11  dbaplus社群    Tags:SELECT   点击:(3)  评论:(0)  加入收藏
10年架构师感悟:从问题出发,而非技术
这些感悟并非来自于具体的技术实现,而是关于我在架构设计和实施过程中所体会到的一些软性经验和领悟。我希望通过这些分享,能够激发大家对于架构设计和技术实践的思考,帮助大家...【详细内容】
2024-04-11  dbaplus社群    Tags:架构师   点击:(2)  评论:(0)  加入收藏
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(5)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(9)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(16)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(14)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(9)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(14)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(10)  评论:(0)  加入收藏
站内最新
站内热门
站内头条