函数调用栈

2023-11-02

函数调用栈

我们在编程中写的函数,会被编译器编译为机器指令,写入可执行文件,程序执行的时候,会把这个可执行文件加载到内存,在虚拟地址空间中的代码段存放。

如果在一个函数中调用另一个函数,编译器就会对应生成一条call指令,当程序执行到这条call指令时,就会跳到对应的函数入口处开始执行,而每一个函数的最后,都有一条ret指令,负责在函数结束后跳回到调用处继续执行。

image-20220210201148148

栈区

函数执行的时候需要有足够的内存空间来存放局部变量,参数,返回值等数据,这些数据存在上图中的栈中。

栈就是先入后出,先入栈的在底部。

在虚拟地址空间的栈区,上面的是高地址,下面是低地址,放了一些数据,栈底通常称为栈基,栈顶又叫栈指针

具体的栈帧布局是:

  • 调用者栈基地址(也就是谁调用了这个函数)
  • 局部变量
  • 调用函数的返回值
  • 参数

通过栈指针加上偏移来定位到每个参数和返回值。

比如栈指针+8字节处,就是栈指针的上一格,通过这种方式来进行偏移。

image-20220210202009209

还记得我们之前说当在A函数中调用B函数时,会在A函数中插入一条call指令,当执行到call指令的时候,会去B函数开始处运行。

那么call指令做的事情就是:

  1. 首先把A函数中下一条指令的地址入栈(栈基地址,当B函数执行完之后,可以再通过这个地址回到A函数的调用处继续执行A函数。)
  2. 跳转到被调用函数的入口处执行(也就是被调用函数的栈帧,而所有的函数栈帧布局都遵循统一的结构约定。)

image-20220210204301454

栈具体的入栈策略

程序执行时,CPU通过特定的寄存器来存运行时的栈基和栈指针,也有指令指针寄存器用来存储下一条要执行的指令地址。

执行指令的过程有两种,第一种是逐步扩张:

  • 如果要执行入栈3这条指令,CPU读取之后,会先把指令指针移向下一条指令,然后栈指针向下移动,入栈数字3。
  • 然后再执行入栈4这条指令,CPU读取之后,再把指令指针移向下一条指令,然后栈指针向下移动,入栈数字4。
  • 一直往复。

image-20220210210301866

Go语言中的是第二种——一次性分配,它会直接将栈指针移动到所需最大栈空间的位置,然后通过右边这种相对寻址的方式,来把对应的值入栈。

image-20220210214502183

Go语言选择使用一次性分配的策略是有原因的,拿下图来讲,下面三个goroutine,初始分配的栈空间只有那么大,如果要逐步扩张的话,如果g2执行到最后了,但是接下来要执行的函数又要用掉很多的空间,如果函数栈是逐步扩张的,执行时就可能会发生栈访问越界。

函数栈帧的大小可以在编译时期确定, 对于栈消耗大的函数,Go编译器会在函数头部插入检测代码,如果发现需要进行栈增长,则会另外分配一段足够大的空间,然后把原来的内容移过来,并释放原来的空间。

image-20220210214938132

call和ret的细节

首先我们可以看到,下面是栈区代码段

当代码段执行到对应的指令时,就会给栈中添加对应的元素,最终再把栈全部出栈。

假如说,我们是在函数A中的a1处调用函数B(函数B开始位置为b1)。

首先,在最开始的时候,寄存器在栈中的情况是这样的:

image-20220211215127647

ip寄存器中存的是下一条要运行的指令,那么当我们的代码段运行到a1的call指令时,会做两件事:

首先会入栈返回地址a2,然后栈指针sp向下一格,然后给ip寄存器b1的指令地址,接下来要去B函数的开始处运行。

call指令就结束了。

image-20220211215327924

接下来就要运行四步函数都要做的事:

  • 第一步是先把栈指针sp移动到足够大的位置——s7上。
  • 第二步是存储一下之前栈基bp寄存器的值,这样可以在运行完之后,还能回到原来的栈基地址。
  • 第三步是把s5存入栈基地址。
  • 接下来就要做函数剩下的指令了——参数,代码等,并一一入栈。

