golang中defer的執行過程是怎樣的?

在同一個goroutine中:

多個defer的調用棧原理是什麼? defer函數是如何調用的?golang

爲了探究其中的奧祕我準備了以下代碼:編程

package main
import "fmt"

func main() {
	xx()
}
func xx() {
	defer aaa(100, "hello aaa")
	defer bbb("hello bbb")
	return
}

func aaa(x int, arg string) {
	fmt.Println(x, arg)
}

func bbb(arg string) {
	fmt.Println(arg)
}

複製代碼

輸出: bbb 100 hello aaa 從輸出結果看很像棧的數據結構特性:後進先出(LIFO)。緩存

首先從彙編入手去查看xx()函數的執行過程,命令以下:bash

go tool compile -S main.go >> main.s數據結構

"".xx STEXT size=198 args=0x0 locals=0x30
	0x0000 00000 (main.go:9)	TEXT	"".xx(SB), ABIInternal, $48-0
	0x0000 00000 (main.go:9)	MOVQ	(TLS), CX
	0x0009 00009 (main.go:9)	CMPQ	SP, 16(CX)
	0x000d 00013 (main.go:9)	JLS	188
	0x0013 00019 (main.go:9)	SUBQ	$48, SP
	0x0017 00023 (main.go:9)	MOVQ	BP, 40(SP)
	0x001c 00028 (main.go:9)	LEAQ	40(SP), BP
	0x0021 00033 (main.go:9)	FUNCDATA	$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0021 00033 (main.go:9)	FUNCDATA	$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
	0x0021 00033 (main.go:9)	FUNCDATA	$3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
	0x0021 00033 (main.go:10)	PCDATA	$2, $0
	0x0021 00033 (main.go:10)	PCDATA	$0, $0
	0x0021 00033 (main.go:10)	MOVL	$24, (SP)
	0x0028 00040 (main.go:10)	PCDATA	$2, $1
	0x0028 00040 (main.go:10)	LEAQ	"".aaa·f(SB), AX
	0x002f 00047 (main.go:10)	PCDATA	$2, $0
	0x002f 00047 (main.go:10)	MOVQ	AX, 8(SP)
	0x0034 00052 (main.go:10)	MOVQ	$100, 16(SP)
	0x003d 00061 (main.go:10)	PCDATA	$2, $1
	0x003d 00061 (main.go:10)	LEAQ	go.string."hello aaa"(SB), AX
	0x0044 00068 (main.go:10)	PCDATA	$2, $0
	0x0044 00068 (main.go:10)	MOVQ	AX, 24(SP)
	0x0049 00073 (main.go:10)	MOVQ	$9, 32(SP)
	0x0052 00082 (main.go:10)	CALL	runtime.deferproc(SB)
	0x0057 00087 (main.go:10)	TESTL	AX, AX
	0x0059 00089 (main.go:10)	JNE	172
	0x005b 00091 (main.go:11)	MOVL	$16, (SP)
	0x0062 00098 (main.go:11)	PCDATA	$2, $1
	0x0062 00098 (main.go:11)	LEAQ	"".bbb·f(SB), AX
	0x0069 00105 (main.go:11)	PCDATA	$2, $0
	0x0069 00105 (main.go:11)	MOVQ	AX, 8(SP)
	0x006e 00110 (main.go:11)	PCDATA	$2, $1
	0x006e 00110 (main.go:11)	LEAQ	go.string."hello bbb"(SB), AX
	0x0075 00117 (main.go:11)	PCDATA	$2, $0
	0x0075 00117 (main.go:11)	MOVQ	AX, 16(SP)
	0x007a 00122 (main.go:11)	MOVQ	$9, 24(SP)
	0x0083 00131 (main.go:11)	CALL	runtime.deferproc(SB)
	0x0088 00136 (main.go:11)	TESTL	AX, AX
	0x008a 00138 (main.go:11)	JNE	156
	0x008c 00140 (main.go:12)	XCHGL	AX, AX
	0x008d 00141 (main.go:12)	CALL	runtime.deferreturn(SB)
