0%

go 闭包实现机制

最近在 hack viewcore,需要了解闭包的实现机制,来完成逆向的类型推导,所以搞了几个小例子,分析了闭包的实现机制,简单记录一下的。

运行时表示

在运行时,闭包是一个 GC 对象,可以用下面的结构来表示。

1
2
3
4
5
6
type closure struct {
pc func
arg1
arg2
...
}

pc 比较好理解,就是对应函数的入口地址。
arg1, arg2, … 则是闭包函数引用的上层局部变量。

如下示例:

1
2
3
4
5
func genClosure(a int, b int) func(int) bool {
return func(n int) bool {
return n*a-b > 0
}
}

生成的汇编如下,核心就是构造一个新的 GC 对象:

1
2
3
4
5
6
7
8
LEAQ runtime.rodata+58080(SB), AX   ## 0x1099b00 <_type.*+0xe2e0>
CALL runtime.newobject(SB)
LEAQ main.genClosure.func1(SB), CX
MOVQ CX, 0(AX)
MOVQ 0x20(SP), CX
MOVQ CX, 0x8(AX)
MOVQ 0x28(SP), CX
MOVQ CX, 0x10(AX)

此时生成的闭包则等效于这样的 struct 对象:

1
2
3
4
5
type closure struct {
pc func
a int
b int
}

调用过程

调用闭包,跟普通的函数调用基本类似,只是把闭包对象放到 DX 寄存器。

例如下面的示例:

1
2
f := genClosure(10, 100)
v := f(10)

生成如下汇编:

1
2
3
4
5
6
7
MOVL $0xa, AX
MOVL $0x64, BX
CALL main.genClosure(SB)
MOVQ 0(AX), CX
MOVQ AX, DX
MOVL $0xa, AX
CALL CX

我们可以看到:

  1. 先从闭包对象中取出函数入口地址,写入 CX 寄存器
  2. 将闭包对象写入 DX 寄存器
  3. CALL CX,调用闭包函数

总结

简单的来说,闭包的实现也比较简单,通过一个 GC 对象,将函数入口地址,以及引用的局部变量,都装进来,就是一个闭包对象了。
调用的时候,将闭包对象,作为函数的第四个参数,也就是使用 DX 寄存器传参。