NOTHING

好读码,不求甚解;每有会意,便欣然忘食。
HOME GITHUB

defer 源码阅读 (go 1.13)

 直接上代码

package main

import "fmt"

func main() {
   defer func() {
       fmt.Println("defer")
   }()
}

反编译
"".main STEXT size=113 args=0x0 locals=0x48
	0x0000 00000 (defer.go:5)	TEXT	"".main(SB), ABIInternal, $72-0
	0x0000 00000 (defer.go:5)	MOVQ	TLS, CX
	0x0009 00009 (defer.go:5)	MOVQ	(CX)(TLS*2), CX
	0x0010 00016 (defer.go:5)	CMPQ	SP, 16(CX)
	0x0014 00020 (defer.go:5)	JLS	106
	0x0016 00022 (defer.go:5)	SUBQ	$72, SP
	0x001a 00026 (defer.go:5)	MOVQ	BP, 64(SP)
	0x001f 00031 (defer.go:5)	LEAQ	64(SP), BP
	0x0024 00036 (defer.go:5)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0024 00036 (defer.go:5)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0024 00036 (defer.go:5)	FUNCDATA	$2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x0024 00036 (defer.go:6)	PCDATA	$0, $0
	0x0024 00036 (defer.go:6)	PCDATA	$1, $0
	0x0024 00036 (defer.go:6)	MOVL	$0, ""..autotmp_1+8(SP)
	0x002c 00044 (defer.go:6)	PCDATA	$0, $1
	0x002c 00044 (defer.go:6)	LEAQ	"".main.func1·f(SB), AX
	0x0033 00051 (defer.go:6)	PCDATA	$0, $0
	0x0033 00051 (defer.go:6)	MOVQ	AX, ""..autotmp_1+32(SP)
	0x0038 00056 (defer.go:6)	PCDATA	$0, $1
	0x0038 00056 (defer.go:6)	LEAQ	""..autotmp_1+8(SP), AX
	0x003d 00061 (defer.go:6)	PCDATA	$0, $0
	0x003d 00061 (defer.go:6)	MOVQ	AX, (SP)
	0x0041 00065 (defer.go:6)	CALL	runtime.deferprocStack(SB)
	0x0046 00070 (defer.go:6)	TESTL	AX, AX
	0x0048 00072 (defer.go:6)	JNE	90
	0x004a 00074 (defer.go:9)	XCHGL	AX, AX
	0x004b 00075 (defer.go:9)	CALL	runtime.deferreturn(SB)
	0x0050 00080 (defer.go:9)	MOVQ	64(SP), BP
	0x0055 00085 (defer.go:9)	ADDQ	$72, SP
	0x0059 00089 (defer.go:9)	RET
	0x005a 00090 (defer.go:6)	XCHGL	AX, AX
	0x005b 00091 (defer.go:6)	CALL	runtime.deferreturn(SB)
	0x0060 00096 (defer.go:6)	MOVQ	64(SP), BP
	0x0065 00101 (defer.go:6)	ADDQ	$72, SP
	0x0069 00105 (defer.go:6)	RET
	0x006a 00106 (defer.go:6)	NOP
	0x006a 00106 (defer.go:5)	PCDATA	$1, $-1
	0x006a 00106 (defer.go:5)	PCDATA	$0, $-1
	0x006a 00106 (defer.go:5)	CALL	runtime.morestack_noctxt(SB)
	0x006f 00111 (defer.go:5)	JMP	0
找出defer 相关的 runtime 方法  `deferprocStack`  `deferreturn`,开读
// deferprocStack queues a new deferred function with a defer record on the stack.
// The defer record must have its siz and fn fields initialized.
// All other fields can contain junk.
// The defer record must be immediately followed in memory by the arguments of the defer.
// Nosplit because the arguments on the stack won't be scanned until the defer record is spliced into the gp._defer list.
//go:nosplit
func deferprocStack(d *_defer) {
	gp := getg()

    // 当前运行的goroutine
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("defer on system stack")
	}
	// siz and fn are already set.
	// The other fields are junk on entry to deferprocStack and
	// are initialized here.
	d.started = false
	d.heap = false
	d.sp = getcallersp()
	d.pc = getcallerpc()
	// The lines below implement:
	//   d.panic = nil
	//   d.link = gp._defer
	//   gp._defer = d
	// But without write barriers. The first two are writes to
	// the stack so they don't need a write barrier, and furthermore
	// are to uninitialized memory, so they must not use a write barrier.
	// The third write does not require a write barrier because we
	// explicitly mark all the defer structures, so we don't need to
	// keep track of pointers to them with a write barrier.
	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

	return0()
	// No code can go here - the C return register has
	// been set and must not be clobbered.
}


deferprocStack 的参数是一个_defer但是我们并没有传,那么这个值是从哪里来的呢?我们回去看下汇编

            MOVL	$0, ""..autotmp_1+8(SP)
            PCDATA	$0, $1
            LEAQ	"".main.func1·f(SB), AX
            PCDATA	$0, $0
            MOVQ	AX, ""..autotmp_1+32(SP)
            PCDATA	$0, $1
            LEAQ	""..autotmp_1+8(SP), AX
            PCDATA	$0, $0
            MOVQ	AX, (SP)
            CALL	runtime.deferprocStack(SB)

