Go语言编程通过dwarf获取内联函数

目录
  • dwarf组成
    • 如何将 addr 转换为行号
    • 内联函数
    • 如何展开内联函数
    • 使用 parca 展开内联函数
    • parca 输出有以下问题

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

调用栈:

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

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

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 写错了。

以上就是Go语言编程通过dwarf获取内联函数的详细内容,更多关于Go dwarf获取内联函数的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go语言defer的一些神奇规则示例详解

    目录 测试题 分析 规则一当defer被声明时,其参数就会被实时解析 规则二 defer可能操作主函数的具名返回值 规则三 延迟函数执行按后进先出顺序执行 坑实例 测试题 defer有一些规则,如果不了解,代码实现的最终结果会与预期不一致.对于这些规则,你了解吗? 这是关于defer使用的代码,可以先考虑一下返回值. package main import ( "fmt" ) /** * @Author: Jason Pang * @Description: 快照 */ func de

  • Android Studio gradle配置packagingOptions打包so库重复

    目录 正文 pickFirst 匹配 doNotStrip 设置 merge 将匹配的文件都添加到APK中 exclude 过滤 正文 在安卓开发中,通常会使用到gradle来编译,在安卓项目的app目录下的build.gradle中是用来对编译进行配置的,packagingOptions 是其中的一个打包配置,常见的设置项有exclude.pickFirst.doNotStrip.merge. 在日常代码开发中,我们需要知其然,而知其所以然,本文章知识也是Android日常瘦身的的必备知识.

  • Dragonfly P2P 传输协议优化代码解析

    目录 优化背景 相关代码分析 优化方案 优化实现 优化结果 优化背景 此前 Dragonfly 的 P2P 下载采用静态限流策略,相关配置项在 dfget.yaml 配置文件中: # 下载服务选项. download: # 总下载限速. totalRateLimit: 1024Mi # 单个任务下载限速. perPeerRateLimit: 512Mi 其中 perPeerRateLimit 为单个任务设置流量上限, totalRateLimit 为单个节点的所有任务设置流量上限. 静态限流策略

  • go variant底层原理深入解析

    目录 varint benchmarks struct variant 为什么 variant 要比 plainstruct 快 variant 可能的优化? varint 今天本来在研究 OpenTelemetry 的基准性能测试 github.com/zdyj3170101…,测试不同网络协议:grpc, grpc-stream, http, websocket 在发送不同大小数据包时消耗 cpu,吞吐 的区别,由 tigrannajaryan 这位大神所写. 好奇翻了翻该大神的 githu

  • Golang Copier入门到入坑探究

    目录 正文 安装 快速入门 入坑 再探坑出坑 再盘一盘坑 结语 正文 github: https://github.com/jinzhu/copier 由于 golang 没有对复杂结构体的 clone 方法,所以,就需要有 copier 这样的工具库. 它看起来很简单,但实际使用中,有些“坑”还是要注意! 本文: 入门为辅,探“坑”为主, 看完再划走,CS我没有. 安装 go get github.com/jinzhu/copier 快速入门 好的,来一段代码快速了解 copier packa

  • Go语言编程通过dwarf获取内联函数

    目录 dwarf组成 如何将 addr 转换为行号 内联函数 如何展开内联函数 使用 parca 展开内联函数 parca 输出有以下问题 dwarf组成 dwarf 由 The Debugging Information Entry . type Entry struct { Offset Offset Tag Tag // 描述其类型 Children bool Field []Field // 包含的字段 } 不同的 entry 有不同的类型: tag compile unit, 在 go

  • js获取内联样式的方法

    本文实例讲述了js获取内联样式的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,target-densitydpi=high-dpi,in

  • C++编程中队内联函数的理解和使用

    函数调用过程 c++经过编译生成可执行程序文件exe,存放在外存储器中.程序启动,系统从外存储器中将可执行文件装载到内存中,从入口地址(main函数起始处)开始执行.程序执行中遇到了对其他函数的调用,就暂停当前函数的执行,并保存下一条指令的地址作为从被调函数返回后继续执行的入口点,保存现场.然后转到被调函数的入口地址执行被调函数.遇到return语句或者被调函数结束后,恢复先前保存的现场,从先前保存的返回地址处继续执行主调函数的其余部分. 内联函数 函数调用需要进行现场保护,以便在函数调用之后继

  • 关于javascript获取内联样式与嵌入式样式的实例

    通过style属性设置背景图案 <!--html--> <div id="change"> change color </div> /*css*/ #change { border: 1px solid black; width: 200px; height: 200px; text-align: center; line-height: 200px; } //js change.style.backgroundColor="purple&

  • C语言中的内联函数(inline)与宏定义(#define)详细解析

    先简明扼要,说下关键:1.内联函数在可读性方面与函数是相同的,而在编译时是将函数直接嵌入调用程序的主体,省去了调用/返回指令,这样在运行时速度更快. 2.内联函数可以调试,而宏定义是不可以调试的.内联函数与宏本质上是两个不同的概念如果程序编写者对于既要求快速,又要求可读的情况下,则应该将函数冠以inline.下面详细介绍一下探讨一下内联函数与宏定义. 一.内联函数是什么?内联函数是代码被插入到调用者代码处的函数.如同 #define 宏(但并不等同,原因见下文),内联函数通过避免被调用的开销来提

  • 浅谈内联函数与宏定义的区别详解

    用内联取代宏:1.内联函数在运行时可调试,而宏定义不可以;2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会: 3.内联函数可以访问类的成员变量,宏定义则不能: 4.在类中声明同时定义的成员函数,自动转化为内联函数.文章(一)内联函数与宏定义 在C中,常用预处理语句#define来代替一个函数定义.例如: #define MAX(a,b) ((a)>(b)?(a):(b)) 该语句使得程序中每个出现MAX(a,b)函数调用的地方都被宏定义中后面的表达式((a)

  • 详解C++中的内联函数和函数重载

    内联函数(内嵌函数,内置函数) 调用函数时需要一定的时间和空间的开销.C++提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开.这种在函数调用处直接嵌入函数体的函数称为内联函数(inline function),又称内嵌函数或内嵌函数. 指定内联函数的方法很简单,只需要在定义函数时增加 inline 关键字. 注意:是在函数定义时增加 inline 关键字,而不是在函数声明时.在函数声明时增加 inline 关键虽然没有错误,但是也没有任何效果 inline 关键

  • 深入探讨:宏、内联函数与普通函数的区别

    内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算求值,然后把表达式的值传递给形式参数.    内联函数与带参数宏定义的另一个区别是,内联函数的参数类型和返回值类型在声明中都有明确的指定:而带参数宏定义的参数没有类型的概念,只有在宏展开以后,才由编译器检查语法,这就存在很多的安全隐患.    使用内联函数时,应注意以下问题:    1)内联函数的定

  • Python 如何定义匿名或内联函数

    问题 你想为 sort() 操作创建一个很短的回调函数,但又不想用 def 去写一个单行函数, 而是希望通过某个快捷方式以内联方式来创建这个函数. 解决方案 当一些函数很简单,仅仅只是计算一个表达式的值的时候,就可以使用lambda表达式来代替了.比如: >>> add = lambda x, y: x + y >>> add(2,3) 5 >>> add('hello', 'world') 'helloworld' >>> 这里使用

  • C++ inline内联函数详解

    函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码.CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,CPU 转而执行被调函数的代码:被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行. 一个 C/C++ 程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条,这个链条的起点是 main(),终点也是 main().当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生

随机推荐