闭包函数是指引用了自由变量的函数,如下

1
2
3
4
5
6
func create() func() int {
	c := 2     // 自由变量
	return func() int {
		return c  // 引用了自由变量
	}
}

从代码出发

本文章分析的完整代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"fmt"
)

func create() func() int {
	c := 2
	return func() int {
		return c
	}
}

func main() {
	f1 := create()
	fmt.Println(f1())
}

create函数返回了闭包函数,该闭包函数引用了自由变量c,下面来分析该闭包的实现

create对应的汇编分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"".create STEXT size=126 args=0x8 locals=0x28 funcid=0x0
        0x0000 00000 (for.go:7) TEXT    "".create(SB), ABIInternal, $40-8
        0x0000 00000 (for.go:7) MOVQ    (TLS), CX
        0x0009 00009 (for.go:7) CMPQ    SP, 16(CX)
        0x000d 00013 (for.go:7) PCDATA  $0, $-2
        0x000d 00013 (for.go:7) JLS     119
        0x000f 00015 (for.go:7) PCDATA  $0, $-1
        0x000f 00015 (for.go:7) SUBQ    $40, SP # SP向下扩栈
        0x0013 00019 (for.go:7) MOVQ    BP, 32(SP) # 保存父函数BP
        0x0018 00024 (for.go:7) LEAQ    32(SP), BP # BP指向当前函数的BP
        0x001d 00029 (for.go:7) FUNCDATA        $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
        0x001d 00029 (for.go:7) FUNCDATA        $1, gclocals·2a5305abe05176240e61b8620e19a815(SB)
        0x001d 00029 (for.go:7) MOVQ    $0, "".~r0+48(SP) # 初始化返回值
        0x0026 00038 (for.go:8) MOVQ    $2, "".c+16(SP) # 初始化变量c
        0x002f 00047 (for.go:9) LEAQ    type.noalg.struct { F uintptr; "".c int }(SB), AX  # 编译时生成的一个闭包结构体
        0x0036 00054 (for.go:9) MOVQ    AX, (SP)  # 将闭包结构体信息保存在SP指向的内存地址,该地址会作为runtime.newobject的输入参数,还记得GO函数调用的约定吧,返回该闭包结构体的指针,返回地址为8(SP)
        0x003a 00058 (for.go:9) PCDATA  $1, $0
        0x003a 00058 (for.go:9) CALL    runtime.newobject(SB) # new闭包结构体对象
        0x003f 00063 (for.go:9) MOVQ    8(SP), AX  # 将闭包指针放入AX
        0x0044 00068 (for.go:9) MOVQ    AX, ""..autotmp_2+24(SP) # 将闭包指针放入24(SP)
        0x0049 00073 (for.go:9) LEAQ    "".create.func1(SB), CX # CX寄存区存入create.func1函数地址
        0x0050 00080 (for.go:9) MOVQ    CX, (AX) # 将create.func1函数地址存入闭包指针中,也就是闭包结构体中的F字段
        0x0053 00083 (for.go:9) MOVQ    ""..autotmp_2+24(SP), AX 
        0x0058 00088 (for.go:9) TESTB   AL, (AX)
        0x005a 00090 (for.go:9) MOVQ    "".c+16(SP), CX # 将自有变量c存入CX寄存器
        0x005f 00095 (for.go:9) MOVQ    CX, 8(AX) #将自有变量c放入闭包结构体中的c字段
        0x0063 00099 (for.go:9) MOVQ    ""..autotmp_2+24(SP), AX
        0x0068 00104 (for.go:9) MOVQ    AX, "".~r0+48(SP)
        0x006d 00109 (for.go:9) MOVQ    32(SP), BP
        0x0072 00114 (for.go:9) ADDQ    $40, SP
        0x0076 00118 (for.go:9) RET
        0x0077 00119 (for.go:9) NOP
        0x0077 00119 (for.go:7) PCDATA  $1, $-1
        0x0077 00119 (for.go:7) PCDATA  $0, $-2
        0x0077 00119 (for.go:7) CALL    runtime.morestack_noctxt(SB)
        0x007c 00124 (for.go:7) PCDATA  $0, $-1
        0x007c 00124 (for.go:7) JMP     0