image-20220211220131455

在函数B运行到最后——ret指令之前,编译器还会插入两条指令:

  • 恢复调用者栈基。最开始我们分配了多少空间,此时就释放多少空间,修改bp寄存器为之前入栈的s1,bp继续指向s1处。
  • 然后就到ret指令了,它首先会弹出call指令压栈的返回地址a2,sp赋值为s3。然后跳转到这个返回地址a2,把ip寄存器赋值为a2。 接下来可以从a2这里继续执行了。

image-20220211220405112

简单来说,call指令会分配栈帧,ret指令又会释放栈帧,恢复到call之前的样子。通过这些指令的配合,就能实现函数的层层嵌套了。

image-20220211220920122

函数传参和返回值

首先看一个例子,下面这个例子是交换两个局部变量的值,可以看到,结果并没有改变:

image-20220211221812987

上面那个函数在栈中的分配如下:

  • 首先分配局部变量的空间,然后把局部变量存进去。
  • 然后分配被调用函数的参数,从右至左分配。先入栈第二个参数,再入栈第一个参数。传参是值拷贝,所以把两个参数的值压入栈。
  • 接下来是call指令存入的返回地址。也就是fmt.Println这一行代码所对应的指令
  • 再接下来就是swap函数栈帧了

image-20220211222902684

当swap函数执行到a,b=b,a时,就会修改参数对应的值,但是调用者的局部变量a和b在上面,交换的并不是它们,所以最终结果显示没有交换成功。

image-20220211223513405

我们再修改一下:

还是和上一次的一样,只是我们把指针作为参数,函数参数还是值类型,所以会拷贝两个地址的值。

再swap函数中,会将对应地址的值进行交换,修改的是调用者的局部变量ab,所以最终修改成功。

image-20220211223653267

通常,返回值是通过寄存器传递的,但是Go语言支持多返回值,所以在栈上分配返回值更合适。

接下来我们看一个有返回值的例子:

  1. 一次性分配main函数栈帧,sp直接到达对应的位置。

  2. 把局部变量压入栈

  3. 压入栈函数返回值(默认为0)——因为栈是先入后出的缘故,所以一个函数的执行步骤要从后往前的压入栈。

  4. 把函数参数压入栈

  5. 保存调用者函数main的栈基地址以方便最后回到main函数。

  6. 接下来进入函数incr的函数栈帧。 首先初始化局部变量b,默认为0,

  7. 然后执行a++指令,把局部变量a的值加1.

  8. 运行到b=a的指令,把参数a赋给局部变量b。

  9. 接下来就是返回值和defer函数的问题。 在Go语言中,是先给返回值赋值,然后再执行defer函数。

image-20220211224655112

  1. 把局部变量b的值,拷贝到返回值空间。
  2. 执行注册的defer函数,在defer函数中,参数a再次自增1,局部变量b也加1。但是需要注意的是它让局部变量b自增1,不代表就把返回值自增1,因为在defer之前,已经给返回值赋值过了,可以看下图,b的值是2,但是返回值还是1.

image-20220211225218764

  1. 把返回值给局部变量b
  2. 输出a,b——0,1

image-20220211225300156

接下来我们看这个例子,用的是命名返回值:

  1. main函数的栈情况还是如下图右边所示。

image-20220211225605954

  1. 接下来会运动到incr函数的a++指令,然后把参数a的值加1.
  2. return a指令赋值a变量的值给返回值局部变量b,此时b=a=1.
  3. 运行defer函数,a++,b++,a和b都是2.
  4. 此时返回值的位置为2,所以会把main函数中的局部变量b赋值为2.
  5. 打印0,2

image-20220211225444411

