[インデックス 16355] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc)におけるレンジループ(OFOR range loop)の処理に関するバグ修正と、それに関連する競合検出器(racewalk)の誤った計測を防ぐための変更を導入しています。具体的には、レンジループの内部表現において不要になったリスト要素をクリアすることで、競合検出器が誤ってその要素を計測し、クラッシュする問題を解決しています。
コミット
commit fc3bec386e85b18152b9893ab6379a33a1706380
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon May 20 23:45:22 2013 +0200
cmd/gc: clear n->list of OFOR range loop after walk.
It contains the LHS of the range clause and gets
instrumented by racewalk, but it doesn't have any meaning.
Fixes #5446.
R=golang-dev, dvyukov, daniel.morsing, r
CC=golang-dev
https://golang.org/cl/9560044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fc3bec386e85b18152b9893ab6379a33a1706380
元コミット内容
cmd/gc: clear n->list of OFOR range loop after walk.
このコミットは、Goコンパイラのcmd/gc部分において、OFORレンジループのwalk処理後にn->listをクリアすることを目的としています。このn->listはレンジ句の左辺(LHS: Left Hand Side)を含んでおり、racewalkによって計測されますが、walk処理後には意味を持たなくなります。この不要な要素がracewalkによって誤って処理されることで発生する問題を修正します。
Fixes #5446.
このコミットは、GoのIssue #5446を修正します。
変更の背景
Go言語には、データ競合(data race)を検出するための組み込みの競合検出器(Race Detector)があります。これは、並行処理において複数のゴルーチンが共有メモリに同時にアクセスし、少なくとも一方が書き込みを行う場合に発生する競合状態を特定するのに役立ちます。
このコミットが修正しようとしている問題は、Goコンパイラ(cmd/gc)がレンジループ(for ... range)を処理する際に発生していました。具体的には、コンパイラのwalkフェーズでレンジループの抽象構文木(AST)が処理された後、n->listという内部データ構造が、レンジ句の左辺(例えば for i, v := range slice の i と v)への参照を保持していました。
このn->listは、walkフェーズが完了すると、その後のコンパイルプロセスでは意味を持たない「ゴミ」のような状態になります。しかし、Goの競合検出器の一部であるracewalkは、このn->listがまだ有効なデータであると誤解し、その内容を計測しようとしました。特に、a[i]のようなインデックス付きアクセスがレンジループの左辺に含まれる場合、racewalkがa[i]を誤って計測しようとしてクラッシュする可能性がありました。
この問題は、GoのIssue #5446として報告されており、このコミットはその問題を解決するために導入されました。
前提知識の解説
- Goコンパイラ (
cmd/gc): Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成など、複数のフェーズがあります。 - 抽象構文木 (AST): ソースコードの構文構造を木構造で表現したものです。コンパイラはASTを操作することで、プログラムの意味を理解し、変換を行います。
- レンジループ (
for ... range): Go言語のイテレーション構文の一つで、配列、スライス、文字列、マップ、チャネルなどのコレクションを簡単に反復処理するために使用されます。
ここで、for index, value := range collection { // ... }indexとvalueがレンジ句の左辺(LHS)にあたります。 - 競合検出器 (Race Detector): Goランタイムに組み込まれたツールで、並行プログラムにおけるデータ競合を検出します。データ競合は、複数のゴルーチンが同じメモリ位置に同時にアクセスし、少なくとも1つが書き込み操作である場合に発生するバグの一種です。競合検出器は、実行時にメモリアクセスを監視し、競合パターンを特定します。
racewalk: 競合検出器の一部として、コンパイル時にコードを計測(instrumentation)するプロセスを指します。これにより、実行時にメモリアクセスを監視し、競合を検出できるようになります。racewalkは、ASTを走査し、共有メモリへのアクセスがある箇所に特別なコードを挿入します。Node構造体: Goコンパイラの内部でASTのノードを表すために使用されるデータ構造です。n->listは、このNode構造体内のフィールドで、関連するノードのリストを指すポインタであると推測されます。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのcmd/gcにおけるwalkrange関数にあります。walkrange関数は、レンジループのASTノードを処理する役割を担っています。
問題は、walkrange関数がレンジループの処理を終えた後も、n->listフィールドがレンジ句の左辺(LHS)への参照を保持し続けていたことにありました。この参照は、walkrangeの処理が完了した時点ではもはや意味を持たない「古い」情報でした。
Goの競合検出器は、コンパイル時にracewalkというプロセスを通じてコードを計測します。racewalkは、ASTを走査し、共有メモリへのアクセスを監視するための特別な命令を挿入します。このracewalkが、walkrangeによって処理された後のn->listを、まだ有効なデータであると誤解し、その内容を計測しようとしました。
特に、for i, a[i] = range b のような形式のレンジループでは、左辺にa[i]のようなインデックス付きアクセスが含まれます。racewalkがこのa[i]を計測しようとした際に、n->listが保持していた古い、意味のない参照を辿ってしまい、結果としてコンパイラがクラッシュするという問題が発生していました。
このコミットは、walkrange関数の最後にn->list = nil;という行を追加することで、この問題を解決しています。nilを代入することで、n->listが保持していた不要な参照が明示的にクリアされ、racewalkが誤ってそれを計測しようとすることがなくなります。これにより、競合検出器の誤動作やコンパイラのクラッシュが防止されます。
また、src/pkg/runtime/race/testdata/mop_test.go に追加されたテストケース TestNoRaceRangeIssue5446 は、この問題が実際に発生していた特定のシナリオを再現し、修正が正しく機能することを確認するためのものです。このテストケースは、for i, a[i] = range b のような形式のレンジループを使用しており、以前は競合検出器の誤計測によってクラッシュしていた状況をシミュレートしています。
コアとなるコードの変更箇所
src/cmd/gc/range.c
--- a/src/cmd/gc/range.c
+++ b/src/cmd/gc/range.c
@@ -129,6 +129,9 @@ walkrange(Node *n)
v2 = N;
if(n->list->next)
v2 = n->list->next->n;
+ // n->list has no meaning anymore, clear it
+ // to avoid erroneous processing by racewalk.
+ n->list = nil;
hv2 = N;
if(v2 == N && t->etype == TARRAY) {
src/pkg/runtime/race/testdata/mop_test.go
--- a/src/pkg/runtime/race/testdata/mop_test.go
+++ b/src/pkg/runtime/race/testdata/mop_test.go
@@ -267,6 +267,19 @@ func TestNoRaceRange(t *testing.T) {
close(ch)
}
+func TestNoRaceRangeIssue5446(t *testing.T) {
+ ch := make(chan int, 3)
+ a := []int{1, 2, 3}
+ b := []int{4}
+ // used to insert a spurious instrumentation of a[i]
+ // and crash.
+ i := 1
+ for i, a[i] = range b {
+ ch <- i
+ }
+ close(ch)
+}
+
func TestRaceRange(t *testing.T) {
const N = 2
var a [N]int
コアとなるコードの解説
src/cmd/gc/range.c の変更
walkrange関数は、Goコンパイラがレンジループを処理する際の主要な関数です。この関数内で、n->listというフィールドが、レンジループの左辺(LHS)のノードを指していました。
追加された以下の3行がこのコミットの核心です。
// n->list has no meaning anymore, clear it
// to avoid erroneous processing by racewalk.
n->list = nil;
// n->list has no meaning anymore, clear it: このコメントは、n->listがこの時点ではもはや意味を持たないことを明確に示しています。// to avoid erroneous processing by racewalk.: このコメントは、n->listをクリアする目的が、racewalkによる誤った処理を防ぐためであることを説明しています。n->list = nil;: この行が実際の修正です。n->listにnil(GoコンパイラのCコードにおけるNULLポインタに相当)を代入することで、以前保持していた参照を解放し、racewalkがこの無効なメモリ領域を計測しようとするのを防ぎます。これにより、コンパイラのクラッシュが回避されます。
src/pkg/runtime/race/testdata/mop_test.go の変更
このファイルは、Goの競合検出器のテストケースを格納しています。TestNoRaceRangeIssue5446という新しいテスト関数が追加されました。
func TestNoRaceRangeIssue5446(t *testing.T) {
ch := make(chan int, 3)
a := []int{1, 2, 3}
b := []int{4}
// used to insert a spurious instrumentation of a[i]
// and crash.
i := 1
for i, a[i] = range b {
ch <- i
}
close(ch)
}
- このテストケースは、
for i, a[i] = range bという形式のレンジループを使用しています。これは、Issue #5446で報告された問題を引き起こしていた具体的なコードパターンを再現しています。 a[i]のようなインデックス付きアクセスがレンジループの左辺に存在することがポイントです。以前は、racewalkがこのa[i]を誤って計測しようとしてクラッシュしていました。ch <- iのようなチャネル操作は、並行処理のコンテキストをシミュレートし、競合検出器が動作する環境を提供します。- このテストが競合を検出せずに正常に完了すれば、修正が正しく機能していることを示します。
TestNoRaceRangeというプレフィックスは、このテストが競合を発生させないことを期待していることを示唆しています。
関連リンク
- Go言語の競合検出器に関する公式ドキュメントやブログ記事:
- The Go Race Detector (Go公式ブログ)
- Go言語のコンパイラに関する情報:
- Go Compiler Design (Go公式ドキュメント)
参考にした情報源リンク
- Go言語のソースコード(特に
cmd/gcディレクトリとsrc/pkg/runtime/raceディレクトリ) - GoのIssueトラッカー(Issue #5446に関する詳細情報)
- (注: 検索では直接的なIssue #5446のリンクは見つかりませんでしたが、コミットメッセージに記載されているため、Goの内部的なIssueトラッカーに存在すると考えられます。)
- Go言語のコンパイラとランタイムに関する一般的な知識。