从汇编中可以看到create函数实际返回的是一个结构体指针,该结构体定义如下

1
2
3
4
type noalg struct {
	F uintptr
	c int
}

这个结构体定义在编译时生成。

main函数汇编分析

我们想要的是一个类型为func () int 的函数,但是返回了一个结构体指针,那么实际在调用的时候发生了什么呢,我们接下来继续看汇编

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"".main STEXT size=485 args=0x0 locals=0xf0 funcid=0x0
        0x0000 00000 (for.go:14)        TEXT    "".main(SB), ABIInternal, $240-0
        0x0000 00000 (for.go:14)        MOVQ    (TLS), CX
        0x0009 00009 (for.go:14)        LEAQ    -112(SP), AX
        0x000e 00014 (for.go:14)        CMPQ    AX, 16(CX)
        0x0012 00018 (for.go:14)        PCDATA  $0, $-2
        0x0012 00018 (for.go:14)        JLS     471
        0x0018 00024 (for.go:14)        PCDATA  $0, $-1
        0x0018 00024 (for.go:14)        SUBQ    $240, SP
        0x001f 00031 (for.go:14)        MOVQ    BP, 232(SP)
        0x0027 00039 (for.go:14)        LEAQ    232(SP), BP
        0x002f 00047 (for.go:14)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x002f 00047 (for.go:14)        FUNCDATA        $1, gclocals·2f4566c65595035c8390cb8744947365(SB)
        0x002f 00047 (for.go:14)        FUNCDATA        $2, "".main.stkobj(SB)
        0x002f 00047 (for.go:15)        PCDATA  $1, $0
        0x002f 00047 (for.go:15)        CALL    "".create(SB) # 期望返回函数对象,实际返回的一个结构体指针
        0x0034 00052 (for.go:15)        MOVQ    (SP), DX # 将结构体指针赋值给DX寄存器
        0x0038 00056 (for.go:15)        MOVQ    DX, "".f1+88(SP)
        0x003d 00061 (for.go:16)        MOVQ    (DX), AX # 取出结构体中的F字段值(实际为闭包函数地址),赋值给AX寄存器
        0x0040 00064 (for.go:16)        CALL    AX # 发起调用
        0x0042 00066 (for.go:16)        MOVQ    (SP), AX
        0x0046 00070 (for.go:16)        MOVQ    AX, ""..autotmp_7+72(SP)
        0x004b 00075 (for.go:16)        MOVQ    AX, (SP)

这里可以发现,对于闭包函数的调用和正常函数的调用有很大区别,一般函数的调用,是caller准备好函数入参后,调用call指令,但是对于闭包函数的调用,这里是将闭包结构体地址存入DX寄存器后,发起调用,这其实是GO语言闭包函数调用的约定。

闭包代码的汇编分析

接下来,继续看看闭包函数的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
"".create.func1 STEXT nosplit size=50 args=0x8 locals=0x10 funcid=0x0
        0x0000 00000 (for.go:9) TEXT    "".create.func1(SB), NOSPLIT|NEEDCTXT|ABIInternal, $16-8
        0x0000 00000 (for.go:9) SUBQ    $16, SP  # 函数栈帧初始化,扩栈操作
        0x0004 00004 (for.go:9) MOVQ    BP, 8(SP) # 保存父函数BP
        0x0009 00009 (for.go:9) LEAQ    8(SP), BP # 设置当前函数BP
        0x000e 00014 (for.go:9) FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (for.go:9) FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (for.go:9) MOVQ    8(DX), AX  # 从DX+8地址读取变量c
        0x0012 00018 (for.go:9) MOVQ    AX, "".c(SP) # 变量c写入到SP地址处
        0x0016 00022 (for.go:9) MOVQ    $0, "".~r0+24(SP)  # 初始化返回值
        0x001f 00031 (for.go:10)        MOVQ    "".c(SP), AX # 将c写入AX
        0x0023 00035 (for.go:10)        MOVQ    AX, "".~r0+24(SP) # 将AX 写入返回值
        0x0028 00040 (for.go:10)        MOVQ    8(SP), BP # 回复函数调用现场,恢复BP
        0x002d 00045 (for.go:10)        ADDQ    $16, SP # 恢复SP
        0x0031 00049 (for.go:10)        RET

