Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14301] ファイルの概要

このコミットは、Goプログラムの静的解析を目的とした実験的な機能である「構造体フィールドトラッキング」と、メソッドがインターフェースを満たすことを防ぐ「go:nointerface」アノテーションを導入します。これらの機能は、Go言語の仕様の一部ではなく、ツールチェインをGOEXPERIMENT=fieldtrackでビルドした場合にのみ有効になります。

コミット

Author: Russ Cox rsc@golang.org Date: Fri Nov 2 00:17:21 2012 -0400

コミットメッセージ: cmd/gc, cmd/ld: struct field tracking

This is an experiment in static analysis of Go programs to understand which struct fields a program might use. It is not part of the Go language specification, it must be enabled explicitly when building the toolchain, and it may be removed at any time.

After building the toolchain with GOEXPERIMENT=fieldtrack, a specific field can be marked for tracking by including go:"track" in the field tag:

    package pkg

    type T struct {
            F int `go:"track"`
            G int // untracked
    }

To simplify usage, only named struct types can have tracked fields, and only exported fields can be tracked.

The implementation works by making each function begin with a sequence of no-op USEFIELD instructions declaring which tracked fields are accessed by a specific function. After the linker's dead code elimination removes unused functions, the fields referred to by the remaining USEFIELD instructions are the ones reported as used by the binary.

The -k option to the linker specifies the fully qualified symbol name (such as my/pkg.list) of a string variable that should be initialized with the field tracking information for the program. The field tracking string is a sequence of lines, each terminated by a \n and describing a single tracked field referred to by the program. Each line is made up of one or more tab-separated fields. The first field is the name of the tracked field, fully qualified, as in "my/pkg.T.F". Subsequent fields give a shortest path of reverse references from that field to a global variable or function, corresponding to one way in which the program might reach that field.

A common source of false positives in field tracking is types with large method sets, because a reference to the type descriptor carries with it references to all methods. To address this problem, the CL also introduces a comment annotation

    //go:nointerface

that marks an upcoming method declaration as unavailable for use in satisfying interfaces, both statically and dynamically. Such a method is also invisible to package reflect.

Again, all of this is disabled by default. It only turns on if you have GOEXPERIMENT=fieldtrack set during make.bash.

R=iant, ken CC=golang-dev https://golang.org/cl/6749064

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3d40062c68b046546aab464185b1ebd24ac8da07

元コミット内容

cmd/gc, cmd/ld: struct field tracking

This is an experiment in static analysis of Go programs
to understand which struct fields a program might use.
It is not part of the Go language specification, it must
be enabled explicitly when building the toolchain,
and it may be removed at any time.

After building the toolchain with GOEXPERIMENT=fieldtrack,
a specific field can be marked for tracking by including
`go:"track"` in the field tag:

        package pkg

        type T struct {
                F int `go:"track"`
                G int // untracked
        }

To simplify usage, only named struct types can have
tracked fields, and only exported fields can be tracked.

The implementation works by making each function begin
with a sequence of no-op USEFIELD instructions declaring
which tracked fields are accessed by a specific function.
After the linker's dead code elimination removes unused
functions, the fields referred to by the remaining
USEFIELD instructions are the ones reported as used by
the binary.

The -k option to the linker specifies the fully qualified
symbol name (such as my/pkg.list) of a string variable that
should be initialized with the field tracking information
for the program. The field tracking string is a sequence
of lines, each terminated by a \n and describing a single
tracked field referred to by the program. Each line is made
up of one or more tab-separated fields. The first field is
the name of the tracked field, fully qualified, as in
"my/pkg.T.F". Subsequent fields give a shortest path of
reverse references from that field to a global variable or
function, corresponding to one way in which the program
might reach that field.

A common source of false positives in field tracking is
types with large method sets, because a reference to the
type descriptor carries with it references to all methods.
To address this problem, the CL also introduces a comment
annotation

        //go:nointerface

that marks an upcoming method declaration as unavailable
for use in satisfying interfaces, both statically and
dynamically. Such a method is also invisible to package
reflect.

Again, all of this is disabled by default. It only turns on
if you have GOEXPERIMENT=fieldtrack set during make.bash.

R=iant, ken
CC=golang-dev
https://golang.org/cl/6749064

変更の背景

