Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ポインタ解析を使った静的解析ツールの話 - tenntenn Conference

ポインタ解析を使った静的解析ツールの話 - tenntenn Conference

この資料はtenntenn Conference 2022にて発表を行った際に用いた資料です。

■ tenntenn Conferenceとは
tenntenn Conferenceはtenntennが主催し、そしてすべてのセッションがtenntennによる登壇のカンファレンスです。

イベントページ:https://tenntenn.connpass.com/event/226562/
ハッシュタグ:#tennconn
資料(Google スライド):https://tenn.in/pointer
動画:https://tenn.in/pointer-video
再生リスト:https://tenn.in/conn22-videolist

■ 内容
このセッションではGoにおけるポインタ解析を使った静的解析ツールの作り方について紹介しています。Goではgolang.org/x/tools/go/pointerパッケージを使うと簡単にポインタ解析を行えます。ポインタ解析によって何が解析できるのか、それが分かると何が嬉しいのかについて解説しています。

・ポインタ解析
・静的解析
・コールグラフ

■ 登壇者&主催者

・名前:tenntenn / 上田拓也
・HP:https://tenntenn.dev
・Twitter:https://twitter.com/tenntenn

メルカリ/メルペイ所属。バックエンドエンジニアとして日々Goを書いている。Google Developer Expert (Go)。一般社団法人Gophers Japan代表。Go Conference主催者。大学時代にGoに出会い、それ以来のめり込む。人類をGopherにしたいと考え、Goの普及に取り組んでいる。複数社でGoに関する技術アドバイザーをしている。大学においてGoに関する集中講義も担当している。マスコットのGopherの絵を描くのも好き。

■ Gopher道場 自習室

https://gopherdojo.org/studyroom/

Gopher道場とは、実践的なGoを体系的に学べる場です。
Gopher道場 自習室では、以下のようなコンテンツや学びの場を提供します。

・Gopher道場の講義を録画した動画(10時間以上分)
・Slackにおける受講者同士のコミュニティ
・Gopher道場卒業生による課題のレビュー(ボランティアでご協力頂いているのでベストエフォートです)

■ Meety(カジュアル面談)

・ソフトウェアエンジニアの地方移住ってどうなの?:https://meety.net/matches/jyZgDkEEwmMk
・メルカリグループにおけるGoの使いどころ:https://meety.net/matches/LbeVbIACxLqk
・地方からの技術コミュニティへの貢献:https://meety.net/matches/gVeMtImLkWJE

■ お仕事の依頼について

副業にて技術顧問やアドバイザーなどを行っています。過去の実績や問い合わせフォームは以下のURLからご確認ください。
https://tenntenn.dev/ja/job/

tenntenn - Takuya Ueda

January 15, 2022
Tweet

More Decks by tenntenn - Takuya Ueda

Other Decks in Programming

