在同一個(gè)goroutine中:
多個(gè)defer的調(diào)用棧原理是什么?
defer函數(shù)是如何調(diào)用的?
為了探究其中的奧秘我準(zhǔn)備了如下代碼:
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
從輸出結(jié)果看很像棧的數(shù)據(jù)結(jié)構(gòu)特性:后進(jìn)先出(LIFO)。
首先從匯編入手去查看xx()函數(shù)的執(zhí)行過程,命令如下:
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)
發(fā)現(xiàn)aaa()函數(shù)的參數(shù)及調(diào)用函數(shù)deferproc(SB):
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)
下面重點(diǎn)代碼的統(tǒng)一說明:
//1, (SP) 將24放入棧頂(24其實(shí)是下面所說的deferd函數(shù)參數(shù)類型的長度和)。
0x0021 00033 (main.go:10) MOVL $24, (SP)
//2, 8(SP) 將aaa函數(shù)指針放入AX;將aaa函數(shù)指針放入到8(SP)中。
0x0028 00040 (main.go:10) LEAQ "".aaa·f(SB), AX
0x002f 00047 (main.go:10) MOVQ AX, 8(SP)
//3, 16(SP)把函數(shù)aaa第一個(gè)參數(shù)100放入到16(SP)中。
0x0034 00052 (main.go:10) MOVQ $100, 16(SP)
//4, 24(SP)獲取第二個(gè)參數(shù)的內(nèi)存地址并賦值給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),將第二個(gè)參數(shù)字符串長度9賦值到32(SP)中。
0x0049 00073 (main.go:10) MOVQ $9, 32(SP)
//調(diào)用runtime.deferproc(SB)
0x0052 00082 (main.go:10) CALL runtime.deferproc(SB)
0(SP) = 24 //aaa(int, string)參數(shù)類型長度和
8(SP) = &aaa(int, string)//deferd函數(shù)指針
16(SP) = 100// 第一個(gè)參數(shù)值100
24(SP) = "hello aaa"http://第二個(gè)參數(shù)
32(SP) = 9//第二個(gè)參數(shù)字符串長度
從以上2部分匯編代碼可以看出,函數(shù)相關(guān)數(shù)據(jù)放到了SP中且連續(xù)。2,發(fā)現(xiàn)
defer aaa(int, string)編譯器會(huì)插入deferproc(SB)函數(shù)。
去看一下源碼:
//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)
發(fā)現(xiàn)這個(gè)函數(shù)的參數(shù)是int32,*funcval。它們兩個(gè)代表什么?我們有g(shù)db去跟蹤一下具體什么意思:

siz=0x18就是說siz=24。而aaa(int, string)的參數(shù)int占8個(gè)字節(jié),string占16個(gè)字節(jié)。為什么string類型占16個(gè)字節(jié)?
因?yàn)閟tring類型的原型是:
type stringStruct struct {
str unsafe.Pointer
len int
}
unsafe.Pointer占8個(gè)字節(jié),int占8個(gè)字節(jié)。
具體字符串講解可以看我以前的文章golang中的string、編碼
接下來看*funcval:它的原型如下:
//runtime/runtime2.go
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
funcval是個(gè)struct,里面的成員是個(gè)fn uintptr,根據(jù)fn字面意思猜測(cè)是函數(shù)的指針。
前文已經(jīng)說過bbb(int, string)函數(shù)的相關(guān)數(shù)據(jù)放到了SP中,那func deferproc(siz int32, fn * funcval) 中的參數(shù)就是運(yùn)行時(shí)系統(tǒng)會(huì)從sp中拿取siz和*fn然后調(diào)用deferproc(siz int32, fn * funcval)。
我們用gdb看一下這里面fn指向的函數(shù)到底是什么:

原來d.fn.fn就是aaa(int, string)函數(shù)的具體指令。
那d代表什么呢,跟蹤發(fā)現(xiàn):
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
}
它是個(gè)結(jié)構(gòu)體。我們先查看siz,fn,link這3個(gè)參數(shù)就好,其他參數(shù)由于篇幅有限下文講解。
siz:deferd函數(shù)參數(shù)原型字節(jié)長度的和。
fn:deferd函數(shù)指針。
link: 是什么意思??????
帶著問題去看一下newdefer(siz)的實(shí)現(xiàn):
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
// 當(dāng)前goroutine的g結(jié)構(gòu)體對(duì)象
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
//當(dāng)前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() {//切換到系統(tǒng)棧
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 {//緩存中沒有創(chuàng)建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如果沒有就創(chuàng)建一個(gè),然后將size,link進(jìn)行賦值。
重點(diǎn)看如下代碼:
d.link = gp._defer
gp._defer = d
以上2行代碼實(shí)現(xiàn)中已經(jīng)有解釋,這里再詳細(xì)解釋一下:
這2句的意思是,將剛剛生成的defer綁定到g._defer上,就是將最新的defer放到
g._defer上作為鏈表頭。然后將g._defer綁定到d.link上,見下方示意圖:
[當(dāng)前的g]{_defer} => [新的d1]{link} => [g]{老的_defer}
如果再有新生成的defer(d2)則鏈表如下:
[當(dāng)前的g]{_defer} => [新的d2]{link} => [新的d1]{link} => [g]{老的_defer}
回到deferproc(siz int32, fn *funcval)函數(shù)中來,newdefer(siz)上面第二行是什么意思呢?:
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
繼續(xù)用gdb跟蹤一下,發(fā)現(xiàn)涉及到argp的在這一行,見下方截圖2:

發(fā)現(xiàn)了memmove函數(shù),它的作用是拷貝。就是將argp位置為起點(diǎn)拷貝siz(這里為24個(gè)字節(jié))字節(jié)到d結(jié)構(gòu)體后后面。
運(yùn)行這行看一下復(fù)制到d結(jié)構(gòu)體后面的數(shù)據(jù)是什么?見圖3:

圖3中紅框中的第一行是0x64 它的10進(jìn)制表示為100。證明這個(gè)是aaa函數(shù)的第一個(gè)參數(shù),同理第二行0x4b9621為第二個(gè)參數(shù)字符串的指針,去看一下是否為預(yù)想的那樣,見圖4:

上圖為10進(jìn)制表示方便ascii中查找對(duì)應(yīng)的字符,從ascii表中可知確實(shí)為aaa函數(shù)的第二個(gè)參數(shù)
hello aaa。從而我得出結(jié)論deferd函數(shù)的參數(shù)是在deferd結(jié)構(gòu)體后面。第三行代表字符串長度。也就是說第二行和第三行代表了字符串原型(結(jié)構(gòu)體)的值。
繼續(xù)跟蹤函數(shù)執(zhí)行過程:
defer bbb("hello bbb")
bbb(string)的執(zhí)行過程和上面aaa(int, string)函數(shù)執(zhí)行過程是一樣的,這里不再重復(fù)演示。
deferproc棧執(zhí)行完之后運(yùn)行return處,見圖5:

然后按s進(jìn)入return實(shí)現(xiàn)處(到了deferreturn棧),見下圖6:

去看一下它的實(shí)現(xiàn):
//rutime/painc.go
//go:nosplit
func deferreturn(arg0 uintptr) {
gp := getg() //獲取當(dāng)前的g
d := gp._defer //獲取當(dāng)前g的_defer鏈表頭
//d為什么可以為nil,因?yàn)閐efer函數(shù)可以嵌套例如:
// defer a -> defer b -> defer c
//deferreturn函數(shù)被調(diào)用至少一次,就是將鏈表里的defer都執(zhí)行完就直接返回了。
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
//將deferd函數(shù)參數(shù)復(fù)制到arg0處,為調(diào)用deferd函數(shù)做準(zhǔn)備。
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize://如果siz的大小為指針大小直接如下復(fù)制,目的是減少cpu運(yùn)算。
*(*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設(shè)置為空
gp._defer = d.link//將當(dāng)前defer的下一個(gè)defer綁定到鏈表頭。
freedefer(d) //將d釋放掉
//fn為deferd函數(shù),第二個(gè)參數(shù)為deferd函數(shù)的參數(shù)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
重點(diǎn)解釋一下上面4行代碼:將鏈表下一個(gè)defer綁定到gp._defer處。將當(dāng)前的defer釋放掉。見下方示意圖:
[當(dāng)前的g]{_defer} => [新的d2]{link} => [新的d1]{link} => [g]{老的_defer}
運(yùn)行完d2:
[當(dāng)前的g]{_defer} => [新的d1]{link} => [g]{老的_defer}
然后看一下下方j(luò)mpdefer函數(shù):
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
這個(gè)函數(shù)是具體執(zhí)行defer函數(shù)地方,我們看它實(shí)現(xiàn)之前先記住下圖圖7的deferreturn入口地址,下面會(huì)說到這個(gè)地址。

jmpdefer函數(shù)實(shí)現(xiàn)見下方代碼:
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
將函數(shù)第一個(gè)參數(shù)fn指針復(fù)制給DX,從而后續(xù)代碼可以從DX中取fn的指針來執(zhí)行deferd函數(shù)。
MOVQ argp+8(FP), BX // caller sp
將函數(shù)第二個(gè)參數(shù)argp指針復(fù)制給BX,這個(gè)指針是deferd函數(shù)第一個(gè)參數(shù)地址。
LEAQ -8(BX), SP // caller sp after CALL
從上面第2條指令可知BX存放的是deferd函數(shù)第一個(gè)參數(shù)地址。因?yàn)榇藭r(shí)gbd調(diào)試的是bbb(string)這個(gè)函數(shù),所以此時(shí)的參數(shù)是個(gè)字符串結(jié)構(gòu)體,總共占16個(gè)字節(jié),前8個(gè)字節(jié)是數(shù)據(jù)指針,后8個(gè)是長度。那-8(BX)里面又是什么數(shù)據(jù)呢,就是說bbb(string)參數(shù)值前面(低位)是什么東東。用gdb跟一下執(zhí)行完這條指令看一下SP(因?yàn)橘x值給了SP)中內(nèi)存的值是啥,見圖8。

第一行就是我們要確定的-8(BX)
第二行是bbb(string)中參數(shù),它是字符串結(jié)構(gòu)體中字符串指針,指向具體的字符串。
第三行是字符串的長度,這里為9。
我們看一下棧的情況見圖9:

0x4872c6是什么,指針?試著去看一下它是否能指向具體內(nèi)存見下圖10

原來是main.xx+145地址處的call runtime.deferreturn指令。還記得剛才的圖7嗎,我再截一下圖7,見圖11:

紅線處下一行就是
0x4872c6與圖10是一樣的值。根據(jù)圖11,這個(gè)地址是rutime.deferreturn(SB)的下一個(gè)指令,就是說這個(gè)地址是rutime.deferreturn(SB)返回地址。仔細(xì)觀察這兩個(gè)地址:
0x4872c1 == rutime.deferreturn(SB)
0x4872c6 == rutime.deferreturn(SB)的下一個(gè)指令地址(也叫返回地址)
發(fā)現(xiàn)他們相差5個(gè)字節(jié)。根據(jù)匯編知識(shí)可知,cpu是如何找到下一個(gè)指令的呢,是通過當(dāng)前指令所占字節(jié)數(shù)所確定的。
len(0x4872c6) - len(0x4872c1) == 5 可知
call runtime.deferreturn(sb)
占5個(gè)字節(jié),所以0x4872c1+5就可得到下一個(gè)指令首地址。
第4行:
MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use)
打印BP的值=0xc000032778
看一下棧的情況,見圖12

