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

[インデックス 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コードをコンパイルする際にクラッシュするという重大なバグを修正するために行われました。問題のコードは、構造体(この場合はTypeIDerrorの2つの戻り値を持つ匿名構造体)を返す関数が、その関数自身を再帰的に呼び出すというパターンを含んでいました。

Goのレース検出器は、プログラムの実行中にデータ競合を検出するために、コンパイル時に特別なインストゥルメンテーションコードを挿入します。このインストゥルメンテーションの過程で、コンパイラは戻り値の構造体を「使用済み(used)」としてマークしていました。しかし、この「使用済み」のマークの仕方が、コンパイラの内部チェックと矛盾し、結果としてコンパイラのクラッシュを引き起こしていました。

開発者Dmitriy Vyukovは、この問題を特定し、修正を提案しました。修正には、コンパイラの内部で「使用済み」とマークするロジックの調整が含まれています。また、このバグを再現するためのテストケースがsrc/pkg/runtime/race/regression_test.goに追加され、将来的な回帰を防ぐための対策も講じられました。

前提知識の解説

Go Race Detector (レース検出器)

Goのレース検出器は、並行プログラムにおけるデータ競合(data race)を検出するためのツールです。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、予測不能なプログラムの動作やバグの主要な原因となります。

レース検出器は、コンパイル時に特別なインストゥルメンテーションコードをプログラムに挿入することで機能します。このインストゥルメンテーションは、すべてのメモリ読み込みと書き込み操作を監視し、競合が発生した可能性のあるアクセスパターンを特定します。実行時に競合が検出されると、レース検出器は詳細なレポート(スタックトレースなど)を出力し、開発者が問題をデバッグできるようにします。

レース検出器を有効にするには、go run -racego build -race、またはgo test -raceのように-raceフラグを使用します。

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する主要なツールチェーンの一部です。コンパイラは、構文解析、型チェック、最適化、コード生成など、複数のフェーズを経て動作します。

racewalk.cは、cmd/gcの一部であり、コンパイラのバックエンドに近い部分で動作します。抽象構文木(AST)を走査し、レース検出に必要なメモリアクセス監視コードをASTに挿入します。

NodeNodeList (Goコンパイラ内部)

Goコンパイラ内部では、プログラムの構造は抽象構文木(AST)として表現されます。ASTの各要素はNode構造体で表されます。例えば、変数、関数呼び出し、演算子、制御フロー文などがそれぞれNodeとして表現されます。NodeListは、これらのNodeのリストを管理するための構造体です。

コンパイラはASTを走査("walk")しながら、様々な変換や最適化を行います。レース検出器のインストゥルメンテーションも、このASTウォークのフェーズで行われます。

USEDマクロ

USEDマクロは、Goコンパイラの内部で使用されるデバッグまたは最適化関連のマクロです。通常、コンパイラが生成するコードにおいて、ある変数が実際に使用されているかどうかを追跡するために使われます。もし変数が定義されているにもかかわらずどこでも使用されていない場合、コンパイラはそれをデッドコードとして削除する可能性があります。USEDマクロは、特定の変数が「使用されている」とコンパイラに明示的に伝えることで、最適化による意図しない削除を防ぐ目的で使われることがあります。

このコミットでは、USED(&n1)からUSED(n1)への変更が見られます。これは、USEDマクロがポインタではなく直接Nodeオブジェクトを受け取るように変更されたことを示唆しています。元のコードでは&n1n1のアドレス)を渡していましたが、新しいコードではn1そのものを渡しています。これは、USEDマクロの定義が変更されたか、あるいはn1がすでにポインタ型であるためにアドレス演算子&が不要になったか、または誤っていたかのいずれかを示しています。コミットメッセージから、この変更が「return struct」が「used」とマークされることに関連していることが示唆されており、USEDマクロの誤用が内部チェックの失敗につながっていた可能性があります。

技術的詳細

このコミットの技術的詳細は、主にracewalk.c内のcallinstr関数の変更と、racewalknode関数内のUSEDマクロの修正に集約されます。

callinstr関数の変更点