Transcript

  1. The Go gopher was designed by Renée French. The gopher

    stickers was made by Takuya Ueda. Licensed under the Creative Commons 3.0 Attributions license. ポインタ解析を使った静 的解析ツールの話 2022/01/15(土) tenntenn Conference 2022 資料:https://tenn.in/pointer 動画:https://tenn.in/pointer-video
  2. 上田拓也 Go ビギナーズ
 Go Conference
 @tenntenn tenntenn.dev Google Developer Expert

    (Go) 一般社団法人 Gophers Japan 代表理事 Experts Team
  3. 解説するツール ▪ ポインタ解析を行い結果を表示するツール • https://github.com/gostaticanalysis/ptrls package main func main() {

    f(map[string]int{}) f(map[string]int{}) } func f(m map[string]int) { println(len(m)) } # a.goのオフセット80(81バイト目)を指定 $ ptrls `pwd`/a.go 80 m a.go:4:18 map[string]int a.go:5:18 map[string]int
  4. 処理の流れ ▪ 構文解析と型チェックを行う • golang.org/x/tools/go/pacakgesパッケージを使う • Linterではない静的解析ツールに使われる ▪ 静的単一代入形式(SSA)に変換 •

    golang.org/x/tools/ssaパッケージを使う • buildssa.Analyzerを参考に依存するパッケージも含めて構築する ▪ ポインター解析 • golang.org/x/tools/pointerパッケージを使う • 指定位置の抽象構文木のノードから対応するSSAの値を取得する • SSAの値を対象にポインタ解析を行う
  5. x/tools/go/packagesパッケージ ▪ 構文解析と型チェックまで行う • パッケージ名からGoファイルの情報などを取得 ◦ https://pkg.go.dev/cmd/go#hdr-Package_lists_and_patterns • 構文解析と型チェックまでを自動で行う ◦

    packages.LoadModeによって不必要な情報は取得しなくできる 6 mode := packages.NeedFiles | packages.NeedSyntax // 構文解析まで cfg := &packages.Config{Mode:mode} pkgs, err := packages.Load(cfg, os.Args[1:]...) if err != nil { /* エラー処理 */ } if packages.PrintErrors(pkgs) > 0 { /* エラー処理 */ } for _, pkg := range pkgs { for _, f := range pkg.Syntax { ast.Print(pkg.Fset, f) } }
  6. packages.Package構造体 ▪ パッケージ単位の解析結果を保持する型 • Syntaxフィールドに抽象構文木 • Fsetフィールドにノードの位置情報 • TypeInfoフィールドに型情報 type

    Package struct { ID string Name string PkgPath string Errors []Error GoFiles []string CompiledGoFiles []string OtherFiles []string IgnoredFiles []string ExportFile string Imports map[string]*Package Types *types.Package Fset *token.FileSet IllTyped bool Syntax []*ast.File TypesInfo *types.Info TypesSizes types.Sizes Module *Module }
  7. 静的単一代入(SSA)形式 8 ▪ 変数への入力を1回だけに制限した形式 • gc(Goのコンパイラ)の中でも使われている技術 ◦ 別パッケージで表現方法は異なる ◦ 最適化に使われている

    • golang.org/x/tools/go/ssaパッケージが担当 ◦ 静的解析ツール用のパッケージ ◦ gcでは使われていない n := 10 n += 10 n0 := 10 n1 := n0 + 10
  8. 静的単一代入形式への変換 ▪ 依存するパッケージも変換する • (*ssa.Program).CreatePackageメソッドで*ssa.Packageを作る • 抽象構文木と型情報をまとめておく ◦ packages.Load関数の結果はパッケージごと //

    デバッグ情報を付加しながら生成する prog := ssa.NewProgram(fset, ssa.GlobalDebug) created := make(map[*types.Package]bool) var createAll func(pkgs []*types.Package) createAll = func(pkgs []*types.Package) { for _, p := range pkgs { if created[p] { continue } created[p] = true prog.CreatePackage(p, nil, nil, true) createAll(p.Imports()) } } var mainPkg *packages.Package var files []*ast.File info := &types.Info{ /* 初期化(略) */ } for _, pkg := range pkgs { // packages.Load関数で得た結果 createAll(pkg.Types.Imports()) // 依存パッケージを処理 mergeTypesInfo(info, pkg.TypesInfo) // 型情報をまとめる files = append(files, pkg.Syntax...) // 抽象構文木をまとめる if pkg.Module != nil && pkg.Module.Main { mainPkg = pkg } } if mainPkg == nil { /* エラー処理 */ } ssapkg := prog.CreatePackage(mainPkg.Types, files, info, true) ssapkg.Build()
  9. ポインタ解析 11 ▪ ポインタがどこを指しているのか解析する • x/tools/go/pointerパッケージを用いる • ポインタpがどの変数を指し示すのか ▪ エスケープ解析の一部で利用される技術

    • コンパイラ内部で行われる • スタックまたはヒープに割り当てるべきか解析する func f() { var n int p := &n g(p) var m int g(&m) } func g(p *int) { /* 略 */ }
  10. ポインタ解析の手順 ▪ 静的単一代入形式に変換 • go/analysisパッケージの場合はbuildssa.Analyzerを利用する • 今回の場合は前述の方法で自力で作成 ▪ クエリの登録 •

    pointer.Config構造体に解析用のクエリを登録する • ポインタ解析は重たい解析なので解析したいものを明示的に登録する type Config struct { Mains []*ssa.Package // 静的単一代入形式 Reflection bool BuildCallGraph bool // コールグラフを生成するか Queries map[ssa.Value]struct{} // クエリ: ptr(x) IndirectQueries map[ssa.Value]struct{} // デリファレンして解析 : ptr(*x) Log io.Writer }
  11. 解析対象を取得(位置の取得) ▪ ファイル名とoffsetからソースコード上の位置を取得 • token.FileSet構造体から取得する • token.Pos型がソースコード上の位置を表す func (prog *Program)

    Pos(filename string, offset int) token.Pos { var pos token.Pos prog.Fset.Iterate(func(f *token.File) bool { if f.Name() == filename { pos = f.Pos(offset); return false } return true }) return pos }
  12. 解析対象を取得(ノードの取得) ▪ 位置情報から抽象構文木のノードを取得 • x/tools/go/ast/astutilパッケージを用いる • astutil.PathEnclosingInterval関数で取得 func (prog *Program)

    Path(pos token.Pos) (path []ast.Node, exact bool) { for _, f := range prog.Files { // token.Pos型は整数なので比較できる if f.Pos() <= pos && pos <= f.End() { return astutil.PathEnclosingInterval(f, pos, pos) } } return nil, false }
  13. クエリの追加 ▪ (*pointer.Config).AddQueryメソッドを使う • 抽象構文木のノードに対応する型を調べて解析可能か調べる • 抽象構文木のノードに対応する静的単一代入形式の値を取得(後述) • クエリに追加する path,

    exact := prog.Path(pos) if !exact { /* 指定位置のノードが取得出来なかった */ } expr, _ := path[0].(ast.Expr) typ := prog.TypesInfo.TypeOf(expr) // 対象の式の型情報取得 if !pointer.CanPoint(typ) { continue } // ポインタ解析できない場合は Skip v := getValue(prog, path, expr) // 対応する静的単一代入の値を取得 if v == nil { continue } value2node[v] = expr // 静的単一代入の値と式を対応付けておく config.AddQuery(v) // クエリに追加
  14. ast.Expr型からssa.Value型を取得 ▪ デバッグ情報を持つ静的単一代入形式なら取得可 • 静的単一代入形式の構築時にssa.GlobalDebugを指定する ◦ 抽象構文木の情報が静的単一代入形式に残る • (*ssa.Function).ValueForExprメソッドを用いる •

    または、(*ssa.Program).VarValueメソッドを用いる func getValue(p *Program, path []ast.Node, expr ast.Expr) ssa.Value { f := ssa.EnclosingFunction(p.Main, path) // 抽象構文木のパスから静的単一代入形式の関数を取得 if f != nil { if v, _ := f.ValueForExpr(expr); v != nil { return v } } var id *ast.Ident switch expr := expr.(type) { case *ast.Ident: id = expr case *ast.SelectorExpr: id = expr.Sel // x.yのような形式の式(yの部分を取得) } o, _ := p.TypesInfo.ObjectOf(id).(*types.Var) // 識別子に対応するオブジェクトを取得(変数) if o != nil { if v, _ := p.SSA.VarValue(o, p.Main, path); v != nil { return v } } return nil }
  15. コールグラフ ▪ 関数の呼び出しグラフ • golang.org/x/tools/go/callgraphで表される • コールグラフを生成するアルゴリズムでいくつか種類がある ◦ golang.org/x/tools/go/callgraph/static ◦

    golang.org/x/tools/go/callgraph/cha ◦ golang.org/x/tools/go/callgraph/rta ◦ golang.org/x/tools/go/callgraph/vta ◦ golang.org/x/tools/go/pointer 20 main() f1() f2() f3() f4()
  16. コールグラフを表す型 ▪ golang.org/x/tools/go/callgraphで定義される 21 type Graph struct { Root *Node

    // ルートノード Nodes map[*ssa.Function]*Node // 関数との対応 } type Node struct { Func *ssa.Function // どの関数に対応するか ID int // 0ベースの連番 In []*Edge // ソートされてない入力エッジのスライス(n.In[*].Callee == n) Out []*Edge // ソートされてない出力エッジのスライス(n.Out[*].Caller == n) } type Edge struct { Caller *Node Site ssa.CallInstruction Callee *Node }
  17. アルゴリズムの違い 22 アルゴリズム 説明 パッケージ 静的取得 静的に決まる関数呼び出しのみ取得する x/tools/go/callgraph/static Andersenのアルゴリズム ポインタ解析とともに取得

    x/tools/go/pointer Class Hierarchy Analysis (CHA) インタフェースを実装しているすべての方 のメソッドにエッジを結ぶ x/tools/go/callgraph/cha Rapid Type Analysis (RTA) ポインタ解析のものより精度は低くなるが 高速 x/tools/go/callgraph/rta Variable Type Analysis (VTA) 変数を起点として解析を行う、精度は良い がコストが高い。 x/tools/go/callgraph/vta 参考:https://ben-holland.com/call-graph-construction-algorithms-explained/
  18. callgraph/vtaパッケージを使った例 ▪ go/analysisパッケージを使うと簡単 • buildssa.Analyzerを使って静的単一代入形式に変換 func run(pass *analysis.Pass) (interface{}, error)

    { s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) cg := vta.CallGraph(ssautil.AllFunctions(s.Pkg.Prog), cha.CallGraph(s.Pkg.Prog)) callgraph.GraphVisitEdges(cg, func(edge *callgraph.Edge) error { caller := edge.Caller.Func if caller.Pkg == pass.Pkg { edges = append(edges, fmt.Sprint(caller, " --> ", edge.Callee.Func)) } return nil }) sort.Strings(edges) for _, edge := range edges { fmt.Println(edge) } fmt.Println() return nil, nil }
  19. ポインタ解析によるコールグラフ生成 ▪ ポインタ経由の関数呼び出しも取得できる • poniter.Config構造体のBuildCallGraphフィールドをtrueにする • pointer.Result構造体のCallGraphフィールドから取得できる 24 func callgraph(result

    *pointer.Result) { var edges []string callgraph.GraphVisitEdges(result.CallGraph, func(edge *callgraph.Edge) error { caller := edge.Caller.Func if caller.Pkg == mainPkg { edges = append(edges, fmt.Sprint(caller, " --> ", edge.Callee.Func)) } return nil }) sort.Strings(edges) for _, edge := range edges { fmt.Println(edge) } fmt.Println() }