> ## Content Index
> Fetch the complete content index at: https://blog.hacking.pub/llms.txt
> Use this file to discover other available public pages before exploring further.

# go源码阅读 defer
- URL: https://blog.hacking.pub/2019/07/24/goyuan-ma-yue-du-defer/
- Published: 2019-07-24T15:13:15.000Z
- Updated: 2019-07-24T15:13:15.000Z
- Author: chainhelen
- Tags: go, go语言, go源码, golang, 源码剖析, 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
}

```

最后链表代码示意图如下

![](https://blog.hacking.pub/qiniu/go%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%20defer/test_defer.svg)

代码追踪链路如下

![](https://blog.hacking.pub/qiniu/go%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%20defer/test_defer2.svg)

###### 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`并没有实现

如果这个问题能弄明白，那么配合下面的图就能理清楚关系了

![](https://blog.hacking.pub/qiniu/go%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%20defer/test_defer3.svg)

二. 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 是啥意思?

这地方一开始我死活没有看懂，后来我看了这篇[文章](https://ieevee.com/tech/2017/11/23/go-panic.html?ref=blog.hacking.pub)才明白了，类似`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](https://ieevee.com/tech/2017/11/23/go-panic.html?ref=blog.hacking.pub)
> 
> [4.3 defer 浅谈Go语言实现](https://draveness.me/golang/keyword/golang-defer.html?ref=blog.hacking.pub)
> 
> [3.6 再论函数 Go语言高级编程](https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-06-func-again.html?ref=blog.hacking.pub)