callinstr関数は、Goのレース検出器がメモリアクセスをインストゥルメントするために呼び出される主要な関数です。この関数は、特定のNode(ASTの要素)がメモリ読み込みまたは書き込み操作を表すかどうかを判断し、必要に応じてracereadまたはracewriteの呼び出しを挿入します。

元のコードでは、callinstrvoidを返していました。しかし、変更後は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;
 	}

この変更のポイントは以下の通りです。

  1. callinstrintを返すようになったため、構造体の各フィールドを再帰的に処理する際に、子フィールドのインストゥルメンテーションが成功したかどうかをres変数で追跡するようになりました。
  2. typecheck(&f, Erv);の呼び出しが、callinstr(f, init, wr, 0)1(インストゥルメンテーションが成功した)を返した場合にのみ実行されるようになりました。
    • 元のバグは、「return struct」がUSEDとマークされることによって発生していました。typecheckはASTノードの型をチェックし、コンパイラの内部状態を更新する重要なステップです。もしインストゥルメンテーションが実際には行われていないにもかかわらずtypecheckが呼び出されると、コンパイラの内部状態が不整合になり、クラッシュにつながる可能性があります。
    • この修正により、実際にインストゥルメンテーションが必要な場合にのみtypecheckが実行されるようになり、コンパイラの内部チェックの失敗を防ぎます。

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ファイルにおいて、以下の変更が行われました。

  1. callinstr関数のシグネチャ変更:

    • 変更前: static void callinstr(Node *n, NodeList **init, int wr, int skip);
    • 変更後: static int callinstr(Node *n, NodeList **init, int wr, int skip);
  2. racewalknode関数内のUSEDマクロの引数変更:

    • 変更前: USED(&n1);
    • 変更後: USED(n1);
  3. callinstr関数内の構造体(TSTRUCT)処理ロジックの変更:

    • res変数の導入と、インストゥルメンテーションが成功した場合にのみtypecheckを呼び出す条件分岐の追加。
    • 関数の最後にreturn res;を追加。
  4. callinstr関数内の早期リターン箇所の変更:

    • return;return 0;に変更し、インストゥルメンテーションが行われなかったことを示す。

コアとなるコードの解説

callinstr関数のシグネチャ変更と戻り値の利用

callinstr関数がvoidからintを返すように変更されたのは、この関数が実際にインストゥルメンテーションを行ったかどうかを呼び出し元に伝えるためです。戻り値1はインストゥルメンテーションが成功したことを、0は行われなかったことを示します。

この変更は、特に構造体のフィールドを再帰的に処理する部分で重要になります。元のコードでは、構造体の各フィールドに対して無条件にtypecheckが呼び出されていました。しかし、フィールドがインストゥルメントの対象外である場合(例えば、アンダースコアで始まる名前のフィールドなど)、callinstrは何もせず早期リターンします。この場合でもtypecheckが呼び出されると、コンパイラの内部状態が不整合になり、クラッシュの原因となっていました。

新しいコードでは、if(callinstr(f, init, wr, 0))という条件分岐が追加され、callinstr1を返した場合(つまり、実際にインストゥルメンテーションが行われた場合)にのみtypecheck(&f, Erv);が実行されるようになりました。これにより、コンパイラの内部状態の一貫性が保たれ、クラッシュが回避されます。

racewalknode関数内のUSEDマクロの引数変更

USED(&n1)からUSED(n1)への変更は、n1が既にポインタ型であるか、またはUSEDマクロの定義が変更され、ポインタではなく直接オブジェクトを受け取るようになったことを示唆しています。この特定のケースでは、n1Node*型である可能性が高く、その場合&n1Node**型となり、USEDマクロが期待する型と一致しない可能性があります。

コミットメッセージにある「The pass marks "return struct" {tt TypeID, err error} as used, and this causes internal check failure.」という記述は、このUSEDマクロの誤用が、構造体の戻り値が不適切に「使用済み」とマークされ、コンパイラの内部状態の不整合を引き起こしていたことを強く示唆しています。正しい引数をUSEDマクロに渡すことで、この不整合が解消され、コンパイラのクラッシュが回避されます。

これらの変更は、Goコンパイラのレース検出器の堅牢性を高め、特定のコードパターンにおけるコンパイラのクラッシュを防ぐ上で不可欠でした。

関連リンク

参考にした情報源リンク