このコミットは、Goプログラムの静的解析能力を向上させるための実験的な取り組みとして導入されました。主な目的は以下の2点です。

  1. 構造体フィールドの使用状況の把握: Goプログラムにおいて、どの構造体フィールドが実際に使用されているかを正確に特定することです。これは、デッドコードの排除や、プログラムの依存関係の理解に役立ちます。特に、大規模なコードベースでは、未使用のフィールドが残っていることが多く、これらを特定することでコードの最適化や保守性の向上が期待できます。
  2. フィールドトラッキングにおける誤検出の削減: フィールドトラッキングを行う際、大きなメソッドセットを持つ型が問題となることがあります。Goの型システムでは、型記述子への参照がその型のすべてのメソッドへの参照を伴うため、実際には使用されていないメソッドであっても、型が参照されるだけで「使用されている」と誤って検出される可能性があります。go:nointerfaceアノテーションは、このような誤検出を減らし、より正確なフィールド使用状況の分析を可能にすることを目的としています。

これらの機能は、Go言語のコア機能としてではなく、ツールチェインの実験的なオプションとして導入されており、将来的に変更または削除される可能性があることが明記されています。これは、Go開発チームが新しいアイデアを試行し、その有用性を評価するための一般的なアプローチです。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびツールチェインに関する前提知識が必要です。

  • Goコンパイラ (cmd/gc): Go言語のソースコードを機械語に変換する主要なコンパイラです。型チェック、最適化、コード生成など、コンパイルプロセスの多くの段階を担当します。
  • Goリンカ (cmd/ld): コンパイラによって生成されたオブジェクトファイルやライブラリを結合し、実行可能なバイナリを生成するツールです。デッドコード排除(DCE: Dead Code Elimination)もリンカの重要な機能の一つです。
  • 静的解析 (Static Analysis): プログラムを実行せずに、ソースコードを分析して潜在的なバグ、脆弱性、または非効率性を特定するプロセスです。このコミットの「構造体フィールドトラッキング」は、静的解析の一種です。
  • デッドコード排除 (Dead Code Elimination, DCE): リンカの最適化手法の一つで、プログラムの実行に影響を与えない(到達不能な)コードを最終的なバイナリから削除することです。これにより、バイナリのサイズが削減され、起動時間やメモリ使用量が改善されます。
  • Goの構造体タグ (Struct Tags): 構造体のフィールドに付加される文字列リテラルで、リフレクションAPIを通じてアクセスできます。通常、JSONエンコーディング/デコーディング、データベースマッピング、バリデーションなどのメタデータとして使用されます。このコミットでは、go:"track"というカスタムタグが導入されます。
  • Goのインターフェース (Interfaces): Goにおけるポリモーフィズムを実現するための強力な機能です。メソッドのシグネチャの集合を定義し、そのシグネチャを満たす任意の型がそのインターフェースを実装していると見なされます。
  • Goのリフレクション (Reflection): 実行時にプログラムの構造(型、フィールド、メソッドなど)を検査および操作する機能です。reflectパッケージを通じて提供されます。
  • GOEXPERIMENT環境変数: Goツールチェインのビルド時に設定できる環境変数で、実験的な機能の有効/無効を切り替えるために使用されます。これにより、開発者はまだ安定版ではない新機能を試すことができます。このコミットの機能は、GOEXPERIMENT=fieldtrackを設定してツールチェインをビルドした場合にのみ有効になります。
  • Goのアセンブリ (AUSEFIELD): Goコンパイラとリンカは、内部的にアセンブリ言語のような中間表現を使用します。AUSEFIELDは、このコミットで導入された新しい「命令」であり、特定のフィールドが使用されていることを示すためのものです。これは実際のCPU命令ではなく、コンパイラ/リンカがフィールド使用状況を追跡するためのメタデータとして機能します。

技術的詳細

このコミットで導入された「構造体フィールドトラッキング」と「go:nointerface」は、Goツールチェインのコンパイラ(cmd/gc)とリンカ(cmd/ld)に深く統合されています。

