3.6.3 PCDATA和FUNCDATA
Go语言中有个runtime.Caller函数可以获取当前函数的调用者列表。我们可以非常容易在运行时定位每个函数的调用位置,以及函数的调用链。因此在panic异常或用log输出信息时,可以精确定位代码的位置。
比如以下代码可以打印程序的启动流程:
func main() {
for skip := 0; ; skip++ {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
break
}
p := runtime.FuncForPC(pc)
fnfile, fnline := p.FileLine(0)
fmt.Printf("skip = %d, pc = 0x%08X\n", skip, pc)
fmt.Printf(" func: file = %s, line = L%03d, name = %s, entry = 0x%08X\n", fnfile, fnline, p.Name(), p.Entry())
fmt.Printf(" call: file = %s, line = L%03d\n", file, line)
}
}其中runtime.Caller先获取当时的PC寄存器值,以及文件和行号。然后根据PC寄存器表示的指令位置,通过runtime.FuncForPC函数获取函数的基本信息。Go语言是如何实现这种特性的呢?
Go语言作为一门静态编译型语言,在执行时每个函数的地址都是固定的,函数的每条指令也是固定的。如果针对每个函数和函数的每个指令生成一个地址表格(也叫PC表格),那么在运行时我们就可以根据PC寄存器的值轻松查询到指令当时对应的函数和位置信息。而Go语言也是采用类似的策略,只不过地址表格经过裁剪,舍弃了不必要的信息。因为要在运行时获取任意一个地址的位置,必然是要有一个函数调用,因此我们只需要为函数的开始和结束位置,以及每个函数调用位置生成地址表格就可以了。同时地址是有大小顺序的,在排序后可以通过只记录增量来减少数据的大小;在查询时可以通过二分法加快查找的速度。
在汇编中有个PCDATA用于生成PC表格,PCDATA的指令用法为:PCDATA tableid, tableoffset。PCDATA有个两个参数,第一个参数为表格的类型,第二个是表格的地址。在目前的实现中,有PCDATA_StackMapIndex和PCDATA_InlTreeIndex两种表格类型。两种表格的数据是类似的,应该包含了代码所在的文件路径、行号和函数的信息,只不过PCDATA_InlTreeIndex用于内联函数的表格。
此外对于汇编函数中返回值包含指针的类型,在返回值指针被初始化之后需要执行一个GO_RESULTS_INITIALIZED指令:
#define GO_RESULTS_INITIALIZED PCDATA $PCDATA_StackMapIndex, $1GO_RESULTS_INITIALIZED记录的也是PC表格的信息,表示PC指针越过某个地址之后返回值才完成被初始化的状态。
Go语言二进制文件中除了有PC表格,还有FUNC表格用于记录函数的参数、局部变量的指针信息。FUNCDATA指令和PCDATA的格式类似:FUNCDATA tableid, tableoffset,第一个参数为表格的类型,第二个是表格的地址。目前的实现中定义了三种FUNC表格类型:FUNCDATA_ArgsPointerMaps表示函数参数的指针信息表,FUNCDATA_LocalsPointerMaps表示局部指针信息表,FUNCDATA_InlTree表示被内联展开的指针信息表。通过FUNC表格,Go语言的垃圾回收器可以跟踪全部指针的生命周期,同时根据指针指向的地址是否在被移动的栈范围来确定是否要进行指针移动。
在前面递归函数的例子中,我们遇到一个NO_LOCAL_POINTERS宏。它的定义如下:
#define FUNCDATA_ArgsPointerMaps 0 /* garbage collector blocks */
#define FUNCDATA_LocalsPointerMaps 1
#define FUNCDATA_InlTree 2
#define NO_LOCAL_POINTERS FUNCDATA $FUNCDATA_LocalsPointerMaps, runtime·no_pointers_stackmap(SB)因此NO_LOCAL_POINTERS宏表示的是FUNCDATA_LocalsPointerMaps对应的局部指针表格,而runtime·no_pointers_stackmap是一个空的指针表格,也就是表示函数没有指针类型的局部变量。
PCDATA和FUNCDATA的数据一般是由编译器自动生成的,手工编写并不现实。如果函数已经有Go语言声明,那么编译器可以自动输出参数和返回值的指针表格。同时所有的函数调用一般是对应CALL指令,编译器也是可以辅助生成PCDATA表格的。编译器唯一无法自动生成是函数局部变量的表格,因此我们一般要在汇编函数的局部变量中谨慎使用指针类型。
对于PCDATA和FUNCDATA细节感兴趣的同学可以尝试从debug/gosym包入手,参考包的实现和测试代码。
学员评价