[インデックス 1103] ファイルの概要
このコミットは、Goコンパイラ(特に6g
、x86-64アーキテクチャ向けの64ビットGoコンパイラ)における型(Type
)のメモリレイアウト計算、特に構造体(struct)の幅(サイズ)とオフセットに関するバグ修正を目的としています。主な変更点は、関数引数や戻り値の内部表現として使われる「関数構造体(function structs)」を通常の構造体と区別し、それらの幅が不適切に計算されたり、孤立して検査されたりしないようにすることです。これにより、コンパイラの安定性と生成されるコードの正確性が向上します。
コミット
width fixes.
* check for uncomputed struct offsets
* distinguish function structs from ordinary structs
* make sure function structs are not examined in isolation
R=ken
OCL=19005
CL=19005
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/792145723e5e9921c336d23504b1110c2d0c9b7d
元コミット内容
width fixes.
* check for uncomputed struct offsets
* distinguish function structs from ordinary structs
* make sure function structs are not examined in isolation
R=ken
OCL=19005
CL=19005
変更の背景
Goコンパイラは、プログラム内の様々なデータ型(構造体、配列など)がメモリ上でどのように配置されるかを決定する必要があります。これには、各型の「幅」(メモリ上のサイズ)と、構造体内の各フィールドの「オフセット」(構造体の先頭からの相対位置)の正確な計算が含まれます。これらの計算が不正確であると、コンパイラが誤ったメモリアドレスを生成し、実行時にプログラムがクラッシュしたり、データが破損したりする原因となります。
このコミットが行われた2008年当時、Go言語はまだ開発の初期段階にあり、コンパイラは急速に進化していました。特に、関数呼び出しのメカニズムや、引数・戻り値の内部的な扱い方に関する設計が固まりつつある時期でした。Goコンパイラは、関数の引数や戻り値を内部的に「構造体」のような複合型として扱うことがあり、これらが通常のユーザー定義構造体と同じルールで幅計算されると問題が生じる可能性がありました。
具体的には、以下の問題が考えられます。
- 未計算のオフセットの使用: 構造体のオフセットがまだ計算されていない状態で、その値を使用しようとすると、不正なメモリ参照が発生する。
- 関数構造体の特殊性: 関数引数や戻り値の内部表現としての構造体は、通常のデータ構造体とは異なるライフサイクルや使用コンテキストを持つため、通常の構造体と同じ幅計算ロジックを適用すると不整合が生じる。
- 孤立した検査: 関数に関連する構造体が、関数全体のコンテキストから切り離されて単独で幅計算されると、誤った結果を招く。これらの構造体は、関数呼び出し規約やスタックフレームのレイアウトと密接に関連しているため、そのコンテキスト内で処理される必要がある。
これらの問題を解決し、コンパイラの堅牢性を高めるために、本コミットで「幅の修正」が導入されました。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラの内部概念と一般的なコンパイラの知識が必要です。
-
Goコンパイラの構造:
src/cmd/6g
: x86-64アーキテクチャ向けのGoコンパイラのバックエンド部分。型情報の処理、コード生成などを行う。src/cmd/gc
: Goコンパイラの共通フロントエンド部分。構文解析、型チェック、中間表現の生成などを行う。align.c
: メモリのアライメント(整列)と型の幅(サイズ)計算に関連する処理を行うファイル。gsubr.c
: Goのサブルーチン(補助関数)に関連する処理を行うファイル。dcl.c
: 宣言(declaration)の処理、特に型定義や変数の宣言に関連する処理を行うファイル。go.h
: コンパイラ全体で共有される型定義や定数を含むヘッダファイル。
-
型(
Type
)と幅(width
):- Goコンパイラ内部では、プログラム内のすべてのデータ型が
Type
構造体で表現されます。 Type
構造体には、その型がメモリ上で占めるサイズを示すwidth
フィールドがあります。このwidth
はバイト単位で表されます。BADWIDTH
: このコミットで導入された定数で、型の幅がまだ計算されていない、または無効であることを示す特殊な値です。
- Goコンパイラ内部では、プログラム内のすべてのデータ型が
-
構造体(
struct
)とオフセット(offset
):- 構造体は、異なる型のフィールドをまとめた複合型です。
- 構造体内の各フィールドは、構造体の先頭から特定の距離(オフセット)に配置されます。コンパイラは、このオフセット情報を使って、特定のフィールドにアクセスするためのメモリアドレスを計算します。
-
関数構造体(
function structs
):- Goコンパイラは、関数の引数リストや戻り値リストを、内部的に単一の「構造体」として扱うことがあります。これは、複数の引数や戻り値をまとめてメモリ上に配置し、効率的にアクセスするための一つの方法です。
- これらの「関数構造体」は、通常のユーザー定義の構造体とは異なり、コンパイラ内部の特殊な目的のために存在します。
-
fatal
関数: コンパイラが回復不能なエラーを検出した際に、プログラムの実行を停止させるために使用される関数です。通常、内部的な矛盾や予期せぬ状態が発生した場合に呼び出されます。
技術的詳細
このコミットの技術的な核心は、Goコンパイラが型の幅とオフセットを計算する際のロジックを改善し、特に「関数構造体」の特殊な性質を考慮に入れることです。
-
BADWIDTH
の導入:src/cmd/gc/go.h
にBADWIDTH = -1000000000
という新しい定数が追加されました。これは、型のwidth
フィールドがまだ計算されていない、または無効な状態であることを明示的に示すためのマーカーとして機能します。これにより、未計算の幅が誤って使用されることを防ぎます。
-
Type
構造体へのfunarg
フィールドの追加:src/cmd/gc/go.h
のType
構造体にuchar funarg;
という新しいフィールドが追加されました。このフィールドは、そのType
インスタンスが関数引数または戻り値の内部表現として使用される「関数構造体」であるかどうかを示すフラグ(1バイトの符号なし文字)です。これにより、コンパイラは通常の構造体と関数構造体を明確に区別できるようになります。
-
dostruct
関数の変更:src/cmd/gc/dcl.c
のdostruct
関数は、構造体やインターフェース、引数リストなどの型を構築する役割を担っています。- この関数に
funarg
というローカル変数が導入され、引数et
(element type)がTFUNC
(関数型)の場合にfunarg
が1
に設定されます。これは、関数引数リストや戻り値リストがdostruct
によって処理される際に、それが関数に関連する構造体であることを識別するためのものです。 - 構築された
Type
インスタンスのt->funarg
フィールドにこのfunarg
の値が設定されます。 - 最も重要な変更は、
checkwidth(t)
の呼び出しがif(!funarg)
という条件で囲まれたことです。これは、「関数構造体」の場合にはcheckwidth
(型の幅を計算する関数)を呼び出さないことを意味します。これにより、関数構造体が孤立して幅計算されることを防ぎ、その計算はより上位の、関数全体のコンテキストで行われるべきであることを強制します。
-
checkwidth
関数の変更:src/cmd/gc/dcl.c
のcheckwidth
関数は、型の幅を計算する主要な関数です。- この関数に、
t->funarg
が設定されている場合にfatal("checkwidth %T", t);
を呼び出すガードが追加されました。これは、dostruct
での変更と連携し、もし関数構造体が誤ってcheckwidth
に渡された場合、即座にコンパイラを停止させることで、不正な幅計算を防ぎます。
-
dowidth
関数の変更:src/cmd/6g/align.c
のdowidth
関数も型の幅を計算する関数の一つです。TSTRUCT
(構造体型)を処理するcase
ブロック内に、if(t->funarg) fatal("dowidth fn struct %T", t);
というチェックが追加されました。これもcheckwidth
と同様に、関数構造体がdowidth
に渡されることを防ぎ、コンパイラの堅牢性を高めます。
-
nodarg
関数の変更:src/cmd/6g/gsubr.c
のnodarg
関数は、ノード(抽象構文木の要素)から引数ノードを生成する際に使用されます。- この関数に
if(t->width == BADWIDTH) fatal("nodarg: offset not computed for %T", t);
というチェックが追加されました。これは、「未計算の構造体オフセットのチェック」というコミットメッセージの項目に直接対応しており、BADWIDTH
が設定されている型(つまり、まだ幅が計算されていない型)のオフセットを使用しようとした場合に、コンパイラを停止させます。
-
functype
関数の変更:src/cmd/gc/dcl.c
のfunctype
関数は、関数型を構築する際に使用されます。dostruct
の呼び出しにおいて、以前はTSTRUCT
を渡していた箇所がTFUNC
に変更されました。これは、関数引数や戻り値のリストを構築する際に、それらが関数に関連する特殊な構造体であることをdostruct
に明示的に伝えるための変更です。
これらの変更により、Goコンパイラは型の幅計算において、特に内部的な「関数構造体」の特殊性を正確に扱い、未計算のオフセットの使用や不適切な幅計算による潜在的なバグを防ぐことができるようになりました。
コアとなるコードの変更箇所
src/cmd/gc/go.h
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -41,6 +41,8 @@ enum
ASTRING,
APTR,
AINTER,
+
+ BADWIDTH = -1000000000
};
/*
@@ -126,6 +128,7 @@ struct Type
uchar printed;
uchar embedded; // TFIELD embedded type
uchar siggen;
+ uchar funarg;
// TFUNCT
uchar thistuple;
src/cmd/gc/dcl.c
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -161,9 +161,9 @@ functype(Node *this, Node *in, Node *out)\n \n \tt = typ(TFUNC);\n \n-\tt->type = dostruct(this, TSTRUCT);\n-\tt->type->down = dostruct(out, TSTRUCT);\n-\tt->type->down->down = dostruct(in, TSTRUCT);\n+\tt->type = dostruct(this, TFUNC);\n+\tt->type->down = dostruct(out, TFUNC);\n+\tt->type->down->down = dostruct(in, TFUNC);\n \n \tt->thistuple = listcount(this);\n \tt->outtuple = listcount(out);\
@@ -498,6 +498,7 @@ loop:\n \tf = typ(TFIELD);\n \tf->type = n->type;\n \tf->note = note;\
+\tf->width = BADWIDTH;\
\n \tif(n->left != N && n->left->op == ONAME) {\n \t\tf->nname = n->left;\
@@ -517,15 +518,23 @@ Type*\n dostruct(Node *n, int et)\n {\n \tType *t;\
+\tint funarg;\
\n \t/*\n \t * convert a parsed id/type list into\n \t * a type for struct/interface/arglist\n \t */\n \n+\tfunarg = 0;\
+\tif(et == TFUNC) {\n+\t\tfunarg = 1;\
+\t\tet = TSTRUCT;\
+\t}\n \tt = typ(et);\
+\tt->funarg = funarg;\
\tstotype(n, &t->type);\
-\tcheckwidth(t);\
+\tif(!funarg)\n+\t\tcheckwidth(t);\
\treturn t;\
}\n \n@@ -1130,6 +1139,11 @@ checkwidth(Type *t)\n {\n \tTypeList *l;\n \n+\t// function arg structs should not be checked\n+\t// outside of the enclosing function.\n+\tif(t->funarg)\n+\t\tfatal("checkwidth %T", t);\n+\
\tif(!defercalc) {\n \t\tdowidth(t);\n \t\treturn;\
src/cmd/6g/align.c
--- a/src/cmd/6g/align.c
+++ b/src/cmd/6g/align.c
@@ -154,6 +154,8 @@ dowidth(Type *t)\n \t\tbreak;\n \n \tcase TSTRUCT:\
+\t\tif(t->funarg)\
+\t\t\tfatal("dowidth fn struct %T", t);\
\t\tw = widstruct(t, 0, 1);\n \t\toffmod(t);\n \t\tbreak;\
src/cmd/6g/gsubr.c
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -243,6 +243,8 @@ nodarg(Type *t, int fp)\n \tn = nod(ONAME, N, N);\n \tn->type = t->type;\n \tn->sym = t->sym;\
+\tif(t->width == BADWIDTH)\
+\t\tfatal("nodarg: offset not computed for %T", t);\
\tn->xoffset = t->width;\
\tn->addable = 1;\
\
コアとなるコードの解説
src/cmd/gc/go.h
BADWIDTH
定数の追加:enum
ブロックにBADWIDTH = -1000000000
が追加されました。これは、型の幅がまだ計算されていない、または無効な状態であることを示すための特別な値です。これにより、コンパイラは未初期化の幅を誤って使用するのを防ぐことができます。Type
構造体へのfunarg
フィールドの追加:struct Type
にuchar funarg;
が追加されました。この1バイトのフラグは、そのType
インスタンスが関数引数または戻り値の内部表現として使われる「関数構造体」であるかどうかを識別するために使用されます。
src/cmd/gc/dcl.c
functype
関数の変更:- 関数型を構築する際に
dostruct
を呼び出す部分で、以前はTSTRUCT
を渡していた引数がTFUNC
に変更されました。これは、関数引数や戻り値のリストが、通常の構造体とは異なる特殊なコンテキストで処理されるべきであることをdostruct
関数に明示的に伝えるためのものです。
- 関数型を構築する際に
loop
ラベル内の変更(f->width = BADWIDTH;
):- 新しいフィールド(
f
)が作成される際に、そのwidth
がBADWIDTH
に初期化されるようになりました。これにより、すべての新しいフィールドは、明示的に幅が計算されるまで「未計算」の状態であることが保証され、未初期化の値が誤って使用されることを防ぎます。
- 新しいフィールド(
dostruct
関数の変更:funarg
というローカル変数が導入され、et
(要素型)がTFUNC
の場合にfunarg
が1
に設定されます。- 構築された
Type
インスタンスのt->funarg
フィールドにこのfunarg
の値が設定されます。 checkwidth(t)
の呼び出しがif(!funarg)
という条件で囲まれました。これは、もしt
が関数構造体(funarg
が1
)であれば、checkwidth
を呼び出さないことを意味します。これにより、関数構造体の幅計算が、関数全体のコンテキスト外で孤立して行われることを防ぎます。
checkwidth
関数の変更:- 関数の冒頭に
if(t->funarg) fatal("checkwidth %T", t);
というガードが追加されました。これは、もし関数構造体が誤ってcheckwidth
に渡された場合、コンパイラが即座にエラーを発生させて停止するようにするためのものです。これにより、関数構造体に対する不適切な幅計算の試みを早期に検出します。
- 関数の冒頭に
src/cmd/6g/align.c
dowidth
関数の変更:TSTRUCT
を処理するcase
ブロック内にif(t->funarg) fatal("dowidth fn struct %T", t);
というチェックが追加されました。これはcheckwidth
と同様に、関数構造体がdowidth
に渡されることを防ぎ、不適切な幅計算を防ぐためのものです。
src/cmd/6g/gsubr.c
nodarg
関数の変更:if(t->width == BADWIDTH) fatal("nodarg: offset not computed for %T", t);
というチェックが追加されました。これは、ノードの引数を処理する際に、その型の幅がまだ計算されていない(BADWIDTH
である)場合に、コンパイラを停止させるためのものです。これにより、「未計算の構造体オフセットのチェック」というコミットメッセージの項目が実装され、未初期化のオフセットが使用されることによる潜在的なバグを防ぎます。
これらの変更は、Goコンパイラの型システムとメモリレイアウト計算の堅牢性を大幅に向上させ、特にGo言語の関数呼び出し規約と密接に関連する内部的な型表現の正確性を保証する上で重要な役割を果たしています。
関連リンク
- Go言語のコンパイラに関する公式ドキュメントや設計資料(当時のものがあれば)
- Go言語の型システムに関する解説
- コンパイラのメモリレイアウト、アライメント、パディングに関する一般的な情報
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gc
とsrc/cmd/6g
ディレクトリ) - Go言語の初期のコミット履歴と関連する議論(GoのメーリングリストやIssueトラッカーなど)
- コンパイラ設計に関する一般的な教科書やオンラインリソース