除非做额外说明, 针对的都是 GOOS=linux GOARCH=amd64, 使用的 Go 版本是 go1.21.9.

如何找到 moduledata

关于 moduledata 理解的验证, 主要依靠 gore. GoReSym 相关的这边文章, 对我的理解也帮助颇多: Ready, Set, Go — Golang Internals and Symbol Recovery.

moduledata 存在 .noptrbss 中, 但其具体位置依赖于先找到 pclntab, moduledata 的第一个字段是指向 pclntab 的指针.

如果存在 .gopclntab.data.rel.ro.gopclntab, 则 pclntab 存在 section 开头. 否则要去 .data.rel.ro 中通过 pcHeader 开头的 magic 定位 pclntab.

pclntab

gore 中的 PCLNTab 展示了一个解析 pclntab 的入口. 核心逻辑在 debug/gosym/pclntab.go.

   247        offset := func(word uint32) uint64 {
   248            return t.uintptr(t.Data[8+word*t.ptrsize:])
   249        }
   250        data := func(word uint32) []byte {
   251            return t.Data[offset(word):]
   252        }
   253    
   254        switch possibleVersion {
   255        case ver118, ver120:
   256            t.nfunctab = uint32(offset(0))
   257            t.nfiletab = uint32(offset(1))
   258            t.textStart = t.PC // use the start PC instead of reading from the table, which may be unrelocated
   259            t.funcnametab = data(3)
   260            t.cutab = data(4)
   261            t.filetab = data(5)
   262            t.pctab = data(6)
   263            t.funcdata = data(7)
   264            t.functab = data(7)
   265            functabsize := (int(t.nfunctab)*2 + 1) * t.functabFieldSize()
   266            t.functab = t.functab[:functabsize]

其中直观可以理解的包括:

  • nfunctab 代表函数的数量
  • nfiletab 代表文件的数量
  • funcnametab 保存了函数名称

go12Funcs 展示了如何从 pclntab 中解析出函数. 其依赖的数据包括:

  • nfunctab, 函数的数量
  • functab, 顺序存储了所有函数的入口地址和保存函数信息的地址
  • funcdata, 存储了具体的函数信息

可以先阅读 Russ Cox 的 Go 1.2 Runtime Symbol Information, 在阅读这段代码理解 functab 的结构.

Specifically, the new function symbol table is a program counter lookup table of the form

  N pc0 func0 pc1 func1 pc2 func2 ... pc(N-1) func(N-1) pcN

This table is a count N followed by a list of alternating pc, function metadata pointer values. To find the function for a given program counter, the runtime does binary search on the pc values. The final pcN value is the address just beyond func(N-1), so that the binary search can distinguish between a pc inside func(N-1) and a pc outside the text segment.

在解析出函数的基础上, 我们可以从 pc 找到对应的 file 和 line.

  • pctab 存储了 pc 到相关信息的映射, 包括 pc 到 fno (file number)
    • 先找到 pc 对应的 func, 再从 func 对应的块开始定位, 是一种加速寻找的方式
  • cutab 存储了 fno 到具体文件信息存储地址的映射
  • 用这个地址可以去 filetab 寻找到对应的文件信息
  • pc -> line 的映射逻辑类似 file, 只是不需要 cutab 这样的角色 上述逻辑的理解可以参考 go12PCToFilego12PCToLine.

inline, FUNCDATA, PCDATA

Go 在 panic 是记录调用栈的信息也依靠的是 pclntab, Caller. 我们可以看到在遍历 Frame 的逻辑中, 有很大一部分是处理 inline 的. 内联是一种编译时的优化, 指编译器将某些短小函数的代码直接加入到调用处, 从而减少运行时的开销.

但在展开调用栈时, 我们显然希望依旧保留相关调用信息, 所以 inline func 需要被特殊处理. 从 _func 的注释中可以看到, inline 的信息被保存在 FUNCDATA 和 PCDATA. FUNCDATA_InlTree 存储了内联函数的具体信息, PCDATA_InlTreeIndex 存储了函数对应的下标, 如果函数是内联函数的话. 展开调用栈中的操作 symtab.go:122 也可以验证这部分逻辑.