[インデックス 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コンパイラとランタイムに関する基本的な概念が必要です。
- Goコンパイラ (
gc
,6g
):gc
(Go Compiler) は、Go言語の公式コンパイラツールチェーンの主要部分です。Goのソースコードを機械語に変換します。6g
は、当時のgc
ツールチェーンの一部で、AMD64 (x86-64) アーキテクチャ向けのGoコンパイラを指していました。Goの初期には、ターゲットアーキテクチャごとに異なるコンパイラ名(例:8g
for ARM,5g
for PowerPC)が使われていました。
- 抽象構文木 (AST):
- コンパイラは、ソースコードを解析して抽象構文木(AST)と呼ばれるツリー構造に変換します。ASTはプログラムの構造を抽象的に表現したもので、コンパイラの各フェーズ(意味解析、最適化、コード生成など)で操作されます。
Node
はASTのノードを表す構造体であり、op
(操作の種類、例:ODCL
は宣言)、ninit
(初期化文のリスト)、left
/right
(子ノード)などのフィールドを持ちます。
switch
文のコンパイル:- Goコンパイラは、
switch
文を直接機械語に変換するのではなく、通常は一連のif-goto
文(またはジャンプテーブル)に変換します。これは、コンパイラがより単純な制御フロー構造を扱うことで、最適化やコード生成を容易にするための一般的な手法です。 swt.c
のようなファイルは、switch
文のAST変換ロジックを実装していると考えられます。
- Goコンパイラは、
- 変数宣言とスコープ:
- Goでは、変数は特定のブロック内で宣言でき、その変数のスコープはそのブロック内に限定されます。
switch
文のcase
節も独自のブロックを形成し、その中で宣言された変数はそのcase
節内でのみ有効です。
- Goでは、変数は特定のブロック内で宣言でき、その変数のスコープはそのブロック内に限定されます。
- エスケープ解析とヒープ割り当て:
- Goコンパイラは、変数がその宣言された関数スコープを超えて「エスケープ」するかどうかを判断する「エスケープ解析」を実行します。
- 変数がエスケープする場合(例: ポインタが関数から返される、グローバル変数に代入されるなど)、その変数はスタックではなくヒープに割り当てられます。ヒープに割り当てられた変数は、ガベージコレクタによって管理されます。
- このバグは、
case
節内の変数がエスケープ解析によってヒープ割り当てと判断された場合に、コンパイラの処理が不適切であったことを示唆しています。
ODCL
:- Goコンパイラの内部表現におけるオペレーションコードの一つで、
Declaration
(宣言)を意味します。ASTノードのop
フィールドに設定され、そのノードが変数の宣言を表すことを示します。
- Goコンパイラの内部表現におけるオペレーションコードの一つで、
技術的詳細
このコミットは、主にsrc/cmd/6g/reg.c
とsrc/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");
}
主な変更点は以下の通りです。
- 関数名の変更:
prepsw
関数がexprswitch
にリネームされました。これは、この関数が式スイッチ(expression switch)の処理を担当することをより明確にするためのリファクタリングです。関連するコメントも更新されています。 case
節内の宣言の特別処理:exprswitch
関数内(式スイッチの処理)と、その後のタイプスイッチの処理ロジックの両方に、新しいコードブロックが追加されました。- このブロックは、現在のASTノード
t
の初期化リストt->ninit
にODCL
(宣言)オペレーションが含まれているかどうかをチェックします。 - もし含まれていれば、その宣言ノードを
cas
リスト(おそらくif-goto
変換後の新しいASTノードのリスト)に追加し、元のt->ninit
をN
(nil/null)に設定します。 - コメントには「この変数がヒープに割り当てられる場合に、宣言を引き出す」と明記されており、これがバグ修正の核心であることがわかります。
- また、「これにより、
switch
ごとに複数の(未使用の)ヒープ割り当てを防ぐためにより良く行われるべきである」というコメントがあり、この修正が初期段階のものであり、将来的な改善の余地があることも示唆しています。これは、case
節が複数回評価される可能性があり、そのたびに変数がヒープに割り当てられることを懸念している可能性があります。
この変更の目的は、switch
文のcase
節内で宣言された変数がヒープに割り当てられる必要がある場合に、その宣言がswitch
文の変換プロセス中に適切に処理されるようにすることです。以前は、この宣言が正しく「引き出され」ていなかったため、ヒープ割り当てが適切に行われなかったり、メモリ管理に問題が生じたりしていたと考えられます。宣言をcas
リストに移動させることで、コンパイラがその変数の生存期間とメモリ割り当てを正しく追跡できるようになります。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更箇所は、src/cmd/gc/swt.c
に追加された以下の2つのブロックです。
-
式スイッチ (
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; + }
-
タイプスイッチの処理ロジック内の変更:
+ // 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言語の初期のコミット履歴: https://github.com/golang/go/commits?author=Ken+Thompson
- Goコンパイラの内部構造に関する一般的な情報(より現代のGoに関するものですが、概念は共通しています):
- Goコンパイラのソースコード: https://github.com/golang/go/tree/master/src/cmd/compile
- Goのエスケープ解析に関する記事(例: "Go's escape analysis" で検索)
参考にした情報源リンク
- Go言語の公式GitHubリポジトリ: https://github.com/golang/go
- Go言語の初期の歴史に関するドキュメントやブログ記事(当時のコンパイラ名や開発プロセスに関する情報)
- コンパイラの設計と実装に関する一般的な知識(AST、中間表現、最適化、コード生成など)
- Goのエスケープ解析に関する技術記事やドキュメント
- Go言語の
switch
文の仕様 6g
やgc
といったGoコンパイラの初期のツール名に関する情報