create.func是我们代码中第9、10行定义的闭包函数,从汇编源码中,可以看到闭包函数的执行逻辑,获取自由变量的逻辑,是从寄存器DX中读取闭包结构体地址+偏移的方式,和一般函数的调用获取参数不一样。

疑问

到这里,我们基本已经了解了GO语言中闭包的实现原理了。

这里可以再思考一下,为什么闭包函数中对于自由变量的读取逻辑需要通过DX寄存器+偏移的方式呢,为啥不能和正常函数调用一样,通过SP+偏移的方式呢?

这是因为SP+偏移的方式,调用者需要知道被调用者入参个数及类型,这样在编译期间,编译器可以根据函数签名安排好函数栈帧布局,分配好return value, param var等栈帧布局,GO语言函数栈帧布局如下

但是,对于闭包来说,调用者实际并不知道闭包函数中具体引用了多少自由变量,如果调用者也使用这种方式传递函数参数,那么编译就需要去分析每一个被调用者的具体代码逻辑,增加了编译器的复杂度和实现难度。所以闭包的调用使用了一种比较取巧的方式,调用者只需要将闭包结构体地址放入DX寄存器中,在闭包的实现逻辑中,按照DX+偏移的方式去取对应的自由变量值即可。

闭包的实现,很多是编译器帮忙处理的,编译期间分析代码,捕获自由变量,生成闭包函数结构体定义等等,闭包函数结构体本身有一个类型原型,在runtime.funcval,定义如下

1
2
3
4
5
// runtime/runtime2.go:198
type funcval struct {
	fn uintptr
	// variable-size, fn-specific data here
}

验证

验证闭包结构体的生成,修改代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
)

func create() func() (int, int) {
	a := 3
	b := 5
	return func() (int, int) {
		return a, b
	}
}

func main() {
	f1 := create()
	fmt.Println(f1())

}

