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

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

コミット

  • コミットハッシュ: 5a23a7e52c8b11defb0e7ae88b6a2808432807c0
  • 作者: Russ Cox rsc@golang.org
  • 日付: 2014年3月27日 木曜日 14:06:15 -0400
  • コミットメッセージの要約: runtime: Goスタックフレームのガベージコレクション中に「不正なポインタ」チェックを有効化

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

https://github.com/golang/go/commit/5a23a7e52c8b11defb0e7ae88b6a2808432807c0

元コミット内容

runtime: enable 'bad pointer' check during garbage collection of Go stack frames

This is the same check we use during stack copying.
The check cannot be applied to C stack frames, even
though we do emit pointer bitmaps for the arguments,
because (1) the pointer bitmaps assume all arguments
are always live, not true of outputs during the prologue,
and (2) the pointer bitmaps encode interface values as
pointer pairs, not true of interfaces holding integers.

For the rest of the frames, however, we should hold ourselves
to the rule that a pointer marked live really is initialized.
The interface scanning already implicitly checks this
because it interprets the type word  as a valid type pointer.

This may slow things down a little because of the extra loads.
Or it may speed things up because we don't bother enqueuing
nil pointers anymore. Enough of the rest of the system is slow
right now that we can't measure it meaningfully.
Enable for now, even if it is slow, to shake out bugs in the
liveness bitmaps, and then decide whether to turn it off
for the Go 1.3 release (issue 7650 reminds us to do this).

The new m->traceback field lets us force printing of fp=
values on all goroutine stack traces when we detect a
bad pointer. This makes it easier to understand exactly
where in the frame the bad pointer is, so that we can trace
it back to a specific variable and determine what is wrong.

Update #7650

LGTM=khr
R=khr
CC=golang-codereviews
https://golang.org/cl/80860044

変更の背景

このコミットは、Goランタイムのガベージコレクション(GC)プロセス中に、Goスタックフレーム内のポインタの健全性を検証する「不正なポインタ」チェックを導入します。このチェックは、スタックコピー時に既に存在していたものと同じロジックをGCにも適用することで、ポインタのライブネス(生存性)に関するバグを早期に発見することを目的としています。

GoのGCは、到達可能なオブジェクトをマークし、到達不能なオブジェクトを解放することでメモリを管理します。このプロセスにおいて、スタック上のポインタが正しくマークされているか、そしてそのポインタが実際に有効なメモリ領域を指しているかを確認することは極めて重要です。もし、GCが不正なポインタを「ライブ」と誤認識した場合、それはクラッシュやメモリ破損につながる可能性があります。

コミットメッセージによると、このチェックはCスタックフレームには適用できません。これは、Cスタックフレームのポインタビットマップが、プロローグ中の出力に対しては真ではない「すべての引数が常にライブである」という仮定に基づいていること、および、整数を保持するインターフェース値がポインタペアとしてエンコードされていないためです。しかし、Goスタックフレームに関しては、ライブとマークされたポインタが実際に初期化されているというルールを厳守すべきであるという思想があります。インターフェースのスキャンは、型ワードを有効な型ポインタとして解釈するため、既に暗黙的にこのチェックを行っています。

この変更は、Go 1.3リリースに向けて、ライブネスビットマップのバグを洗い出すためのデバッグ支援策として導入されました。パフォーマンスへの影響は不明確であり、追加のロードによる若干の速度低下の可能性と、nilポインタのエンキューを省略することによる速度向上の可能性の両方が言及されています。最終的なGo 1.3での有効/無効の判断は、issue 7650で追跡されることになっています。

さらに、不正なポインタが検出された際に、デバッグを容易にするための機能強化も含まれています。具体的には、m->tracebackフィールドを導入し、不正なポインタが検出された場合にすべてのゴルーチンのスタックトレースでfp=(フレームポインタ)の値を強制的に出力するようにしました。これにより、不正なポインタがスタックフレームのどの正確な位置にあるかを特定し、問題の原因となっている変数を追跡することが容易になります。

