管中窥go_01_pclntab
除非做额外说明, 针对的都是 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 这样的角色 上述逻辑的理解可以参考 go12PCToFile 和 go12PCToLine.
inline, FUNCDATA, PCDATA
Go 在 panic 是记录调用栈的信息也依靠的是 pclntab, Caller. 我们可以看到在遍历 Frame 的逻辑中, 有很大一部分是处理 inline 的. 内联是一种编译时的优化, 指编译器将某些短小函数的代码直接加入到调用处, 从而减少运行时的开销.
但在展开调用栈时, 我们显然希望依旧保留相关调用信息, 所以 inline func 需要被特殊处理. 从 _func 的注释中可以看到, inline 的信息被保存在 FUNCDATA 和 PCDATA. FUNCDATA_InlTree 存储了内联函数的具体信息, PCDATA_InlTreeIndex 存储了函数对应的下标, 如果函数是内联函数的话. 展开调用栈中的操作 symtab.go:122 也可以验证这部分逻辑.