[Go]从汇编层面理解Go的closures机制

Prologue

Go除了对并发的良好的支持外,设计思路也是他颇有特色的一点。其设计上也借鉴了面向函数编程的思想, 闭包就是其中一个有趣的机制。
闭包,术语化的讲法是:一个函数以及他引用环境的集合。我们首先看一下,一个相对简单的闭包函数:

package main

func main(){
     
	intseq := getfunc(1)
	intseq()
}

func getfunc(a int) func() int{
     
	i := 1
	return func() int{
     
		i = i + a
		return i
	}
}

下面是大致的汇编代码,可以通过go tool objdump -s "main\.main" test2以及go tool objdump -s "main\.getfunc" test2获取
[Go]从汇编层面理解Go的closures机制_第1张图片
[Go]从汇编层面理解Go的closures机制_第2张图片
[Go]从汇编层面理解Go的closures机制_第3张图片

main函数内部逻辑

[Go]从汇编层面理解Go的closures机制_第4张图片
核心逻辑可能分为如图所示的四部分。

  1. Part1:修改%rsp以及%rbp,同时保存caller的%rbp
  2. Part2:将函数的参数(1)放入栈顶,然后调用getfunc
  3. Part3:将结果从堆栈中(%rsp+8)取出,然后存入%rdx,之后将*%rdx存入%rax,然后调用得到的函数。
  4. Part4:恢复堆栈,ret。

main非常简单易懂,主要注意的就是Go通过栈来完成对于函数参数以及结果的传递。

getfunc的内部逻辑

[Go]从汇编层面理解Go的closures机制_第5张图片

  1. 同main,栈相关
  2. 在堆上创建一个新对象,通过后面的movq $0x1, 0(%rax)不难看出来他是创建了一个指针,指向的地址的内容为1,我们可以通过gdb具体查看执行的细节:
    [Go]从汇编层面理解Go的closures机制_第6张图片
    可以看到,runtime.newobject()读取了0x461FA0作为参数,这个参数的类型是*_Type,这里对应的值是0x08,我没有深入的考究这个导致指代哪个类型
    [Go]从汇编层面理解Go的closures机制_第7张图片
    在runtime创建对象完毕之后,结果被保存在0x08(%rsp)里面,因为之后我们还要调用一次newobject,所以编译器又将结果转移到了0x10(%rsp)
  3. 和上面类似,但是这一次新分配到的地址就是就是闭包函数的首地址了(leaq main.getfunc.func1, CX),同时因为之后没有再调用newobject,所以不需要再转移到其他位置。
  4. 最后的工作就是将闭包里面的其他变量——也就是函数的引用环境,全部放到一个存储结构里面。我们可以看一下最后(%rax)附近是个什么样的景象
    [Go]从汇编层面理解Go的closures机制_第8张图片
    我们最终返回的就是%rax,他被存在0x30(%rsp)里面,在我们ret之后,就正好是0x8(%rsp),之后的main里面操作就是将这个结构存储在%rdx里面,然后从%rdx中取出函数的首地址进行调用。
    [Go]从汇编层面理解Go的closures机制_第9张图片

匿名函数的内部逻辑

[Go]从汇编层面理解Go的closures机制_第10张图片
非常简单,%rdx之后0x08是i的指针,而0x10则是a,他们相加,将i的值更新,然后放入堆栈。因为这个运算非常简单,所以编译器也没有进行栈相关的操作。

Epilogue

在上面的一个例子里面,返回值直接放到了%rdx里面,然后进行之后的操作,那么,如果有多个函数该怎么办呢?
[Go]从汇编层面理解Go的closures机制_第11张图片
从这张图里面,我们可以看到几个重要的信息:

  1. 如果有多个闭包函数或者匿名函数,他们会存在堆栈里面
  2. 非闭包的匿名函数,也是将自己的地址存在内存中,然后访问这个内存去取址的,不同的是他们的结构里面仅有一个函数的地址,没有其他内部变量的地址了。

Reference

  1. A Quick Guide to Go’s Assembler
  2. Golang 闭包实现原理
  3. Package types
  4. AT&T Assembly Syntax
  5. [译] 解析 Go 中的函数调用

你可能感兴趣的:(Go,go,函数闭包)