当函数A中调用函数B和函数C时,栈的寻址策略
  1. 首先分配A函数的局部变量空间。

  2. 因为后面有两个函数要执行,又因为Go是一次性分配空间的,所以会分配最大的参数和返回值空间,函数B比函数C的空间要大,就以函数B所需要的空间标准来分配,如下图r2~p1这么大的空间。

  3. 接下来把函数B的参数和返回值压入栈,进入函数B的栈帧。

image-20220211230158564

  1. 当函数B执行完毕之后,会释放这两片空间。把函数C的参数和返回值压入栈,但是此时空间还是那么大,r1p1是存在这片空间的上面,还是下面,还是中间?

image-20220211230543070

最终的答案就是,会把r1p1分配到最下面,和函数C的栈指针挨着,这样虽然上面会空出来一块,但是被调用者通过栈指针相对寻址自己的参数和返回值时会比较方便。

image-20220211230624897

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

函数调用栈 的相关文章

  • golang.org 包和标准库之间的区别

    我使用 go 已经有一段时间了 我注意到 Go 标准库 和 golang org x 之间存在重复的包 我的问题是 为什么它们被释放两次 在这两者中 我应该使用哪一个 更新的 规范的等 到目前为止我注意到的一些示例包已发布两次 golang
  • Golang 结构体初始化

    有一个像这样的简单结构 type Event struct Id int Name string 这两种初始化方法有什么区别呢 e1 Event Id 1 Name event 1 e2 Event Id 2 Name event 2 为什
  • 有没有办法间歇性地执行重复性任务?

    有没有办法在 Go 中执行重复的后台任务 我在想类似的事情Timer schedule task delay period 在爪哇 我知道我可以用 goroutine 来做到这一点Time sleep 但我想要一些容易停止的东西 这是我得到
  • 关闭长度未知的通道

    当不了解频道时我无法关闭频道 length package main import fmt time func gen ch chan int var i int for time Sleep time Millisecond 10 ch
  • 关于编写惯用的 Golang 的建议

    我正在掌握 Golang 的做事方式 首先是一些示例代码 package main import log os func logIt s string f os OpenFile errors log os O RDWR os O CREA
  • go json marshal 的默认大小写选项?

    我有以下结构要导出为 json type ExportedIncident struct Title string json title Host string json host Status string json status Dat
  • 运行最新版本时没有“最新”消息?

    我正在尝试使用Sparkle https sparkle project org与 Qt Go 的绑定 https github com therecipe qt app 闪光 m import
  • 云存储 API 的错误导入“系统调用”

    我正在按照以下说明进行操作https cloud google com appengine docs go googlecloudstorageclient download开始将一些代码从现已弃用的文件 API 迁移到新的 Cloud S
  • 如何读取 UDP 连接直至超时?

    我需要读取 UDP 流量 直到超时 我可以通过在 UDPConn 上调用 SetDeadline 并循环直到出现 I O 超时错误来做到这一点 但这看起来很黑客 基于错误条件的流量控制 下面的代码片段看起来更正确 但并没有终止 在生产中 这
  • 从 Golang 调用 C 函数

    我想在 Golang 中编写控制器逻辑并处理 json 和数据库 同时在 C 中使用我的数学处理模型 在我看来 调用 C 函数的开销必须尽可能低 就像设置寄存器 rcx rdx rsi rdi 一样 执行一些操作fastcall 并获取 r
  • Golang:如何在HTTP客户端的TLS配置中指定证书

    我有一个证书文件 该位置是 usr abc my crt我想将该证书用于我的 tls 配置 以便我的 http 客户端在与其他服务器通信时使用该证书 我当前的代码如下 mTLSConfig tls Config CipherSuites u
  • ioutil.ReadFile - 没有这样的文件或目录[重复]

    这个问题在这里已经有答案了 对于以下代码 package main import fmt io ioutil strings func main b err ioutil ReadFile shakespeare txt if err ni
  • 如何在GO中执行HEAD请求?

    我想使用 GO net http 获取页面的内容长度 我可以在终端中使用curl i X HEAD https golang org然后检查内容长度字段 use http Head https golang org pkg net http
  • 我使用go语言打印到POS打印机,但打印出的中文字符很糟糕。 (英语没问题)

    我在用着this https github com alexbrainman printer去打包 一切都很好 但汉字打印得很差 我不擅长这种低级 硬件 编码的事情 所以有人提示我如何纠正我的代码 更新代码 func main testWi
  • 如何访问主包之外的标志?

    We 解析标志 http golang org pkg flag FlagSet Parse当然 在 main 包中的 main go 中 然后我们有另一个包 我们想在其中读取一些标志的值 flags Args http golang or
  • 是否可以将 Go 代码作为脚本运行?

    随着Go正在成为 系统 的语言 我想知道是否可以将 Go 代码作为脚本运行而不编译它 有可能这样做吗 动机 因为有关于动机的问题 取自如何使用 Scala 作为脚本语言 http alvinalexander com scala how t
  • 带有导出字段的私有类型

    在 Go 教程的第二天有这样的练习 为什么拥有带有导出字段的私有类型会很有用 例如 package geometry type point struct X Y int name string 请注意point是小写的 因此不会导出 而字段
  • 在 Go 中解析 RFC-3339 / ISO-8601 日期时间字符串

    我尝试解析日期字符串 2014 09 12T11 45 26 371Z 在围棋中 该时间格式定义为 RFC 3339 日期时间 https datatracker ietf org doc html rfc3339 section 5 6
  • 限制 FormFile 中的文件大小

    我让用户使用 FormFile 上传文件 我应该在什么时候检查文件大小是否太大 当我做 file header fileErr r FormFile file 文件对象已经创建 那么我是否已经产生了读取整个文件的成本 https golan
  • 在 Go 中,如何将函数的 stdout 捕获到字符串中?

    例如 在 Python 中 我可以执行以下操作 realout sys stdout sys stdout StringIO StringIO some function prints to stdout get captured in t

