【“骚”操作】AST实现代码调用跟踪

tohearts · · 115 次点击 · 开始浏览    置顶

# 思考 当你接手一个新的Go语言项目时,如果前任开发者没有留下设计文档,或者文档已经过时、代码注释也不够清楚,那你可能会觉得很难快速上手。 这就跟旅行一样,如果你有一张地图,旅途就会顺利很多。因此我想到了一个叫go-callvis的工具,它可以通过分析项目的语法树来展示所有函数之间的调用关系,并以网页的形式呈现出来,方便查看。 不过,在实际使用中我发现这个工具生成网页图的速度有点慢。 为了解决这个问题,我自己开发了两个小工具来辅助。 # goanalysis 此前,我曾开发过这一工具,但遗憾的是,当时的代码并未妥善保存,也未上传至GitHub。因此,本次决定将该工具公开分享。此工具具备以下两大功能: 1. analysis: 通过项目生成相关调用链图,如: ![](https://cdn.nlark.com/yuque/0/2024/png/22935287/1730166073888-8895b42a-e6f5-4a23-ad1b-4f377852af99.png) 2. track: 将项目内所有函数都内置上defer trace(),在运行过程中,将goroutine中所有调用的函数以及参数输出,比如: ```go time=2024-10-28T23:12:07.398+08:00 level=INFO msg=->example/inner/B.init.0 gid=1 params="[[]], " time=2024-10-28T23:12:07.415+08:00 level=INFO msg=*<-example/inner/B.init.0 gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=->example/inner/A.init.0 gid=1 params="[[]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*->example/inner/B.CalledA gid=1 params="[[]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=**<-example/inner/B.CalledA gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*<-example/inner/A.init.0 gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=->main.main gid=1 params="[[]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*->example/inner/A.NewCallA gid=1 params="[[tly]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=**<-example/inner/A.NewCallA gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*->example/inner/A.CallA.PrintB gid=1 params="[[]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=**->example/inner/B.NewCallB gid=1 params="[[levi]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=***<-example/inner/B.NewCallB gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=**->example/inner/B.CallB.PrintB gid=1 params="[[]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=***<-example/inner/B.CallB.PrintB gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=**<-example/inner/A.CallA.PrintB gid=1 time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*->example/inner/A.RecursionA gid=1 params="[[%!s(int=1) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=**->example/inner/A.RecursionA gid=1 params="[[%!s(int=2) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=***->example/inner/A.RecursionA gid=1 params="[[%!s(int=3) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=****->example/inner/A.RecursionA gid=1 params="[[%!s(int=4) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*****->example/inner/A.RecursionA gid=1 params="[[%!s(int=5) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=******->example/inner/A.RecursionA gid=1 params="[[%!s(int=6) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*******->example/inner/A.RecursionA gid=1 params="[[%!s(int=7) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=********->example/inner/A.RecursionA gid=1 params="[[%!s(int=8) %!s(int=10)]], " time=2024-10-28T23:12:07.416+08:00 level=INFO msg=*********->example/inner/A.RecursionA gid=1 params="[[%!s(int=9) %!s(int=10)]], " time=2024-10-28T23:12:07.417+08:00 level=INFO msg=**********->example/inner/A.RecursionA gid=1 params="[[%!s(int=10) %!s(int=10)]], " time=2024-10-28T23:12:07.417+08:00 level=INFO msg=***********<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=**********<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=*********<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=********<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=*******<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=******<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=*****<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=****<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=***<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=**<-example/inner/A.RecursionA gid=1 time=2024-10-28T23:12:07.417+08:00 level=INFO msg=*<-main.main gid=1 ``` # 原理说明 ## analysis ### 通过标准库获取编译后callGraph ```go // 1. 加载项目代码所有的package名称 initial, _ := packages.Load(&packages.Config{}, p.Dir+"/...") if packages.PrintErrors(initial) > 0 { return fmt.Errorf("packages contain errors") } // 2. 基于指定的package名称,创建SSA项目(包含所有引用的包) prog, _ := ssautil.AllPackages(initial, 0) prog.Build() // 3. 通过不同的算法,获取相关的调用图 switch p.algo { case CallGraphTypeStatic: p.callGraph = static.CallGraph(prog) case CallGraphTypeCha: p.callGraph = cha.CallGraph(prog) case CallGraphTypeRta: mains, err := p.GetMainPackage(prog.AllPackages()) if err != nil { return err } var roots []*ssa.Function for _, main := range mains { roots = append(roots, main.Func("main")) } p.callGraph = rta.Analyze(roots, true).CallGraph case CallGraphTypeVta: p.callGraph = vta.CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) } ``` ### 通过callgraph生成项目整体的语法树 ```go type FuncNode struct { Key string `json:"key"` // 唯一表示 Pkg string `json:"pkg"` Name string `json:"name"` Parent []string `json:"parent"` // 通过key来索引 Children []string `json:"children"` // 通过key来索引 } err := callgraph.GraphVisitEdges(p.callGraph, func(edge *callgraph.Edge) error { // 获取调用方 caller := edge.Caller // 获取被调用方 callee := edge.Callee if isSynthetic(edge) { return nil } // 排除标准库 if inStd(caller) || inStd(callee) { return nil } if isInter(edge) { return nil } // 排除需要忽略的库, 在启动前可以指定 if inIgnores(caller) || inIgnores(callee) { return nil } // caller是否存在 var pNode, qNode *FuncNode var ok bool // 如果不存在, 则创建 if pNode, ok = p.tree.Nodes[caller.String()]; !ok { pNode = &FuncNode{ Key: caller.String(), Pkg: caller.Func.Pkg.Pkg.Path(), Name: caller.Func.RelString(caller.Func.Pkg.Pkg), } p.tree.Nodes[pNode.Key] = pNode } if qNode, ok = p.tree.Nodes[callee.String()]; !ok { qNode = &FuncNode{ Key: callee.String(), Pkg: callee.Func.Pkg.Pkg.Path(), Name: callee.Func.RelString(callee.Func.Pkg.Pkg), } p.tree.Nodes[qNode.Key] = qNode } if strings.HasSuffix(caller.String(), "main") && p.tree.MainKey == "" { p.tree.MainKey = caller.String() } pNode.Children = append(pNode.Children, qNode.Key) qNode.Parent = append(qNode.Parent, pNode.Key) fmt.Printf("%s to %s \n", caller, callee) return nil }) ``` 通过以上操作将所有函数都保存到内存以及缓存文件中,这样就可以进行各种相关链路图片的生成了。 ### 举例 在github:[https://github.com/toheart/goanalysis](https://github.com/toheart/goanalysis) 中,存在测试的example,获取其中`B.PrintB`的相关链路关系,可以执行一下命令: ```bash go run . analysis "D:\code\goanalysis\example" -p "n44:(example/inner/B.CallB).PrintB" ``` 其中`B.PrintB`的key可以通过缓存文件来查询: ![B.PrintB](https://cdn.nlark.com/yuque/0/2024/png/22935287/1730168329210-03beaae4-1e4a-4c66-95ca-76d8bab3a342.png) ## track 想快速的分析代码运行时如何执行,在函数中添加入口函数以及defer函数是最快的方式。 按照一般思路,我们需要手动在代码内部一行行的加入相同代码。 对于程序员来说,”懒惰“是优良品质,所有的重复性劳动都需要思考如何自动化完成。 那么自带干电池的”go“也提供这种方式。 在上面我们了解了ast相关库的使用,其中`parser.ParseFile`可以用来静态解析文件,通过分析出当前文件语法树包含的所有函数。 代码:[https://github.com/toheart/goanalysis/blob/main/pkg/track/rewrite.go](https://github.com/toheart/goanalysis/blob/main/pkg/track/rewrite.go) 核心流程: ```go func (r *Rewrite) RewriteFile() { flag := false // 插入defer函数 for _, item := range r.f.Decls { funcDel, ok := item.(*ast.FuncDecl) if !ok { continue } // 判断是否需要插入defer函数 if r.HasSameDefer(funcDel) { continue } elts := r.genTraceParams(funcDel.Type) deferStmt := r.genDefer(elts) // 将defer语句添加到函数体的开头 funcDel.Body.List = append([]ast.Stmt{deferStmt}, funcDel.Body.List...) flag = true } if flag { // 如果上面插入了defer函数, 那么就说明需要插入import r.ImportFunctrace() } buf := &bytes.Buffer{} err := format.Node(buf, r.fset, r.f) if err != nil { return } if debug { fmt.Println(buf.String()) return } // 重写文件 if err = os.WriteFile(r.fullPath, buf.Bytes(), 0666); err != nil { fmt.Printf("write %s error: %v\n", r.fullPath, err) return } } ``` # 总结 对于这个小工具,还有一个“装逼”功能没有完成:通过gitlab的merge,输出当前合并所有改动影响。(后续补坑吧)。 如果对于你后续阅读源码有帮助,一键三连,支持一下。 # 参考说明 1. [https://tonybai.com/2020/12/10/a-kind-of-thinking-about-how-to-trace-function-call-chain/](https://tonybai.com/2020/12/10/a-kind-of-thinking-about-how-to-trace-function-call-chain/) 2. https://mattermost.com/blog/instrumenting-go-code-via-ast/ 3. [https://yuroyoro.github.io/goast-viewer/](https://yuroyoro.github.io/goast-viewer/)

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

115 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传