对应create函数的汇编如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"".create STEXT size=158 args=0x8 locals=0x30 funcid=0x0
        0x0000 00000 (for.go:7) TEXT    "".create(SB), ABIInternal, $48-8
        0x0000 00000 (for.go:7) MOVQ    (TLS), CX
        0x0009 00009 (for.go:7) CMPQ    SP, 16(CX)
        0x000d 00013 (for.go:7) PCDATA  $0, $-2
        0x000d 00013 (for.go:7) JLS     148
        0x0013 00019 (for.go:7) PCDATA  $0, $-1
        0x0013 00019 (for.go:7) SUBQ    $48, SP
        0x0017 00023 (for.go:7) MOVQ    BP, 40(SP)
        0x001c 00028 (for.go:7) LEAQ    40(SP), BP
        0x0021 00033 (for.go:7) FUNCDATA        $0, gclocals·2a5305abe05176240e61b8620e19a815(SB)
        0x0021 00033 (for.go:7) FUNCDATA        $1, gclocals·2a5305abe05176240e61b8620e19a815(SB)
        0x0021 00033 (for.go:7) MOVQ    $0, "".~r0+56(SP)
        0x002a 00042 (for.go:8) MOVQ    $3, "".a+24(SP)
        0x0033 00051 (for.go:9) MOVQ    $5, "".b+16(SP)
        0x003c 00060 (for.go:10)        LEAQ    type.noalg.struct { F uintptr; "".a int; "".b int }(SB), AX   # 闭包对象结构体定义
        0x0043 00067 (for.go:10)        MOVQ    AX, (SP)
        0x0047 00071 (for.go:10)        PCDATA  $1, $0
        0x0047 00071 (for.go:10)        CALL    runtime.newobject(SB)
        0x004c 00076 (for.go:10)        MOVQ    8(SP), AX
        0x0051 00081 (for.go:10)        MOVQ    AX, ""..autotmp_3+32(SP)
        0x0056 00086 (for.go:10)        LEAQ    "".create.func1(SB), CX
        0x005d 00093 (for.go:10)        MOVQ    CX, (AX)
        0x0060 00096 (for.go:10)        MOVQ    ""..autotmp_3+32(SP), AX
        0x0065 00101 (for.go:10)        TESTB   AL, (AX)
        0x0067 00103 (for.go:10)        MOVQ    "".a+24(SP), CX
        0x006c 00108 (for.go:10)        MOVQ    CX, 8(AX)
        0x0070 00112 (for.go:10)        MOVQ    ""..autotmp_3+32(SP), AX
        0x0075 00117 (for.go:10)        TESTB   AL, (AX)
        0x0077 00119 (for.go:10)        MOVQ    "".b+16(SP), CX
        0x007c 00124 (for.go:10)        MOVQ    CX, 16(AX)
        0x0080 00128 (for.go:10)        MOVQ    ""..autotmp_3+32(SP), AX
        0x0085 00133 (for.go:10)        MOVQ    AX, "".~r0+56(SP)
        0x008a 00138 (for.go:10)        MOVQ    40(SP), BP
        0x008f 00143 (for.go:10)        ADDQ    $48, SP
        0x0093 00147 (for.go:10)        RET

main函数中的闭包调用代码

1
2
3
4
5
6
7
8
"".main STEXT size=682 args=0x0 locals=0x140 funcid=0x0
...
        0x0032 00050 (for.go:16)        CALL    "".create(SB) # 创建闭包对象
        0x0037 00055 (for.go:16)        MOVQ    (SP), DX  # 将闭包结构体地址存储到DX集群器
        0x003b 00059 (for.go:16)        MOVQ    DX, "".f1+112(SP)
        0x0040 00064 (for.go:17)        MOVQ    (DX), AX
        0x0043 00067 (for.go:17)        CALL    AX # 发起调用
...

闭包的实现代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
"".create.func1 STEXT nosplit size=78 args=0x10 locals=0x18 funcid=0x0
        0x0000 00000 (for.go:10)        TEXT    "".create.func1(SB), NOSPLIT|NEEDCTXT|ABIInternal, $24-16
        0x0000 00000 (for.go:10)        SUBQ    $24, SP
        0x0004 00004 (for.go:10)        MOVQ    BP, 16(SP)
        0x0009 00009 (for.go:10)        LEAQ    16(SP), BP
        0x000e 00014 (for.go:10)        FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (for.go:10)        FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x000e 00014 (for.go:10)        MOVQ    8(DX), AX # 通过DX+偏移获取自由变量
        0x0012 00018 (for.go:10)        MOVQ    AX, "".a+8(SP)
        0x0017 00023 (for.go:10)        MOVQ    16(DX), AX # 通过DX+偏移获取自由变量
        0x001b 00027 (for.go:10)        MOVQ    AX, "".b(SP)
        0x001f 00031 (for.go:10)        MOVQ    $0, "".~r0+32(SP)
        0x0028 00040 (for.go:10)        MOVQ    $0, "".~r1+40(SP)
        0x0031 00049 (for.go:11)        MOVQ    "".a+8(SP), AX
        0x0036 00054 (for.go:11)        MOVQ    AX, "".~r0+32(SP)
        0x003b 00059 (for.go:11)        MOVQ    "".b(SP), AX
        0x003f 00063 (for.go:11)        MOVQ    AX, "".~r1+40(SP)
        0x0044 00068 (for.go:11)        MOVQ    16(SP), BP
        0x0049 00073 (for.go:11)        ADDQ    $24, SP
        0x004d 00077 (for.go:11)        RET