随机推荐

  • Mysql8完全卸载教程

    0 备份所有需要保留的数据库 在Navicat中选择对应库表选择导出为Sql文件即可 如果卸载数据库的来由是因为数据库已经无法正常使用打开所以需要卸载数据库进行重新安装的话可以跳过这一步 1 停止所有Mysql相关服务 Win11直接搜索服
  • 农作物地块范围识别(图像分割)

    来源 机器学习AI算法工程 知乎 Now more 本文约5500字 建议阅读15分钟本文为你介绍 以薏仁米作物识别以及产量预测为比赛命题 及对对应获奖的开发算法模型 农作物的资产盘点与精准产量预测是实现农业精细化管理的核心环节 当前 我国
  • Unity实现全景图

    1 前言 实现全景查看有两种方式 一种是创建天空盒 另外一种是全景图渲染到一个球形上 从内球内部看 两者都可以用天空盒材质Skybox实现 2 创建天空盒材质 先创建一个材质球命名为Sky 在 Shader 下拉选单中 单击 Skybox
  • 【JavaScript 逆向】521 加速乐多层响应 Cookie 逆向

    前言 现在一些网站对 JavaScript 代码采取了一定的保护措施 比如变量名混淆 执行逻辑混淆 反调试 核心逻辑加密等 有的还对数据接口进行了加密 这次的案例是对加速乐 AAEncode OB 混淆方式的破解 声明 本文章中所有内容仅供
  • Hadoop使用

    1 常用命令 1 namenode格式化 hdfs namenode format 2 启动历史服务器 mapred daemon start historyserver 或者mr jobhistory daemon sh stop his
  • Git仓库版本的回退/前进

    前言 版本库做为一个可迭代更新的仓库 不可能只是单纯提供版本更新操作 其最重要的还是提供了历史版本回滚功能 在使用版本回滚功能前 我们会使用一系列的辅助查询命令 比如查看版本信息 仓库状态能命令 辅助是否需要进行回滚操作 一 查看版本信息
  • 运算放大器的应用(一)放大器

    在硬件电路设计中 运算放大器的应用非常广泛 今天来记录一下作为放大器的应用和公式推导 放大器典型电路 根据放大器基本原理 虚短 特性可知 U0 U1 根据 虚断 特性可知 U1点电流近似为0 则流过R1的电流等于流过R2的电流 根据欧姆定律
  • Photoshop 2023 Beta 内置Ai绘图功能介绍&安装教程

    距离Adobe软件公司首次将图像编辑及数字绘画软件Photoshop推出到大众面前已经过去35年 最近该公司又再次书写了属于Photoshop的历史新篇章 最近 Adobe 宣布 Photoshop Beta 迎来更新 新增 创意填充 Ge
  • linux下使用selenium

    1 需要linux安装chrome浏览器 安装命令 yum localinstall google chrome stable current x86 64 rpm 2 得的谷歌浏览器版本号 根据版本号选择最接近的内核版本 linux命令
  • 反射获取类的成员变量值

    public class ClassFieldTest public ClassFieldTest List
  • machine unlearning 论文阅读笔记

    文章目录 前言 概要 导论 unlearning 定义 SISA 系统 时间复杂度 实验测试 总结 前言 做一篇发表在顶会S P 4 2 n d 42 nd
  • Connection reset by peer

    今天升级完openssh8 6以后突然发现无法连接服务器了 注意前提是升级成功了 连接时报错如下 Connection reset by peer 解决方案 使用root执行命令 echo sshd ALL gt gt etc hosts
  • 【mcuclub】水流量检测模块-YF-S401

    一 实物图 型号 YF S401 二 原理图 编号 名称 功能 1 VCC 电源正 红色线 2 GND 电源地 黑色线 3 OUT 输出引脚 黄色线 三 简介 水流量传感器主要由塑料阀体 水流转子组件和霍尔传感器组成 它装在电器的进水端 用
  • JVM--调优--03--开发配置

    JVM 调优 03 开发配置 1 本地配置 Xmx1g Xms1g Xmn900m XX UseG1GC Xloggc gc log XX PrintGCDetails 2 linux tomcat 配置 进入tomcat的bin目录 修改
  • 软工作业 双人项目代码规范

    双人项目代码编写规范 蔡东杰 房林尧 以下共分为四个部分 缩进代码 命名规则 代码注释 异常处理 部分规范已做实例说明 后续编代码时需要严格遵守 一 关于缩进代码 一行里面只写一行代码 包括定义变量 一行只定义一个 左括号和后一个字符之间不
  • Java分支和循环语句及方法的使用

    1 分支语句No3 顺序语句 分支语句 循环语句 1 1 分支语句 选择结构 分支语句满足某种条件则执行某一部分 满足另一种条件则执行另一部分 1 2 if语句 简单分支语句 if boolean类型表达式 语句 如果小括号中的表达式成立
  • STM32中断知识点简介

    中断服务函数存放路径为启动文件 startup stm32f10x hd 1 常用的中断服务函数 外部中断服务函数 EXTI0 IRQHandler EXTI0 IRQn EXTI1 IRQHandler EXTI1 IRQn EXTI2
  • jenkins默认会存放目录

    jenkins默认会存放在用户主目录下的 jenkins文件夹中 如 Linux root用户 root jenkins 注意 这是linux版本的 windows系统请自行更改 这个值在Jenkins运行时是不能更改的 请先将Jenkin
  • 自定义coco数据集

    1 环境 anaconda环境安装配置 2 工具 安装labelme工具 3 安装软件 3 1 打开anaconda控制台 3 2 创建虚拟环境 conda create n labelme python 3 7 3 3 激活环境 cond
  • 函数调用栈

    函数调用栈 我们在编程中写的函数 会被编译器编译为机器指令 写入可执行文件 程序执行的时候 会把这个可执行文件加载到内存 在虚拟地址空间中的代码段存放 如果在一个函数中调用另一个函数 编译器就会对应生成一条call指令 当程序执行到这条ca