當(dāng)前的棧已經(jīng)是main.xx了。
第5行:
SUBQ $5, (SP) # return to CALL again
從第3行中的解釋可知,如果SP所指向的數(shù)據(jù)(runtime.deferreturn返回地址)減5的話,正好是runtime.deferreturn(SB)的指令入口。見圖13:

第6,7行:
MOVQ 0(DX), BX
JMP BX // but first run the deferred function
將DX所指向的函數(shù)指令賦值給BX
執(zhí)行fn.fn也就是bbb(string)。
執(zhí)行到bbb(string)處,見圖14

此時(shí)的rsp向低地址移動(dòng)了0x70個(gè)字節(jié)。
將bbb(string)末尾打上斷點(diǎn)并執(zhí)行到那里見圖15:

圖14中SP向低地址移動(dòng)了0x70。
圖15中SP向高地址移動(dòng)了0x70。
就是SP會(huì)恢復(fù)到之前的指向狀態(tài)。之前的SP指向哪里呢?就是圖13演示中的runtime.deferreturn(SB)入口處。
在看圖15
add rsp, 0x70指令下一行是個(gè)ret指令。這個(gè)在bbb(string)函數(shù)是沒有的,是編譯器添加上去的,目的是pop當(dāng)前棧頂?shù)?個(gè)字節(jié)到rip寄存器中,這樣cpu執(zhí)行rip里的指令就會(huì)執(zhí)行到runtime.deferreturn(SB)里從而實(shí)現(xiàn)了類似遞歸的調(diào)用deferreturn(SB)的作用。這樣就依次可以把deferd鏈上的執(zhí)行完。
繼續(xù)到runtime.deferreturn(SB)中
如下代碼:
if d == nil {
return
}
這個(gè)個(gè)if語句就是判斷defer鏈上是否還有deferd函數(shù),如果沒有就直接返回了。從而避免無限遞歸循環(huán)下去。
里面還有幾句代碼:
sp := getcallersp()
if d.sp != sp {
return
}
有興趣的小伙伴可以去試著看一下這里為什么這么寫,由于時(shí)間有限這段代碼的研究就不在這里展開了。
這篇文章主要是講解defer的執(zhí)行過程,由于篇幅原因,我把panic、recover、還有容易出錯(cuò)的defer語句的探究在下一篇中講解,敬請(qǐng)期待~