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

[インデックス 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 フラグをセットするロジックが不足していました。

このコミットでは、以下の変更が導入されました。

  1. outervalue 関数の導入/公開:

    • src/cmd/gc/go.hNode* outervalue(Node*); の関数宣言が追加されました。
    • src/cmd/gc/walk.c に定義されている outervalue 関数が、static から通常の関数に変更され、他のファイルから呼び出し可能になりました。
    • outervalue 関数は、与えられたASTノードから、それが属する最も外側の構造体または配列のノード(ベースとなる変数)を特定する役割を担います。例えば、x[0].f のような式に対して outervalue を呼び出すと、最終的に x のノードを返します。
  2. typecheck.c における OADDR 処理の改善:

    • typecheck.ctypecheck 関数内で、& 演算子(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.xz.y のようなインターフェース型のフィールドにアクセスします。zx の要素のコピーですが、その内部のインターフェース値は元の配列 x のメモリを参照している可能性があります。このテストは、x がライブネス解析によって正しく保持されない場合に、クラッシュまたは不正な出力が発生することを確認するために設計されています。

コアとなるコードの解説

このコミットの核心は、Goコンパイラがアドレス演算子 & を処理する際の「ライブネス」の伝播方法を改善した点にあります。

  1. outervalue 関数の役割: outervalue 関数は、ASTノードツリーを上方向に辿り、与えられたノードが最終的にどの「ベース変数」(配列や構造体)に由来するかを特定します。例えば、&myArray[0].field という式があった場合、outervalue(&myArray[0].field)myArray のノードを返します。これは、myArray[0].field のアドレスを取ることは、実質的に myArray のメモリ領域の一部のアドレスを取ることを意味するため、myArray 自体もライブであると見なされるべきだからです。

  2. 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言語のガベージコレクションに関するドキュメント: 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