Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

上田拓也 Go ビギナーズ
 Go Conference
 @tenntenn tenntenn.dev Google Developer Expert (Go) 一般社団法人 Gophers Japan 代表理事 Experts Team

Slide 3

Slide 3 text

この活動を支える企業 ■ 株式会社バニッシュ・スタンダード ● https://www.v-standard.com/ ● 採用情報:https://www.wantedly.com/projects/253980 ● https://github.com/gostaticanalysis/findnil (開発中)

Slide 4

Slide 4 text

解説するツール ■ ポインタ解析を行い結果を表示するツール ● 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

Slide 5

Slide 5 text

処理の流れ ■ 構文解析と型チェックを行う ● golang.org/x/tools/go/pacakgesパッケージを使う ● Linterではない静的解析ツールに使われる ■ 静的単一代入形式(SSA)に変換 ● golang.org/x/tools/ssaパッケージを使う ● buildssa.Analyzerを参考に依存するパッケージも含めて構築する ■ ポインター解析 ● golang.org/x/tools/pointerパッケージを使う ● 指定位置の抽象構文木のノードから対応するSSAの値を取得する ● SSAの値を対象にポインタ解析を行う

Slide 6

Slide 6 text

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) } }

Slide 7

Slide 7 text

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 }

Slide 8

Slide 8 text

静的単一代入(SSA)形式 8 ■ 変数への入力を1回だけに制限した形式 ● gc(Goのコンパイラ)の中でも使われている技術 ○ 別パッケージで表現方法は異なる ○ 最適化に使われている ● golang.org/x/tools/go/ssaパッケージが担当 ○ 静的解析ツール用のパッケージ ○ gcでは使われていない n := 10 n += 10 n0 := 10 n1 := n0 + 10

Slide 9

Slide 9 text

静的単一形式の構成 ■ x/tools/go/ssaパッケージのSSAの構成 ● 関数が基本ブロックで構成されている ● 基本ブロックは命令で構成されている ● 命令は複数のオペランドを持つ 9 Program Package Function ︙ ︙ BasicBlock ︙ ︙ Instruction オペランド Value ︙

Slide 10

Slide 10 text

静的単一代入形式への変換 ■ 依存するパッケージも変換する ● (*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()

Slide 11

Slide 11 text

ポインタ解析 11 ■ ポインタがどこを指しているのか解析する ● x/tools/go/pointerパッケージを用いる ● ポインタpがどの変数を指し示すのか ■ エスケープ解析の一部で利用される技術 ● コンパイラ内部で行われる ● スタックまたはヒープに割り当てるべきか解析する func f() { var n int p := &n g(p) var m int g(&m) } func g(p *int) { /* 略 */ }

Slide 12

Slide 12 text

ポインタ解析の手順 ■ 静的単一代入形式に変換 ● 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 }

Slide 13

Slide 13 text

解析対象を取得(位置の取得) ■ ファイル名と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 }

Slide 14

Slide 14 text

解析対象を取得(ノードの取得) ■ 位置情報から抽象構文木のノードを取得 ● 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 }

Slide 15

Slide 15 text

クエリの追加 ■ (*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) // クエリに追加

Slide 16

Slide 16 text

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 }

Slide 17

Slide 17 text

デモ

Slide 18

Slide 18 text

まとめ ■ 簡単にポインタ解析ができる ● 準標準パッケージでできる ● 実行しなくてもポインタの挙動が追える ● インタフェースを実装していた型ではなく実際の値も追える ● 静的解析で得た情報をフルに使う

Slide 19

Slide 19 text

時間が余ったら

Slide 20

Slide 20 text

コールグラフ ■ 関数の呼び出しグラフ ● 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()

Slide 21

Slide 21 text

コールグラフを表す型 ■ 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 }

Slide 22

Slide 22 text

アルゴリズムの違い 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/

Slide 23

Slide 23 text

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 }

Slide 24

Slide 24 text

ポインタ解析によるコールグラフ生成 ■ ポインタ経由の関数呼び出しも取得できる ● 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() }

Slide 25

Slide 25 text

デモ