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. 字句解析からポインタ解析まで Goの静的解析のすべて 2022/01/15(土) tenntenn Conference 2022 資料:https://tenn.in/analysis-conn22 動画:https://tenn.in/analysis-conn22-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://andpad.co.jp/ ● https://engineer.andpad.co.jp/ ● 採用情報:https://hrmos.co/pages/andpad/jobs/0000131

Slide 4

Slide 4 text

早い段階でバグを見つけた方がよい ■ あとのフェーズになると深刻化・発見が困難になる ● テスト時よりQAの時に見つかる方がコストが高い ● リリース後に見つかるとユーザにも影響を与えてしまう ● 実環境で動いているサービスから問題を見つけるのは困難 ○ 参考:メルペイでのSpannerとの戦いの日々 4 コーディング 01010 01010 00101 000 コンパイル 01010 01010 00101 000 デプロイ リリース テスト QA 監視 深刻化していく・・・

Slide 5

Slide 5 text

コンパイル前にバグを見つける方法 ■ 静的解析ツールを使用する ● ソースコードレベルでバグがないか調べる ● コンパイルエラーにはならないバグを見つける 5 コーディング 010100 101000 101000 コンパイル 010100 101000 101000 デプロイ リリース テスト QA 監視 静的解析

Slide 6

Slide 6 text

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なのに数値が渡されている

Slide 7

Slide 7 text

静的解析ツールを自作する ■ 自作する必要性 ● プロジェクトの独自ルール ○ 例:インポートルール ● 決められたパッケージからしかインポートできない ● レイヤードアーキテクチャのレイヤーをまたぐインポートの場合など ● 特定のライブラリの使い方を検証したい ○ 例:github.com/gcpug/zagane ● Google Cloud Spannerのよくあるミスを静的解析ツールで指摘する ● Google Cloud Spannerのセッションリークを静的解析で防ぐ - Mercari Engineering Blog ● 欲しい静的解析ツールがない ○ かゆいところに手が届くものがない ○ なければ作るしかない ○ レビューで毎回指摘しているようなものはツールにする

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

静的解析とコンパイラ 10 ■ goパッケージはコンパイラでは使われていない ● あくまで静的解析用のパッケージ ● GoのコンパイラはもともとCで書かれていた ● コンパイラで用いているデータ構造と異なる場合がある ○ 特に静的単一代入形式はまったく異なる ■ リリース当初からgoパッケージは存在する ● 言語設計時からツールが作りやすさを念頭においていた ○ gofmt, go fix, godoc ● 標準パッケージでサポートしている重要性 ○ 言語仕様に追従していく 参考:Go at Google: Language Design in the Service of Software Engineering

Slide 11

Slide 11 text

静的解析のフェーズ ■ 静的解析はいくつかのフェーズに分かれている ● 後のフェーズにいくこと、さらに詳しい情報が手に入る ● 各フェーズで手に入る情報を使い分けながら解析する ● 各フェーズで手に入る情報の紐付けの仕方がキモ ○ このノードの型情報は?など 11 構文解析 型チェック 静的単一代入形式 ポインタ解析

Slide 12

Slide 12 text

■ 入力された文字列をトークンとして分解 字句解析- go/scanner,go/token IDENT ADD INT トークン ソースコード: v + 1 12

Slide 13

Slide 13 text

構文解析 - go/parser,go/ast ■ トークンを抽象構文木(AST)に変換 ● AST: Abstract Syntax Tree v + 1 IDENT ADD INT ソースコード: + v 1 BinaryExpr Ident BasicLit トークン: 抽象構文木(AST): 13

Slide 14

Slide 14 text

例:import文の重複 ■ 同じインポートパスのimport文を見つける ● 別名をつけると同じインポートパスのパッケージをインポート可能 ● https://github.com/gostaticanalysis/dupimport 14 package main import fmt1 "fmt" import fmt2 "fmt" func main() { fmt1.Println("Hello") fmt2.Println("World") } *File []Decl *GenDecl *FuncDecl *ImportSpec

Slide 15

Slide 15 text

構文解析で分からないこと ■ 型情報 ● 型の不一致など ● 変数の型や式の型 ■ 定数式の結果 ● 定数式の計算や定数の型 ■ 識別子の解決 ● どの識別子がどこで定義されているのか ● どの識別子がどこで使用されているのか 15 + 100 "hoge" BinaryExpr BasicLit BasicLit 100 + "hello" 型が合わなくても文法上は問題はない

Slide 16

Slide 16 text

型チェック - go/types,go/constant ■ 型情報を抽象構文木から抽出 ● 識別子の解決 ● 型の推論 ● 定数の評価 n := 100 + 200 m := n + 300 定数の評価 = 300 型の推論 -> int 識別子の解決 16

Slide 17

Slide 17 text

型チェックで分かること ■ 型情報 ● 型の不一致など ● 変数の型や式の型 ■ 定数式の結果 ● 定数式の計算や定数の型 ■ 識別子の解決 ● どの識別子がどこで定義されているのか ● どの識別子がどこで使用されているのか 17

Slide 18

Slide 18 text

静的解析ツールのモジュール化 ■ golang.org/x/tools/go/analysisパッケージ ● 静的解析ツールのモジュール化を提供するパッケージ ● Go1.12からgo vetでも使われるようになった

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

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の結果

Slide 21

Slide 21 text

