Golang内部构件,第4部分:目标文件和函数元数据

今天,我们将仔细研究其Func结构,并讨论有关垃圾回收在Go中如何工作的一些细节。

这篇文章是“Golang内部知识,第3部分:链接器和Go对象文件”的继续,并使用相同的示例程序。因此,我们强烈建议您在继续之前先阅读上一部分。

函数元数据的结构

重定位背后的主要思想应该从第3部分中清楚地了解。现在,让我们看一下Funcmain方法的结构

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运行时使用的函数元数据。该文章介绍了在不同领域的具体格式和意义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
}

您可以看到并非目标文件中的所有信息都已直接映射。某些字段仅由链接器使用。不过,这里最有趣的是pcsppcfilepcln领域,当其使用程序计数器相应地转换成堆栈指针,文件名和行号。

例如,这在panic发生时是必需的。在那一刻,运行时仅知道已触发的当前汇编指令的程序计数器panic。因此,运行时使用该计数器来获取当前文件,行号和完整的堆栈跟踪。使用pcfilepcln字段直接解析文件和行号。使用递归解决堆栈跟踪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行,而计数器从39next_function_program_counter - 1对应于第5行。为了节省空间,存储下面的映射就足够了。

26 - 4
39 - 5
…

这几乎就是编译器所做的。该pcln字段指向映射中与当前功能的第一个程序计数器相对应的特定偏移量。在知道此偏移量以及下一个功能的第一个程序计数器的偏移量后,运行时可以使用二进制搜索来找到与给定程序计数器相对应的行号。

在Go中,这个想法是广义的。不仅行号或堆栈指针可以映射到程序计数器,而且任何整数值都可以映射。这是通过PCDATA指令完成的。每次链接器都会找到以下指令。
(这个东西是用来辅助GC的,在go源码中搜索pcdatavalue可以看出具体是如何使用的。参考scanframeworker)(https://zhuanlan.zhihu.com/p/...

它不会生成任何实际的汇编程序指令。相反,它将此指令的第二个参数存储在具有当前程序计数器的映射中,而第一个参数指示使用的是哪个映射。使用第一个参数,我们可以轻松地添加新映射,这对于编译器和运行时是已知的,但对于链接程序却是不透明的。

垃圾收集器如何使用函数元数据

函数元数据中仍然需要澄清的最后一件事是FuncData数组。它包含垃圾收集所需的信息。Go使用分阶段运行的标记清除垃圾收集器(GC)。在第一阶段(标记),它将遍历所有仍在使用的对象并将它们标记为可到达。在第二(扫描)阶段中,所有未标记的对象都将被删除。

因此,垃圾收集器首先在几个已知的位置中查找可访问的对象,例如全局变量,处理器寄存器,堆栈帧和已到达对象中的指针。但是,如果仔细考虑一下,则在堆栈帧中查找指针绝非易事。因此,当运行时执行垃圾回收时,如何区分堆栈中的变量是指针还是属于非指针类型?这就是FuncData发挥作用的地方。

对于每个函数,编译器都会创建两个变量。一个包含用于堆栈帧的参数区域的位图矢量。另一个包含帧其余部分的位图,该位图包含函数中定义的指针类型的所有局部变量。这些变量中的每一个都告诉垃圾收集器,指针在堆栈帧中的确切位置,并且该信息足以完成其工作。

这也是值得一提的喜欢PCDATAFUNCDATA也由伪围棋汇编指令产生的。

0x001a 00026 (test.go:3)    FUNCDATA    $0,gclocals·3280bececceccd33cb74587feedb1f9f+0(SB)`

该指令的第一个自变量表示这是自变量的函数数据还是局部变量区域。第二个实际上是对包含GC掩码的隐藏变量的引用。

在接下来的文章中,我们将研究Go引导过程,这是了解Go运行时如何工作的关键。

你可能感兴趣的:(golang)