golang defer陷阱
2019 年 8 月 7 日
golang defer陷阱
[TOC]
测验
下面程序的输出是什么?
package main
//!+f
func main() {
println(f1())
println(f2())
println(f3())
}
func f1() (r int) {
defer func() {
r++
}()
return 0
}
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
答案
如果你的结果是
1
5
1
恭喜你,答对了。这篇文章您不必看了。
如果答错了。请继续阅读
一般解答
预备知识
- 函数调用方负责开辟栈空间,包括形参和返回值的空间。
- 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值。
f1
defer语句后面的匿名函数是对函数返回值r的闭包引用,f1函数的逻辑如下:
- r是函数的有名返回值,被分配在栈上,其地址又被称为返回值所在栈区。首先r被初始化为0.
- “return 0″会复制0 到返回值栈区,返回值r被赋值为0。
- 执行defer语句,由于匿名函数对返回值r是闭包引用,所以r++执行后,函数返回值被修改为1。
- defer语句执行完后RET返回,此时函数的返回值仍为1。
图解如下
f1指令序列 | 内存值 |
---|---|
r=0 | r=0 |
copy 0 to r | r=0 |
defer r++ | r=1 |
RET | r=1 |
f2
f2函数的逻辑如下:
- 首先r被初始化为0.
- 引入局部变量t, 并初始化为5.
- 复制t的值5到返回值r所在的栈区。
- 执行defer语句,由于匿名函数对t是闭包引用,所以t=t+5执行后,修改的是局部变量t, t的值被设置为10。
- defer语句执行完后RET返回,此时函数的返回值是5。
图解如下
f2指令序列 | 内存值 |
---|---|
r=0 | r=0 |
t=5 | t=5 |
copy t to r | r=5 |
defer t=t+5 | t=10 |
RET | r=5 |
f3
f3函数的逻辑如下:
- 首先r被初始化为0.
- 复制1到返回值r所在的栈区。
- 执行defer语句,由于匿名函数使用的传参调用,修改的是匿名函数的形参,对返回值r无影响。
- defer语句执行完后RET返回,此时函数的返回值是1。
图解如下
//r1为返回值, r2为匿名函数形参
f3指令序列 | 内存值 |
---|---|
r1=0 | r1=0 |
copy r1 to r2 | r2=0 |
defer r2=r2+5 | r2=5 |
copy 1 to r1 | r1=1 |
RET | r1=1 |
总结
对于带defer的函数返回整体上有三个步骤。
1. 执行return的值拷贝,将return语句返回的值复制到函数返回值栈区。
2. 执行defer语句,多个defer按照FILO顺序执行。
3. 执行调整RET指令。
其实在defer中修改函数返回值本身是种不良的编程实践,不推荐。
汇编解答
预备知识
- return x不是一个原子命令,return x分为了两步,第一步将返回值置为x,最后RET,使PC恢复到调用位置的下一个指令位置;
- 返回值,在程序中是如何处理的,更深入点,就是在机器码或者汇编上是如何处理的。要知道在调用函数前,首先要处理参数和返回值,go语言的参数和返回值都是通过栈传递的,调用前,返回值入栈,参数N入栈,…,参数1入栈。所以return x,就是要将x传递到函数调用前的栈中;
- defer是在返回前调用的,即在RET前。
简而言之就是
返回值 = xxx
调用defer函数
空的return
栈结构图
+———–+———–
| 返回值N |
+———–+
| … |
+———–+
| 返回值1 |
+———+-+
| 参数2 | 在调用函数中,上述代码就是main函数中
+———–+
| … |
+———–+
| 参数1 |
+———–+
| 返回地址 | 调用结束后PC指向的位置
+———–+———bp值
| 局部变量 |
| … | 被调用数栈祯
| |
+———–+———sp值
f1
"".f1 STEXT size=124 args=0x8 locals=0x20
0x0000 00000 (main.go:13) TEXT "".f1(SB), ABIInternal, $32-8 ;函数栈大小32,参数大小8
0x0000 00000 (main.go:13) MOVQ (TLS), CX
0x0009 00009 (main.go:13) CMPQ SP, 16(CX)
0x000d 00013 (main.go:13) JLS 117
0x000f 00015 (main.go:13) SUBQ $32, SP ;开始SP应该在本函数的虚拟地址32位置,运行时应指到32-32=0的位置
0x0013 00019 (main.go:13) MOVQ BP, 24(SP) ;调用f1的函数,这里即main函数的BP,保存在当前函数栈24(SP)下
0x0018 00024 (main.go:13) LEAQ 24(SP), BP ;lea不解引用,即直接把24(sp)的地址复制到BP,而不是24(SP)的内容给BP,即BP指向24位置
0x001d 00029 (main.go:13) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:13) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:13) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x001d 00029 (main.go:13) PCDATA $2, $0
0x001d 00029 (main.go:13) PCDATA $0, $0
0x001d 00029 (main.go:13) MOVQ $0, "".r+40(SP) ;返回值在40位置,而本函数栈最高32,即r在该函数外,即是调用者的栈中
0x0026 00038 (main.go:14) MOVL $8, (SP);8复制到0(SP)上,deferproc有两个参数,这里可能是其参数1,表示defer的函数f1需要8B大小的参数,即那个闭包参数r
0x002d 00045 (main.go:14) PCDATA $2, $1
0x002d 00045 (main.go:14) LEAQ "".f1.func1·f(SB), AX;将匿名函数地址复制到AX中
0x0034 00052 (main.go:14) PCDATA $2, $0
0x0034 00052 (main.go:14) MOVQ AX, 8(SP) ;f1.func1放在栈的8位置上,因为没有参数,所以接下来我们没有看到处理参数部分
0x0039 00057 (main.go:14) PCDATA $2, $1
0x0039 00057 (main.go:14) LEAQ "".r+40(SP), AX ;40(sp)的地址复制到AX中,这里我们就认为是40了,这是虚拟地址
0x003e 00062 (main.go:14) PCDATA $2, $0
0x003e 00062 (main.go:14) MOVQ AX, 16(SP) ;16(SP)记录的是r的地址
0x0043 00067 (main.go:14) CALL runtime.deferproc(SB) ;注册defer函数
0x0048 00072 (main.go:14) TESTL AX, AX
0x004a 00074 (main.go:14) JNE 101
0x004c 00076 (main.go:17) MOVQ $0, "".r+40(SP)
0x0055 00085 (main.go:17) XCHGL AX, AX
0x0056 00086 (main.go:17) CALL runtime.deferreturn(SB);真正的调用defer,闭包中的r值会在这改变,
0x005b 00091 (main.go:17) MOVQ 24(SP), BP;24(SP)记录的是f1的调用者main的BP值,所以,这是要返回了
0x0060 00096 (main.go:17) ADDQ $32, SP;SP=SP+32,回到调用f1的位置
0x0064 00100 (main.go:17) RET ;return返回,PC跳到调用函数的下一条指令,defer在RET前,但返回值的赋值在defer之前
0x0065 00101 (main.go:14) XCHGL AX, AX
0x0066 00102 (main.go:14) CALL runtime.deferreturn(SB)
0x006b 00107 (main.go:14) MOVQ 24(SP), BP
0x0070 00112 (main.go:14) ADDQ $32, SP
0x0074 00116 (main.go:14) RET
0x0075 00117 (main.go:14) NOP
0x0075 00117 (main.go:13) PCDATA $0, $-1
0x0075 00117 (main.go:13) PCDATA $2, $-1
0x0075 00117 (main.go:13) CALL runtime.morestack_noctxt(SB)
0x007a 00122 (main.go:13) JMP 0
0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 66 48 eH..%....H;a.vfH
0x0010 83 ec 20 48 89 6c 24 18 48 8d 6c 24 18 48 c7 44 .. H.l$.H.l$.H.D
0x0020 24 28 00 00 00 00 c7 04 24 08 00 00 00 48 8d 05 $(......$....H..
0x0030 00 00 00 00 48 89 44 24 08 48 8d 44 24 28 48 89 ....H.D$.H.D$(H.
0x0040 44 24 10 e8 00 00 00 00 85 c0 75 19 48 c7 44 24 D$........u.H.D$
0x0050 28 00 00 00 00 90 e8 00 00 00 00 48 8b 6c 24 18 (..........H.l$.
0x0060 48 83 c4 20 c3 90 e8 00 00 00 00 48 8b 6c 24 18 H.. .......H.l$.
0x0070 48 83 c4 20 c3 e8 00 00 00 00 eb 84 H.. ........
rel 5+4 t=16 TLS+0
rel 48+4 t=15 "".f1.func1·f+0
rel 68+4 t=8 runtime.deferproc+0
rel 87+4 t=8 runtime.deferreturn+0
rel 103+4 t=8 runtime.deferreturn+0
rel 118+4 t=8 runtime.morestack_noctxt+0
f2
"".f2 STEXT size=137 args=0x8 locals=0x28
0x0000 00000 (main.go:20) TEXT "".f2(SB), ABIInternal, $40-8
0x0000 00000 (main.go:20) MOVQ (TLS), CX
0x0009 00009 (main.go:20) CMPQ SP, 16(CX)
0x000d 00013 (main.go:20) JLS 127
0x000f 00015 (main.go:20) SUBQ $40, SP
0x0013 00019 (main.go:20) MOVQ BP, 32(SP)
0x0018 00024 (main.go:20) LEAQ 32(SP), BP
0x001d 00029 (main.go:20) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:20) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:20) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x001d 00029 (main.go:20) PCDATA $2, $0
0x001d 00029 (main.go:20) PCDATA $0, $0
0x001d 00029 (main.go:20) MOVQ $0, "".r+48(SP)
0x0026 00038 (main.go:21) MOVQ $5, "".t+24(SP)
0x002f 00047 (main.go:22) MOVL $8, (SP)
0x0036 00054 (main.go:22) PCDATA $2, $1
0x0036 00054 (main.go:22) LEAQ "".f2.func1·f(SB), AX
0x003d 00061 (main.go:22) PCDATA $2, $0
0x003d 00061 (main.go:22) MOVQ AX, 8(SP)
0x0042 00066 (main.go:22) PCDATA $2, $1
0x0042 00066 (main.go:22) LEAQ "".t+24(SP), AX
0x0047 00071 (main.go:22) PCDATA $2, $0
0x0047 00071 (main.go:22) MOVQ AX, 16(SP)
0x004c 00076 (main.go:22) CALL runtime.deferproc(SB)
0x0051 00081 (main.go:22) TESTL AX, AX
0x0053 00083 (main.go:22) JNE 111
0x0055 00085 (main.go:25) MOVQ "".t+24(SP), AX
0x005a 00090 (main.go:25) MOVQ AX, "".r+48(SP)
0x005f 00095 (main.go:25) XCHGL AX, AX
0x0060 00096 (main.go:25) CALL runtime.deferreturn(SB)
0x0065 00101 (main.go:25) MOVQ 32(SP), BP
0x006a 00106 (main.go:25) ADDQ $40, SP
0x006e 00110 (main.go:25) RET
0x006f 00111 (main.go:22) XCHGL AX, AX
0x0070 00112 (main.go:22) CALL runtime.deferreturn(SB)
0x0075 00117 (main.go:22) MOVQ 32(SP), BP
0x007a 00122 (main.go:22) ADDQ $40, SP
0x007e 00126 (main.go:22) RET
0x007f 00127 (main.go:22) NOP
0x007f 00127 (main.go:20) PCDATA $0, $-1
0x007f 00127 (main.go:20) PCDATA $2, $-1
0x007f 00127 (main.go:20) CALL runtime.morestack_noctxt(SB)
0x0084 00132 (main.go:20) JMP 0
0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 70 48 eH..%....H;a.vpH
0x0010 83 ec 28 48 89 6c 24 20 48 8d 6c 24 20 48 c7 44 ..(H.l$ H.l$ H.D
0x0020 24 30 00 00 00 00 48 c7 44 24 18 05 00 00 00 c7 $0....H.D$......
0x0030 04 24 08 00 00 00 48 8d 05 00 00 00 00 48 89 44 .$....H......H.D
0x0040 24 08 48 8d 44 24 18 48 89 44 24 10 e8 00 00 00 $.H.D$.H.D$.....
0x0050 00 85 c0 75 1a 48 8b 44 24 18 48 89 44 24 30 90 ...u.H.D$.H.D$0.
0x0060 e8 00 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 90 .....H.l$ H..(..
0x0070 e8 00 00 00 00 48 8b 6c 24 20 48 83 c4 28 c3 e8 .....H.l$ H..(..
0x0080 00 00 00 00 e9 77 ff ff ff .....w...
rel 5+4 t=16 TLS+0
rel 57+4 t=15 "".f2.func1·f+0
rel 77+4 t=8 runtime.deferproc+0
rel 97+4 t=8 runtime.deferreturn+0
rel 113+4 t=8 runtime.deferreturn+0
rel 128+4 t=8 runtime.morestack_noctxt+0
f3
"".f3 STEXT size=123 args=0x8 locals=0x20
0x0000 00000 (main.go:28) TEXT "".f3(SB), ABIInternal, $32-8
0x0000 00000 (main.go:28) MOVQ (TLS), CX
0x0009 00009 (main.go:28) CMPQ SP, 16(CX)
0x000d 00013 (main.go:28) JLS 116
0x000f 00015 (main.go:28) SUBQ $32, SP
0x0013 00019 (main.go:28) MOVQ BP, 24(SP)
0x0018 00024 (main.go:28) LEAQ 24(SP), BP
0x001d 00029 (main.go:28) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:28) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:28) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x001d 00029 (main.go:28) PCDATA $2, $0
0x001d 00029 (main.go:28) PCDATA $0, $0
0x001d 00029 (main.go:28) MOVQ $0, "".r+40(SP)
0x0026 00038 (main.go:29) MOVL $8, (SP)
0x002d 00045 (main.go:29) PCDATA $2, $1
0x002d 00045 (main.go:29) LEAQ "".f3.func1·f(SB), AX
0x0034 00052 (main.go:29) PCDATA $2, $0
0x0034 00052 (main.go:29) MOVQ AX, 8(SP)
0x0039 00057 (main.go:29) MOVQ $0, 16(SP)
0x0042 00066 (main.go:29) CALL runtime.deferproc(SB)
0x0047 00071 (main.go:29) TESTL AX, AX
0x0049 00073 (main.go:29) JNE 100
0x004b 00075 (main.go:32) MOVQ $1, "".r+40(SP)
0x0054 00084 (main.go:32) XCHGL AX, AX
0x0055 00085 (main.go:32) CALL runtime.deferreturn(SB)
0x005a 00090 (main.go:32) MOVQ 24(SP), BP
0x005f 00095 (main.go:32) ADDQ $32, SP
0x0063 00099 (main.go:32) RET
0x0064 00100 (main.go:29) XCHGL AX, AX
0x0065 00101 (main.go:29) CALL runtime.deferreturn(SB)
0x006a 00106 (main.go:29) MOVQ 24(SP), BP
0x006f 00111 (main.go:29) ADDQ $32, SP
0x0073 00115 (main.go:29) RET
0x0074 00116 (main.go:29) NOP
0x0074 00116 (main.go:28) PCDATA $0, $-1
0x0074 00116 (main.go:28) PCDATA $2, $-1
0x0074 00116 (main.go:28) CALL runtime.morestack_noctxt(SB)
0x0079 00121 (main.go:28) JMP 0
0x0000 65 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 65 48 eH..%....H;a.veH
0x0010 83 ec 20 48 89 6c 24 18 48 8d 6c 24 18 48 c7 44 .. H.l$.H.l$.H.D
0x0020 24 28 00 00 00 00 c7 04 24 08 00 00 00 48 8d 05 $(......$....H..
0x0030 00 00 00 00 48 89 44 24 08 48 c7 44 24 10 00 00 ....H.D$.H.D$...
0x0040 00 00 e8 00 00 00 00 85 c0 75 19 48 c7 44 24 28 .........u.H.D$(
0x0050 01 00 00 00 90 e8 00 00 00 00 48 8b 6c 24 18 48 ..........H.l$.H
0x0060 83 c4 20 c3 90 e8 00 00 00 00 48 8b 6c 24 18 48 .. .......H.l$.H
0x0070 83 c4 20 c3 e8 00 00 00 00 eb 85 .. ........
rel 5+4 t=16 TLS+0
rel 48+4 t=15 "".f3.func1·f+0
rel 67+4 t=8 runtime.deferproc+0
rel 86+4 t=8 runtime.deferreturn+0
rel 102+4 t=8 runtime.deferreturn+0
rel 117+4 t=8 runtime.morestack_noctxt+0
参考
- 主要摘抄自《go语言核心编程》
- 用汇编分析go的defer闭包处理