我们用一个简单的示例来揭开defer的秘密。
test.go
package main
import ()
func main() {
defer println(0x11)
}
反编译:
$ go build -o test test.go
$ go tool objdump -s "main.main" test
TEXT main.main(SB) test.go
test.go:5 0x204f SUBQ $0x18, SP
test.go:6 0x2053 MOVQ $0x11, 0x10(SP) // arg 0x11
test.go:6 0x205c MOVL $0x8, 0(SP) // arg size
test.go:6 0x2063 LEAQ 0x8379e(IP), AX // 0x8379e(0x206a) = 0x85808 print function
test.go:6 0x206a MOVQ AX, 0x8(SP) // +--- IP 指向下一条指令
test.go:6 0x206f CALL runtime.deferproc(SB)
test.go:6 0x2074 CMPL $0x0, AX
test.go:6 0x2077 JNE 0x2084
test.go:7 0x2079 NOPL
test.go:7 0x207a CALL runtime.deferreturn(SB)
test.go:7 0x207f ADDQ $0x18, SP
test.go:7 0x2083 RET
$ nm test | grep "85808"
0000000000085808 s main.print.1.f
编译器将defer处理成两个函数调用,deferproc定义一个延迟调用对象,然后在函数结束前通过deferreturn完成最终调用。
和前面一样,对于这类参数不确定的都是用funcval处理,siz是目标函数参数长度。
runtime2.go
type _defer struct {
siz int32
started bool
sp uintptr // 调用 deferproc 时的 SP
pc uintptr // 调用 deferproc 时的 IP
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
panic.go
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
sp := getcallersp(unsafe.Pointer(&siz))
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc(unsafe.Pointer(&siz))
systemstack(func() {
d := newdefer(siz)
d.fn = fn
d.pc = callerpc
d.sp = sp
memmove(add(unsafe.Pointer(d), unsafe.Sizeof(*d)),
unsafe.Pointer(argp), uintptr(siz))
})
// 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()
}
这个函数粗看没什么复杂的地方,但有两个问题:第一,参数被复制到了defer对象后面的内存空间;第二,匿名函数中创建的d不知保存在哪里。
panic.go
func newdefer(siz int32) *_defer {
var d *_defer
// 参数长度对齐后,获取缓存等级
sc := deferclass(uintptr(siz))
mp := acquirem()
// 未超出缓存大小
if sc < uintptr(len(p{}.deferpool)) {
pp := mp.p.ptr()
// 如果 P 本地缓存已空,从全局提取一批到本地
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
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)
}
}
// 从本地缓存尾部提取
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]
}
}
// 新建。很显然分配的空间大小除 _defer 外,还有参数
if d == nil {
// Allocate new defer+args.
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, 0))
}
d.siz = siz
// 将 d 保存到 G._defer 链表
gp := mp.curg
d.link = gp._defer
gp._defer = d
releasem(mp)
return d
}
runtime2.go
type p struct {
deferpool [5][]*_defer
}
type g struct {
_defer *_defer
}
defer同样使用了二级缓存,这个没兴趣深究。newdefer函数解释了前面的两个问题:一次性为defer和参数分配空间;d被挂到G._defer链表。
那么,退出前deferreturn自然是从G._defer获取并执行延迟函数了。
panic.go
func deferreturn(arg0 uintptr) {
gp := getg()
// 提取 defer 延迟对象
d := gp._defer
if d == nil {
return
}
// 对比 SP,避免调用其他栈帧的延迟函数。(arg0 也就是 deferproc siz 参数)
sp := getcallersp(unsafe.Pointer(&arg0))
if d.sp != sp {
return
}
mp := acquirem()
// 将延迟函数的参数复制到堆栈(这会覆盖掉 siz、fn,不过没有影响)
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
fn := d.fn
d.fn = nil
// 调整 G._defer 链表
gp._defer = d.link
// 释放 _defer 对象,放回缓存
systemstack(func() {
freedefer(d)
})
releasem(mp)
// 执行延迟函数
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
freedefer将_defer放回P.deferpool缓存,当数量超出时会转移部分到sched.deferpool。垃圾回收时,clearpools会清理掉sched.deferpool缓存。
汇编实现的jmpdefer函数很有意思。
首先通过arg0参数,也就是调用deferproc时压入的第一参数siz获取main.main SP。当main调用deferreturn时,用SP-8就可以获取当时保存的main IP值。因为IP保存了下一条指令地址,那么用该地址减去CALL指令长度,自然又回到了main调用deferreturn函数的位置。将这个计算得来的地址入栈,加上jmpdefer没有保存现场,那么延迟函数fn RET自然回到CALL deferreturn,如此就实现了多个defer延迟调用循环。
asm_amd64.s
TEXT runtime•jmpdefer(SB), NOSPLIT, $0-16
MOVQ fv+0(FP), DX // 延迟函数 fn 地址
MOVQ argp+8(FP), BX // argp+8 是 arg0 地址,也就是 main 的 SP
LEAQ -8(BX), SP // 将 SP-8 获取的其实是 call deferreturn 是压入的 main IP
SUBQ $5, (SP) // CALL 指令长度 5,-5 返回的就是 call deferreturn 指令地址
MOVQ 0(DX), BX // 执行 fn 函数
JMP BX
费好大力气,真有必要这么做吗?
虽然整个调用堆栈的defer都挂在G._defer链表,但在deferreturn里面通过sp值的比对,可避免调用其他栈帧的延迟函数。
如中途用Goexit终止,它会负责处理整个调用堆栈的延迟函数。
panic.go
func Goexit() {
gp := getg()
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
d._panic = nil
}
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
d.started = true
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
if gp._defer != d {
throw("bad defer entry in Goexit")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
}
goexit1()
}