阅读 27

通过 dwarf 获取内联函数

dwarf 由 The Debugging Information Entry 组成。

type Entry struct {     Offset   Offset     Tag      Tag // 描述其类型     Children bool     Field    []Field // 包含的字段 } 复制代码

不同的 entry 有不同的类型:

  • tag compile unit, 在 go 中就表示一个 package 下的所有源代码文件。

  • tag sub program, 表示函数

一个 entry 有不同的 attr:

  • AT_low_pc, AT_high_pc 分别代表函数的 起始/结束 PC地址

  • AttrName 表示名字

对于函数:

package s     func Leaf(lx, ly int) int {         return (lx << 7) ^ (ly >> uint32(lx&7))     }     func Top(tq int) int {         var tv [10]int         tr := Leaf(tq-13, tq+13)         return tr + tv[tr&3]     } 复制代码

对应的 entry:

DW_TAG_complication_unit{ // package s DW_TAG_subprogram {       DW_AT_name:            s.Top       DW_TAG_formal_parameter {          DW_AT_name:         tq  // 参数名          DW_AT_type:         ... // 参数类型       }      } }    复制代码

如何将 addr 转换为行号

  • seekpc 返回该 pc 对应的 complication unit。(类似于线性搜索,并且下一次调用 seekpc,会在上一次的之后开始搜索,所以 pc 最好需要排序)

  • dwarf.Reader.Next() 将会循环读取 entry,如果是函数并且地址在范围内,就认为找到了对应 address 的函数名。

  • dwarf line reader 将会返回该 complication unit 对应的 line 信息。

 // go1.19/src/cmd/pprof/pprof.go:300 func pctoLine(f *elf.File, pc uint64) []driver.Frame { dwarf, _ := f.DWARF() r := dwarf.Reader() unit, _ := r.SeekPC(pc) lines, _ := dwarf.LineReader(unit) var lentry godwarf.LineEntry if err := lines.SeekPC(pc, &lentry); err != nil { log.Fatal(err) } // Try to find the function name. name := "" FindName: for entry, err := r.Next(); entry != nil && err == nil; entry, err = r.Next() { if entry.Tag == godwarf.TagSubprogram { ranges, err := dwarf.Ranges(entry) if err != nil { log.Fatal(err) } for _, pcs := range ranges { if pcs[0] <= pc && pc < pcs[1] { var ok bool // TODO: AT_linkage_name, AT_MIPS_linkage_name. name, ok = entry.Val(godwarf.AttrName).(string) if ok { break FindName } } } } } frames := []driver.Frame{ { Func: name, File: lentry.File.Name, Line: lentry.Line, }, } return frames } 复制代码

内联函数

使用 pprof 获取地址:

其中 simplify1 是被内联的函数。

2152: 0x5a51a8 M=1 regexp/syntax.simplify1 /usr/local/go1.18/go/src/regexp/syntax/simplify.go:148 s=0              regexp/syntax.(*Regexp).Simplify /usr/local/go1.18/go/src/regexp/syntax/simplify.go:100 s=0 复制代码

调用栈:

截屏2022-11-22 下午7.53.16.png

使用正常的方式获取地址:

被内联的函数消失了,但是行号还是正确的。

regexp/syntax.(*Regexp).Simplify /usr/local/go1.18/go/src/regexp/syntax/simplify.go 148 复制代码

如何展开内联函数

inline 内联设计:go.googlesource.com/proposal/+/…

如果我们有一个函数:

package s     func Leaf(lx, ly int) int {         return (lx << 7) ^ (ly >> uint32(lx&7))     }     func Top(tq int) int {         var tv [10]int         tr := Leaf(tq-13, tq+13)         return tr + tv[tr&3]     } 复制代码

那么对于 top 这个程序,我们会包含以下 entry:

  • tag_subprogram: 表示 top 这个函数

  • tag_subprogram: 表示 leaf 这个内联函数的抽象(含有函数名,不含有地址范围)

  • TAG_inlined_subroutine: 表示 leaf 这个内联函数的实体。(包含地址范围等信息)

DW_TAG_subprogram {       DW_AT_name:            s.Top       DW_TAG_formal_parameter {          DW_AT_name:         tq          DW_AT_type:         ...       }         // abstract inline function DW_TAG_subprogram {   // offset: D1       DW_AT_name:            s.Leaf       DW_AT_inline : DW_INL_inlined (not declared as inline but inlined)       ...       DW_TAG_formal_parameter {   // offset: D2          DW_AT_name:         lx          DW_AT_type:         ...       }       DW_TAG_formal_parameter {    // offset: D3          DW_AT_name:         ly          DW_AT_type:         ...       }       ...    }       // inlined body of 'Leaf'       DW_TAG_inlined_subroutine {          DW_AT_abstract_origin: // reference to D1 above          DW_AT_call_file: 1          DW_AT_call_line: 15          DW_AT_ranges         : ...          DW_TAG_formal_parameter {             DW_AT_abstract_origin: // reference to D2 above             DW_AT_location:        ...          }          DW_TAG_formal_parameter {             DW_AT_abstract_origin: // reference to D3 above             DW_AT_location:        ...          }       }    } 复制代码

因此,通过 pc 地址不断的循环遍历 inline_subroutine 这种类型的 entry,我们就可以获取所有的内联函数。

func inlineStackInternal(stack []*godwarf.Tree, n *godwarf.Tree, pc uint64) []*godwarf.Tree { switch n.Tag { case dwarf.TagSubprogram, dwarf.TagInlinedSubroutine, dwarf.TagLexDwarfBlock: if pc == 0 || n.ContainsPC(pc) { for _, child := range n.Children { stack = inlineStackInternal(stack, child, pc) } if n.Tag == dwarf.TagInlinedSubroutine { stack = append(stack, n) } } } return stack } 复制代码

然后,我们通过该 entry 的 AbstractOrigin 字段,获取 abstract function,然后就可以得到函数名。

abstractOrigin := f.abstractSubprograms[e.Val(dwarf.AttrAbstractOrigin).(dwarf.Offset)] 复制代码

使用 parca 展开内联函数

使用 parca 获取内联:

package main import ( "debug/elf" "log" "os" "strconv" parcadwarf "github.com/parca-dev/parca/pkg/symbol/addr2line" "github.com/parca-dev/parca/pkg/symbol/demangle" ) func main() { f, _ := elf.Open(os.Args[1]) debug, _ := parcadwarf.DWARF(nil, f, demangle.NewDemangler("simple", false)) pc, _ := strconv.ParseUint(os.Args[2], 16, 64) log.Println(debug.PCToLines(pc)) } 复制代码

pprof raw 的输出,该 address fe1475 总共代表三个函数:

1951: 0xfe1475 M=1 google.golang.org/grpc/metadata.Join /home/gitlab-runner/go/pkg/mod/google.golang.org/grpc@v1.48.0/metadata/metadata.go:141 s=0              google.golang.org/grpc/metadata.MD.Copy /home/gitlab-runner/go/pkg/mod/google.golang.org/grpc@v1.48.0/metadata/metadata.go:92 s=0              go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.UnaryServerInterceptor.func1 /home/gitlab-runner/go/pkg/mod/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc@v0.33.0/interceptor.go:304 s=0 复制代码

输出:

./dwarf/dwarf /home/data/server/otel-collector/data/otelcol-contrib  fe1475 [{297 name:"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.UnaryServerInterceptor.func1" filename:"/home/gitlab-runner/go/pkg/mod/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc@v0.33.0/interceptor.go"} {138 name:"?" filename:"/home/gitlab-runner/go/pkg/mod/google.golang.org/grpc@v1.48.0/metadata/metadata.go"} {92 name:"?" filename:"/home/gitlab-runner/go/pkg/mod/google.golang.org/grpc@v1.48.0/metadata/metadata.go"}] 复制代码

parca 的输出有以下问题:

  • 无法正确的获取内联的函数名

  • 内联函数的行号不正确。

  • 内联函数顺序不对

对于第一个问题,其实是 parca 只会将 pc 地址表示的当前 complication unit 进行内联函数映射。

对于途中就是 interceptor 这个库。

而内联的函数在 meatadata 这个库,所以无法正确的获取函数名。

对于第二个问题,由于内联函数展开后,获取的是 DW_TAG_subprogram,它映射一个范围内的地址,自然也无法精确的获取行号。

对于第三个问题,parca 写错了。


作者:用户杨杰
链接:https://juejin.cn/post/7168807606146826254


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