今天,我们来看一下 Func 结构体,还会讨论一些关于 Go 垃圾回收的一些细节。
本文是 《Go语言内幕(3):链接器、链接器、重定位》的后续,我会使用相同的示例程序。因此,如果你没有读过前面这篇博客,我强烈去读一下再来阅读这篇博客。
函数元数据的结构体
重定位背后的原理在本系列的第三部分就已经讲得很清楚了。接下来,我们来看一下 main 方法中的 Func 结构体:
Func: &goobj.Func{ Args: 0, Frame: 8, Leaf: false, NoSplit: false, Var: { }, PCSP: goobj.Data{Offset:255, Size:7}, PCFile: goobj.Data{Offset:263, Size:3}, PCLine: goobj.Data{Offset:267, Size:7}, PCData: { {Offset:276, Size:5}, }, FuncData: { { Sym: goobj.SymID{Name:"gclocals·3280bececceccd33cb74587feedb1f9f", Version:0}, Offset: 0, }, { Sym: goobj.SymID{Name:"gclocals·3280bececceccd33cb74587feedb1f9f", Version:0}, Offset: 0, }, }, File: {"/home/adminone/temp/test.go"}, },
你可以认为这个结构体就是由编译器在目标文件中创建的函数元数据。在 Go 运行时(rumtime)中会用到这个数据结构。这篇文章解释了 Func 结构体中不同域的格式以及其含义。在这儿,我主要分析运行时是如何使用这个元数据的。
在运行时包内,这个结构体被映射为如下的结构体:
type _func struct { entry uintptr // start pc nameoff int32 // function name args int32 // in/out args size frame int32 // legacy frame size; use pcsp if possible pcsp int32 pcfile int32 pcln int32 npcdata int32 nfuncdata int32 }
你可以看到并不是目标文件中的所有信息都被直接映射过来。一些域只是为了提供给链接器使用。同样,这其中最有意思的是 pcsp、pcfile 与 pln。在程序计数器(program counter)被转换成栈指针、文件名、以及行号时会分别用到这三个域。
例如,在发生 panic 时,这种转换很有必要的。这时候,运行时只知道触发 panic 的汇编指令的程序计数器值。所以,所以运行时需要通过计数器获得当前文件、当前行以及整个栈轨迹。文件和行号可以直接使用 pcfile 与 pcln 获得。栈轨迹则使用 pcsp 递规获得。
如果我们已经有了程序计数器值,问题就变成了我们怎么取得相应的行号呢?要回答这个问题,你需要去看一下汇编代码并搞明白行号是如何存在目标文件中的:
0x001a 00026 (test.go:4) MOVQ $1,(SP) 0x0022 00034 (test.go:4) PCDATA $0,$0 0x0022 00034 (test.go:4) CALL ,runtime.printint(SB) 0x0027 00039 (test.go:5) ADDQ $8,SP 0x002b 00043 (test.go:5) RET ,
我们可以看到 26 到 38 的程序计数器包含了相应的行号 4。计数器从 39 一直到 next_program_counter — 对应于行 5。为了存储的效率,使用下面的映射就可以存储这些信息了:
26 - 4 39 - 5 …
实际上编译器就是这么做的。 pcln 域指向在 map 中一个偏移位置,这个位置处存储了当前函数起始程序计数器值。知道偏移值和下一个函数的起始程序计数器的偏移值后,运行时就可以用二分查找的方式找到给定程序计数器对应的行号了。
在 Go 语言中, 这种想法还是挺常见的。不仅仅是行号或者栈指针可以直接映射到程序计数器,任何整数值都可以使用 PCDATA 指令来做这样的映射。每一次链接器发现下面这样的指令时:
0x0022 00034 (test.go:4) PCDATA $0,$0
它并不会生成任何汇编指令。而是将程序计数器与第二个参数一起存储到一个映射中,其中第一个参数表明了使用哪一个映射。通过其第一个参数,我们很方便就可以添加一个新的映射,这也就意味着映射对于编译器和运行时是可见的,对链接器却是透明的。
垃圾收集器(GC)是如何使用函数元数据的呢?
函数元数据中最后一个值得分析的就是 FuncData 数组。这个数组存储了垃圾收集器所必需的信息。Go 语言使用标记-清除(mark-and-sweep)垃圾收集器。这种垃圾收集器分为两个阶段的工作。第一阶段为标记阶段,GC 遍历所有仍然在使用的对象,并将其标记为可达。第二阶段为清除阶段,所有没有被标记的对象都在该阶段被删除。
垃圾收集器从几个位置处搜索可达的对象,包括全局变量、寄存器、栈帧以及可达对象内的指针。如果你细想一下,在栈中搜索指针并不是一个简单的工作。因为在运行时执行垃圾收集过程时,它到底是如何区分栈中的变量是一个指针还是非指针类型呢?这就需要 FuncData 来发挥作用了。
对于每个函数,编译器都会为其创建两个位图向量。其中一个表示函数的参数的范围。另一个则表示栈帧中存储局部变量的区域。这两个位图变量可以告诉垃圾收集器栈帧中哪些位置上是指针,这些信息就可以帮助垃圾收集器完成垃圾收集工作了。
值得一提的是,和 PCDATA 类似,FUNCDATA 也是由 Go 伪汇编指令生成的:
0x001a 00026 (test.go:3) FUNCDATA $0,gclocals·3280bececceccd33cb74587feedb1f9f+0(SB)
这条指令的第一个参数指明这是参数区的函数数据还是局部变量区的函数数据。每二个参数实际上是一个引用,它引用了一个存储 GC 掩码(mask)的隐藏变量。
下一篇
在接下来的文章中, 我会讲解 Go 的自举(bootstrap)过程。这是理解 Go 运行时的关键所在。下周见。
有疑问加站长微信联系(非本文作者)