複製代碼

發現aaa()函數的參數及調用函數deferproc(SB):app

0x0021 00033 (main.go:10)   MOVL    $24, (SP)
 0x0028 00040 (main.go:10)   PCDATA  $2, $1 
 0x0028 00040 (main.go:10)   LEAQ    "".aaa·f(SB), AX
 0x002f 00047 (main.go:10)   PCDATA  $2, $0 
 0x002f 00047 (main.go:10)   MOVQ    AX, 8(SP)
 0x0034 00052 (main.go:10)   MOVQ    $100, 16(SP)
 0x003d 00061 (main.go:10)   PCDATA  $2, $1 
 0x003d 00061 (main.go:10)   LEAQ    go.string."hello aaa"(SB), AX
 0x0044 00068 (main.go:10)   PCDATA  $2, $0 
 0x0044 00068 (main.go:10)   MOVQ    AX, 24(SP)
 0x0049 00073 (main.go:10)   MOVQ    $9, 32(SP)
 0x0052 00082 (main.go:10)   CALL    runtime.deferproc(SB)
複製代碼

下面重點代碼的統一說明:less

//1, (SP) 將24放入棧頂(24實際上是下面所說的deferd函數參數類型的長度和)。
 0x0021 00033 (main.go:10)   MOVL    $24, (SP)

//2, 8(SP) 將aaa函數指針放入AX;將aaa函數指針放入到8(SP)中。
 0x0028 00040 (main.go:10)   LEAQ    "".aaa·f(SB), AX
 0x002f 00047 (main.go:10)   MOVQ    AX, 8(SP)

//3, 16(SP)把函數aaa第一個參數100放入到16(SP)中。
 0x0034 00052 (main.go:10)   MOVQ    $100, 16(SP)

//4, 24(SP)獲取第二個參數的內存地址並賦值給AX;AX中值賦值給24(SP)。
 0x003d 00061 (main.go:10)   LEAQ    go.string."hello aaa"(SB), AX
 0x0044 00068 (main.go:10)   MOVQ    AX, 24(SP)

//5,32(SP),將第二個參數字符串長度9賦值到32(SP)中。
 0x0049 00073 (main.go:10)   MOVQ    $9, 32(SP)

//調用runtime.deferproc(SB)
 0x0052 00082 (main.go:10)   CALL    runtime.deferproc(SB)
複製代碼

0(SP) = 24 //aaa(int, string)參數類型長度和函數

8(SP) = &aaa(int, string)//deferd函數指針ui

16(SP) = 100// 第一個參數值100編碼

24(SP) = "hello aaa"//第二個參數

32(SP) = 9//第二個參數字符串長度

從以上2部分彙編代碼能夠看出,函數相關數據放到了SP中且連續。2,發現 defer aaa(int, string)編譯器會插入deferproc(SB)函數。 去看一下源碼:

//runtime/panic.go

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
	if getg().m.curg != getg() {
		throw("defer on system stack")
	}
	sp := getcallersp()
	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
	callerpc := getcallerpc()

	d := newdefer(siz)
	if d._panic != nil {
		throw("deferproc: d.panic != nil after newdefer")
	}
	d.fn = fn
	d.pc = callerpc
	d.sp = sp
	switch siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
	default:
		memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
	}
	return0()
}
複製代碼
deferproc(siz int32, fn *funcval)
複製代碼

發現這個函數的參數是int32,*funcval。它們兩個表明什麼?咱們有gdb去跟蹤一下具體什麼意思:

圖1
siz=0x18就是說siz=24。而aaa(int, string)的參數int佔8個字節,string佔16個字節。爲何string類型佔16個字節? 由於string類型的原型是:

