[インデックス 14664] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)のデータ競合検出器(Race Detector)関連のコード(racewalk
)におけるコンパイラクラッシュのバグを修正するものです。特定の配列インデックス計算を含むGoコードがコンパイル時にセグメンテーション違反を引き起こす問題が解決されました。
コミット
commit ca4b868e9ae9ef11515a3224a9e311c7437e2c9c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Dec 17 12:55:41 2012 +0400
cmd/gc: racewalk: fix compiler crash
The code:
func main() {
v := make([]int64, 10)
i := 1
_ = v[(i*4)/3]
}
crashes compiler with:
Program received signal SIGSEGV, Segmentation fault.
0x000000000043c274 in walkexpr (np=0x7fffffffc9b8, init=0x0) at src/cmd/gc/walk.c:587
587 *init = concat(*init, n->ninit);\n
(gdb) bt
#0 0x000000000043c274 in walkexpr (np=0x7fffffffc9b8, init=0x0) at src/cmd/gc/walk.c:587
#1 0x0000000000432d15 in copyexpr (n=0x7ffff7f69a48, t=<optimized out>, init=0x0) at src/cmd/gc/subr.c:2020
#2 0x000000000043f281 in walkdiv (init=0x0, np=0x7fffffffca70) at src/cmd/gc/walk.c:2901
#3 walkexpr (np=0x7ffff7f69760, init=0x0) at src/cmd/gc/walk.c:956
#4 0x000000000043d801 in walkexpr (np=0x7ffff7f69bc0, init=0x0) at src/cmd/gc/walk.c:988
#5 0x000000000043cc9b in walkexpr (np=0x7ffff7f69d38, init=0x0) at src/cmd/gc/walk.c:1068
#6 0x000000000043c50b in walkexpr (np=0x7ffff7f69f50, init=0x0) at src/cmd/gc/walk.c:879
#7 000000000043c50b in walkexpr (np=0x7ffff7f6a0c8, init=0x0) at src/cmd/gc/walk.c:879
#8 0x0000000000440a53 in walkexprlist (l=0x7ffff7f6a0c8, init=0x0) at src/cmd/gc/walk.c:357
#9 0x000000000043d0bf in walkexpr (np=0x7fffffffd318, init=0x0) at src/cmd/gc/walk.c:566
#10 0x00000000004402bf in vmkcall (fn=<optimized out>, t=0x0, init=0x0, va=0x7fffffffd368) at src/cmd/gc/walk.c:2275
#11 0x000000000044059a in mkcall (name=<optimized out>, t=0x0, init=0x0) at src/cmd/gc/walk.c:2287
#12 0x000000000042862b in callinstr (np=0x7fffffffd4c8, init=0x7fffffffd568, wr=0, skip=<optimized out>) at src/cmd/gc/racewalk.c:478
#13 0x00000000004288b7 in racewalknode (np=0x7ffff7f68108, init=0x7fffffffd568, wr=0, skip=0) at src/cmd/gc/racewalk.c:287
#14 0x0000000000428781 in racewalknode (np=0x7ffff7f65840, init=0x7fffffffd568, wr=0, skip=0) at src/cmd/gc/racewalk.c:302
#15 0x0000000000428abd in racewalklist (l=0x7ffff7f65840, init=0x0) at src/cmd/gc/racewalk.c:97
#16 0x0000000000428d0b in racewalk (fn=0x7ffff7f5f010) at src/cmd/gc/racewalk.c:63
#17 0x0000000000402b9c in compile (fn=0x7ffff7f5f010) at src/cmd/6g/../gc/pgen.c:67
#18 0x0000000000419f86 in funccompile (n=0x7ffff7f5f010, isclosure=0) at src/cmd/gc/dcl.c:1414
#19 0x0000000000424161 in p9main (argc=<optimized out>, argv=<optimized out>) at src/cmd/gc/lex.c:431
#20 0x0000000000401739 in main (argc=<optimized out>, argv=<optimized out>) at src/lib9/main.c:35
The problem is nil init passed to mkcall().
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6940045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ca4b868e9ae9ef11515a3224a9e311c7437e2c9c
元コミット内容
このコミットは、Goコンパイラが特定のGoコードをコンパイルする際にクラッシュするバグを修正します。問題のコードは以下の通りです。
func main() {
v := make([]int64, 10)
i := 1
_ = v[(i*4)/3] // ここでクラッシュが発生
}
このコードをコンパイルすると、コンパイラはセグメンテーション違反(Segmentation fault)で異常終了します。スタックトレースから、src/cmd/gc/walk.c
のwalkexpr
関数内の*init = concat(*init, n->ninit);
という行でクラッシュしていることが示されています。根本原因は、mkcall()
関数にnil
のinit
引数が渡されたことにあると特定されています。
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための「Race Detector」というツールが組み込まれています。このツールは、コンパイル時に特定のインストゥルメンテーションコードを挿入することで機能します。
このバグは、Race Detectorが有効な状態で、配列のインデックス計算に複雑な算術式(例: (i*4)/3
)が含まれる場合に発生しました。コンパイラがこのような式を処理し、同時にRace Detectorのためのraceread
(読み込み競合検出)またはracewrite
(書き込み競合検出)の呼び出しを挿入しようとした際に、内部的な初期化リストの処理が正しく行われず、nil
ポインタの逆参照が発生し、コンパイラがクラッシュしていました。
コンパイラのクラッシュは、開発プロセスを中断させ、Goプログラムのビルドを妨げる重大な問題です。そのため、このバグの修正はGoコンパイラの安定性と信頼性を確保するために不可欠でした。
前提知識の解説
このコミットの理解には、以下の概念が役立ちます。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。コンパイルプロセスは複数のフェーズに分かれており、構文解析、型チェック、中間表現(AST)の生成、最適化、コード生成などがあります。 - 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。コンパイラはASTを操作しながら、コードの意味解析や変換を行います。Goコンパイラでは、
Node
構造体がASTの各ノードを表します。 walk
フェーズ: Goコンパイラの重要なフェーズの一つで、ASTを走査(walk)しながら、高レベルなASTノードをより低レベルなノードに変換したり、特定の最適化やコード変換を行ったりします。このフェーズで、関数呼び出しの準備や、変数の初期化などが処理されます。- Go Race Detector: Goランタイムに組み込まれたデータ競合検出ツールです。並行実行されるゴルーチン間で共有データへのアクセスが適切に同期されていない場合に発生するデータ競合を特定します。コンパイル時に
raceread
やracewrite
といった特殊な関数呼び出しを挿入することで、実行時にメモリアクセスを監視します。 mkcall
関数: Goコンパイラの内部関数で、AST内に新しい関数呼び出しノードを生成するために使用されます。この関数は、呼び出す関数、戻り値の型、引数、そして初期化文のリスト(init
リスト)を受け取ります。init
リストは、関数呼び出しの引数を評価する際などに必要となる一時的な初期化文を格納するために使われます。NodeList
とinit
リスト:NodeList
はASTノードのリンクリストです。コンパイラは、文(statement)や式(expression)の評価に伴って生成される初期化処理を、このNodeList
としてinit
リストに蓄積します。これらの初期化文は、後続のコード生成フェーズで適切な位置に挿入されます。SIGSEGV
(Segmentation Fault): プログラムが不正なメモリアドレスにアクセスしようとしたときに発生するエラーです。通常、nil
ポインタの逆参照(nil
であるポインタが指すメモリ領域にアクセスしようとすること)が原因で発生します。
技術的詳細
このバグは、Goコンパイラのracewalk
フェーズで発生していました。racewalk
は、Race Detectorのためにメモリアクセス(読み込み/書き込み)を監視するraceread
やracewrite
といった関数呼び出しをASTに挿入する役割を担っています。
問題のGoコード_ = v[(i*4)/3]
では、配列v
へのアクセスが行われています。このアクセスは、Race Detectorによってraceread
呼び出しに変換される必要があります。racewalk.c
内のcallinstr
関数は、このraceread
またはracewrite
呼び出しをASTに挿入する処理を担当しています。
callinstr
関数内で、mkcall
関数が使用され、raceread
またはracewrite
の関数呼び出しノードが生成されます。元のコードでは、このmkcall
関数に渡されるinit
引数がnil
となっていました。
// src/cmd/gc/racewalk.c (修正前)
f = mkcall(wr ? "racewrite" : "raceread", T, nil, uintptraddr(n));
mkcall
関数は、生成される関数呼び出しに必要な初期化文を格納するためのinit
リストを受け取ります。配列のインデックス計算 (i*4)/3
のような複雑な式は、評価の過程で一時的な変数の割り当てや、中間結果の計算といった初期化文を生成する可能性があります。
mkcall
にnil
が渡された場合、これらの初期化文は適切にinit
リストに連結されません。その結果、コンパイラのwalk
フェーズの後半で、src/cmd/gc/walk.c:587
の*init = concat(*init, n->ninit);
という行が実行される際に問題が発生します。この行は、現在のノードn
に関連する初期化文n->ninit
を、親のinit
リストに連結しようとします。しかし、racewalk
フェーズでmkcall
にnil
が渡されたために、init
ポインタがnil
のままとなり、*init
を逆参照しようとした際にセグメンテーション違反が発生し、コンパイラがクラッシュしていました。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、src/cmd/gc/racewalk.c
ファイルにあります。
diff --git a/src/cmd/gc/racewalk.c b/src/cmd/gc/racewalk.c
index 2d216ec67a..1840c6529e 100644
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -475,8 +475,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)\n \t\t*np = n;\n \t}\n \tn = treecopy(n);\n-\t\tf = mkcall(wr ? "racewrite" : "raceread", T, nil, uintptraddr(n));\n-\t\t//typecheck(&f, Etop);\n+\t\tf = mkcall(wr ? "racewrite" : "raceread", T, init, uintptraddr(n));\n \t*init = list(*init, f);\n \treturn 1;\n }\
また、このバグを再現し、修正を検証するためのテストケースがsrc/pkg/runtime/race/testdata/regression_test.go
に追加されました。
diff --git a/src/pkg/runtime/race/testdata/regression_test.go b/src/pkg/runtime/race/testdata/regression_test.go
index 442379d7ed..c48f7b8600 100644
--- a/src/pkg/runtime/race/testdata/regression_test.go
+++ b/src/pkg/runtime/race/testdata/regression_test.go
@@ -121,3 +121,9 @@ func TestNoRaceRpcChan(t *testing.T) {\n \t\tt.Fatalf(\"makeChanCalls %d, expected 1\\n\", makeChanCalls)\n \t}\n }\n+\n+func divInSlice() {\n+\tv := make([]int64, 10)\n+\ti := 1\n+\t_ = v[(i*4)/3]\n+}\
コアとなるコードの解説
修正の核心は、src/cmd/gc/racewalk.c
のcallinstr
関数におけるmkcall
の呼び出しです。
元のコード:
f = mkcall(wr ? "racewrite" : "raceread", T, nil, uintptraddr(n));
修正後のコード:
f = mkcall(wr ? "racewrite" : "raceread", T, init, uintptraddr(n));
変更点は、mkcall
関数の第3引数(init
リスト)にnil
ではなく、callinstr
関数自身が受け取ったinit
ポインタ(NodeList **init
)を渡すようにしたことです。
これにより、racewrite
またはraceread
呼び出しを生成する際に、その呼び出しに必要なすべての初期化文(例えば、配列インデックスの計算結果など)が、正しく親のinit
リストに連結されるようになります。init
ポインタがnil
でなくなるため、後続のwalkexpr
関数内で*init = concat(*init, n->ninit);
のような操作が行われても、nil
ポインタの逆参照が発生せず、コンパイラのクラッシュが回避されます。
この修正は、Race Detectorが挿入するコードが、コンパイラの他の部分と正しく連携し、ASTの初期化リストの管理規則に従うことを保証します。
関連リンク
- Go Gerrit Change: https://golang.org/cl/6940045
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード(
src/cmd/gc
ディレクトリ) - Go Race Detectorに関する一般的な情報