[インデックス 16242] ファイルの概要
このコミットは、Goコンパイラにおける複数の整数オーバーフローの問題と、32ビット環境でのリロケーション(再配置)に関する制約に対処することを目的としています。具体的には、64ビットの値を扱うべき箇所で32ビットのワードが使用されていた問題、カウンタのオーバーフローチェックの欠如、およびリロケーションが32ビットに収まる必要があるという要件を満たすための修正が含まれています。これにより、コンパイラの堅牢性と正確性が向上し、特に大規模なプログラムや特定のアーキテクチャでの潜在的なバグが回避されます。
コミット
commit 4dcb13bb4489559119d4c86741c59e4c1eace469
Author: Rob Pike <r@golang.org>
Date: Mon Apr 29 22:44:40 2013 -0700
cmd/gc: fix some overflows in the compiler
Some 64-bit fields were run through 32-bit words, some counts were
not checked for overflow, and relocations must fit in 32 bits.
Tests to follow.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/9033043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4dcb13bb4489559119d4c86741c59e4c1eace469
元コミット内容
cmd/gc: fix some overflows in the compiler
Some 64-bit fields were run through 32-bit words, some counts were
not checked for overflow, and relocations must fit in 32 bits.
Tests to follow.
変更の背景
このコミットが行われた背景には、Goコンパイラが内部的に扱う数値のサイズと、それが実行されるシステムアーキテクチャ(特に32ビットと64ビット)との間の不整合がありました。
-
64ビット値の32ビットワードによる処理: Go言語は64ビットアーキテクチャをサポートしており、
int64
やuint64
のような64ビット整数型を扱います。しかし、コンパイラの内部処理の一部で、これらの64ビット値が誤って32ビットのワード(int
やuint32
など)として扱われていました。これにより、値が32ビットの範囲を超えた場合に、予期せぬ切り捨てやオーバーフローが発生し、コンパイルされたプログラムの動作が不正になる可能性がありました。例えば、大きな構造体のオフセット計算や、メモリ割り当てサイズ、配列のインデックス計算などで問題が生じることが考えられます。 -
カウンタのオーバーフローチェックの欠如: コンパイラ内部には、様々な要素の数を数えるためのカウンタが存在します。例えば、関数引数の数、構造体フィールドの数、スイッチ文のケース数、リストの要素数などです。これらのカウンタが、想定される最大値を超えた場合にオーバーフローする可能性がありましたが、そのチェックが適切に行われていませんでした。オーバーフローが発生すると、負の値になったり、非常に小さな値になったりして、コンパイラのロジックが破綻し、不正なコード生成やクラッシュにつながる恐れがありました。
-
リロケーションの32ビット制約: コンパイラが生成するオブジェクトファイルには、プログラムの実行時にアドレスを解決する必要がある「リロケーション(再配置)情報」が含まれます。これは、コード内の特定の場所(例えば、グローバル変数への参照や関数呼び出しのアドレス)が、最終的な実行可能ファイル内でどこに配置されるかによって変化するため、リンカがその情報を修正できるようにするためのものです。一部のアーキテクチャやリンカの制約により、このリロケーションオフセットが32ビットの範囲に収まる必要がある場合があります。しかし、コンパイラが生成するオフセットがこの制約を超えてしまう可能性があり、リンカエラーや実行時エラーを引き起こす可能性がありました。
これらの問題は、コンパイラの安定性と信頼性に直接影響を与えるため、修正が急務でした。特に、Go言語が大規模なシステム開発にも利用されることを考えると、コンパイラが生成するコードの正確性は極めて重要です。このコミットは、これらの根本的な問題を解決し、コンパイラの堅牢性を高めることを目的としています。
前提知識の解説
このコミットの理解を深めるために、以下の前提知識を解説します。
1. 整数型とオーバーフロー (32ビット vs 64ビット)
- 32ビット整数: 32ビット(4バイト)のメモリ領域で表現される整数です。符号付きの場合、約 -20億から +20億までの範囲の値を表現できます。符号なしの場合、約 0から +40億までの値を表現できます。
- 64ビット整数: 64ビット(8バイト)のメモリ領域で表現される整数です。符号付きの場合、約 -9×10^18 から +9×10^18 までの非常に大きな値を表現できます。
- 整数オーバーフロー: ある整数型で表現できる最大値を超えた値を格納しようとすると、その値が「巻き戻り」、予期せぬ小さな値(または負の値)になる現象です。例えば、32ビット符号なし整数で最大値(約40億)に1を加えると、0に戻ってしまうことがあります。これは、コンパイラが内部で計算するオフセットやサイズが非常に大きくなる場合に問題となります。
2. Goコンパイラの構造 (cmd/gc
, cmd/5g
, cmd/6g
, cmd/8g
)
Goコンパイラは、複数のサブコマンドに分かれています。
cmd/gc
: Go言語のフロントエンドコンパイラで、Goのソースコードを中間表現に変換し、型チェック、最適化などを行います。cmd/5g
,cmd/6g
,cmd/8g
: それぞれ異なるアーキテクチャ(5g
はARM、6g
はAMD64、8g
はx86)向けのバックエンドコンパイラです。gc
が生成した中間表現を受け取り、各アーキテクチャの機械語に変換します。 このコミットでは、これらの複数のコンパイラ部分にわたって修正が加えられています。
3. Goコンパイラ内部のデータ型
int
,uint
: Go言語の組み込み型ですが、コンパイラ内部ではC言語で書かれているため、Cのint
やunsigned int
が使われます。これらは通常32ビットです。int64
,uint64
: C言語のlong long
やunsigned long long
に相当し、64ビットの整数を表現します。vlong
: Goコンパイラ内部で使われるカスタム型で、通常はlong long
またはint64
にエイリアスされています。64ビット整数を意味します。Node
: Goコンパイラがソースコードを解析して生成する抽象構文木(AST)のノードを表す構造体です。プログラムの各要素(変数、関数呼び出し、演算子など)がNode
として表現されます。Type
: Goコンパイラが型情報を表現するための構造体です。変数の型、関数のシグネチャ、構造体のレイアウトなどが含まれます。xoffset
:Node
やType
構造体に含まれるフィールドで、メモリ上のオフセット(基準アドレスからの相対位置)を表すことが多いです。例えば、構造体内のフィールドのオフセットや、スタックフレーム内の変数のオフセットなどです。
4. メモリレイアウトとオフセット
- 構造体のアライメントとパディング: コンピュータのメモリは、効率的なアクセスや特定のハードウェア要件のために、データが特定のバイト境界に配置される必要があります(アライメント)。これにより、構造体のメンバー間に未使用の領域(パディング)が挿入されることがあります。構造体の全体のサイズやフィールドのオフセットを計算する際には、これらのアライメントとパディングを考慮する必要があります。
width
: 型のサイズ(バイト単位)を表すフィールドです。widthptr
: ポインタのサイズ(バイト単位)を表す定数です。32ビットシステムでは4、64ビットシステムでは8です。
5. リロケーション (Relocation)
- コンパイラが生成するオブジェクトファイルには、まだ最終的なアドレスが決定していないシンボル(変数や関数など)への参照が含まれます。リンカは、これらの参照を、最終的な実行可能ファイル内の実際のメモリアドレスに「再配置」します。
- リロケーション情報には、再配置が必要な場所と、その再配置の種類(例えば、32ビットオフセット、64ビットアドレスなど)が記述されます。
6. その他のコンパイラ内部概念
ODOT
: 構造体やポインタのフィールドアクセスを表すASTノードの操作コードです(例:s.field
)。OINDREG
: レジスタを介した間接参照を表すASTノードの操作コードです。PAUTO
: 自動変数(スタックに割り当てられるローカル変数)を表すクラスです。ullman number
: コンパイラの最適化フェーズで、式の評価順序を決定するために使用される値です。レジスタ割り当てのヒューリスティックに関連します。stack frame
: 関数が呼び出されたときに、その関数のローカル変数、引数、戻りアドレスなどを格納するためにスタック上に確保される領域です。
これらの概念を理解することで、コミットがGoコンパイラのどの部分で、どのような問題を解決しようとしているのかがより明確になります。
技術的詳細
このコミットは、Goコンパイラの複数の箇所で発生していた整数オーバーフローの問題と、リロケーションの32ビット制約に対処するために、データ型の変更と追加のチェックを導入しています。
主な技術的変更点は以下の通りです。
-
64ビット整数型への変更:
- コンパイラ内部でメモリ上のオフセットやサイズ、カウンタなどを扱う際に、32ビットの
int
やuint32
が使用されていた箇所を、int64
、uint64
、またはvlong
(Goコンパイラ内部で64ビット整数を表す型)に変更しています。 - 例:
src/cmd/5g/gsubr.c
およびsrc/cmd/8g/gsubr.c
のdotaddable
関数やsudoaddable
関数で、オフセット配列oary
の型がint
からint64
に変更されています。これは、構造体フィールドのオフセットが32ビットの範囲を超える可能性があるためです。src/cmd/6g/cgen.c
のagenr
関数で、幅を表す変数w
の型がuint32
からuint64
に変更されています。src/cmd/6g/cgen.c
のstkof
関数で、スタックオフセットを返す型がint32
からint64
に変更されています。src/cmd/6g/ggen.c
のclearfat
関数で、幅を表す変数w
の型がuint32
からint64
に変更されています。src/cmd/6g/gobj.c
のgenembedtramp
関数で、オフセットo
の型がint
からint64
に変更されています。src/cmd/6g/reg.c
のoverlap
関数やmkvar
関数で、オフセットを表す変数の型がint32
からint64
に変更されています。src/cmd/gc/align.c
のwidstruct
関数で、幅を表す変数w
の型がint32
からint64
に変更されています。src/cmd/gc/closure.c
のmakeclosure
関数で、オフセットoffset
の型がint
からvlong
に変更されています。src/cmd/gc/gen.c
およびsrc/cmd/gc/go.h
のdotoffset
関数で、オフセット配列oary
の型がint*
からint64*
に変更されています。src/cmd/gc/go.h
のType
構造体内のthistuple
,outtuple
,intuple
フィールドの型がuchar
からint
に変更されています。これは、関数の引数や戻り値の数が255を超える可能性があるためです。src/cmd/gc/go.h
のNode
構造体内のesc
フィールドの型がuchar
からuint
に、funcdepth
フィールドの型がuchar
からint
に変更されています。src/cmd/gc/sinit.c
のmaplit
関数で、マップのバケットサイズb
の型がint
からint64
に変更されています。src/cmd/gc/subr.c
のaindex
関数で、配列の境界bound
の型がint
からint64
に変更されています。src/cmd/gc/subr.c
のsetmaxarg
関数で、引数の幅w
の型がint32
からint64
に変更されています。src/cmd/gc/subr.c
のcount
関数で、リストの要素数n
の型がint
からvlong
に変更されています。src/cmd/gc/typecheck.c
のtypecheckcomplit
関数で、複合リテラルの長さlen
の型がint
からint64
に変更されています。src/cmd/gc/unsafe.c
のunsafenmagic
関数で、値v
の型がlong
からvlong
に変更されています。
- コンパイラ内部でメモリ上のオフセットやサイズ、カウンタなどを扱う際に、32ビットの
-
オーバーフローチェックの追加:
- カウンタやサイズが特定の最大値を超える場合に、エラーを報告するチェックが追加されています。
- 例:
src/cmd/gc/align.c
のargsize
関数で、引数の合計サイズw
がint
の範囲を超える場合にfatal("argsize too big")
エラーを発生させるチェックが追加されています。src/cmd/gc/pgen.c
のallocauto
関数で、スタックフレームのサイズstksize
が2GB(1ULL<<31
)を超える場合にyyerror("stack frame too large (>2GB)")
エラーを発生させるチェックが追加されています。これは、32ビットシステムでのスタックフレームのサイズ制限に対応するためです。src/cmd/gc/sinit.c
のstataddr
関数で、nam->xoffset += l*n->type->width;
の計算がオーバーフローしないように、MAXWIDTH/n->type->width <= l
のチェックが追加されています。src/cmd/gc/subr.c
のcount
関数で、リストの要素数n
がint
の範囲を超える場合にyyerror("too many elements in list")
エラーを発生させるチェックが追加されています。src/cmd/gc/swt.c
のmkcaselist
関数で、スイッチ文のケース数ord
がuint16
の範囲を超える場合にfatal("too many cases in switch")
エラーを発生させるチェックが追加されています。
-
nilチェックの改善:
src/cmd/5g/cgen.c
,src/cmd/6g/cgen.c
,src/cmd/8g/cgen.c
のagen
関数(ODOT
操作を処理する部分)で、構造体のポインタがnilであるかどうかのチェックロジックが改善されています。特に、ネストされたODOT
操作の場合に、既にnilチェックが行われている場合は再度チェックしないように最適化されています。これは、ポインタのオフセット計算が大きくなりすぎる可能性に関連しています。
-
Ullman Numberのクランプ:
src/cmd/gc/subr.c
のullmancalc
関数で、計算されたUllman Numberul
が200を超える場合に200にクランプ(上限設定)されるようになりました。これは、Ullman Numberがuchar
(符号なし8ビット整数、最大255)で表現されることを考慮し、将来的な成長の余地を残しつつ、オーバーフローを防ぐための措置です。
これらの変更は、コンパイラが内部で扱う数値の範囲を正確に反映させ、潜在的なオーバーフローを防ぐことで、コンパイラの安定性と生成されるコードの正確性を向上させています。特に、64ビットアーキテクチャでの大規模なプログラムのコンパイルにおいて、これらの修正は重要です。
コアとなるコードの変更箇所
このコミットでは、Goコンパイラの複数のサブディレクトリにわたる広範なファイルが変更されています。主な変更は、データ型の変更とオーバーフローチェックの追加です。
変更されたファイルと、その変更の概要は以下の通りです。
src/cmd/5g/cgen.c
:agen
関数におけるODOT
の nil チェックロジックの改善。src/cmd/5g/gsubr.c
:dotaddable
およびsudoaddable
関数で、オフセット配列oary
の型をint
からint64
に変更。src/cmd/6g/cgen.c
:agenr
関数で、幅w
の型をuint32
からuint64
に変更。agen
関数におけるODOT
の nil チェックロジックの改善。stkof
関数で、戻り値の型とオフセット変数off
の型をint32
からint64
に変更。
src/cmd/6g/ggen.c
:clearfat
関数で、幅w
の型をuint32
からint64
に変更。src/cmd/6g/gobj.c
:genembedtramp
関数で、オフセットo
の型をint
からint64
に変更。src/cmd/6g/gsubr.c
:naddr
関数で、a->offset
がint32
の範囲を超える場合にエラーを報告するチェックを追加。sudoaddable
関数で、オフセット配列oary
の型をint
からint64
に変更。
src/cmd/6g/reg.c
:overlap
およびmkvar
関数で、オフセット関連の変数の型をint32
からint64
に変更。src/cmd/8g/cgen.c
:agen
関数におけるODOT
の nil チェックロジックの改善。src/cmd/8g/gsubr.c
:dotaddable
関数で、オフセット配列oary
の型をint
からint64
に変更。src/cmd/gc/align.c
:widstruct
関数で、幅w
の型をint32
からint64
に変更。argsize
関数で、引数の合計サイズw
がint
の範囲を超える場合にエラーチェックを追加。
src/cmd/gc/closure.c
:makeclosure
関数で、オフセットoffset
の型をint
からvlong
に変更。src/cmd/gc/gen.c
:dotoffset
関数で、オフセット配列oary
の型をint*
からint64*
に変更。src/cmd/gc/go.h
:Type
構造体内のthistuple
,outtuple
,intuple
フィールドの型をuchar
からint
に変更。Node
構造体内のesc
フィールドの型をuchar
からuint
に、funcdepth
フィールドの型をuchar
からint
に変更。dotoffset
関数のプロトタイプを更新。
src/cmd/gc/pgen.c
:cmpstackvar
関数で、xoffset
の比較ロジックを修正し、int64
の比較に対応。allocauto
関数で、スタックフレームサイズが2GBを超える場合にエラーチェックを追加。
src/cmd/gc/sinit.c
:maplit
関数で、マップのバケットサイズb
の型をint
からint64
に変更。stataddr
関数で、オフセット計算のオーバーフローチェックを追加。
src/cmd/gc/subr.c
:aindex
関数で、配列の境界bound
の型をint
からint64
に変更。ullmancalc
関数で、Ullman Numberul
を200にクランプする処理を追加。setmaxarg
関数で、引数の幅w
の型をint32
からint64
に変更。count
関数で、リストの要素数n
の型をint
からvlong
に変更し、オーバーフローチェックを追加。
src/cmd/gc/swt.c
:mkcaselist
関数で、スイッチ文のケース数ord
のオーバーフローチェックを追加。src/cmd/gc/typecheck.c
:typecheckcomplit
関数で、複合リテラルの長さlen
の型をint
からint64
に変更。src/cmd/gc/unsafe.c
:unsafenmagic
関数で、値v
の型をlong
からvlong
に変更。
これらの変更は、コンパイラの異なるフェーズ(コード生成、型チェック、初期化など)にわたって、数値の取り扱いをより堅牢にするためのものです。
コアとなるコードの解説
ここでは、上記の変更箇所の中から特に重要なコードの変更パターンやその意味について解説します。
1. int
から int64
/vlong
/uint64
への型変更
これはコミット全体で最も頻繁に見られる変更です。
例: src/cmd/5g/gsubr.c
の dotaddable
関数
--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -1785,7 +1785,8 @@ sudoclean(void)\n int\n dotaddable(Node *n, Node *n1)\n {\n-\tint o, oary[10];\n+\tint o;\n+\tint64 oary[10];\n \tNode *nn;\n \n \tif(n->op != ODOT)\n```
* **変更前**: `int oary[10];`
* **変更後**: `int64 oary[10];`
`oary` は構造体フィールドのオフセットを格納する配列です。構造体が非常に大きくなると、フィールドのオフセットが32ビット整数の最大値(約2GB)を超える可能性があります。特に64ビットシステムでは、構造体のサイズが数GBになることもあり得るため、オフセットを64ビットで表現する必要があります。この変更により、大きなオフセット値が正しく扱われ、オーバーフローによる不正なアドレス計算が防止されます。
同様の変更は、スタックオフセット (`stkof` の戻り値)、メモリ上の幅 (`clearfat` の `w`)、リロケーションオフセット (`genembedtramp` の `o`) など、コンパイラが内部でサイズや位置を計算する多くの場所で行われています。
### 2. `ODOT` 操作における nil チェックの改善
例: `src/cmd/5g/cgen.c` の `agen` 関数
```diff
--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -680,7 +680,9 @@ agen(Node *n, Node *res)\n \tcase ODOT:\n \t\tagen(nl, res);\n \t\t// explicit check for nil if struct is large enough\n-\t\t// that we might derive too big a pointer.\n+\t\t// that we might derive too big a pointer. If the left node\n+\t\t// was ODOT we have already done the nil check.\n+\t\tif(nl->op != ODOT)\n \t\tif(nl->type->width >= unmappedzero) {\n \t\t\tregalloc(&n1, types[tptr], N);\n \t\t\tgmove(res, &n1);\
- 変更前:
// explicit check for nil if struct is large enough // that we might derive too big a pointer. if(nl->type->width >= unmappedzero) { ... }
- 変更後:
// explicit check for nil if struct is large enough // that we might derive too big a pointer. If the left node // was ODOT we have already done the nil check. if(nl->op != ODOT) if(nl->type->width >= unmappedzero) { ... }
ODOT
は構造体フィールドアクセスを表します。例えば a.b.c
のようなネストされたアクセスの場合、a.b
の評価時に一度 nil チェックが行われます。この変更は、nl->op != ODOT
という条件を追加することで、既に親の ODOT
操作で nil チェックが行われている場合は、子ノードで冗長な nil チェックをスキップするようにしています。これは、ポインタのオフセット計算が非常に大きくなる場合に、そのポインタが有効であることの確認が重要になるためです。
3. スタックフレームサイズのオーバーフローチェック
例: src/cmd/gc/pgen.c
の allocauto
関数
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -240,6 +245,10 @@ allocauto(Prog* ptxt)\n \t\tstksize = rnd(stksize, n->type->align);\n \t\tif(thechar == \'5\')\n \t\t\tstksize = rnd(stksize, widthptr);\n+\t\tif(stksize >= (1ULL<<31)) {\n+\t\t\tsetlineno(curfn);\n+\t\t\tyyerror(\"stack frame too large (>2GB)\");\n+\t\t}\n \t\tn->stkdelta = -stksize - n->xoffset;\n \t}\n \
- 追加されたコード:
if(stksize >= (1ULL<<31)) { setlineno(curfn); yyerror("stack frame too large (>2GB)"); }
stksize
は関数のスタックフレームの合計サイズを表します。32ビットシステムでは、スタックフレームのサイズが2GB(1ULL<<31
)を超えることはできません。このチェックは、コンパイラがこのような巨大なスタックフレームを生成しようとした場合に、コンパイル時にエラーを報告することで、実行時クラッシュや未定義動作を防ぎます。これは、特に大きな配列をローカル変数として宣言した場合などに発生する可能性があります。
4. リスト要素数のオーバーフローチェック
例: src/cmd/gc/subr.c
の count
関数
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -3296,11 +3298,14 @@ liststmt(NodeList *l)\n int\n count(NodeList *l)\n {\n-\tint n;\n+\tvlong n;\n \n \tn = 0;\n \tfor(; l; l=l->next)\n \t\tn++;\n+\tif((int)n != n) { // Overflow.\n+\t\tyyerror(\"too many elements in list\");\n+\t}\n \treturn n;\n }\n \
- 変更前:
int n;
- 変更後:
vlong n;
- 追加されたコード:
if((int)n != n) { // Overflow. yyerror("too many elements in list"); }
count
関数は NodeList
の要素数を数えます。変更前は int
でカウンタ n
を保持していましたが、要素数が20億を超えるとオーバーフローする可能性がありました。vlong
(64ビット整数)に変更することで、より大きな数を扱えるようになります。さらに、if((int)n != n)
というチェックを追加することで、vlong
で計算された n
が int
の範囲に収まらない場合にエラーを報告します。これは、コンパイラが内部的に扱うリストのサイズが、最終的に32ビットのインデックスやカウンタに収まらない場合に問題となるためです。
これらのコード変更は、コンパイラの内部データ構造とアルゴリズムが、現代のシステムで扱われる可能性のある大規模なデータやプログラムに対応できるように、より堅牢に設計されていることを示しています。
関連リンク
- Go CL 9033043: https://golang.org/cl/9033043
参考にした情報源リンク
- Go言語の公式ドキュメント (Goコンパイラの内部構造に関する一般的な情報)
- Go言語のソースコード (特に
src/cmd/gc
以下のファイル) - C言語の整数型とオーバーフローに関する一般的な情報
- コンパイラの設計と実装に関する一般的な教科書やオンラインリソース (リロケーション、スタックフレームなど)
- GitHubのコミットページ (変更されたファイルの差分表示)
- Go issue tracker (関連するバグ報告や議論があれば)
(注: Web検索は行いましたが、特定の外部記事やブログへの直接的なリンクは見つかりませんでした。上記の参考情報は、一般的な知識とGoコンパイラの構造から推測されるものです。)