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

[インデックス 1798] ファイルの概要

このコミットは、Go言語の初期のコンパイラ(6gおよびgc)における、switch文のcase節内で宣言された変数とヒープ割り当ての相互作用に関するバグ修正を扱っています。具体的には、case節内で宣言された変数がヒープに割り当てられるべき場合に発生していた問題に対処し、コンパイラのswitch文の処理ロジックを改善しています。

コミット

commit 0c4f4587d7b9acb6bdfb32a4d23fefc935cbee55
Author: Ken Thompson <ken@golang.org>
Date:   Tue Mar 10 16:49:34 2009 -0700

    bug with interaction of variables
    declared in cases and heap allocation
    
    R=r
    OCL=26064
    CL=26064

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0c4f4587d7b9acb6bdfb32a4d23fefc935cbee55

元コミット内容

bug with interaction of variables
declared in cases and heap allocation

R=r
OCL=26064
CL=26064

変更の背景

このコミットは、Go言語がまだ非常に初期段階にあった2009年3月に行われました。当時のGoコンパイラ(6gは64ビットシステム向けのコンパイラ、gcはGoコンパイラのフロントエンド/ミドルエンド部分)には、switch文のcase節内で変数を宣言し、その変数がエスケープ解析によってヒープに割り当てられる必要がある場合に、正しく処理されないバグが存在していました。

Go言語では、変数はその生存期間や使用範囲に応じてスタックまたはヒープに割り当てられます。コンパイラは「エスケープ解析」というプロセスを通じて、変数が関数スコープ外に「エスケープ」するかどうかを判断し、必要に応じてヒープに割り当てます。switch文のcase節内で宣言された変数は、そのcase節のスコープに限定されますが、それでもエスケープ解析の結果によってはヒープに割り当てられる可能性があります。

このバグは、このような特定のシナリオにおいて、コンパイラが変数の宣言とヒープ割り当ての間の相互作用を誤って処理し、結果としてメモリの不正な使用、メモリリーク、またはプログラムのクラッシュを引き起こす可能性があったと考えられます。Ken Thompsonによるこの修正は、Goコンパイラの堅牢性を高め、より複雑なコードパターンを正しくコンパイルできるようにするための重要なステップでした。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラとランタイムに関する基本的な概念が必要です。

  1. Goコンパイラ (gc, 6g):
    • gc (Go Compiler) は、Go言語の公式コンパイラツールチェーンの主要部分です。Goのソースコードを機械語に変換します。
    • 6g は、当時のgcツールチェーンの一部で、AMD64 (x86-64) アーキテクチャ向けのGoコンパイラを指していました。Goの初期には、ターゲットアーキテクチャごとに異なるコンパイラ名(例: 8g for ARM, 5g for PowerPC)が使われていました。
  2. 抽象構文木 (AST):
    • コンパイラは、ソースコードを解析して抽象構文木(AST)と呼ばれるツリー構造に変換します。ASTはプログラムの構造を抽象的に表現したもので、コンパイラの各フェーズ(意味解析、最適化、コード生成など)で操作されます。
    • Node はASTのノードを表す構造体であり、op(操作の種類、例: ODCLは宣言)、ninit(初期化文のリスト)、left/right(子ノード)などのフィールドを持ちます。
  3. switch文のコンパイル:
    • Goコンパイラは、switch文を直接機械語に変換するのではなく、通常は一連のif-goto文(またはジャンプテーブル)に変換します。これは、コンパイラがより単純な制御フロー構造を扱うことで、最適化やコード生成を容易にするための一般的な手法です。
    • swt.c のようなファイルは、switch文のAST変換ロジックを実装していると考えられます。
  4. 変数宣言とスコープ:
    • Goでは、変数は特定のブロック内で宣言でき、その変数のスコープはそのブロック内に限定されます。switch文のcase節も独自のブロックを形成し、その中で宣言された変数はそのcase節内でのみ有効です。
  5. エスケープ解析とヒープ割り当て:
    • Goコンパイラは、変数がその宣言された関数スコープを超えて「エスケープ」するかどうかを判断する「エスケープ解析」を実行します。
    • 変数がエスケープする場合(例: ポインタが関数から返される、グローバル変数に代入されるなど)、その変数はスタックではなくヒープに割り当てられます。ヒープに割り当てられた変数は、ガベージコレクタによって管理されます。
    • このバグは、case節内の変数がエスケープ解析によってヒープ割り当てと判断された場合に、コンパイラの処理が不適切であったことを示唆しています。
  6. ODCL:
    • Goコンパイラの内部表現におけるオペレーションコードの一つで、Declaration(宣言)を意味します。ASTノードのopフィールドに設定され、そのノードが変数の宣言を表すことを示します。

