defer 链如何被遍历
去年开始写文章的第一篇就是关于 defer,名字比较文艺:《Golang 之轻松化解 defer 的温柔陷阱》,还被吐槽了。因为这篇文章,到《Go 夜读》讲了一期。不过当时纯粹是应用层面的,也还没有跳进 Go 源码这个大坑,文章看着比较清新,也没有大段的源码解析。
自从听了曹大在《Go 夜读》分享的 Go 汇编,以及研读了阿波张的 Go 调度器源码分析的文章后,各种源码、汇编满天飞……
上次欧神写了一篇《Go GC 20 问》,全文也没有一行源码,整体读下来很畅快。今天这篇也来尝试一下这种写法,不过,我们先从一个小的主题开始:defer 链表是如何被遍历并执行的。
关于 defer 的源码分析文章,网络上也有很多。不过,很少有能完全说明白这个话题的,除了阿波张的。
我们知道,为了在退出函数前执行一些资源清理的操作,例如关闭文件、释放连接等。会在函数里写上多个 defer 语句,被 defered 的函数,以“先进后出”的顺序,在 RET
指令前得以执行。
在一条函数调用链中,多个函数中会出现多个 defer 语句。例如:a() -> b() -> c()
中,每个函数里都有 defer 语句,而这些 defer 语句会创建对应个数的 _defer
结构体,这些结构体以链表的形式挂在 goroutine
结构体下。看起来像这样:
在编译器的加持下,defer 语句会先调用 deferporc 函数,new 一个 _defer
结构体,挂到 g 上。当然,这里的 new 会优先从当前绑定的 P 的 defer pool 里取,没取到会去全局的 defer pool 里取,实在没有的话就新建一个,很熟悉的套路。
这样做好之后,等待函数体执行完,在 RET 指令之前(注意不是 return 之前),调用 deferreturn
函数完成 _defer
链表的遍历,执行完这条链上所有被 defered
的函数(如关闭文件、释放连接等)。这里的问题是在 deferreturn
函数的最后,会使用 jmpdefer
跳转到之前被 defered 的函数,这时控制权转移到了用户自定义的函数。这只是执行了一个被 defered 的函数,这条链上其他的被 defered 的函数,该如何得到执行呢?
答案就是控制权会再次交给 runtime,并再次执行 deferreturn 函数,完成 defer 链表的遍历。那这一切是如何完成的呢?
这就要从 Go 汇编的栈帧说起了。先看一个汇编函数的声明:
TEXT runtime·gogo(SB), NOSPLIT, $16-8
最后两个数字表示 gogo 函数的栈帧大小为 16B,即函数的局部变量和为调用子函数准备的参数和返回值需要 16B 的栈空间;参数和返回值的大小加起来是 8B。实际上 gogo 函数的声明是这样的:
// func gogo(buf *gobuf)
参数及返回值的大小是给调用者“看”的,调用者根据这个数字可以构造栈:准备好被调函数需要的参数及返回值。
典型的函数调用场景下参数布局图如下图:
左图中,主调函数准备好调用子函数的参数及返回值,执行 CALL
指令,将返回地址压入栈顶,相当于执行了 PUSH IP
,之后,将 BP 寄存器的值入栈,相当于执行了 PUSH BP
,再 jmp 到被调函数。
图中 return address
表示子函数执行完毕后,返回到上层函数中调用子函数语句的下一条要执行的指令,它属于 caller 的栈帧。而调用者的 BP 则属于被调函数的栈帧。
子函数执行完毕后,执行 RET
指令:首先将子函数栈底部的值赋到 CPU 的 BP 寄存器中,于是 BP 指向上层函数的 BP;再将 return address
赋到 IP 寄存器中,这时 SP 回到左图所示的位置。相当于还原了整个调用子函数的现场,像是一切都没发生过;接着,CPU 继续执行 IP 寄存器里的下一条指令。
再回到 defer 上来,其实在构造 _defer
结构体的时候,需要将当前函数的 SP、被 defered 的函数指针保存到 _defer
结构体中。并且会将被 defered 的函数所需要的参数 copy 到 _defer 结构体相邻的位置。最终在调用被 defered 的函数的时候,用的就是这时被 copy 的值,相当于使用了它的一个快照,如果此参数不是指针或引用类型的话,会产生一些意料之外的 bug。
最后,在 deferreturn 函数里,这些被 defered 的函数得以执行,_defer
链表也会被逐渐“消耗”完。
使用一个阿波张文章中的例子:
package main
import "fmt"
func sum(a, b int) {
c := a + b
fmt.Println("sum:" , c)
}
func f(a, b int) {
defer sum(a, b)
fmt.Printf("a: %d, b: %d\n", a, b)
}
func main() {
a, b := 1, 2
f(a, b)
}
执行完 f
函数时,最终会进入 deferreturn 函数:
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
......
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) // 移动参数
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
免不了还是要看一下代码,不然的话很难讲清楚。
因为我们是在遍历 _defer
链表,所以得有一个终止的条件:
d := gp._defer
if d == nil {
return
}
也就是当 _defer 链表为空的时候,终止遍历。在后面的代码里会看到,每执行完一个被 defered 的函数后,都会将 _defer 结构体从链表中删除并回收,所以 _defer 链表会越来越短。
switch
语句里要做的就是准备好被 defered 的函数(例子中就是 sum 函数)所需要的 a,b 两个 int 型参数。参数从哪来呢?从 _defer 结构体相邻的位置,还记得吗,这是在 deferproc 函数里 copy 过去的。deferArgs(d)
返回的就是当时 copy 的目的地址。那现在要拷贝到哪去呢?答案是:unsafe.Pointer(&arg0)
。我们知道,arg0 是 deferreturn 函数的参数,我们又知道,在 Go 汇编中,一个函数的参数是由它的主调函数准备的。因此 arg0 的地址实际上就是它的上层函数(在这里就是 f 函数)的栈上放参数的位置。
函数的最后,通过 jmpdefer
跳转到被 defered 的 sum 函数:
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
核心在于 jmpdefer 所做的事:
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // fn // defer 的函数的地址
MOVQ argp+8(FP), BX
LEAQ -8(BX), SP // caller sp after CALL
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use)
SUBQ $5, (SP) // return to CALL again
MOVQ 0(DX), BX
JMP BX // but first run the deferred function
首先将 sum 函数的地址放到 DX 寄存器中,最后通过 JMP 指令去执行。
MOVQ argp+8(FP), BX
LEAQ -8(BX), SP // caller sp after CALL // 执行 CALL 指令后 f 函数的栈顶
这两行实际上是调整了下当前 SP 寄存器的值,因为 argp+8(FP) 实际上是 jmpdefer 的第二个参数(它在 deferreturn 函数中),它指向 f 函数栈帧中的刚被 copy 过来的 sum 函数的参数。而 -8(BX)
就代表了 f 函数调用 deferreturn 的返回地址,实际上就是 deferreturn 函数的下一条指令地址。
接着,MOVQ -8(SP), BP
这条指令则重置了 BP 寄存器,使它指向了 f 栈帧 的 BP。这样,SP、BP 寄存器回到了 f 函数调用 deferreturn 之前的状态:f 刚准备好调用 deferreturn 的参数,并且把返回值压栈了。相当于抛弃了 deferreturn 函数的栈帧,不过,确实也没什么用了。
接着 SUBQ $5, (SP)
把返回地址减少了 5B,刚好是一个 CALL 指令的长度。什么意思?当执行完 deferreturn 函数之后,执行流程会返回到 CALL deferreturn
的下一条指令,将这个值减少 5B,也就又回到了 CALL deferreturn
指令,从而实现了“递归地”调用 deferreturn 函数的效果。当然,栈却不会在增长!
jmpdefer
函数的最后会执行 sum 函数,看起来就像是 f 函数亲自调用 sum 函数一样,参数、返回值都是就绪的。
等到 sum 函数执行完,执行流程就会跳转到 call deferreturn
指令处重新进入 deferreturn 函数,遍历完所有的 _defer 结构体,执行完所有的被 defered 的函数,才真正执行完 deferretrun 函数。
到这里,全文就结束了。我们可以看到,实现遍历 defer 链表的关键就是 jmpdefer 函数所做的一些“见不得人”的工作,将调用 deferreturn 函数的返回地址减少了 5 个字节,使得被 defered 的函数执行完后,又回到 CALL deferreturn
指令处,从而实现“递归地”调用 deferreturn 函数,完成 _defer 链表的遍历。
参考资料
【阿波张 defer 源码分析】https://mp.weixin.qq.com/s/iEtMbRXW4yYyCG0TTW5y9g
【阿波张 panic&recover】https://mp.weixin.qq.com/s/0JTBGHr-bV4ikLva-8ghEw
【阿波张 defer 基础】https://mp.weixin.qq.com/s/QmeQTONUuWlr_sRNP8b5Tw
【汇编分析】https://segmentfault.com/a/1190000019804120?utm_medium=referral&utm_source=tuicool
【曹大 Go 汇编分享】https://github.com/cch123/asmshare/blob/master/layout.md
【曹大 Go 汇编】https://xargin.com/plan9-assembly
【曹大利用汇编写的 goid 获取】https://github.com/cch123/goroutineid
defer 链如何被遍历的更多相关文章
- javascript中的对象,原型,原型链和面向对象
一.javascript中的属性.方法 1.首先,关于javascript中的函数/“方法”,说明两点: 1)如果访问的对象属性是一个函数,有些开发者容易认为该函数属于这个对象,因此把“属性访问”叫做 ...
- 职责链模式(Chain of Responsibility)的Java实现
职责链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系.将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它 ...
- PEB及LDR链
PEB地址的取得在NT内核系统中fs寄存器指向TEB结构,TEB+0x30处指向PEB结构,PEB+0x0c处指向PEB_LDR_DATA结构,PEB_LDR_DATA+0x1c处存放一些指向动态链接 ...
- 设计模式之第13章-职责链模式(Java实现)
设计模式之第13章-职责链模式(Java实现) “请假都那么麻烦,至于么.”“咋的了?”“这不快过年了么,所以我想早两天回去,准备一下,买买东西什么的,然后去给项目经理请假,但是他说快过年了,所以这个 ...
- 【PHP数据结构】图的遍历:深度优先与广度优先
在上一篇文章中,我们学习完了图的相关的存储结构,也就是 邻接矩阵 和 邻接表 .它们分别就代表了最典型的 顺序存储 和 链式存储 两种类型.既然数据结构有了,那么我们接下来当然就是学习对这些数据结构的 ...
- jquery插件扩展的学习
jquery插件的学习可以点击这里 举个例子 //首先先来一个插件 (function($){ $.fn.extent({ bigfont:function(){ return this.css('f ...
- 什么?你还不会写JQuery 插件
前言 如今做web开发,jquery 几乎是必不可少的,就连vs神器在2010版本开始将Jquery 及ui 内置web项目里了.至于使用jquery好处这里就不再赘述了,用过的都知道.今天我们来讨论 ...
- 写JQuery 插件 什么?你还不会写JQuery 插件
http://www.cnblogs.com/Leo_wl/p/3409083.html 前言 如今做web开发,jquery 几乎是必不可少的,就连vs神器在2010版本开始将Jquery 及ui ...
- RTTI的实现(vc)--转载
RTTI (Run Time Type info) 这个神奇的东西用于存储类的相关信息,用于在运行时识别类对象的信息.C++ 里面只记录的类的名字和类的继承关系链.使得编译成二进制的代码,对象可以知道 ...
随机推荐
- 签名旧版的pom文件
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven ...
- 良知VS野心,苹果为何要翻新手机?
前不久,苹果在大中华区推出了iPhone和iPad换机服务,消息一经发出便引发了果粉们的狂欢,那些丢弃在抽屉里的iPhone4S们看上去终于有着落了,也更坚定了"我是果粉,我骄傲" ...
- 导入项目@override 报错处理
将项目导入MyEclipse中后总有错:@override总是报错,没关系,不用着急,偶来告诉你解决办法. @override报错,鼠标放上去让你移除,是因为你的JDK版本太低,一般JDK是要在1.6 ...
- 使用apache mail发送邮件错误解决办法
今天在写发送邮件的程序时发现了以下两个些错误,贴出来跟大家分享分享 希望对大家有帮助. 错误一: Exception in thread "main" java.lang.NoCl ...
- 手机视频APP将关闭 生态梦成空的三星如何自救?
生态梦成空的三星如何自救?"> 三星如今的处境,只能用"屋漏偏逢连夜雨"来形容.继营收.利润.智能手机销量等大幅下滑之后,裁员也接踵而来,股价的下跌也自然在情理之中 ...
- 从846家初创倒下 看A轮融资后的悬崖
看A轮融资后的悬崖" title="从846家初创倒下 看A轮融资后的悬崖"> 相比往年,今年的寒冷冬天来得更早.在互联网行业,今年的"大雪"更 ...
- 10——PHP中的两种数组【索引数组】与【关联数组】
[索引数组] 用数字作为键名的数组一般叫做索引数组.用字符串表示键的数组就是下面要介绍的关联数组.索引数组的键是整数,而且从0开始以此类推. 索引数组初始化例: <pre name=" ...
- PyQt5之俄罗斯方块
上个礼拜有个需求,对csv里的数据按条件进行拆分计算.一想到要做计算,少不了pandas.还有个要求最好是生成命令行工具或者带有界面. 于是尝试下,使用PyQt5做了个简单的UI界面给程序包个壳子,然 ...
- C++ 迷宫寻路问题
迷宫寻路应该是栈结构的一个非常经典的应用了, 最近看数据结构算法应用时看到了这个问题, 想起来在校求学时参加算法竞赛有遇到过相关问题, 感觉十分亲切, 在此求解并分享过程, 如有疏漏, 欢迎指正 问题 ...
- JAVA 16bit CRC_CCITT
JAVA 16bit CRC_CCITT public class CRC_CCITT { static int CRC16_ccitt_table[] = { 0x0000, 0x1189, 0x2 ...