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

[インデックス 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.crace_amd64.s

  • race.c: Goのレース検出器のC言語で書かれた部分です。メモリアクセスの監視、競合の検出ロジック、スタックトレースの収集など、レース検出器の主要な機能が実装されています。
  • race_amd64.s: x86-64アーキテクチャ向けのアセンブリ言語で書かれたレース検出器のコードです。C言語から呼び出される低レベルの関数や、特定のレジスタ操作が必要な部分がここに実装されています。

6. runtime·lessstackruntime·mheap->arena_start / runtime·mheap->arena_used

  • runtime·lessstack: Goのスタックは動的に拡張・縮小します。lessstack は、スタックが小さすぎる場合に、より大きなスタックに切り替えるための特別なPC値を示すマーカーです。レース検出器がスタックトレースを収集する際に、このマーカーを検出すると、スタックの切り替えを考慮して正しい呼び出し元を特定する必要があります。
  • runtime·mheap->arena_start / runtime·mheap->arena_used: これらはGoのヒープメモリ領域の開始アドレスと使用済み領域の終了アドレスを示します。ヒープは、動的に確保されるメモリ(オブジェクトなど)が配置される場所です。クロージャのコードはヒープに配置されることがあり、レース検出器がPCを追跡する際に、PCがヒープ領域にあるかどうかをチェックすることがありました。これは、クロージャの呼び出しが通常の関数呼び出しとは異なるスタックトレースを持つ可能性があるためです。

技術的詳細

このコミットは、主に以下の2つのファイルに対する変更を含んでいます。

  1. src/pkg/runtime/race.c
  2. src/pkg/runtime/race_amd64.s

問題の根本原因は、racefuncenter 関数が呼び出される際に、引数として渡されるべきプログラムカウンタ(PC)の値が、アセンブリコードレベルでのスタック操作によって誤って上書きされてしまうことでした。

src/pkg/runtime/race.c の変更

race.c では、runtime·racefuncenter1memoryaccessrangeaccess の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·racefuncenter1memoryaccessrangeaccess の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·racefuncenterpc uintptr を引数として受け取るため、この pc の値がレジスタに格納されて渡されます。

問題は、PUSHQ DX が、racefuncenter に渡されたPC引数の位置をずらしたり、あるいは DX レジスタに意図しない値(例えば、クロージャのデータブロックポインタ)が格納されており、それがスタックにプッシュされることで、runtime·racefuncenter1 が期待するPC引数の位置に誤った値が配置されてしまう、というものでした。

変更後では、より明示的かつ安全なスタック操作が行われています。

  1. TEXT runtime·racefuncenter(SB), 7, $16: 関数 runtime·racefuncenter の定義。$16 は、この関数がスタックフレームに16バイトのローカル変数領域を確保することを示します。
  2. MOVQ DX, saved-8(SP): DX レジスタの現在の内容を、スタックフレーム内の saved-8(SP) という位置に保存します。これは、DX レジスタが他の目的で使用される前にその値を一時的に退避させるための標準的な方法です。
  3. MOVQ pc+0(FP), DX: pc+0(FP) は、フレームポインタ FP を基準とした pc 引数のメモリ位置を示します。この命令は、runtime·racefuncenter に渡された正しいPC引数の値を DX レジスタにロードします。
  4. MOVQ DX, arg-16(SP): DX レジスタにロードされた正しいPC値を、runtime·racefuncenter1 に渡すための引数として、スタックフレーム内の arg-16(SP) という位置に移動します。これにより、runtime·racefuncenter1 はスタックから正しいPC値を取得できるようになります。
  5. CALL runtime·racefuncenter1(SB): runtime·racefuncenter1 を呼び出します。
  6. MOVQ saved-8(SP), DX: runtime·racefuncenter1 の呼び出し後、saved-8(SP) に保存しておいた元の DX レジスタの内容を DX に復元します。
  7. RET: 関数からリターンします。

このアセンブリコードの変更により、racefuncenter に渡されたPC引数が、スタックを介して runtime·racefuncenter1 に確実に正しく渡されるようになり、以前のコミットで発生していた引数破損の問題が根本的に解決されました。

関連リンク

参考にした情報源リンク