analysis.Diagnostic ■ token.Pos(位置)に関連付けられた静的解析結果 ● 任意の位置へのエラーを表現するために使う ○ 例:〇行目に〇〇というエラーがあります ■ (*analysis.Pass).Reportfメソッド ● Diagnosticを生成するメソッド ● fmt.Fprintf感覚で使える func (pass *Pass) Reportf(pos token.Pos, format string, args ...interface{})

Slide 22

Slide 22 text

簡単な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 }

Slide 23

Slide 23 text

analysistestパッケージ ■ Analyzerのテストを簡単に行うためのパッケージ ● 実際の解析対象となるソースコードをファイルとして用意すればよい ● テストデータはtestdata以下に用意する ● コメントによって該当行にDiagnosticが出力されるかチェックする func Test(t *testing.T) { testdata := analysistest.TestData() analysistest.Run(t, testdata, fourcetypeassert.Analyzer, "a") }

Slide 24

Slide 24 text

テストデータ 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 正規表現が使える

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

go vetからの実行 ■ go vetの-vettoolオプションを用いる ● unitchecker.Main関数を呼び出す実行可能ファイルを指定できる ● 相対パスではなく、絶対パス ● フラグは-called.funcs=log.Fatalのように指定できる 26 $ go vet -vettool=$(which myvet) pkgname

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

skeletonを使えば簡単につくれる ■ 思い立ったらすぐに作れる 11:46 12:46 簡単なものは1時間で作れる!

Slide 29

Slide 29 text

gostaticanalysis ■ Goの静的解析に関するリポジトリを集めたもの ● https://github.com/gostaticanalysis ● 便利なライブラリやAnalyzerを提供している ○ skeleton ○ analysisutilパッケージ ○ astquery ○ knife

Slide 30

Slide 30 text

静的解析とコード生成 ■ ソースコードをプログラムによって生成する ● 冗長なコードの自動生成 ● テストコードの生成 ● 静的解析の解析結果やデータベースのスキーマなどから生成 静的解析 コード生成 ソースコード 抽象構文木 型情報 DB スキーマ

Slide 31

Slide 31 text

コード生成の必要性 31 ■ 冗長なコードの自動生成 ● 自動生成が可能だけど人力で書くのは手間 ● テストコードの生成 ● 最適化されたテクニカルなコード ○ 可読性が低いがパフォーマンスが良いコード

Slide 32

Slide 32 text

コード生成と静的解析 ■ 静的解析した結果を元にコードを生成する ● 型や構造体タグやコメントを解析 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]] } コード生成

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

基本ブロック ■ 関数を構成する単位 ● 条件分岐単位などで基本ブロックに分けられる ● 関数は基本ブロックをノードとしたコントロールフローグラフを構成する ● Goのif、for、switchなどは全部If命令とJump命令になる 35 func f() { n := 10 if n < 10 { println("n < 10") } else { println("n >= 10") } }

Slide 36

Slide 36 text

例:エラー処理のミス ■ 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

Slide 37

Slide 37 text

例: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() // (略) }

Slide 38

Slide 38 text

セッションリークの検出方法 - 1 - ■ コントロールフローグラフを有向グラフとして処理 ● 基本ブロックをノードとする ● 注目している基本ブロックを始端とする ● 関数のreturn文を終端とする ● StopまたはDoメソッドを呼んでいるノードに☆マークをつける 38 始端 終端 終端 Stop() Do()

Slide 39

Slide 39 text

セッションリークの検出方法 - 2 - ■ StopまたはDoメソッドを呼んでいるノードを取り除く ● ☆マークのついたノードをグラフから取り除く ● 残ったエッジを通って始端から終端までいけるか検証する ● 終端に行ける場合はセッションリークが発生する可能性がある 39 始端 終端 終端 Stop() Do()

Slide 40

Slide 40 text

静的単一代入形式で分かること ■ コントロールフローグラフ ● 分岐や繰り返しの簡略化 ● 処理の前後関係を追いやすい ● 有向グラフなのでグラフ理論のアルゴリズムが使える ■ 単一の代入であることが保証されている ● 同じ値に対する処理を見つけやすい ○ ある値のメソッドを呼び出しているか? ● https://github.com/gostaticanalysis/called 40

Slide 41

Slide 41 text

静的単一代入形式で分からないこと ■ 公開された変数への代入 ● 外部パッケージから変更される可能性がある ■ ポインタを介した変更 ● どう変更されるか分からない ● unsafe.Pointerを用いるとポインタ演算されてしまう ■ インタフェースを介した処理 ● どう代入されるか分からない ■ リフレクションを介した動作 ● 動的に決まるので難しい 41

Slide 42

Slide 42 text

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 }

Slide 43

Slide 43 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 43 main() f1() f2() f3() f4()

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

ポインタ解析で分かること ■ どのポインタがどの変数を指すか ● ポインタから指し示される可能性のある変数を取得できる ■ インタフェースを介したメソッド呼び出しを追える ● 呼び出されたメソッドの実態を取得できる 47 type C struct{} func (C) m() { /* 略 */ } func f() { var i I = C{} g(i) } func g(i I) { i.m() } 実態を辿ることができる

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

型パラメタと静的解析 ■ 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/

Slide 50

Slide 50 text

まとめ ■ 静的解析は面白い ● ソースコードから知れることは多い ○ 静的解析 = 抽象構文木(AST)の解析だけではない ● リファクタリングなどにも使える ○ 大きい関数を洗い出す ● コードリーディングルールは静的解析で担保する