構造体フィールドトラッキング

  1. go:"track"タグ:

    • 構造体のフィールドにgo:"track"タグを付与することで、そのフィールドがトラッキング対象としてマークされます。
    • 例: type T struct { F int go:"track" }
    • 制約として、トラッキングできるのは名前付き構造体型(type MyStruct struct { ... }のように定義されたもの)の、エクスポートされた(大文字で始まる)フィールドのみです。これは、実装の複雑さを軽減し、一般的なユースケースに焦点を当てるためと考えられます。
  2. AUSEFIELD命令の導入:

    • コンパイラ(cmd/gc)は、go:"track"タグが付与されたフィールドがコード内でアクセスされるたびに、その関数内に「AUSEFIELD」という新しい種類のno-op(何もしない)命令を挿入します。
    • この命令は、特定のフィールドがその関数によって使用されていることをリンカに伝えるためのマーカーとして機能します。
    • src/cmd/5a/lex.c, src/cmd/6a/lex.c, src/cmd/8a/lex.cなどのアセンブラの字句解析器にUSEFIELDが追加され、src/cmd/5g/gsubr.c, src/cmd/6g/gsubr.c, src/cmd/8g/gsubr.cgtrack関数でAUSEFIELD命令が生成されます。
    • src/cmd/gc/walk.cusefield関数が、ODOT(フィールドアクセス)やODOTPTR(ポインタ経由のフィールドアクセス)のノードを処理する際に、このAUSEFIELD命令を挿入するロジックを含んでいます。
  3. リンカによるフィールド使用状況の収集:

    • リンカ(cmd/ld)は、デッドコード排除のプロセス中に、最終的にバイナリに残る関数を特定します。
    • これらの残った関数に含まれるAUSEFIELD命令をスキャンし、それらの命令が参照するフィールドを「使用されているフィールド」として認識します。
    • リンカの-kオプションを使用すると、フィールドトラッキング情報を特定の文字列変数に書き出すことができます。この文字列は、各行がタブ区切りでフィールド名(例: my/pkg.T.F)と、そのフィールドに到達するための最短パス(グローバル変数や関数からの逆参照)を含む形式です。
    • src/cmd/ld/obj.c-kオプションのパースが追加され、src/cmd/ld/go.cmarkflood関数が、AUSEFIELD命令によって参照されるシンボルを到達可能としてマークするロジックを含んでいます。

//go:nointerfaceアノテーション

  1. 目的:

    • 大きなメソッドセットを持つ型が、実際にはインターフェースを満たすために使用されていないメソッドであっても、型記述子への参照によって誤って「使用されている」と検出される問題を解決します。
    • これにより、フィールドトラッキングの精度が向上します。
  2. 機能:

    • メソッド宣言の直前に//go:nointerfaceというコメントを記述することで、そのメソッドがインターフェースを満たすために使用できないようにマークされます。
    • 例:
      type MyType struct {}
      //go:nointerface
      func (m MyType) MyMethod() {}
      
    • このアノテーションが付与されたメソッドは、静的(コンパイル時)にも動的(実行時、リフレクション)にもインターフェースを満たすことができません。
    • reflectパッケージからもこのメソッドは不可視になります。
  3. 実装:

    • コンパイラ(cmd/gc)の字句解析器(src/cmd/gc/lex.c)が//go:nointerfaceコメントを認識し、nointerfaceというフラグを設定します。
    • src/cmd/gc/go.hType構造体とNode構造体にnointerfaceフィールドが追加され、この情報が保持されます。
    • src/cmd/gc/dcl.caddmethod関数やsrc/cmd/gc/go.yのパーサーが、メソッド宣言時にnointerfaceフラグを適切に設定するように変更されています。
    • src/cmd/gc/reflect.cmethods関数やsrc/cmd/gc/subr.cimplements関数が、nointerfaceフラグが設定されたメソッドをインターフェースの候補から除外するように修正されています。

実験的機能としての位置づけ

  • これらの機能は、GOEXPERIMENT=fieldtrack環境変数を設定してGoツールチェインをビルドした場合にのみ有効になります。これは、src/cmd/gc/lex.cexper配列にfieldtrackが追加され、fieldtrack_enabled変数が制御されることで実現されています。
  • デフォルトでは無効であり、Go言語の公式仕様の一部ではありません。これは、Go開発チームが新しいアイデアを試すためのサンドボックス的なアプローチです。

コアとなるコードの変更箇所