前提知識の解説

1. ガベージコレクション (GC) とスタックスキャン

Goのガベージコレクタは、プログラムが使用しなくなったメモリを自動的に回収するシステムです。GCは、プログラムの実行中に「ルート」と呼ばれる既知のポインタ(グローバル変数、レジスタ、そして実行中のゴルーチンのスタックなど)から到達可能なオブジェクトを特定します。スタックスキャンは、このGCプロセスの一部であり、各ゴルーチンのスタックフレームを走査して、そこに存在するポインタを識別し、それらが指すオブジェクトを「ライブ」(生存している)としてマークします。

2. ポインタビットマップとライブネス情報

Goのコンパイラは、各関数のスタックフレーム内のどの位置にポインタが存在するかを示す「ポインタビットマップ」を生成します。これは、GCがスタックをスキャンする際に、どのメモリワードがポインタであるかを正確に識別するために使用されます。このビットマップは、特定のプログラムカウンタ(PC)の値(つまり、関数内のどの命令が実行されているか)に基づいて変化することがあります。これにより、GCはスタック上のポインタの「ライブネス」(そのポインタが現在有効なオブジェクトを指しているかどうか)を正確に判断できます。

3. スタックフレームとフレームポインタ (FP)

関数が呼び出されると、その関数はスタック上に自身の「スタックフレーム」を構築します。スタックフレームには、関数の引数、ローカル変数、戻りアドレスなどが格納されます。フレームポインタ(FP)は、現在のスタックフレームの特定の基準点(通常はスタックフレームの開始アドレスまたは終了アドレス)を指すレジスタです。デバッグ時には、FPの値を知ることで、スタックフレーム内の変数の位置を特定し、スタックトレースをより詳細に解析することが可能になります。

4. GOTRACEBACK 環境変数

GOTRACEBACKはGoランタイムの動作を制御する環境変数の一つです。この変数は、パニック発生時やシグナル受信時に出力されるスタックトレースの詳細度を制御します。

  • 0: スタックトレースを出力しない。
  • 1 (デフォルト): 簡潔なスタックトレースを出力する。
  • 2: すべてのゴルーチンのスタックトレースを出力する。
  • crash: スタックトレースを出力し、クラッシュさせる。 このコミットでは、m->tracebackフィールドと連携して、不正なポインタ検出時にGOTRACEBACK2以上であるかのように振る舞い、詳細なスタックトレース(fp=値を含む)を強制的に出力するようになります。

5. PageSize

PageSizeは、オペレーティングシステムがメモリを管理する際の最小単位であるページサイズを指します。通常、4KB(4096バイト)です。このコミットのコードでは、ポインタがPageSizeよりも小さい値(つまり、非常に小さいアドレス)を指している場合に、それが不正なポインタである可能性が高いと判断する基準として使用されています。これは、有効なポインタは通常、システムメモリのより高いアドレス空間を指すためです。

技術的詳細

このコミットの主要な技術的変更点は、src/pkg/runtime/mgc0.c内のscanbitvector関数に「不正なポインタ」チェックを導入したことです。この関数は、GC中にスタックフレーム内のポインタビットマップを走査し、ライブなポインタを識別してキューに入れます。

scanbitvector関数の変更

変更前は、scanbitvector関数は単にポインタがnilでない場合にenqueue1(GCキューに追加)していました。変更後は、preciseという新しい引数が追加され、これがtrueの場合に以下のチェックが追加されました。

if(precise && p < (byte*)PageSize) {
    // Looks like a junk value in a pointer slot.
    // Liveness analysis wrong?
    m->traceback = 2;
    runtime·printf("bad pointer in frame %s at %p: %p\n", runtime·funcname(f), scanp, p);
    runtime·throw("bad pointer in scanbitvector");
}

このコードスニペットは、precisetrue(Goスタックフレームの場合)であり、かつポインタpPageSizeよりも小さいアドレスを指している場合に、それを「不正なポインタ」と見なします。このようなポインタは、通常、有効なメモリ領域を指すことはなく、ライブネス解析の誤りを示唆しています。