技術的詳細

このコミットは、主にsrc/cmd/6g/reg.csrc/cmd/gc/swt.cの2つのファイルを変更しています。

src/cmd/6g/reg.cの変更

このファイルは、レジスタ割り当てやシンボル処理に関連する部分であると考えられます。変更点は非常に小さいですが、重要な意味を持ちます。

--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -787,7 +787,9 @@ mkvar(Reg *r, Adr *a)
  	s = a->sym;
  	if(s == S)
  		goto none;
- 	if(s->name[0] == '!' || s->name[0] == '.')
+// 	if(s->name[0] == '!')
+// 	 	goto none;
+ 	if(s->name[0] == '.')
  		goto none;
  	et = a->etype;
  	o = a->offset;
  • 元のコードでは、シンボル名が ! または . で始まる場合に goto none していました。これは、コンパイラ内部で生成される一時的なシンボルや特殊なシンボルをスキップするためのロジックだった可能性があります。
  • 変更後、! で始まるシンボルに対するチェックがコメントアウトされ、. で始まるシンボルのみがスキップの対象となりました。
  • この変更は、case節内で宣言された変数(またはそれに関連する内部シンボル)が、以前は ! で始まるシンボルとして扱われ、何らかの理由で不適切にスキップされていた可能性を示唆しています。このスキップが、ヒープ割り当てに関するバグの一因となっていたのかもしれません。swt.cでの変更と合わせて、case節内の変数が正しく処理されるようにするための調整と考えられます。

src/cmd/gc/swt.cの変更

このファイルは、Goコンパイラにおけるswitch文の処理ロジックを実装しています。ここでの変更がこのコミットの核心です。

--- a/src/cmd/gc/swt.c
+++ b/src/cmd/gc/swt.c
@@ -189,7 +189,6 @@ casebody(Node *sw)
  	br = nod(OBREAK, N, N);
 
  loop:
-\
  	if(t == N) {
  		if(oc == N && os != N)
  			yyerror("first switch statement must be a case");
@@ -259,12 +258,12 @@ loop:
   * rebulid case statements into if .. goto
   */
 void
-prepsw(Node *sw, int arg)
+exprswitch(Node *sw, int arg)
 {
  	Iter save;
  	Node *name, *bool, *cas;
  	Node *t, *a;
-//dump("prepsw before", sw->nbody->left);
+//dump("exprswitch before", sw->nbody->left);
 
  	cas = N;
  	name = N;
@@ -281,7 +280,7 @@ prepsw(Node *sw, int arg)
  loop:
  	if(t == N) {
  		sw->nbody->left = rev(cas);
-//dump("prepsw after", sw->nbody->left);
+//dump("exprswitch after", sw->nbody->left);
  		return;
  	}
 
@@ -291,6 +290,16 @@ loop:
  		goto loop;
  	}
 
+	// pull out the dcl in case this
+	// variable is allocated on the heap.
+	// this should be done better to prevent
+	// multiple (unused) heap allocations per switch.
+	if(t->ninit != N && t->ninit->op == ODCL) {
+//dump("exprswitch case init", t->ninit);
+		cas = list(cas, t->ninit);
+		t->ninit = N;
+	}
+
  	if(t->left->op == OAS) {
  		if(bool == N) {
  			bool = nod(OXXX, N, N);
@@ -394,6 +403,18 @@ loop:
  		goto loop;
  	}
 
+	// pull out the dcl in case this
+	// variable is allocated on the heap.
+	// this should be better to prevent
+	// multiple (unused) heap allocations per switch.
+	// not worth doing now -- make a binary search
+	// on contents of signature instead.
+	if(t->ninit != N && t->ninit->op == ODCL) {
+//dump("typeswitch case init", t->ninit);
+		cas = list(cas, t->ninit);
+		t->ninit = N;
+	}
+
  	a = t->left->left;		// var
  	a = nod(OLIST, a, bool);	// var,bool
 
@@ -476,7 +497,7 @@ walkswitch(Node *sw)
  	/*
  	 * convert the switch into OIF statements
  	 */
- 	prepsw(sw, arg);
+ 	exprswitch(sw, arg);
  	walkstate(sw->nbody);
 //print("normal done\\n");
  }

主な変更点は以下の通りです。

  1. 関数名の変更: prepsw 関数が exprswitch にリネームされました。これは、この関数が式スイッチ(expression switch)の処理を担当することをより明確にするためのリファクタリングです。関連するコメントも更新されています。
  2. case節内の宣言の特別処理:
    • exprswitch 関数内(式スイッチの処理)と、その後のタイプスイッチの処理ロジックの両方に、新しいコードブロックが追加されました。
    • このブロックは、現在のASTノード t の初期化リスト t->ninitODCL(宣言)オペレーションが含まれているかどうかをチェックします。
    • もし含まれていれば、その宣言ノードを cas リスト(おそらくif-goto変換後の新しいASTノードのリスト)に追加し、元の t->ninitN(nil/null)に設定します。
    • コメントには「この変数がヒープに割り当てられる場合に、宣言を引き出す」と明記されており、これがバグ修正の核心であることがわかります。
    • また、「これにより、switchごとに複数の(未使用の)ヒープ割り当てを防ぐためにより良く行われるべきである」というコメントがあり、この修正が初期段階のものであり、将来的な改善の余地があることも示唆しています。これは、case節が複数回評価される可能性があり、そのたびに変数がヒープに割り当てられることを懸念している可能性があります。

この変更の目的は、switch文のcase節内で宣言された変数がヒープに割り当てられる必要がある場合に、その宣言がswitch文の変換プロセス中に適切に処理されるようにすることです。以前は、この宣言が正しく「引き出され」ていなかったため、ヒープ割り当てが適切に行われなかったり、メモリ管理に問題が生じたりしていたと考えられます。宣言をcasリストに移動させることで、コンパイラがその変数の生存期間とメモリ割り当てを正しく追跡できるようになります。

コアとなるコードの変更箇所

このコミットのコアとなるコードの変更箇所は、src/cmd/gc/swt.c に追加された以下の2つのブロックです。

  1. 式スイッチ (exprswitch) 内の変更:

    +	// pull out the dcl in case this
    +	// variable is allocated on the heap.
    +	// this should be done better to prevent
    +	// multiple (unused) heap allocations per switch.
    +	if(t->ninit != N && t->ninit->op == ODCL) {
    +//dump("exprswitch case init", t->ninit);
    +		cas = list(cas, t->ninit);
    +		t->ninit = N;
    +	}
    
  2. タイプスイッチの処理ロジック内の変更:

    +	// pull out the dcl in case this
    +	// variable is allocated on the heap.
    +	// this should be done better to prevent
    +	// multiple (unused) heap allocations per switch.
    +	// not worth doing now -- make a binary search
    +	// on contents of signature instead.
    +	if(t->ninit != N && t->ninit->op == ODCL) {
    +//dump("typeswitch case init", t->ninit);
    +		cas = list(cas, t->ninit);
    +		t->ninit = N;
    +	}
    

また、src/cmd/6g/reg.c の以下の変更も、関連する重要な変更です。

- 	if(s->name[0] == '!' || s->name[0] == '.')
+// 	if(s->name[0] == '!')
+// 	 	goto none;
+ 	if(s->name[0] == '.')

コアとなるコードの解説

src/cmd/gc/swt.c の変更点の解説

これらのコードブロックは、switch文のAST変換中に、case節内で宣言された変数の処理を改善します。

  • if(t->ninit != N && t->ninit->op == ODCL):

    • t は現在のASTノードを表します。ninit はそのノードに関連付けられた初期化文のリストです。
    • この条件は、現在のノード t に初期化文があり、かつその初期化文が変数の宣言 (ODCL) である場合に真となります。
    • これは、case節内で var x = ...x := ... のように変数が宣言されている状況を検出しています。
  • cas = list(cas, t->ninit);:

    • cas は、switch文がif-goto構造に変換される際に構築される新しいASTノードのリスト(またはチェーン)であると考えられます。
    • この行は、検出された変数の宣言 (t->ninit) をこの cas リストに追加しています。これにより、変数の宣言がswitch文の変換後のコードパスに適切に組み込まれ、コンパイラがその変数の存在とスコープを正しく認識できるようになります。
  • t->ninit = N;:

    • 元のノード t から初期化文 (ninit) を切り離し、N (nil/null) に設定します。これは、宣言がcasリストに移動されたため、元の場所からは削除されるべきであることを意味します。これにより、重複した処理や不適切なコード生成を防ぎます。

このロジックの追加により、case節内で宣言され、ヒープに割り当てられるべき変数が、switch文の変換後も正しくその宣言が保持され、コンパイラがエスケープ解析やガベージコレクションのために必要な情報を得られるようになります。

コメントにある「複数の(未使用の)ヒープ割り当てを防ぐためにより良く行われるべきである」という注意書きは、switch文がif-gotoに変換される際に、各case節のコードが独立したブロックとして扱われるため、もし同じ変数が複数のcase節で宣言され、それぞれがヒープ割り当てを必要とする場合、最適化が不十分だと冗長なヒープ割り当てが発生する可能性があることを示唆しています。このコミットはバグ修正を優先し、その後の最適化は将来の課題としています。

src/cmd/6g/reg.c の変更点の解説

if(s->name[0] == '!' || s->name[0] == '.') から if(s->name[0] == '.') への変更は、! で始まるシンボルがもはや特殊な内部シンボルとしてスキップされるべきではないことを示しています。これは、swt.c での変更と連携している可能性が高いです。

  • もしcase節内で宣言された変数が、コンパイラの内部で一時的に!で始まるシンボル名を与えられていたと仮定すると、以前のreg.cのロジックではこれらのシンボルが不適切に無視されていた可能性があります。
  • swt.cでの修正により、これらの変数の宣言がswitch文の変換プロセスで適切に「引き出され」るようになったため、reg.cでの!シンボルの特殊扱いが不要になった、あるいはむしろ問題を引き起こすようになったと考えられます。この変更は、case節内の変数が通常の変数として扱われ、レジスタ割り当てやその他の最適化の対象となるようにするための調整です。

関連リンク

参考にした情報源リンク

  • Go言語の公式GitHubリポジトリ: https://github.com/golang/go
  • Go言語の初期の歴史に関するドキュメントやブログ記事(当時のコンパイラ名や開発プロセスに関する情報)
  • コンパイラの設計と実装に関する一般的な知識(AST、中間表現、最適化、コード生成など)
  • Goのエスケープ解析に関する技術記事やドキュメント
  • Go言語のswitch文の仕様
  • 6ggcといったGoコンパイラの初期のツール名に関する情報