[インデックス 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
フィールドと連携して、不正なポインタ検出時にGOTRACEBACK
が2
以上であるかのように振る舞い、詳細なスタックトレース(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");
}
このコードスニペットは、precise
がtrue
(Goスタックフレームの場合)であり、かつポインタp
がPageSize
よりも小さいアドレスを指している場合に、それを「不正なポインタ」と見なします。このようなポインタは、通常、有効なメモリ領域を指すことはなく、ライブネス解析の誤りを示唆しています。
不正なポインタが検出された場合、以下の処理が行われます。
m->traceback = 2;
: 現在のM(マシン、OSスレッドに相当)のtraceback
フィールドを2
に設定します。これにより、後続のスタックトレース出力時に詳細な情報(fp=
値)が強制的に含まれるようになります。runtime·printf(...)
: 不正なポインタの詳細(関数名、スキャン中のアドレス、不正なポインタの値)を標準エラー出力に表示します。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.h
にm->traceback
という新しいuint8
型のフィールドがM
構造体に追加されました。このフィールドは、不正なポインタが検出された際に2
に設定され、スタックトレースの出力挙動を制御します。
src/pkg/runtime/runtime.c
のruntime·gotraceback
関数が変更され、m->traceback
の値が考慮されるようになりました。もしGOTRACEBACK
環境変数が設定されていないか空の場合でも、m->traceback
が0
でない場合はその値が返されるようになりました。これにより、不正なポインタ検出時にGOTRACEBACK=2
が設定されたかのように振る舞い、詳細なスタックトレースが強制的に出力されます。
src/pkg/runtime/traceback_arm.c
とsrc/pkg/runtime/traceback_x86.c
のruntime·gentraceback
関数も変更され、fp=
値の出力条件にgotraceback >= 2
が追加されました。これにより、m->traceback
が2
に設定された場合に、フレームポインタの値がスタックトレースに含まれるようになり、デバッグ情報が強化されます。
scanframe
関数の変更
src/pkg/runtime/mgc0.c
のscanframe
関数は、スタックフレームをスキャンする際にscanbitvector
関数を呼び出します。このコミットでは、scanframe
関数にprecise
というローカル変数が追加され、Goスタックフレームのローカル変数と引数をスキャンする際にscanbitvector
にtrue
を渡すようになりました。これにより、Goスタックフレームに対してのみ、前述の「不正なポインタ」チェックが有効になります。
adjustpointers
関数の変更
src/pkg/runtime/stack.c
のadjustpointers
関数にも同様の「不正なポインタ」チェックが追加されました。この関数はスタックコピー中にポインタを調整する際に使用されます。ここでも、ポインタがPageSize
よりも小さいアドレスを指している場合にm->traceback = 2
を設定し、パニックを引き起こすようになりました。これは、GC中のチェックと同様に、スタックコピー時のポインタの健全性を保証するためのものです。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
scanbitvector
関数のシグネチャが変更され、Func *f
とbool precise
引数が追加されました。scanbitvector
関数内で、precise
がtrue
の場合にポインタp
がPageSize
より小さいかをチェックし、不正な場合は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 issue 7650: cmd/gc: make stack maps more precise (このコミットで更新されたissue)
- Go CL 80860044: https://golang.org/cl/80860044 (このコミットのGerritコードレビューページ)
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事
- Goのランタイムソースコード
- Goのスタック管理に関する資料
GOTRACEBACK
環境変数に関するGoのドキュメント