[インデックス 14113] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
ディレクトリ内のracewalk.c
ファイルに対する変更です。racewalk.c
は、Goのレース検出器(Race Detector)がプログラムのメモリアクセスを監視するために必要なインストゥルメンテーションコードを生成する役割を担っています。具体的には、共有メモリへの読み書き操作を検出し、データ競合(data race)の可能性を報告するためのフックを挿入します。
コミット
Goコンパイラがレースインストゥルメンテーション中にクラッシュするバグを修正するコミットです。特定のコードパターン(構造体を返す関数内で再帰的に自身を呼び出すケース)で発生するコンパイラの内部チェック失敗が原因でした。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21b2ce724aa1310b3efb2f722d4a647be770e835
元コミット内容
commit 21b2ce724aa1310b3efb2f722d4a647be770e835
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Oct 10 18:09:23 2012 +0400
cmd/gc: fix compiler crash during race instrumentation
The compiler is crashing on the following code:
type TypeID int
func (t *TypeID) encodeType(x int) (tt TypeID, err error) {
switch x {
case 0:
return t.encodeType(x * x)
}
return 0, nil
}
The pass marks "return struct" {tt TypeID, err error} as used,
and this causes internal check failure.
I've added the test to:
https://golang.org/cl/6525052/diff/7020/src/pkg/runtime/race/regression_test.go
R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6611049
変更の背景
この変更は、Goコンパイラが特定のGoコードをコンパイルする際にクラッシュするという重大なバグを修正するために行われました。問題のコードは、構造体(この場合はTypeID
とerror
の2つの戻り値を持つ匿名構造体)を返す関数が、その関数自身を再帰的に呼び出すというパターンを含んでいました。
Goのレース検出器は、プログラムの実行中にデータ競合を検出するために、コンパイル時に特別なインストゥルメンテーションコードを挿入します。このインストゥルメンテーションの過程で、コンパイラは戻り値の構造体を「使用済み(used)」としてマークしていました。しかし、この「使用済み」のマークの仕方が、コンパイラの内部チェックと矛盾し、結果としてコンパイラのクラッシュを引き起こしていました。
開発者Dmitriy Vyukovは、この問題を特定し、修正を提案しました。修正には、コンパイラの内部で「使用済み」とマークするロジックの調整が含まれています。また、このバグを再現するためのテストケースがsrc/pkg/runtime/race/regression_test.go
に追加され、将来的な回帰を防ぐための対策も講じられました。
前提知識の解説
Go Race Detector (レース検出器)
Goのレース検出器は、並行プログラムにおけるデータ競合(data race)を検出するためのツールです。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、予測不能なプログラムの動作やバグの主要な原因となります。
レース検出器は、コンパイル時に特別なインストゥルメンテーションコードをプログラムに挿入することで機能します。このインストゥルメンテーションは、すべてのメモリ読み込みと書き込み操作を監視し、競合が発生した可能性のあるアクセスパターンを特定します。実行時に競合が検出されると、レース検出器は詳細なレポート(スタックトレースなど)を出力し、開発者が問題をデバッグできるようにします。
レース検出器を有効にするには、go run -race
、go build -race
、またはgo test -race
のように-race
フラグを使用します。
Goコンパイラ (cmd/gc
)
cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する主要なツールチェーンの一部です。コンパイラは、構文解析、型チェック、最適化、コード生成など、複数のフェーズを経て動作します。
racewalk.c
は、cmd/gc
の一部であり、コンパイラのバックエンドに近い部分で動作します。抽象構文木(AST)を走査し、レース検出に必要なメモリアクセス監視コードをASTに挿入します。
Node
とNodeList
(Goコンパイラ内部)
Goコンパイラ内部では、プログラムの構造は抽象構文木(AST)として表現されます。ASTの各要素はNode
構造体で表されます。例えば、変数、関数呼び出し、演算子、制御フロー文などがそれぞれNode
として表現されます。NodeList
は、これらのNode
のリストを管理するための構造体です。
コンパイラはASTを走査("walk")しながら、様々な変換や最適化を行います。レース検出器のインストゥルメンテーションも、このASTウォークのフェーズで行われます。
USED
マクロ
USED
マクロは、Goコンパイラの内部で使用されるデバッグまたは最適化関連のマクロです。通常、コンパイラが生成するコードにおいて、ある変数が実際に使用されているかどうかを追跡するために使われます。もし変数が定義されているにもかかわらずどこでも使用されていない場合、コンパイラはそれをデッドコードとして削除する可能性があります。USED
マクロは、特定の変数が「使用されている」とコンパイラに明示的に伝えることで、最適化による意図しない削除を防ぐ目的で使われることがあります。
このコミットでは、USED(&n1)
からUSED(n1)
への変更が見られます。これは、USED
マクロがポインタではなく直接Node
オブジェクトを受け取るように変更されたことを示唆しています。元のコードでは&n1
(n1
のアドレス)を渡していましたが、新しいコードではn1
そのものを渡しています。これは、USED
マクロの定義が変更されたか、あるいはn1
がすでにポインタ型であるためにアドレス演算子&
が不要になったか、または誤っていたかのいずれかを示しています。コミットメッセージから、この変更が「return struct」が「used」とマークされることに関連していることが示唆されており、USED
マクロの誤用が内部チェックの失敗につながっていた可能性があります。
技術的詳細
このコミットの技術的詳細は、主にracewalk.c
内のcallinstr
関数の変更と、racewalknode
関数内のUSED
マクロの修正に集約されます。
callinstr
関数の変更点
callinstr
関数は、Goのレース検出器がメモリアクセスをインストゥルメントするために呼び出される主要な関数です。この関数は、特定のNode
(ASTの要素)がメモリ読み込みまたは書き込み操作を表すかどうかを判断し、必要に応じてraceread
またはracewrite
の呼び出しを挿入します。
元のコードでは、callinstr
はvoid
を返していました。しかし、変更後はint
を返すようになりました。この戻り値は、実際にインストゥルメンテーションが行われたかどうかを示すフラグとして使用されます。
最も重要な変更は、構造体(TSTRUCT
)の処理ロジックです。
変更前:
if (t->etype == TSTRUCT) {
for(t1=t->type; t1; t1=t1->down) {
if(t1->sym && strncmp(t1->sym->name, "_", sizeof("_")-1)) {
n = treecopy(n);
f = nod(OXDOT, n, newname(t1->sym));
typecheck(&f, Erv);
callinstr(f, init, wr, 0);
}
}
return;
}
変更後:
if(t->etype == TSTRUCT) {
res = 0;
for(t1=t->type; t1; t1=t1->down) {
if(t1->sym && strncmp(t1->sym->name, "_", sizeof("_")-1)) {
n = treecopy(n);
f = nod(OXDOT, n, newname(t1->sym));
if(callinstr(f, init, wr, 0)) {
typecheck(&f, Erv);
res = 1;
}
}
}
return res;
}
この変更のポイントは以下の通りです。
callinstr
がint
を返すようになったため、構造体の各フィールドを再帰的に処理する際に、子フィールドのインストゥルメンテーションが成功したかどうかをres
変数で追跡するようになりました。typecheck(&f, Erv);
の呼び出しが、callinstr(f, init, wr, 0)
が1
(インストゥルメンテーションが成功した)を返した場合にのみ実行されるようになりました。- 元のバグは、「return struct」が
USED
とマークされることによって発生していました。typecheck
はASTノードの型をチェックし、コンパイラの内部状態を更新する重要なステップです。もしインストゥルメンテーションが実際には行われていないにもかかわらずtypecheck
が呼び出されると、コンパイラの内部状態が不整合になり、クラッシュにつながる可能性があります。 - この修正により、実際にインストゥルメンテーションが必要な場合にのみ
typecheck
が実行されるようになり、コンパイラの内部チェックの失敗を防ぎます。
- 元のバグは、「return struct」が
racewalknode
関数内のUSED
マクロの変更
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -200,7 +203,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
racewalknode(&n->left, init, 0, 0);
if(istype(n->left->type, TMAP)) {
// crashes on len(m[0]) or len(f())\n-\t\t\tUSED(&n1);\n+\t\t\tUSED(n1);\n /*
tn1 = nod(OADDR, n->left, N);
tn1 = conv(n1, types[TUNSAFEPTR]);
USED(&n1);
からUSED(n1);
への変更は、n1
が既にポインタ型であるか、またはUSED
マクロの期待する引数型が変更されたことを示唆しています。コミットメッセージの「The pass marks "return struct" {tt TypeID, err error} as used, and this causes internal check failure.」という記述から、このUSED
マクロの誤用が、構造体の戻り値が不適切に「使用済み」とマークされ、コンパイラの内部状態の不整合を引き起こしていた可能性が高いです。USED
マクロに正しい型の引数を渡すことで、この不整合が解消され、クラッシュが回避されます。
callinstr
の戻り値の型変更
static void callinstr(...)
から static int callinstr(...)
への変更は、関数が処理を行ったかどうかを示すフラグを返すようにしたことを意味します。これにより、呼び出し元(特に構造体のフィールドを再帰的に処理する部分)が、実際にインストゥルメンテーションが行われた場合にのみ追加の処理(例: typecheck
)を実行できるようになります。これは、コンパイラの内部状態の一貫性を保つ上で非常に重要です。
コアとなるコードの変更箇所
src/cmd/gc/racewalk.c
ファイルにおいて、以下の変更が行われました。
-
callinstr
関数のシグネチャ変更:- 変更前:
static void callinstr(Node *n, NodeList **init, int wr, int skip);
- 変更後:
static int callinstr(Node *n, NodeList **init, int wr, int skip);
- 変更前:
-
racewalknode
関数内のUSED
マクロの引数変更:- 変更前:
USED(&n1);
- 変更後:
USED(n1);
- 変更前:
-
callinstr
関数内の構造体(TSTRUCT
)処理ロジックの変更:res
変数の導入と、インストゥルメンテーションが成功した場合にのみtypecheck
を呼び出す条件分岐の追加。- 関数の最後に
return res;
を追加。
-
callinstr
関数内の早期リターン箇所の変更:return;
をreturn 0;
に変更し、インストゥルメンテーションが行われなかったことを示す。
コアとなるコードの解説
callinstr
関数のシグネチャ変更と戻り値の利用
callinstr
関数がvoid
からint
を返すように変更されたのは、この関数が実際にインストゥルメンテーションを行ったかどうかを呼び出し元に伝えるためです。戻り値1
はインストゥルメンテーションが成功したことを、0
は行われなかったことを示します。
この変更は、特に構造体のフィールドを再帰的に処理する部分で重要になります。元のコードでは、構造体の各フィールドに対して無条件にtypecheck
が呼び出されていました。しかし、フィールドがインストゥルメントの対象外である場合(例えば、アンダースコアで始まる名前のフィールドなど)、callinstr
は何もせず早期リターンします。この場合でもtypecheck
が呼び出されると、コンパイラの内部状態が不整合になり、クラッシュの原因となっていました。
新しいコードでは、if(callinstr(f, init, wr, 0))
という条件分岐が追加され、callinstr
が1
を返した場合(つまり、実際にインストゥルメンテーションが行われた場合)にのみtypecheck(&f, Erv);
が実行されるようになりました。これにより、コンパイラの内部状態の一貫性が保たれ、クラッシュが回避されます。
racewalknode
関数内のUSED
マクロの引数変更
USED(&n1)
からUSED(n1)
への変更は、n1
が既にポインタ型であるか、またはUSED
マクロの定義が変更され、ポインタではなく直接オブジェクトを受け取るようになったことを示唆しています。この特定のケースでは、n1
はNode*
型である可能性が高く、その場合&n1
はNode**
型となり、USED
マクロが期待する型と一致しない可能性があります。
コミットメッセージにある「The pass marks "return struct" {tt TypeID, err error} as used, and this causes internal check failure.」という記述は、このUSED
マクロの誤用が、構造体の戻り値が不適切に「使用済み」とマークされ、コンパイラの内部状態の不整合を引き起こしていたことを強く示唆しています。正しい引数をUSED
マクロに渡すことで、この不整合が解消され、コンパイラのクラッシュが回避されます。
これらの変更は、Goコンパイラのレース検出器の堅牢性を高め、特定のコードパターンにおけるコンパイラのクラッシュを防ぐ上で不可欠でした。
関連リンク
- Go CL (Change List) 6525052 (テストケース): https://golang.org/cl/6525052
- Go CL (Change List) 6611049 (このコミットに対応するCL): https://golang.org/cl/6611049
参考にした情報源リンク
- Go Race Detector: https://go.dev/blog/race-detector
- Go Compiler Internals (一般的な情報): https://go.dev/doc/articles/go_compiler_internals.html (これは一般的な情報源であり、特定のコミットの理解に直接使用したわけではありませんが、背景知識として関連します。)
- Go Source Code (GitHub): https://github.com/golang/go