go源码阅读 defer

defer

这个话题有点大,只是翻阅了关键代码,细节太多了

主要defer要弄明白两个函数,函数目录在/usr/local/go/src/runtime/panic.go中的deferprocdeferreturn

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的过后需要重新拿到parentpc,这个时候被SUBQ $5, (SP)偷偷给换了

参考

Golang: 深入理解panic and recover

4.3 defer 浅谈Go语言实现

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