このコミットは、Goコンパイラ(cmd/gc)とリンカ(cmd/ld)の複数のファイルにわたる広範な変更を含んでいます。

  • src/cmd/{5,6,8}a/lex.c: 各アーキテクチャのアセンブラの字句解析器に、新しい命令USEFIELDが追加されています。
  • src/cmd/{5,6,8}g/gg.h: コンパイラのヘッダファイルにgtrack(Sym*)関数のプロトタイプが追加されています。
  • src/cmd/{5,6,8}g/gsubr.c: gtrack関数の実装が追加されています。この関数は、AUSEFIELD命令を生成し、指定されたシンボル(フィールド)を参照します。
  • src/cmd/{5,6,8}l/{5,6,8}.out.h: 各アーキテクチャのリンカのヘッダファイルに、新しい命令AUSEFIELDが列挙型asに追加されています。
  • src/cmd/{5,6,8}l/l.h: リンカのシンボル構造体Symに、フィールドトラッキングのためのreachparentqueueフィールドが追加されています。
  • src/cmd/{5,6,8}l/obj.c: リンカのメイン処理に、フィールドトラッキング情報を出力する-kオプションのパースが追加されています。
  • src/cmd/{5,6,8}l/optab.c: 各アーキテクチャの命令テーブルにAUSEFIELD命令のエントリが追加されています。
  • src/cmd/{5,6,8}l/span.c: 命令の処理ロジックにAUSEFIELDが追加されています。
  • src/cmd/gc/dcl.c: メソッド宣言を処理するaddmethod関数にnointerface引数が追加され、Type構造体のnointerfaceフィールドが設定されるようになっています。
  • src/cmd/gc/export.c: エクスポートされる型情報に//go:nointerfaceコメントが出力されるロジックが追加されています。
  • src/cmd/gc/go.h:
    • Type構造体にnointerface(メソッドがインターフェースを満たさないことを示す)とouter(構造体フィールドの親構造体への参照)、lastfn(フィールドが最後に参照された関数)が追加されています。
    • Node構造体にnointerface(関数ノード用)とparamfld(フィールドノード用)が追加されています。
    • 新しい擬似パッケージtrackpkgが宣言されています。
    • グローバル変数nointerfacefieldtrack_enabledが宣言されています。
    • addmethod関数のプロトタイプが変更され、usefield関数のプロトタイプが追加されています。
  • src/cmd/gc/go.y: Go言語の文法定義ファイル(Yaccソース)が変更され、メソッド宣言時にnointerfaceフラグが設定されるようになっています。
  • src/cmd/gc/lex.c:
    • GOEXPERIMENT=fieldtrackを有効にするためのfieldtrack_enabled変数がexper配列に追加されています。
    • 新しい擬似パッケージtrackpkgの初期化が追加されています。
    • //go:nointerfaceコメントを解析し、nointerfaceグローバル変数を設定するロジックがgetlinepragma関数に追加されています。
  • src/cmd/gc/pgen.c: 関数コンパイル時に、curfn->paramfld(関数がアクセスするフィールドのリスト)をgtrack関数で処理するロジックが追加されています。
  • src/cmd/gc/reflect.c:
    • methods関数が、nointerfaceフラグが設定されたメソッドをインターフェースの候補から除外するように変更されています。
    • 新しいtracksym関数が追加され、フィールドトラッキング用のシンボル名を生成します。
  • src/cmd/gc/subr.c: implements関数が、nointerfaceフラグが設定されたメソッドをインターフェース実装のチェックから除外するように変更されています。
  • src/cmd/gc/typecheck.c: lookdot関数(フィールドアクセスを解決する)が、フィールドのparamfldを設定するように変更され、typecheckfunc関数がaddmethodを呼び出す際にnointerfaceフラグを渡すようになっています。
  • src/cmd/gc/walk.c:
    • ODOT(フィールドアクセス)とODOTPTR(ポインタ経由のフィールドアクセス)のノードを処理する際に、新しいusefield関数が呼び出されるようになっています。
    • usefield関数は、go:"track"タグが付与されたフィールドへのアクセスを検出し、field->lastfnfield->outerを設定し、現在の関数のparamfldリストにフィールドを追加します。
  • src/cmd/gc/y.tab.c: go.yから生成されたパーサーのCソースファイルで、行番号の変更と、nointerfaceフラグの設定に関する変更が反映されています。
  • src/cmd/ld/data.c: リンカが文字列データを追加する際の処理に、tracksymが設定された場合のreachableフラグの伝播に関する変更が追加されています。
  • src/cmd/ld/go.c:
    • リンカのデッドコード排除ロジックが大幅に変更されています。特に、mark関数とmarkflood関数が導入され、到達可能なシンボルをキューベースで効率的にマークするようになっています。
    • AUSEFIELD命令によって参照されるシンボルが到達可能としてマークされるようになっています。
    • parsemethod関数が//go:nointerfaceコメントを認識し、メソッド定義をスキップするロジックが追加されています。