不正なポインタが検出された場合、以下の処理が行われます。

  1. m->traceback = 2;: 現在のM(マシン、OSスレッドに相当)のtracebackフィールドを2に設定します。これにより、後続のスタックトレース出力時に詳細な情報(fp=値)が強制的に含まれるようになります。
  2. runtime·printf(...): 不正なポインタの詳細(関数名、スキャン中のアドレス、不正なポインタの値)を標準エラー出力に表示します。
  3. runtime·throw("bad pointer in scanbitvector");: ランタイムパニックを引き起こし、プログラムを終了させます。これは、このような状況が深刻なバグであることを示しているためです。

また、scanbitvector関数内でスライス(Slice)の健全性チェックも強化されています。

if(((Slice*)(scanp - PtrSize))->cap < ((Slice*)(scanp - PtrSize))->len) {
    m->traceback = 2;
    runtime·printf("bad slice in frame %s at %p: %p/%p/%p\n", runtime·funcname(f), scanp, ((byte**)scanp)[0], ((byte**)scanp)[1], ((byte**)scanp)[2]);
    runtime·throw("slice capacity smaller than length");
}

これは、スライスのcap(容量)がlen(長さ)よりも小さいという不正な状態を検出した場合にパニックを引き起こします。これもまた、ライブネス解析の誤りやメモリ破損を示唆する可能性があります。

m->tracebackフィールドの導入と利用

src/pkg/runtime/runtime.hm->tracebackという新しいuint8型のフィールドがM構造体に追加されました。このフィールドは、不正なポインタが検出された際に2に設定され、スタックトレースの出力挙動を制御します。

src/pkg/runtime/runtime.cruntime·gotraceback関数が変更され、m->tracebackの値が考慮されるようになりました。もしGOTRACEBACK環境変数が設定されていないか空の場合でも、m->traceback0でない場合はその値が返されるようになりました。これにより、不正なポインタ検出時にGOTRACEBACK=2が設定されたかのように振る舞い、詳細なスタックトレースが強制的に出力されます。

src/pkg/runtime/traceback_arm.csrc/pkg/runtime/traceback_x86.cruntime·gentraceback関数も変更され、fp=値の出力条件にgotraceback >= 2が追加されました。これにより、m->traceback2に設定された場合に、フレームポインタの値がスタックトレースに含まれるようになり、デバッグ情報が強化されます。

scanframe関数の変更

src/pkg/runtime/mgc0.cscanframe関数は、スタックフレームをスキャンする際にscanbitvector関数を呼び出します。このコミットでは、scanframe関数にpreciseというローカル変数が追加され、Goスタックフレームのローカル変数と引数をスキャンする際にscanbitvectortrueを渡すようになりました。これにより、Goスタックフレームに対してのみ、前述の「不正なポインタ」チェックが有効になります。

adjustpointers関数の変更

src/pkg/runtime/stack.cadjustpointers関数にも同様の「不正なポインタ」チェックが追加されました。この関数はスタックコピー中にポインタを調整する際に使用されます。ここでも、ポインタがPageSizeよりも小さいアドレスを指している場合にm->traceback = 2を設定し、パニックを引き起こすようになりました。これは、GC中のチェックと同様に、スタックコピー時のポインタの健全性を保証するためのものです。

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

src/pkg/runtime/mgc0.c

  • scanbitvector関数のシグネチャが変更され、Func *fbool precise引数が追加されました。
  • scanbitvector関数内で、precisetrueの場合にポインタpPageSizeより小さいかをチェックし、不正な場合はm->traceback = 2を設定してruntime·throwを呼び出すロジックが追加されました。
  • scanbitvector関数内で、スライスのcap < lenチェックが強化され、不正な場合はm->traceback = 2を設定してruntime·throwを呼び出すロジックが追加されました。
  • scanframe関数内で、ローカル変数preciseが追加され、scanbitvector呼び出し時にtrueが渡されるようになりました。

