[インデックス 15493] ファイルの概要
コミット
commit e2f9e816b77b9c1b6625abb2dd32c7dc897cf25a
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Feb 28 07:32:29 2013 +0100
runtime: fix racefuncenter argument corruption.
Revision 6a88e1893941 corrupts the argument to
racefuncenter by pushing the data block pointer
to the stack.
Fixes #4885.
R=dvyukov, rsc
CC=golang-dev
https://golang.org/cl/7381053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e2f9e816b77b9c1b6625abb2dd32c7dc897cf25a
元コミット内容
このコミットは、Goランタイムのレース検出器(race detector)におけるバグ修正です。具体的には、以前のコミット 6a88e1893941
によって racefuncenter
関数への引数が破損するという問題が発生していました。この破損は、データブロックポインタがスタックにプッシュされることによって引き起こされていました。このコミットは、その引数破損を修正し、Go issue #4885 を解決します。
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための強力なツールであるレース検出器が組み込まれています。レース検出器は、プログラムの実行中に共有メモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に警告を発します。
このコミットの背景にある問題は、Goランタイムのレース検出器の一部である racefuncenter
関数が、特定の状況下で誤った引数を受け取ってしまうというものでした。これは、コミットメッセージに記載されている Revision 6a88e1893941
によって導入された回帰バグでした。このリビジョンは、おそらくパフォーマンス最適化や他の機能追加のために行われた変更でしたが、その副作用として racefuncenter
の引数渡しに問題を引き起こしました。
具体的には、racefuncenter
関数が呼び出される際に、本来渡されるべきプログラムカウンタ(PC)の値が、誤ってデータブロックポインタ(おそらくクロージャのコンテキストなどに関連するデータ)によって上書きされてしまうという問題でした。これにより、レース検出器が関数のエントリポイントを正しく追跡できなくなり、誤ったレポートや検出漏れが発生する可能性がありました。
この問題は Go issue #4885 として報告され、このコミットはその問題を解決するために作成されました。
前提知識の解説
1. Goのレース検出器 (Race Detector)
Goのレース検出器は、並行プログラムにおけるデータ競合を検出するためのツールです。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生し、プログラムの予測不能な動作やバグの原因となります。レース検出器は、コンパイル時に -race
フラグを付けてビルドすることで有効になります。有効にすると、ランタイムがメモリアクセスを監視し、競合が検出された場合に詳細なスタックトレースとともに警告を出力します。
2. プログラムカウンタ (Program Counter, PC)
プログラムカウンタは、CPUが次に実行する命令のアドレスを保持するレジスタです。関数呼び出しの際には、呼び出し元の命令の次のアドレス(リターンアドレス)や、呼び出される関数のエントリポイントのアドレスなどがPCとして扱われます。レース検出器は、どの関数がどのメモリにアクセスしたかを追跡するために、PCの情報を利用します。
3. スタック (Stack)
スタックは、プログラムの実行中に一時的なデータを格納するために使用されるメモリ領域です。関数呼び出しの際には、引数、ローカル変数、リターンアドレスなどがスタックにプッシュ(格納)され、関数からのリターン時にポップ(解放)されます。x86-64アーキテクチャでは、関数呼び出し規約(calling convention)によって、引数がレジスタ(例: DI
, SI
, DX
, CX
, R8
, R9
)またはスタックを通じて渡されます。
4. runtime
パッケージ
runtime
パッケージは、Goプログラムの実行環境を管理するGoランタイムのC言語およびアセンブリ言語で書かれた部分です。ガベージコレクション、スケジューラ、メモリ管理、そしてレース検出器のような低レベルの機能が含まれています。
5. race.c
と race_amd64.s
race.c
: Goのレース検出器のC言語で書かれた部分です。メモリアクセスの監視、競合の検出ロジック、スタックトレースの収集など、レース検出器の主要な機能が実装されています。race_amd64.s
: x86-64アーキテクチャ向けのアセンブリ言語で書かれたレース検出器のコードです。C言語から呼び出される低レベルの関数や、特定のレジスタ操作が必要な部分がここに実装されています。
6. runtime·lessstack
と runtime·mheap->arena_start
/ runtime·mheap->arena_used
runtime·lessstack
: Goのスタックは動的に拡張・縮小します。lessstack
は、スタックが小さすぎる場合に、より大きなスタックに切り替えるための特別なPC値を示すマーカーです。レース検出器がスタックトレースを収集する際に、このマーカーを検出すると、スタックの切り替えを考慮して正しい呼び出し元を特定する必要があります。runtime·mheap->arena_start
/runtime·mheap->arena_used
: これらはGoのヒープメモリ領域の開始アドレスと使用済み領域の終了アドレスを示します。ヒープは、動的に確保されるメモリ(オブジェクトなど)が配置される場所です。クロージャのコードはヒープに配置されることがあり、レース検出器がPCを追跡する際に、PCがヒープ領域にあるかどうかをチェックすることがありました。これは、クロージャの呼び出しが通常の関数呼び出しとは異なるスタックトレースを持つ可能性があるためです。
技術的詳細
このコミットは、主に以下の2つのファイルに対する変更を含んでいます。
src/pkg/runtime/race.c
src/pkg/runtime/race_amd64.s
問題の根本原因は、racefuncenter
関数が呼び出される際に、引数として渡されるべきプログラムカウンタ(PC)の値が、アセンブリコードレベルでのスタック操作によって誤って上書きされてしまうことでした。
src/pkg/runtime/race.c
の変更
race.c
では、runtime·racefuncenter1
、memoryaccess
、rangeaccess
の3つの関数において、pc
または callpc
の値が runtime·lessstack
であるか、またはヒープ領域内にあるかどうかのチェックが行われていました。
変更前:
// runtime·racefuncenter1, memoryaccess, rangeaccess 内の条件
if(pc == (uintptr)runtime·lessstack ||
(pc >= (uintptr)runtime·mheap->arena_start && pc < (uintptr)runtime·mheap->arena_used))
runtime·callers(..., &pc, 1);
変更後:
// runtime·racefuncenter1, memoryaccess, rangeaccess 内の条件
if(pc == (uintptr)runtime·lessstack)
runtime·callers(..., &pc, 1);
この変更により、PCがヒープ領域内にあるかどうかのチェックが削除されました。これは、以前のコミット 6a88e1893941
が導入した問題が、ヒープ上のクロージャのPCを誤って扱うことに関連していたためと考えられます。このチェックを削除することで、racefuncenter
に渡されるPCが、ヒープ上のクロージャのPCであっても、特別な処理をせずに直接利用されるようになります。これにより、アセンブリレベルでの引数破損が修正された後、Cコード側で不必要なスタックウォーク(runtime·callers
)を避けることができます。
src/pkg/runtime/race_amd64.s
の変更
このファイルは、x86-64アーキテクチャ向けのアセンブリコードであり、runtime·racefuncenter
関数の実装が含まれています。この関数は、Goのレース検出器が関数のエントリポイントを記録するために呼び出されます。
変更前:
TEXT runtime·racefuncenter(SB),7,$0
PUSHQ DX // save function entry context (for closures)
CALL runtime·racefuncenter1(SB)
POPQ DX
RET
変更後:
// func runtime·racefuncenter(pc uintptr)
TEXT runtime·racefuncenter(SB), 7, $16
MOVQ DX, saved-8(SP) // save function entry context (for closures)
MOVQ pc+0(FP), DX
MOVQ DX, arg-16(SP)
CALL runtime·racefuncenter1(SB)
MOVQ saved-8(SP), DX
RET
このアセンブリコードの変更が、引数破損の直接的な修正です。
-
変更前:
PUSHQ DX
:DX
レジスタの内容をスタックにプッシュします。Goのx86-64呼び出し規約では、DX
レジスタは第3引数(またはそれ以降の引数の一部)を保持するか、あるいは特定のコンテキスト情報(例えばクロージャのデータブロックポインタ)を保持するために使用されることがあります。この命令は、DX
の値を保存しようとしています。CALL runtime·racefuncenter1(SB)
:runtime·racefuncenter1
を呼び出します。この関数は、runtime·racefuncenter
のC言語実装であり、引数としてPCを受け取ります。POPQ DX
: スタックから値をポップしてDX
レジスタに戻します。
問題は、
PUSHQ DX
が行われることで、racefuncenter
に渡されるべきPCの値が、スタック上の別のデータ(おそらくクロージャのデータブロックポインタ)によって上書きされてしまうことでした。racefuncenter
は引数としてPCを受け取ることを期待していますが、PUSHQ DX
がその引数の位置をずらしたり、誤った値をスタックにプッシュしたりした可能性があります。 -
変更後:
TEXT runtime·racefuncenter(SB), 7, $16
: 関数のプロローグです。$16
は、この関数がスタックフレームに16バイトのローカル変数領域を確保することを示します。MOVQ DX, saved-8(SP)
:DX
レジスタの内容を、現在のスタックポインタSP
から8バイトオフセットした位置(saved-8(SP)
)に移動(保存)します。これは、DX
の値をスタック上のローカル変数として明示的に保存する安全な方法です。MOVQ pc+0(FP), DX
:FP
(フレームポインタ) から0バイトオフセットした位置にある値(つまり、racefuncenter
の最初の引数であるpc
)をDX
レジスタに移動します。これにより、DX
レジスタにはracefuncenter
に渡された正しいPC値が格納されます。MOVQ DX, arg-16(SP)
:DX
レジスタの内容(正しいPC値)を、runtime·racefuncenter1
に渡すための引数として、スタック上のarg-16(SP)
の位置に移動します。Goのx86-64呼び出し規約では、最初の引数は通常レジスタで渡されますが、ここでは明示的にスタックに配置しています。CALL runtime·racefuncenter1(SB)
:runtime·racefuncenter1
を呼び出します。この時点で、runtime·racefuncenter1
はスタック上の正しいPC値を受け取ることができます。MOVQ saved-8(SP), DX
:saved-8(SP)
に保存しておいた元のDX
レジスタの内容をDX
に戻します。RET
: 関数からリターンします。
この変更により、racefuncenter
に渡されたPC引数が、DX
レジスタを介して runtime·racefuncenter1
に正しく渡されるようになり、引数破損の問題が解決されました。
コアとなるコードの変更箇所
src/pkg/runtime/race.c
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -94,10 +94,7 @@ runtime·racefuncenter1(uintptr pc)
{
// If the caller PC is lessstack, use slower runtime·callers
// to walk across the stack split to find the real caller.
- // Same thing if the PC is on the heap, which should be a
- // closure trampoline.
- if(pc == (uintptr)runtime·lessstack ||
- (pc >= (uintptr)runtime·mheap->arena_start && pc < (uintptr)runtime·mheap->arena_used))
+ if(pc == (uintptr)runtime·lessstack)
runtime·callers(2, &pc, 1);
m->racecall = true;
@@ -162,8 +159,7 @@ memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
m->racecall = true;
racectx = g->racectx;
if(callpc) {
- if(callpc == (uintptr)runtime·lessstack ||
- (callpc >= (uintptr)runtime·mheap->arena_start && callpc < (uintptr)runtime·mheap->arena_used))
+ if(callpc == (uintptr)runtime·lessstack)
runtime·callers(3, &callpc, 1);
runtime∕race·FuncEnter(racectx, (void*)callpc);
}
@@ -198,8 +194,7 @@ rangeaccess(void *addr, uintptr size, uintptr step, uintptr callpc, uintptr pc,
m->racecall = true;
racectx = g->racectx;
if(callpc) {
- if(callpc == (uintptr)runtime·lessstack ||
- (callpc >= (uintptr)runtime·mheap->arena_start && callpc < (uintptr)runtime·mheap->arena_used))
+ if(callpc == (uintptr)runtime·lessstack)
runtime·callers(3, &callpc, 1);
runtime∕race·FuncEnter(racectx, (void*)callpc);
}
src/pkg/runtime/race_amd64.s
--- a/src/pkg/runtime/race_amd64.s
+++ b/src/pkg/runtime/race_amd64.s
@@ -4,8 +4,11 @@
// +build race
-TEXT runtime·racefuncenter(SB),7,$0
- PUSHQ DX // save function entry context (for closures)
+// func runtime·racefuncenter(pc uintptr)
+TEXT runtime·racefuncenter(SB), 7, $16
+ MOVQ DX, saved-8(SP) // save function entry context (for closures)
+ MOVQ pc+0(FP), DX
+ MOVQ DX, arg-16(SP)
CALL runtime·racefuncenter1(SB)
- POPQ DX
+ MOVQ saved-8(SP), DX
RET
コアとなるコードの解説
src/pkg/runtime/race.c
の解説
このC言語のコードは、レース検出器の主要なロジックを担っています。変更された箇所は、runtime·racefuncenter1
、memoryaccess
、rangeaccess
の3つの関数内の条件分岐です。
これらの関数は、レース検出器が関数のエントリポイント(pc
)や呼び出し元のPC(callpc
)を追跡する際に使用されます。以前は、PCが runtime·lessstack
であるか、またはGoのヒープ領域(runtime·mheap->arena_start
から runtime·mheap->arena_used
の間)にある場合に、runtime·callers
関数を呼び出してより遅いスタックウォークを実行していました。
このコミットでは、PCがヒープ領域にあるかどうかのチェックが削除されました。これは、race_amd64.s
でのアセンブリレベルの引数破損が修正されたため、ヒープ上のクロージャのPCを特別扱いする必要がなくなったことを示唆しています。つまり、アセンブリコードがPCを正しく渡すようになったため、Cコード側でヒープ上のPCに対して不必要な追加のスタックウォークを行う必要がなくなった、ということです。これにより、レース検出器のオーバーヘッドがわずかに削減される可能性があります。
src/pkg/runtime/race_amd64.s
の解説
このアセンブリコードは、runtime·racefuncenter
関数の実装です。この関数は、Goのレース検出器が関数のエントリ時に呼び出すフックです。
変更前は、PUSHQ DX
という命令が使われていました。これは DX
レジスタの内容をスタックにプッシュするものです。Goのx86-64呼び出し規約では、関数に渡される引数は通常、レジスタ(DI
, SI
, DX
, CX
, R8
, R9
)に格納されます。runtime·racefuncenter
は pc uintptr
を引数として受け取るため、この pc
の値がレジスタに格納されて渡されます。
問題は、PUSHQ DX
が、racefuncenter
に渡されたPC引数の位置をずらしたり、あるいは DX
レジスタに意図しない値(例えば、クロージャのデータブロックポインタ)が格納されており、それがスタックにプッシュされることで、runtime·racefuncenter1
が期待するPC引数の位置に誤った値が配置されてしまう、というものでした。
変更後では、より明示的かつ安全なスタック操作が行われています。
TEXT runtime·racefuncenter(SB), 7, $16
: 関数runtime·racefuncenter
の定義。$16
は、この関数がスタックフレームに16バイトのローカル変数領域を確保することを示します。MOVQ DX, saved-8(SP)
:DX
レジスタの現在の内容を、スタックフレーム内のsaved-8(SP)
という位置に保存します。これは、DX
レジスタが他の目的で使用される前にその値を一時的に退避させるための標準的な方法です。MOVQ pc+0(FP), DX
:pc+0(FP)
は、フレームポインタFP
を基準としたpc
引数のメモリ位置を示します。この命令は、runtime·racefuncenter
に渡された正しいPC引数の値をDX
レジスタにロードします。MOVQ DX, arg-16(SP)
:DX
レジスタにロードされた正しいPC値を、runtime·racefuncenter1
に渡すための引数として、スタックフレーム内のarg-16(SP)
という位置に移動します。これにより、runtime·racefuncenter1
はスタックから正しいPC値を取得できるようになります。CALL runtime·racefuncenter1(SB)
:runtime·racefuncenter1
を呼び出します。MOVQ saved-8(SP), DX
:runtime·racefuncenter1
の呼び出し後、saved-8(SP)
に保存しておいた元のDX
レジスタの内容をDX
に復元します。RET
: 関数からリターンします。
このアセンブリコードの変更により、racefuncenter
に渡されたPC引数が、スタックを介して runtime·racefuncenter1
に確実に正しく渡されるようになり、以前のコミットで発生していた引数破損の問題が根本的に解決されました。
関連リンク
- Go issue #4885: https://github.com/golang/go/issues/4885
- Go CL 7381053: https://golang.org/cl/7381053 (このコミットに対応するGoの変更リスト)
- Revision 6a88e1893941 (問題を引き起こしたコミット): https://github.com/golang/go/commit/6a88e1893941
参考にした情報源リンク
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- x86-64 Calling Conventions (System V AMD64 ABI): https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI
- Go Assembly Language: https://go.dev/doc/asm
- Go Runtime Source Code (GitHub): https://github.com/golang/go/tree/master/src/runtime
- Go issue #4885 の議論 (GitHub): https://github.com/golang/go/issues/4885
- Go CL 7381053 の詳細 (Gerrit): https://go-review.googlesource.com/c/go/+/7381053 (GerritはGoプロジェクトのコードレビューシステム)