コアとなるコードの解説

このコミットの核となる変更は、Goコンパイラとリンカが連携して構造体フィールドの使用状況を追跡し、特定のメソッドがインターフェースを満たさないようにするメカニズムを導入した点にあります。

フィールドトラッキングのフロー

  1. コンパイラ (cmd/gc):

    • src/cmd/gc/lex.cGOEXPERIMENT=fieldtrackが有効になっているかを確認します。
    • src/cmd/gc/go.hType構造体にouterlastfnフィールドが追加され、Node構造体にparamfldが追加されます。これらはフィールドトラッキング情報を保持するために使用されます。
    • src/cmd/gc/walk.cwalkexpr関数が、ODOTODOTPTR(構造体フィールドへのアクセス)を検出すると、usefield関数を呼び出します。
    • usefield関数は、アクセスされたフィールドにgo:"track"タグが付いているかを確認します。タグが付いている場合、そのフィールドが現在の関数(curfn)によって使用されたことを記録し、field->lastfn = curfnを設定します。また、フィールドの親構造体型をfield->outerに設定します。
    • src/cmd/gc/pgen.ccompile関数内で、現在の関数がアクセスするトラッキング対象フィールドのリスト(curfn->paramfld)をgtrack関数に渡します。
    • src/cmd/{5,6,8}g/gsubr.cgtrack関数は、AUSEFIELDという新しいアセンブリ命令を生成します。この命令は、特定のフィールドシンボルを参照するno-op命令です。これにより、各関数がどのトラッキング対象フィールドにアクセスするかをリンカに伝えます。
  2. リンカ (cmd/ld):

    • src/cmd/{5,6,8}l/l.hSym構造体にreachparentqueueフィールドが追加され、到達可能性グラフの構築に使用されます。
    • src/cmd/ld/go.cdeadcode関数内で、markfloodという新しい関数が呼び出されます。
    • markflood関数は、プログラムのエントリポイント(main関数など)から到達可能なすべてのシンボル(関数、データなど)を再帰的にマークします。この際、AUSEFIELD命令によって参照されるフィールドシンボルも到達可能としてマークされます。
    • リンカは、到達可能とマークされたシンボルのみを最終的なバイナリに含めます。これにより、未使用の関数やデータが削除されます。
    • -kオプションが指定されている場合、リンカは到達可能とマークされたトラッキング対象フィールドのリストを、指定された文字列変数に書き出します。このリストには、フィールドの完全修飾名と、そのフィールドに到達するためのパスが含まれます。

//go:nointerfaceのフロー

  1. コンパイラ (cmd/gc):
    • src/cmd/gc/lex.cgetlinepragma関数が、ソースコード中の//go:nointerfaceコメントを検出します。
    • このコメントが検出されると、グローバル変数nointerface1に設定されます。
    • src/cmd/gc/go.yのパーサーは、メソッド宣言を処理する際に、このnointerface変数の値を見て、宣言されるメソッドのNodeおよびType構造体のnointerfaceフィールドを設定します。
    • src/cmd/gc/dcl.caddmethod関数も、このnointerfaceフラグを受け取り、メソッドのType構造体に設定します。
    • src/cmd/gc/reflect.cmethods関数は、型が実装するメソッドのリストを生成する際に、nointerfaceフラグが設定されたメソッドをスキップします。これにより、リフレクションを通じてインターフェースを満たすことができなくなります。
    • src/cmd/gc/subr.cimplements関数は、型がインターフェースを実装しているかをチェックする際に、メソッドのnointerfaceフラグを確認し、設定されている場合はそのメソッドをインターフェース実装の候補から除外します。

これらの変更により、Goツールチェインは、プログラムの実行に影響を与えずに、構造体フィールドの使用状況を詳細に分析し、特定のメソッドがインターフェースのセマンティクスに影響を与えないように制御する実験的な機能を提供します。

関連リンク

参考にした情報源リンク