golang 疑问1:defer 调用之后到底发生了什么
先来看一个个函数,并猜猜它们的返回值
func f1() (result int) {
defer func() {
result *= 7
}()
return 6
}
尝试打印这个函数的返回值看看
fmt.Println(f1())
//stdout:42
Why ‘42’ ???
‘42’ 的前世今生
首先,我们先用最简单的方式:单步调试看一下返回值的变化情况。
figure-1
figure-2
figure-3
figure-4
通过上面的简单截图,我们可以看到:
- 返回值
result
初始值为 0 - 程序先在 figure-2 的时候,通过语句
return 6
将返回值修改为 6 - 然后在 figure-3的时候开始执行 defer 闭包
- 在 defer 执行完之后,返回值被修改成了42
通过上面简单分析,我们可以看到是 defer 最终修改了返回值,但是要想知道修改的机制,就要看下汇编了。
汇编分析
首先通过命令 GOOS=linux GOARCH=amd64 go tool compile -l -S main.go
获得如下汇编代码(注释是我自己加的):
"".f1 STEXT size=124 args=0x8 locals=0x20
0x0000 00000 (main.go:7) TEXT "".f1(SB), ABIInternal, $32-8 // 生命函数即将分配的 栈帧32字节 参数8字节
0x0000 00000 (main.go:7) MOVQ (TLS), CX // 这三行是检测栈空间是否足够,不够的话会跳转到runtime.morestack_noctxt(SB) 申请更多空间
0x0009 00009 (main.go:7) CMPQ SP, 16(CX)
0x000d 00013 (main.go:7) JLS 117
0x000f 00015 (main.go:7) SUBQ $32, SP // 申请32字节栈空间
0x0013 00019 (main.go:7) MOVQ BP, 24(SP) // 保存 caller BP ,供函数执行完成之后恢复 caller 使用
0x0018 00024 (main.go:7) LEAQ 24(SP), BP // 将f1() 的栈低地址传入BP寄存器
0x001d 00029 (main.go:7) MOVQ $0, "".result+40(SP) //赋初始值0给 返回参数 result
0x0026 00038 (main.go:8) MOVL $8, (SP) // 将 deferproc 第一个参数压栈,runtime.panic.go:92 deferproc(siz int32, fn *funcval)
0x002d 00045 (main.go:8) LEAQ "".f1.func1·f(SB), AX //保存匿名函数 AX
0x0034 00052 (main.go:8) MOVQ AX, 8(SP) // 压栈
0x0039 00057 (main.go:8) LEAQ "".result+40(SP), AX // 保存返回值地址 AX
0x003e 00062 (main.go:8) MOVQ AX, 16(SP) //压栈, 和下方的deferproc 没太多关系,deferproc 只接受2个参数
0x0043 00067 (main.go:8) CALL runtime.deferproc(SB) //调用 deferproc
0x0048 00072 (main.go:8) TESTL AX, AX
0x004a 00074 (main.go:8) JNE 101
0x004c 00076 (main.go:11) MOVQ $6, "".result+40(SP) //将数字6传入 返回参数 result
0x0055 00085 (main.go:11) XCHGL AX, AX // 据说是只是为了填充指令 nop
0x0056 00086 (main.go:11) CALL runtime.deferreturn(SB) //执行匿名函数, 在 runtime.panic.go:351 中,getcallersp() 获取了当前函数栈,
0x005b 00091 (main.go:11) MOVQ 24(SP), BP //恢复BP寄存器
0x0060 00096 (main.go:11) ADDQ $32, SP // 释放32字节栈空间
0x0064 00100 (main.go:11) RET
0x0065 00101 (main.go:8) XCHGL AX, AX
0x0066 00102 (main.go:8) CALL runtime.deferreturn(SB)
0x006b 00107 (main.go:8) MOVQ 24(SP), BP
0x0070 00112 (main.go:8) ADDQ $32, SP
0x0074 00116 (main.go:8) RET
0x0075 00117 (main.go:8) NOP
0x0075 00117 (main.go:7) CALL runtime.morestack_noctxt(SB)
0x007a 00122 (main.go:7) JMP 0
"".f1.func1 STEXT nosplit size=20 args=0x8 locals=0x0 // 这里的栈结构要看 runtime.panic.go:345 deferreturn(arg0 uintptr)
0x0000 00000 (main.go:8) TEXT "".f1.func1(SB), NOSPLIT|ABIInternal, $0-8
0x0000 00000 (main.go:9) MOVQ "".&result+8(SP), AX // result 指针放入AX
0x0005 00005 (main.go:9) MOVQ (AX), CX // AX指向的值放入 CX
0x0008 00008 (main.go:9) LEAQ (CX)(CX*2), DX //DX = 2*CX + CX -> 2*result + result = 3*result
0x000c 00012 (main.go:9) LEAQ (CX)(DX*2), CX //CX = 2*DX + CX -> 2*(3*result) + result = 7*result
0x0010 00016 (main.go:9) MOVQ CX, (AX) // 将AX指向的值更新为 7*result
0x0013 00019 (main.go:10) RET
--未完待续--
参考连接
https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md
http://xargin.com/plan9-assembly/
https://github.com/golang/go/files/447163/GoFunctionsInAssembly.pdf
https://github.com/chai2010/advanced-go-programming-book/tree/master/ch3-asm
http://mcll.top/2018/04/15/golang%E4%B8%AD%E7%9A%84defer%E5%AE%9E%E7%8E%B0/
如果计算机体系不太熟,建议先看看这个
https://www.bilibili.com/video/av31289365/?p=6
http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html