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.
}
  1. 多个deferproc都是正常执行return 0的时候,那么实际跟上文解释的一样,只需要一个deferreturn就可以完成所有循环执行defer函数了(为什么多出来的一个)
  2. 当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) 偷偷给换了

参考

Golang: 深入理解panic and recover

4.3 defer 浅谈Go语言实现

3.6 再论函数 Go语言高级编程