src/pkg/runtime/runtime.c

  • runtime·gotraceback関数内で、GOTRACEBACK環境変数が設定されていないか空の場合でも、m->tracebackの値が考慮されるようになりました。

src/pkg/runtime/runtime.h

  • M構造体にuint8 traceback;フィールドが追加されました。

src/pkg/runtime/stack.c

  • adjustpointers関数内で、ポインタがPageSizeより小さいかをチェックし、不正な場合はm->traceback = 2を設定してruntime·throwを呼び出すロジックが追加されました。

src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c

  • runtime·gentraceback関数内で、fp=値の出力条件にgotraceback >= 2が追加されました。

コアとなるコードの解説

src/pkg/runtime/mgc0.c

このファイルはGoランタイムのガベージコレクタの主要部分を含んでいます。

  • scanbitvector関数は、スタックフレームやヒープオブジェクト内のポインタビットマップを走査し、ライブなポインタを識別してGCのマークキューに追加する役割を担います。今回の変更で、この関数にpreciseというフラグが追加されました。これは、Goスタックフレームのようにポインタ情報が正確な場合にtrueとなり、その際にポインタがPageSize(通常4KB)よりも小さいアドレスを指していないかという厳密なチェックを行います。もしそのようなポインタが見つかった場合、それはメモリ破損やライブネス解析の誤りを示唆するため、runtime·throwによってパニックを引き起こします。これは、GCが不正なポインタを追跡しようとすることで発生する可能性のあるクラッシュを防ぎ、デバッグを容易にするための重要な安全策です。
  • スライスのcap < lenチェックも同様に、スライスの内部構造が破損している場合にパニックを引き起こすことで、早期に問題を検出します。
  • scanframe関数は、個々のGoルーチンのスタックフレームをスキャンし、その中でscanbitvectorを呼び出します。preciseフラグをtrueで渡すことで、Goスタックフレームに対してのみ厳密なポインタチェックが適用されるようにしています。

src/pkg/runtime/runtime.c

このファイルには、Goランタイムの基本的なユーティリティ関数が含まれています。

  • runtime·gotraceback関数は、GOTRACEBACK環境変数の値を読み取り、スタックトレースの出力レベルを決定します。今回の変更で、m->tracebackフィールドの値がこの決定に影響を与えるようになりました。これにより、たとえGOTRACEBACK環境変数が設定されていなくても、ランタイムが内部的に不正な状態を検出した場合(例:不正なポインタ)、強制的に詳細なスタックトレースを出力させることができます。

src/pkg/runtime/runtime.h

このヘッダーファイルには、ランタイムの主要なデータ構造の定義が含まれています。

  • M構造体(OSスレッドを表す)にtracebackフィールドが追加されました。このフィールドは、不正なポインタ検出などの特定のランタイムイベントが発生した際に設定され、スタックトレースの出力挙動を動的に変更するために使用されます。

src/pkg/runtime/stack.c

このファイルは、スタックの管理と操作に関連する関数を含んでいます。

  • adjustpointers関数は、スタックのコピーや移動の際に、スタック上のポインタを新しいアドレスに調整する役割を担います。この関数にもscanbitvectorと同様の「不正なポインタ」チェックが追加されました。これは、スタックコピーの過程でポインタが不正な値を持つことによって発生する可能性のある問題を早期に検出するためです。

src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c

これらのファイルは、それぞれARMアーキテクチャとx86アーキテクチャにおけるスタックトレースの生成ロジックを含んでいます。

  • runtime·gentraceback関数は、スタックトレースを生成し、その情報を出力します。今回の変更で、gotraceback >= 2の場合にfp=(フレームポインタ)の値がスタックトレースに含まれるようになりました。これにより、不正なポインタが検出された際に、そのポインタがスタックフレーム内のどの位置にあるかをより正確に特定できるようになり、デバッグの効率が大幅に向上します。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事
  • Goのランタイムソースコード
  • Goのスタック管理に関する資料
  • GOTRACEBACK環境変数に関するGoのドキュメント