[インデックス 18541] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における重要なバグ修正を扱っています。具体的には、配列の要素のアドレスが取られる際(例: &x[0]
)、コンパイラのライブネス解析が配列変数 x
自体のアドレスが取られていることを正しく認識せず、その結果、x
が早期にガベージコレクションの対象となったり、メモリが上書きされたりする問題に対処しています。この問題は、特にガベージコレクタが無効化されている環境(GOGC=0
)で、net
パッケージのテストがクラッシュする原因となっていました。
コミット
commit 1a3ee6794c007c0a6c9481cdb26ed50e93f2697d
Author: Russ Cox <rsc@golang.org>
Date: Sat Feb 15 20:01:15 2014 -0500
cmd/gc: record &x[0] as taking address of x, if x is an array
Not recording the address being taken was causing
the liveness analysis not to preserve x in the absence
of direct references to x, which in turn was making the
net test fail with GOGC=0.
In addition to the test, this fixes a bug wherein
GOGC=0 go test -short net
crashed if liveness analysis was in use (like at tip, not like Go 1.2).
TBR=ken2
CC=golang-codereviews
https://golang.org/cl/64470043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1a3ee6794c007c0a6c9481cdb26ed50e93f2697d
元コミット内容
Goコンパイラ(cmd/gc
)において、配列 x
の要素 x[0]
のアドレスが取られる場合(&x[0]
)、x
自体のアドレスが取られていることを記録するように変更します。
この記録が行われていなかったため、直接的な x
への参照がない場合にライブネス解析が x
を保持せず、その結果 GOGC=0
の設定で net
テストが失敗していました。
この修正は、テストの修正に加えて、ライブネス解析が有効な場合(Go 1.2ではなく、現在の開発版のように)に GOGC=0 go test -short net
がクラッシュするバグも修正します。
変更の背景
Go言語のガベージコレクタは、プログラムの実行中に不要になったメモリを自動的に解放する役割を担っています。この効率性を高めるために、コンパイラは「ライブネス解析(Liveness Analysis)」という最適化手法を使用します。ライブネス解析は、プログラムの特定の位置でどの変数が将来的に使用される可能性があるか(すなわち「ライブ」であるか)を判断します。ライブな変数はガベージコレクションの対象から外され、そのメモリは保持されます。
このコミット以前のGoコンパイラでは、配列の要素のアドレスを取る操作(例: &myArray[0]
)が行われた際に、コンパイラがその操作が「配列 myArray
全体」のアドレスが取られていることを正しく認識していませんでした。これは、myArray[0]
のアドレスは myArray
の先頭アドレスと等しいため、実質的に配列全体への参照が確立されるにも関わらず、ライブネス解析がその関連性を見落としていたことを意味します。
結果として、myArray
がプログラムの他の部分で直接参照されていない場合、ライブネス解析は myArray
を「デッド」(不要)と誤判断し、ガベージコレクタがそのメモリを早期に解放してしまう可能性がありました。特に、ガベージコレクタを無効にする環境変数 GOGC=0
が設定されている場合、この誤った判断はメモリの不正アクセスやクラッシュに直結しました。net
パッケージの特定のテストがこのシナリオに遭遇し、クラッシュを引き起こしていたことが、このバグ修正の直接的なトリガーとなりました。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。ソースコードを機械語に変換する過程で、構文解析、型チェック、最適化、コード生成など、様々なフェーズを実行します。 - ライブネス解析 (Liveness Analysis): コンパイラのデータフロー解析の一種で、プログラムの特定のポイントにおいて、どの変数の値が将来的に読み取られる可能性があるか(ライブであるか)を決定します。ライブな変数は、その値がまだ必要であるため、ガベージコレクタによって保持される必要があります。
- ガベージコレクション (Garbage Collection, GC): プログラムが動的に確保したメモリのうち、もはや到達不可能(参照されていない)になったものを自動的に解放するプロセスです。Go言語のGCは並行・低遅延で動作するように設計されています。
GOGC
環境変数: Goランタイムのガベージコレクタの動作を制御する環境変数です。GOGC=0
に設定すると、ガベージコレクションが無効になります。これはデバッグや特定のパフォーマンス測定の際に使用されることがありますが、メモリリークやライブネス解析のバグを顕在化させる可能性があります。- 抽象構文木 (Abstract Syntax Tree, AST): ソースコードの構文構造を木構造で表現したものです。コンパイラの各フェーズ(構文解析、型チェック、最適化など)は、このASTを操作して処理を進めます。
&
(アドレス演算子): Go言語では、変数のメモリ上のアドレスを取得するために使用されます。例えば、&x
は変数x
のアドレスを返します。配列の要素x[0]
のアドレス&x[0]
は、配列x
の先頭アドレスと同じです。addrtaken
フラグ: Goコンパイラの内部で、ある変数のアドレスが取られたかどうかを示すフラグです。このフラグがセットされている変数は、ガベージコレクタがそのメモリを解放しないように、ライブネス解析によって特別に扱われる必要があります。
技術的詳細
この修正は、Goコンパイラの型チェックフェーズ(src/cmd/gc/typecheck.c
)におけるアドレス演算子(OADDR
)の処理ロジックを改善することで実現されています。
以前の実装では、&
演算子が構造体のフィールドアクセス(ODOT
)に適用された場合、例えば &s.f
のように、s.f
から s
へと遡って addrtaken
フラグをセットしていました。しかし、配列の要素アクセス(例: &x[0]
)の場合、ODOT
ノードの連鎖として扱われず、配列 x
自体まで遡って addrtaken
フラグをセットするロジックが不足していました。
このコミットでは、以下の変更が導入されました。
-
outervalue
関数の導入/公開:src/cmd/gc/go.h
にNode* outervalue(Node*);
の関数宣言が追加されました。src/cmd/gc/walk.c
に定義されているoutervalue
関数が、static
から通常の関数に変更され、他のファイルから呼び出し可能になりました。outervalue
関数は、与えられたASTノードから、それが属する最も外側の構造体または配列のノード(ベースとなる変数)を特定する役割を担います。例えば、x[0].f
のような式に対してoutervalue
を呼び出すと、最終的にx
のノードを返します。
-
typecheck.c
におけるOADDR
処理の改善:typecheck.c
のtypecheck
関数内で、&
演算子(OADDR
)が処理される部分が変更されました。- 変更前は、
n->left
(&
のオペランド)からODOT
ノードを辿ってaddrtaken
フラグをセットしていました。 - 変更後は、まず
r = outervalue(n->left);
を呼び出し、n->left
が属する最も外側の変数(配列や構造体)のノードr
を取得します。 - 次に、
for(l = n->left; l != r; l = l->left)
というループが導入されました。このループは、&
のオペランド(例:x[0]
)から始まり、outervalue
が返したベース変数(例:x
)に至るまでのAST上の全てのノードに対してl->addrtaken = 1;
を設定します。 - これにより、
&x[0]
のような式の場合、x[0]
ノードだけでなく、その親であるx
ノードにもaddrtaken
フラグが正しく伝播されるようになります。
この修正により、ライブネス解析は &x[0]
のような操作が行われた際に、配列 x
が依然としてライブであることを正確に認識できるようになり、GOGC=0
環境下でのクラッシュや不正なメモリ解放が防止されます。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -1452,6 +1452,7 @@ void walkstmt(Node **np);
void walkstmtlist(NodeList *l);
Node* conv(Node*, Type*);
int candiscard(Node*);
+Node* outervalue(Node*);
/*
* arch-specific ggen.c/gsubr.c/gobj.c/pgen.c/plive.c
outervalue
関数のプロトタイプ宣言が追加され、この関数がコンパイラの他の部分から利用可能になったことを示します。
src/cmd/gc/typecheck.c
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -721,7 +721,8 @@ reswitch:
if(n->left->type == T)
goto error;
checklvalue(n->left, "take the address of");
- for(l=n->left; l->op == ODOT; l=l->left)
+ r = outervalue(n->left);
+ for(l = n->left; l != r; l = l->left)
l->addrtaken = 1;
if(l->orig != l && l->op == ONAME)
fatal("found non-orig name node %N", l);
&
演算子(OADDR
)の型チェックロジックが変更されています。outervalue
を呼び出してベースとなるノード r
を取得し、n->left
から r
までの全てのノードに対して addrtaken
フラグをセットするように修正されています。
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2205,7 +2205,7 @@ reorder3save(Node **np, NodeList *all, NodeList *stop, NodeList **early)
* what's the outer value that a write to n affects?
* outer value means containing struct or array.
*/
-static Node*
+Node*
outervalue(Node *n)
{
for(;;) {
outervalue
関数の定義が static
から通常の関数に変更され、外部から呼び出し可能になりました。この関数は、与えられたノードが属する最も外側の構造体または配列のノードを返します。
test/fixedbugs/bug483.go
--- /dev/null
+++ b/test/fixedbugs/bug483.go
@@ -0,0 +1,36 @@
+// run
+
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Test for a garbage collection bug involving not
+// marking x as having its address taken by &x[0]
+// when x is an array value.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "runtime"
+)
+
+func main() {
+ var x = [4]struct{ x, y interface{} }{
+ {"a", "b"},
+ {"c", "d"},
+ {"e", "f"},
+ {"g", "h"},
+ }
+
+ var buf bytes.Buffer
+ for _, z := range x {
+ runtime.GC()
+ fmt.Fprintf(&buf, "%s %s ", z.x.(string), z.y.(string))
+ }
+
+ if buf.String() != "a b c d e f g h " {
+ println("BUG wrong output\n", buf.String())
+ }
+}
この新しいテストケースは、配列 x
を定義し、その要素を for range
ループでイテレートします。ループ内で runtime.GC()
を明示的に呼び出し、z.x
や z.y
のようなインターフェース型のフィールドにアクセスします。z
は x
の要素のコピーですが、その内部のインターフェース値は元の配列 x
のメモリを参照している可能性があります。このテストは、x
がライブネス解析によって正しく保持されない場合に、クラッシュまたは不正な出力が発生することを確認するために設計されています。
コアとなるコードの解説
このコミットの核心は、Goコンパイラがアドレス演算子 &
を処理する際の「ライブネス」の伝播方法を改善した点にあります。
-
outervalue
関数の役割:outervalue
関数は、ASTノードツリーを上方向に辿り、与えられたノードが最終的にどの「ベース変数」(配列や構造体)に由来するかを特定します。例えば、&myArray[0].field
という式があった場合、outervalue(&myArray[0].field)
はmyArray
のノードを返します。これは、myArray[0].field
のアドレスを取ることは、実質的にmyArray
のメモリ領域の一部のアドレスを取ることを意味するため、myArray
自体もライブであると見なされるべきだからです。 -
typecheck.c
におけるaddrtaken
の伝播:typecheck.c
の変更は、&
演算子(OADDR
)が検出されたときに発動します。r = outervalue(n->left);
:まず、&
のオペランド(例:x[0]
)に対してoutervalue
を呼び出し、そのオペランドが属する最も外側の変数(例:x
)のノードr
を取得します。for(l = n->left; l != r; l = l->left) l->addrtaken = 1;
:次に、&
のオペランドからr
に至るまでのAST上の全てのノード(例:x[0]
とx
)に対してaddrtaken = 1
フラグをセットします。- この
addrtaken
フラグは、その変数のアドレスがプログラムのどこかで取られたことをコンパイラに通知します。ライブネス解析は、このフラグがセットされた変数を「ライブ」であると判断し、ガベージコレクションの対象から除外します。
この修正により、&x[0]
のような操作が行われた場合でも、配列 x
がライブネス解析によって正しく「ライブ」と認識され、GOGC=0
のようなガベージコレクタが無効な環境下でも、x
のメモリが不正に解放されたり上書きされたりするのを防ぐことができます。これは、Goプログラムの堅牢性と安定性を向上させる上で重要な修正です。
関連リンク
- Go issue tracker (関連する可能性のあるバグ報告): https://github.com/golang/go/issues?q=is%3Aissue+is%3Aclosed+bug483 (直接的なissueは見つかりませんでしたが、
bug483.go
というテスト名から、関連するissueが存在する可能性があります。) - Go Change List 64470043: https://golang.org/cl/64470043 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
参考にした情報源リンク
- Go言語のガベージコレクションに関するドキュメント: https://go.dev/doc/gc-guide
- Goコンパイラの内部構造に関する一般的な情報 (Goのソースコードや関連する論文):
- "The Go Programming Language" (Alan A. A. Donovan, Brian W. Kernighan)
- Goのソースコードリポジトリ: https://github.com/golang/go
- コンパイラのライブネス解析に関する一般的な情報 (計算機科学の教科書など)
GOGC
環境変数に関する情報: https://go.dev/doc/diagnose-gc