第五行 AX, “”..autotmp_1+32(SP) 把AX的值放入SP+32这个地址中,AX寄存器可以从第三行看到,

是func1的地址,然后看倒数第二行MOVQ AX, (SP) 和第一行 MOVL $0, “”..autotmp_1+8(SP) ,

deferprocStack的参数是一个_defer指针 sp+8 地址就是作为这个结构体参数的首地址。

首地址为SP+8。那么SP+32 func1 方法的地址,偏移量为24。

    type _defer struct {
        siz     int32 // 占用4字节  //偏移0
        started bool  // 占用1字节  //偏移4
        heap    bool  // 占用1字节  //偏移6
        // 内存对齐 这里会补上两个2节
        sp      uintptr // 64位机器下是8字节 //偏移8
        pc      uintptr // 64位机器下是8字节 //偏移16
        fn      *funcval // 64位机器下是8字节 //偏移24 
        _panic  *_panic  //  64位机器下是8字节
        link    *_defer  // 64位机器下是8字节
    }

可以发现fn 正好是24偏移字节的位置,根据以上猜测sp+32位子存放的是func地址sp+0存放siz参数。siz参数是哪里来的呢,是sp+8的地址,sp+8的地址由第一行可以看到是0。

(这个地方如果使用goland的debug打入断点去跟踪d.siz,结果会与汇编不同,猜测是在编译时做了优化,如果用dlv或者gdb工具,debug汇编代码能验证上面的推论。)

搞清楚了这一点,deferprocStack方法的内容就变得索然无味,就是对_defer的其他几个 “junk” 字段做初始化赋值。然后把他放入到当前G中。

deferprocStack搞清楚了我们接下来看deferreturn

// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.
// If there is a deferred function, this will call runtime·jmpdefer,
// which will jump to the deferred function such that it appears
// to have been called by the caller of deferreturn at the point
// just before deferreturn was called. The effect is that deferreturn
// is called again and again until there are no more deferred functions.
// Cannot split the stack because we reuse the caller's frame to
// call the deferred function.

// The single argument isn't actually used - it just has its address
// taken so it can be matched against pending defers.
//go:nosplit
func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	sp := getcallersp()
	if d.sp != sp {
		return
	}

	// Moving arguments around.
	//
	// Everything called after this point must be recursively
	// nosplit because the garbage collector won't know the form
	// of the arguments until the jmpdefer can flip the PC over to
	// fn.
	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)
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

注释第一行说明了,这个deferreturn这个方法是编译器自动添加。具体干的事情其实就是把defer执行defer方法,但是它是怎么执行的呢?可以看到最后它调用了jmpdefer 方法,这个方法是使用汇编实现的,那么就到了喜闻乐见的读汇编环节了。


// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
	MOVQ	fv+0(FP), DX	// fn
	MOVQ	argp+8(FP), BX	// caller sp
	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

这里需要结合前面反编译生成的汇编来结合阅读,理解上下文。前面反编译的汇编我们只用关注下面几句就行。

   0x0024 00036 (defer.go:6)	MOVL	$0, ""..autotmp_1+8(SP)
   ...
   0x0038 00056 (defer.go:6)	LEAQ	""..autotmp_1+8(SP), AX
   0x003d 00061 (defer.go:6)	MOVQ	AX, (SP)
   0x0041 00065 (defer.go:6)	CALL	runtime.deferprocStack(SB)
   ...
   0x004b 00075 (defer.go:9)	CALL	runtime.deferreturn(SB)
   0x0050 00080 (defer.go:9)	MOVQ	64(SP), BP
   0x0055 00085 (defer.go:9)	ADDQ	$72, SP
   0x0059 00089 (defer.go:9)	RET
   0x005a 00090 (defer.go:6)	XCHGL	AX, AX
   0x005b 00091 (defer.go:6)	CALL	runtime.deferreturn(SB)
   0x0060 00096 (defer.go:6)	MOVQ	64(SP), BP

众所周知call F指令是 push ip,esp 加上jump F指令实现。也就是说,通过CALL调用的方法,它的物理SP寄存器就是存放的他caller的IP寄存器。

由上面的代码可知,0x005b 00091 (defer.go:6) CALL runtime.deferreturn(SB) 这里传入的参数,是SP寄存器,其值为 第一层调用栈的SP+8,然后对这个值取地址-8后,得到第一层caller的SP,由于他CALL了一个函数,那么对这个SP-8 ,得到的是应该是他push进去的IP。这里地方IP应该是00080,然后把他-5,得到00075,这个值刚好是 0x004b 00075 (defer.go:9) CALL runtime.deferreturn(SB)这条语句的IP。

妙啊,这里是一个递归方法!一直执行到defer没有值为止。

29 Aug 2020