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

字句解析からポインタ解析までGoの静的解析のすべて - tenntenn Conference 2022

字句解析からポインタ解析までGoの静的解析のすべて - tenntenn Conference 2022

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

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

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

■ 内容
このセッションではGoにおける静的解析について字句解析からポインタ解析までどのような解析ができるのか網羅的に説明しています。静的解析という言葉を聞いたことがあるけど、イマイチ何ができるのかピンと来てない方にちょうどいい内容です。

・静的解析
・goパッケージ
・字句解析
・構文解析/抽象構文木(Abstract Syntax Tree; AST)
・型チェック
・静的単一代入(Static Single Assign; SSA)形式
・ポインタ解析
・コールグラフ

■ 登壇者&主催者

・名前: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. 字句解析からポインタ解析まで Goの静的解析のすべて 2022/01/15(土) tenntenn Conference 2022 資料:https://tenn.in/analysis-conn22 動画:https://tenn.in/analysis-conn22-video
  2. 上田拓也 Go ビギナーズ
 Go Conference
 @tenntenn tenntenn.dev Google Developer Expert

    (Go) 一般社団法人 Gophers Japan 代表理事 Experts Team
  3. go vet ▪ 公式の静的解析ツール • コンパイラでは発見できないバグを見つける • go testを走らせれば自動で実行される(Go1.10から) •

    The Go Playgroundでも実行される ◦ go.dev/playでは実行されなくなった! 6 package main import "fmt" func main() { fmt.Printf("%s\n", 100) } %sなのに数値が渡されている
  4. 静的解析ツールを自作する ▪ 自作する必要性 • プロジェクトの独自ルール ◦ 例:インポートルール • 決められたパッケージからしかインポートできない •

    レイヤードアーキテクチャのレイヤーをまたぐインポートの場合など • 特定のライブラリの使い方を検証したい ◦ 例:github.com/gcpug/zagane • Google Cloud Spannerのよくあるミスを静的解析ツールで指摘する • Google Cloud Spannerのセッションリークを静的解析で防ぐ - Mercari Engineering Blog • 欲しい静的解析ツールがない ◦ かゆいところに手が届くものがない ◦ なければ作るしかない ◦ レビューで毎回指摘しているようなものはツールにする
  5. goパッケージ go/ast 抽象構文木(AST)を提供 go/build パッケージに関する情報を集める go/constant 定数に関する型を提供 go/doc ドキュメントをASTから取り出す go/format

    コードフォーマッタ機能を提供 go/importer コンパイラに適したImporterを提供 go/parser 構文解析 機能を提供 go/printer AST 表示機能を提供 go/scanner 字句解析 機能を提供 go/token トークンに関する型を提供 go/types 型チェックに関する機能を提供 8
  6. x/tools/goパッケージ analysis 静的解析ツールをモジュール化するパッケージ ast AST関連のユーティリティ callgraph call graph関連 cfg control

    flow graph関連 expect 構造化されたコメントを処理する packages Go Modulesを前提としたパッケージ情報の収集から構文解析、 型チェックまでを行うパッケージ pointer ポインタ解析 ssa Static Single Assignment (SSA) 関連 types 型情報関連 9
  7. 静的解析とコンパイラ 10 ▪ goパッケージはコンパイラでは使われていない • あくまで静的解析用のパッケージ • GoのコンパイラはもともとCで書かれていた • コンパイラで用いているデータ構造と異なる場合がある

    ◦ 特に静的単一代入形式はまったく異なる ▪ リリース当初からgoパッケージは存在する • 言語設計時からツールが作りやすさを念頭においていた ◦ gofmt, go fix, godoc • 標準パッケージでサポートしている重要性 ◦ 言語仕様に追従していく 参考:Go at Google: Language Design in the Service of Software Engineering
  8. 構文解析 - go/parser,go/ast ▪ トークンを抽象構文木(AST)に変換 • AST: Abstract Syntax Tree

    v + 1 IDENT ADD INT ソースコード: + v 1 BinaryExpr Ident BasicLit トークン: 抽象構文木(AST): 13
  9. 構文解析で分からないこと ▪ 型情報 • 型の不一致など • 変数の型や式の型 ▪ 定数式の結果 •

    定数式の計算や定数の型 ▪ 識別子の解決 • どの識別子がどこで定義されているのか • どの識別子がどこで使用されているのか 15 + 100 "hoge" BinaryExpr BasicLit BasicLit 100 + "hello" 型が合わなくても文法上は問題はない
  10. 型チェック - go/types,go/constant ▪ 型情報を抽象構文木から抽出 • 識別子の解決 • 型の推論 •

    定数の評価 n := 100 + 200 m := n + 300 定数の評価 = 300 型の推論 -> int 識別子の解決 16
  11. 型チェックで分かること ▪ 型情報 • 型の不一致など • 変数の型や式の型 ▪ 定数式の結果 •

    定数式の計算や定数の型 ▪ 識別子の解決 • どの識別子がどこで定義されているのか • どの識別子がどこで使用されているのか 17
  12. analysis.Analyzer ▪ go/analysisの静的解析の1つの単位を表す構造体 • Runフィールドに処理の本体を書く • Requiresに依存するAnalyzerを書く type Analyzer struct

    { Name string Doc string Flags flag.FlagSet Run func(*analysis.Pass) (interface{}, error) RunDespiteErrors bool Requires []*analysis.Analyzer ResultType reflect.Type FactTypes []Fact }
  13. analysis.Pass ▪ 静的解析に使う情報が入った構造体 • Analyzer.Runフィールドの引数で用いられる type Pass struct { Analyzer

    *analysis.Analyzer Fset *token.FileSet Files []*ast.File OtherFiles []string Pkg *types.Package TypesInfo *types.Info TypesSizes types.Sizes Report func(analysis.Diagnostic) ResultOf map[*analysis.Analyzer]interface{} ImportObjectFact func(obj types.Object, fact analysis.Fact) bool ImportPackageFact func(pkg *types.Package, fact analysis.Fact) bool ExportObjectFact func(obj types.Object, fact analysis.Fact) ExportPackageFact func(fact analysis.Fact) } ファイル上の位置に関する情報 構文解析の結果の抽象構文木 型チェックの結果の型情報 Analyzerの結果
  14. 簡単なAnalyzerの例 22 var Analyzer = &analysis.Analyzer{ Name: "simple", Doc: "simple

    is simple Analyzer", Run: run, Requires: []*analysis.Analyzer{inspect.Analyzer}, } func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) inspect.Preorder([]ast.Node{new(ast.Ident)}, func(n ast.Node) { switch n := n.(type) { case *ast.Ident: if n.Name == "gopher" { pass.Reportf(n.Pos(), "identifier is gopher") } } }) return nil, nil }
  15. テストデータ package a func f() { var a interface{} _

    = a.(int) // want "must not do fource type assertion" _, _ = a.(int) // OK switch a := a.(type) { // OK case int: println(a) } } ▪ wantで始まるコメントで期待する結果を書く • 対応するDiagnosticが出力されていればOK 正規表現が使える
  16. unitchecker ▪ unitchecker.Main • 複数のAnalyzerからなるmain関数を提供する • 各Analyzerはゴールーチンで1回だけ実行される • go vet

    -vettool によって呼ばれる前提 • 各種設定はgo vetからもらう package main import ( "simple" "golang.org/x/tools/go/analysis/unitchecker" ) func main() { unitchecker.Main(simple.Analyzer) }
  17. skeleton ▪ go/analysis用のスケルトンコードジェネレータ • https://github.com/gostaticanalysis/skeleton • 簡単に静的解析ツールを始めることができる • Analyzer、テストコード、main.goの雛形作ってくれる $

    skeleton myanalyzer myanalyzer ├── cmd │ └── myanalyzer │ └── main.go ├── myanalyzer.go ├── myanalyzer_test.go └── testdata └── src └── a └── a.go
  18. コード生成と静的解析 ▪ 静的解析した結果を元にコードを生成する • 型や構造体タグやコメントを解析 32 package main //go:generate stringer

    -type=MyStatus type MyStatus int const ( A MyStatus = iota B C ) // Code generated by "stringer -type=MyStatus"; DO NOT EDIT. package main import "strconv" /* 略 */ const _MyStatus_name = "ABC" var _MyStatus_index = [...]uint8{0, 1, 2, 3} func (i MyStatus) String() string { if i < 0 || i >= MyStatus(len(_MyStatus_index)-1) { return "MyStatus(" + strconv.FormatInt(int64(i), 10) + ")" } return _MyStatus_name[_MyStatus_index[i]:_MyStatus_index[i+1]] } コード生成
  19. 静的単一代入(SSA)形式 33 ▪ 変数への入力を1回だけに制限した形式 • gc(Goのコンパイラ)の中でも使われている技術 ◦ 別パッケージで表現方法は異なる ◦ 最適化に使われている

    • golang.org/x/tools/go/ssaパッケージが担当 ◦ 静的解析ツール用のパッケージ ◦ gcでは使われていない n := 10 n += 10 n0 := 10 n1 := n0 + 10
  20. 例:エラー処理のミス ▪ nil以外を返すべきところでnilを返してるバグを発見 • https://github.com/gostaticanalysis/nilerr • err != nilと比較しているのにnilを返している •

    基本ブロックのフローを調べることで簡単に見つけられる ◦ If命令のジャンプ先がReturn命令でnilを返していたら 36 func f() error { err := do() if err != nil { return nil // ミス } } Return命令(nil) If命令(err != nil) Jump命令 ・・・ then else
  21. 例:Spannerのセッションリーク検出 ▪ gcpug/zagane • https://github.com/gcpug/zagane • *spanner.RowIteratorのStopメソッド(またはDoメソッド)が呼ばれてい るかだけチェックできる ◦ 呼ばれていないとセッションリークする可能性がある

    • 参考:Google Cloud Spannerのセッションリークを静的解析で防ぐ 37 iter := client.Single().Query(ctx, stmt) for { row, err := iter.Next() // (略) } iter := client.Single().Query(ctx, stmt) defer iter.Stop() for { row, err := iter.Next() // (略) }
  22. セッションリークの検出方法 - 1 - ▪ コントロールフローグラフを有向グラフとして処理 • 基本ブロックをノードとする • 注目している基本ブロックを始端とする

    • 関数のreturn文を終端とする • StopまたはDoメソッドを呼んでいるノードに☆マークをつける 38 始端 終端 終端 Stop() Do()
  23. 静的単一代入形式で分かること ▪ コントロールフローグラフ • 分岐や繰り返しの簡略化 • 処理の前後関係を追いやすい • 有向グラフなのでグラフ理論のアルゴリズムが使える ▪

    単一の代入であることが保証されている • 同じ値に対する処理を見つけやすい ◦ ある値のメソッドを呼び出しているか? • https://github.com/gostaticanalysis/called 40
  24. 静的単一代入形式で分からないこと ▪ 公開された変数への代入 • 外部パッケージから変更される可能性がある ▪ ポインタを介した変更 • どう変更されるか分からない •

    unsafe.Pointerを用いるとポインタ演算されてしまう ▪ インタフェースを介した処理 • どう代入されるか分からない ▪ リフレクションを介した動作 • 動的に決まるので難しい 41
  25. buildssaパッケージ ▪ 静的単一代入形式を構築するAnalyzerを提供 • Analyzer.Requiresフィールドにbuildssa.Analyzer変数を指定 • SSA.SrcFuncsフィールドからソースコード中の関数を取得 42 func run(pass

    *analysis.Pass) (interface{}, error) { s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) for _, f := range s.SrcFuncs { fmt.Println(f) for _, b := range f.Blocks { fmt.Printf("\tBlock %d\n", b.Index) for _, instr := range b.Instrs { fmt.Printf("\t\t%[1]T\t%[1]v(%[1]p)\n", instr) for _, v := range instr.Operands(nil) { if v != nil { fmt.Printf("\t\t\t%[1]T\t%[1]v(%[1]p)\n", *v) } } } } } return nil, nil } type SSA struct { Pkg *ssa.Package SrcFuncs []*ssa.Function }
  26. コールグラフ ▪ 関数の呼び出しグラフ • 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 43 main() f1() f2() f3() f4()
  27. コールグラフを表す型 ▪ golang.org/x/tools/go/callgraphで定義される 44 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 }
  28. アルゴリズムの違い 45 アルゴリズム 説明 パッケージ 静的取得 静的に決まる関数呼び出しのみ取得する 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/
  29. ポインタ解析 46 ▪ ポインタがどこを指しているのか解析する • x/tools/go/pointerパッケージを用いる • ポインタpがどの変数を指し示すのか ▪ エスケープ解析の一部で利用される技術

    • コンパイラ内部で行われる • スタックまたはヒープに割り当てるべきか解析する func f() { var n int p := &n g(p) var m int g(&m) } func g(p *int) { /* 略 */ }
  30. ポインタ解析によるコールグラフ生成 ▪ ポインタ経由の関数呼び出しも取得できる • poniter.Config構造体のBuildCallGraphフィールドをtrueにする • pointer.Result構造体のCallGraphフィールドから取得できる 48 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() }
  31. 型パラメタと静的解析 ▪ Go1.18から型パラメタが導入される • 当然、goパッケージのアップデートが入る ◦ https://github.com/golang/go/issues/47781 ◦ https://github.com/golang/go/issues/47916 ◦

    抽象構文木(AST)や型情報で型パラメタが扱える ▪ 詳しくはmercari.go#18で話ます • https://mercari.connpass.com/event/235116/
  32. まとめ ▪ 静的解析は面白い • ソースコードから知れることは多い ◦ 静的解析 = 抽象構文木(AST)の解析だけではない •

    リファクタリングなどにも使える ◦ 大きい関数を洗い出す • コードリーディングルールは静的解析で担保する