type stringStruct struct {
	str unsafe.Pointer
	len int
}
複製代碼

unsafe.Pointer佔8個字節,int佔8個字節。 具體字符串講解能夠看我之前的文章golang中的string、編碼 接下來看*funcval:它的原型以下:

//runtime/runtime2.go

type funcval struct {
   fn uintptr
   // variable-size, fn-specific data here
}
複製代碼

funcval是個struct,裏面的成員是個fn uintptr,根據fn字面意思猜想是函數的指針。

前文已經說過bbb(int, string)函數的相關數據放到了SP中,那func deferproc(siz int32, fn * funcval) 中的參數就是運行時系統會從sp中拿取siz和*fn而後調用deferproc(siz int32, fn * funcval)。

咱們用gdb看一下這裏面fn指向的函數究竟是什麼:

圖2
原來d.fn.fn就是aaa(int, string)函數的具體指令。 那d表明什麼呢,跟蹤發現:

d := newdefer(siz)
複製代碼

去看一下它的原型:

func newdefer(siz int32) *_defer
複製代碼

它的返回值是*_defer,看一下它的定義:

//runtime/runtime2.go

type _defer struct {
	siz     int32 
	started bool
	sp      uintptr // sp at time of defer
	pc      uintptr
	fn      *funcval 
	_panic  *_panic // panic that is running defer
	link    *_defer
}
複製代碼

它是個結構體。咱們先查看siz,fn,link這3個參數就好,其餘參數因爲篇幅有限下文講解。 siz:deferd函數參數原型字節長度的和。

fn:deferd函數指針。

link: 是什麼意思??????

帶着問題去看一下newdefer(siz)的實現:

func newdefer(siz int32) *_defer {
	var d *_defer
	sc := deferclass(uintptr(siz))
    // 當前goroutine的g結構體對象  
	gp := getg()
	if sc < uintptr(len(p{}.deferpool)) {
                //當前goroutine綁定的p
		pp := gp.m.p.ptr()
                
		if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
			// Take the slow path on the system stack so
			// we don't grow newdefer's stack.
			systemstack(func() {//切換到系統棧
				lock(&sched.deferlock)
                //從全局deferpool拿一些defer放到p的本地deferpool
				for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
					d := sched.deferpool[sc]
					sched.deferpool[sc] = d.link
					d.link = nil
					pp.deferpool[sc] = append(pp.deferpool[sc], d)
				}
				unlock(&sched.deferlock)
			})
		}
		if n := len(pp.deferpool[sc]); n > 0 {
			d = pp.deferpool[sc][n-1]
			pp.deferpool[sc][n-1] = nil
			pp.deferpool[sc] = pp.deferpool[sc][:n-1]
		}
	}
	if d == nil {//緩存中沒有建立defer
		// Allocate new defer+args.
		systemstack(func() {
			total := roundupsize(totaldefersize(uintptr(siz)))
			d = (*_defer)(mallocgc(total, deferType, true))
		})
		if debugCachedWork {
			// 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 } } d.siz = siz //賦值siz //將g的_defer賦值給d.link d.link = gp._defer //d賦值給g._defer gp._defer = d return d } 複製代碼

以上是defer生成過程,大致意思就是先從緩存中找defer若是沒有就建立一個,而後將size,link進行賦值。 重點看以下代碼:

d.link = gp._defer
	gp._defer = d
複製代碼

以上2行代碼實現中已經有解釋,這裏再詳細解釋一下: 這2句的意思是,將剛剛生成的defer綁定到g._defer上,就是將最新的defer放到 g._defer上做爲鏈表頭。而後將g._defer綁定到d.link上,見下方示意圖:

[當前的g]{_defer} => [新的d1]{link} => [g]{老的_defer}

若是再有新生成的defer(d2)則鏈表以下:

[當前的g]{_defer} => [新的d2]{link} => [新的d1]{link} => [g]{老的_defer}

回到deferproc(siz int32, fn *funcval)函數中來,newdefer(siz)上面第二行是什麼意思呢?:

argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
複製代碼

繼續用gdb跟蹤一下,發現涉及到argp的在這一行,見下方截圖2:

圖2
發現了memmove函數,它的做用是拷貝。就是將argp位置爲起點拷貝siz(這裏爲24個字節)字節到d結構體後後面。 運行這行看一下複製到d結構體後面的數據是什麼?見圖3:

圖3

圖3中紅框中的第一行是0x64 它的10進製表示爲100。證實這個是aaa函數的第一個參數,同理第二行0x4b9621爲第二個參數字符串的指針,去看一下是否爲預想的那樣,見圖4:

圖4
上圖爲10進製表示方便ascii中查找對應的字符,從ascii表中可知確實爲aaa函數的第二個參數 hello aaa。從而我得出結論deferd函數的參數是在deferd結構體後面。第三行表明字符串長度。也就是說第二行和第三行表明了字符串原型(結構體)的值。

繼續跟蹤函數執行過程:

defer bbb("hello bbb")
複製代碼

bbb(string)的執行過程和上面aaa(int, string)函數執行過程是同樣的,這裏再也不重複演示。 deferproc棧執行完以後運行return處,見圖5:

圖5

而後按s進入return實現處(到了deferreturn棧),見下圖6:

圖6

去看一下它的實現:

//rutime/painc.go

//go:nosplit
func deferreturn(arg0 uintptr) {
	gp := getg() //獲取當前的g 
	d := gp._defer //獲取當前g的_defer鏈表頭

  //d爲何能夠爲nil,由於defer函數能夠嵌套例如:
  //  defer a -> defer b -> defer c
  //deferreturn函數被調用至少一次,就是將鏈表裏的defer都執行完就直接返回了。
	if d == nil {
		return
	}
	sp := getcallersp()
	if d.sp != sp {
		return
	}

    //將deferd函數參數複製到arg0處,爲調用deferd函數作準備。
	switch d.siz {
	case 0:
		// Do nothing.
	case sys.PtrSize://若是siz的大小爲指針大小直接以下複製,目的是減小cpu運算。
		*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
	default:
		memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
	}

	fn := d.fn //將d.fn拷貝一份
	d.fn = nil //將d.fn設置爲空
	gp._defer = d.link//將當前defer的下一個defer綁定到鏈表頭。
	freedefer(d) //將d釋放掉
       //fn爲deferd函數,第二個參數爲deferd函數的參數
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
複製代碼
fn := d.fn
d.fn = nil 
gp._defer = d.link
freedefer(d) 

複製代碼

重點解釋一下上面4行代碼:將鏈表下一個defer綁定到gp._defer處。將當前的defer釋放掉。見下方示意圖:

[當前的g]{_defer} => [新的d2]{link} => [新的d1]{link} => [g]{老的_defer}

運行完d2:

[當前的g]{_defer} => [新的d1]{link} => [g]{老的_defer}

而後看一下下方jmpdefer函數:

jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
複製代碼

這個函數是具體執行defer函數地方,咱們看它實現以前先記住下圖圖7的deferreturn入口地址,下面會說到這個地址。

圖7

jmpdefer函數實現見下方代碼:

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
複製代碼

一行一行解釋:

MOVQ    fv+0(FP), DX    // fn
複製代碼

將函數第一個參數fn指針複製給DX,從然後續代碼能夠從DX中取fn的指針來執行deferd函數。

MOVQ    argp+8(FP), BX  // caller sp
複製代碼

將函數第二個參數argp指針複製給BX,這個指針是deferd函數第一個參數地址。

LEAQ    -8(BX), SP  // caller sp after CALL
複製代碼

從上面第2條指令可知BX存放的是deferd函數第一個參數地址。由於此時gbd調試的是bbb(string)這個函數,因此此時的參數是個字符串結構體,總共佔16個字節,前8個字節是數據指針,後8個是長度。那-8(BX)裏面又是什麼數據呢,就是說bbb(string)參數值前面(低位)是什麼東東。用gdb跟一下執行完這條指令看一下SP(由於賦值給了SP)中內存的值是啥,見圖8。

圖8
第一行就是咱們要肯定的-8(BX) 第二行是bbb(string)中參數,它是字符串結構體中字符串指針,指向具體的字符串。 第三行是字符串的長度,這裏爲9。 咱們看一下棧的狀況見圖9:
圖9

0x4872c6是什麼,指針?試着去看一下它是否能指向具體內存見下圖10

圖10
原來是main.xx指令。還記得剛纔的圖7嗎,我在截一下圖7,見圖11:
圖11
紅線處下一行就是 0x4872c6與圖10是同樣的值。根據圖11,這個地址是rutime.deferreturn(SB)的下一個指令,就是說這個地址是rutime.deferreturn(SB)返回地址。 仔細觀察這兩個地址:

0x4872c1 == rutime.deferreturn(SB)

0x4872c6 == rutime.deferreturn(SB)的下一個指令地址(也叫返回地址)

發現他們相差5個字節。根據彙編知識可知,cpu是如何找到下一個指令的呢,是經過當前指令所佔字節數所肯定的。 len(0x4872c6) - len(0x4872c1) == 5 可知

call runtime.deferreturn(sb)
複製代碼

佔5個字節,因此0x4872c1+5就可獲得下一個指令首地址。

第4行:

MOVQ    -8(SP), BP  // restore BP as if deferreturn returned (harmless if framepointers not in use)
複製代碼

打印BP的值=0xc000032778 看一下棧的狀況,見圖12

圖12
當前的棧已是main.xx了。

第5行:

SUBQ  $5, (SP)  # return to CALL again
複製代碼

從第3行中的解釋可知,若是SP所指向的數據(runtime.deferreturn返回地址)減5的話,正好是runtime.deferreturn(SB)的指令入口。見圖13:

圖13

第6,7行:

MOVQ    0(DX), BX
JMP BX  // but first run the deferred function
複製代碼

將DX所指向的函數指令賦值給BX 執行fn.fn也就是bbb(string)。 執行到bbb(string)處,見圖14

圖14
此時的rsp向低地址移動了0x70個字節。 將bbb(string)末尾打上斷點並執行到那裏見圖15:
圖15
圖14中SP向低地址移動了0x70。 圖15中SP向高地址移動了0x70。 就是SP會恢復到以前的指向狀態。以前的SP指向哪裏呢?就是圖13演示中的runtime.deferreturn(SB)入口處。 在看圖15 add rsp, 0x70指令下一行是個 ret指令。這個在bbb(string)函數是沒有的,是編譯器添加上去的,目的是pop當前棧頂的8個字節到rip寄存器中,這樣cpu執行rip裏的指令就會執行到runtime.deferreturn(SB)裏從而實現了相似遞歸的調用deferreturn(SB)的做用。這樣就依次能夠把deferd鏈上的執行完。

繼續到runtime.deferreturn(SB)中 以下代碼:

if d == nil {
        return
    }
複製代碼

這個個if語句就是判斷defer鏈上是否還有deferd函數,若是沒有就直接返回了。從而避免無限遞歸循環下去。 裏面還有幾句代碼:

sp := getcallersp()
    if d.sp != sp {
        return
    }
複製代碼

有興趣的小夥伴能夠去試着看一下這裏爲何這麼寫,因爲時間有限這段代碼的研究就不在這裏展開了。

這篇文章主要是講解defer的執行過程,因爲篇幅緣由,我把panic、recover、還有容易出錯的defer語句的探究在下一篇中講解,敬請期待~

參考 defer ---go語言核心編程技術 golang中的defer

相關文章
相關標籤/搜索