go源码阅读 defer
defer
这个话题有点大,只是翻阅了关键代码,细节太多了
主要defer
要弄明白两个函数,函数目录在/usr/local/go/src/runtime/panic.go
中的deferproc
和deferreturn
deferproc
//主要这里面都不能使用栈分裂,不能进行逃逸分析,因为defer是希望利用栈内存处理
//go:nosplit
func deferproc(siz int32, fn *funcval) {
...
d := newdefer(siz) // 最核心的就是这里
...
}
// 核心新建defer的逻辑
func newdefer(siz int32) *_defer {
//获取当前协程
gp := getg()
... // 这中间主要优化defer内存逻辑,看能不能复用
// 如果d==nil,说明上面没有获取有效大小,需要创建
if d == nil {
// 分配defer和其args的内存
systemstack(func() {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
})
if debugCachedWork {
// 为什么会有下面的逻辑没有看懂,跟括号外的代码重复了,看起来跟checkPut有关系
// 好像不需要这个判断也行。。。
// Duplicate the tail below so if there's a
// crash in checkPut we can tell if d was just
// allocated or came from the pool.
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
}
// 这部分代码 gp._defer 指向 新的_defer d,而 新的_defer d指向之前的 defer 链表
// 形成一个栈,这里面需要明白 defer的代码并没有直接执行,只是进行了压栈,
// 真正拿出来的执行的地方是deferreturn
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
最后链表代码示意图如下
代码追踪链路如下
deferreturn
func deferreturn(arg0 uintptr) {
// arg0其实是调用者的硬件寄存器sp地址
gp := getg()
...
// 抠出来fn准备执行,gp._defer往指向新的栈顶元素
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
// jmpdefer是使用汇编代码,对应不同的cpu会有不同的汇编
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
// 只拿这个asm_amd64.s来看
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // defer的函数地址
MOVQ argp+8(FP), BX // 获取到调用者的硬件寄存器sp(对应当前被调用者的FP),具体这个argp是得怎么来的,暂时还没看懂
LEAQ -8(BX), SP // 如果BX是调用者的sp(上一行),那么-8(BX)所保存的值就是调用者的返回pc
MOVQ -8(SP), BP // 恢复调用者BP寄存器,一开始一直想不明白为什么有这句,结合了下面的语句就明白了
SUBQ $5, (SP) // 将(SP) 减去5,是为了可以重新跳到runtime.deferreturn的位置,从而完成循环
// 5的计算,可以看下事例
MOVQ 0(DX), BX // 对应第一行 defer的函数地址
JMP BX // 运行defer函数
// 0x719 - 0x714 = 0x5,就是上面的减去5的原因
main.go:10 0x714 e800000000 CALL 0x719 R_CALL:runtime.deferreturn
main.go:10 0x719 488bac2480000000 MOVQ 0x80(SP), BP
main.go:10 0x721 4881c488000000 ADDQ $0x88, SP
自问自答
一. defferretunrn
运行时的argp
(调用者的sp)是怎么得到的,从汇编上面看确实.go
并没有实现
如果这个问题能弄明白,那么配合下面的图就能理清楚关系了
二. deferreturn、deferproc成对出现?
实际我观察到的并不是成对出现,网上好多解释都有问题,实际应该是len(deferreturn) = len(deferproc) + 1
注意deferproc
的代码块里面的注释
func deferproc () {
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
- 多个
deferproc
都是正常执行return 0
的时候,那么实际跟上文解释的一样,只需要一个deferreturn
就可以完成所有循环执行defer
函数了(为什么多出来的一个) - 当
deferproc
发生return 1
,就会跳到对应成对的deferreturn
;发生return 1
的情况主要是这个defer stop a panic
例子
// main.go
package main
import "fmt"
func test_defer() int {
t := 5
defer fmt.Printf("%d\n", 10)
t += 4
defer fmt.Printf("%d\n", 20)
return t
}
// 编译
GOSSAFUNC=test_defer /usr/local/go/pkg/tool/linux_amd64/compile -o ./_pkg_.a ./main.go
// objdump
/usr/local/go/pkg/tool/linux_amd64/objdump _pkg_.a > 1.txt
...
main.go:7 0x5ee e800000000 CALL 0x5f3 [1:5]R_CALL:runtime.deferproc
...
main.go:9 0x65f e800000000 CALL 0x664 [1:5]R_CALL:runtime.deferproc
main.go:9 0x664 85c0 TESTL AX, AX
main.go:9 0x666 751c JNE 0x684
main.go:10 0x668 48c784248000000009000000 MOVQ $0x9, 0x80(SP)
main.go:10 0x674 90 NOPL
main.go:10 0x675 e800000000 CALL 0x67a [1:5]R_CALL:runtime.deferreturn
main.go:10 0x67a 488b6c2470 MOVQ 0x70(SP), BP
main.go:10 0x67f 4883c478 ADDQ $0x78, SP
main.go:10 0x683 c3 RET
main.go:9 0x684 90 NOPL
main.go:9 0x685 e800000000 CALL 0x68a [1:5]R_CALL:runtime.deferreturn
main.go:9 0x68a 488b6c2470 MOVQ 0x70(SP), BP
main.go:9 0x68f 4883c478 ADDQ $0x78, SP
main.go:9 0x693 c3 RET
main.go:7 0x694 90 NOPL
main.go:7 0x695 e800000000 CALL 0x69a [1:5]R_CALL:runtime.deferreturn
main.go:7 0x69a 488b6c2470 MOVQ 0x70(SP), BP
main.go:7 0x69f 4883c478 ADDQ $0x78, SP
main.go:7 0x6a3 c3 RET
main.go:5 0x6a4 e800000000 CALL 0x6a9 [1:5]R_CALL:runtime.morestack_noctxt
main.go:5 0x6a9 e9affeffff JMP %22%22.test_defer(SB)
长度有限,所以只截取了其中一部分,注意每一个[1:5]R_CALL:runtime.deferproc
后续都有一个TESTL AX AX
例如上面的第二个deferproc
main.go:9 0x664 85c0 TESTL AX, AX
main.go:9 0x666 751c JNE 0x684
AX
就是deferproc
的返回值,如果不为0
,那么跳转到JNE 0x684
就是其成对出现的deferreturn
main.go:10 0x668 48c784248000000009000000 MOVQ $0x9, 0x80(SP)
对于源码中的t:=4 ;t = t + 5
直接被优化成了t = 9
三. stop a panic 是啥意思?
这地方一开始我死活没有看懂,后来我看了这篇文章才明白了,类似defer func()
,其实只是调用deferproc
将函数挂到了g_defer
,等到defereturn
的时候,pc
寄存器重新指向了func()
代码的位置,正常都是ok
但是当发生panic
的时候,defer func(){recover()}
,为了恢复剩下的defer
流程,pc
寄存器会指向当前 defer
的下一行,也就是TESTL AX, AX
,代码可以在recover
里面找到
func recovery(gp *g) {
....
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}
这里面还有一些细节可能需要放到后面去扣,例如当发生panic
的时候(对应painic.go
的函数gopanic
),会直接运行当前协程的defer
函数链表,如果存在调用recovery
(对应panic.go函数gorecover
)进行恢复,当执行逻辑回到gopainic
的时候,会调用runtime.recover
(就是上面的代码)切换上下文
四. 混淆的点
一直反复混淆的点主要是在于deferreturn
执行完某个fn
过后,是如何回到deferreturn
主要有一点要记住,defer func(){}
这个func()
是有RET
的,RET
的过后需要重新拿到parent
的pc
,这个时候被SUBQ $5, (SP)
偷偷给换了