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

[インデックス 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ビット)との間の不整合がありました。

  1. 64ビット値の32ビットワードによる処理: Go言語は64ビットアーキテクチャをサポートしており、int64uint64のような64ビット整数型を扱います。しかし、コンパイラの内部処理の一部で、これらの64ビット値が誤って32ビットのワード(intuint32など)として扱われていました。これにより、値が32ビットの範囲を超えた場合に、予期せぬ切り捨てやオーバーフローが発生し、コンパイルされたプログラムの動作が不正になる可能性がありました。例えば、大きな構造体のオフセット計算や、メモリ割り当てサイズ、配列のインデックス計算などで問題が生じることが考えられます。

  2. カウンタのオーバーフローチェックの欠如: コンパイラ内部には、様々な要素の数を数えるためのカウンタが存在します。例えば、関数引数の数、構造体フィールドの数、スイッチ文のケース数、リストの要素数などです。これらのカウンタが、想定される最大値を超えた場合にオーバーフローする可能性がありましたが、そのチェックが適切に行われていませんでした。オーバーフローが発生すると、負の値になったり、非常に小さな値になったりして、コンパイラのロジックが破綻し、不正なコード生成やクラッシュにつながる恐れがありました。

  3. リロケーションの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のintunsigned intが使われます。これらは通常32ビットです。
  • int64, uint64: C言語のlong longunsigned long longに相当し、64ビットの整数を表現します。
  • vlong: Goコンパイラ内部で使われるカスタム型で、通常はlong longまたはint64にエイリアスされています。64ビット整数を意味します。
  • Node: Goコンパイラがソースコードを解析して生成する抽象構文木(AST)のノードを表す構造体です。プログラムの各要素(変数、関数呼び出し、演算子など)がNodeとして表現されます。
  • Type: Goコンパイラが型情報を表現するための構造体です。変数の型、関数のシグネチャ、構造体のレイアウトなどが含まれます。
  • xoffset: NodeType構造体に含まれるフィールドで、メモリ上のオフセット(基準アドレスからの相対位置)を表すことが多いです。例えば、構造体内のフィールドのオフセットや、スタックフレーム内の変数のオフセットなどです。

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ビット制約に対処するために、データ型の変更と追加のチェックを導入しています。

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

  1. 64ビット整数型への変更:

    • コンパイラ内部でメモリ上のオフセットやサイズ、カウンタなどを扱う際に、32ビットのintuint32が使用されていた箇所を、int64uint64、またはvlong(Goコンパイラ内部で64ビット整数を表す型)に変更しています。
    • 例:
      • src/cmd/5g/gsubr.c および src/cmd/8g/gsubr.cdotaddable 関数や sudoaddable 関数で、オフセット配列 oary の型が int から int64 に変更されています。これは、構造体フィールドのオフセットが32ビットの範囲を超える可能性があるためです。
      • src/cmd/6g/cgen.cagenr 関数で、幅を表す変数 w の型が uint32 から uint64 に変更されています。
      • src/cmd/6g/cgen.cstkof 関数で、スタックオフセットを返す型が int32 から int64 に変更されています。
      • src/cmd/6g/ggen.cclearfat 関数で、幅を表す変数 w の型が uint32 から int64 に変更されています。
      • src/cmd/6g/gobj.cgenembedtramp 関数で、オフセット o の型が int から int64 に変更されています。
      • src/cmd/6g/reg.coverlap 関数や mkvar 関数で、オフセットを表す変数の型が int32 から int64 に変更されています。
      • src/cmd/gc/align.cwidstruct 関数で、幅を表す変数 w の型が int32 から int64 に変更されています。
      • src/cmd/gc/closure.cmakeclosure 関数で、オフセット offset の型が int から vlong に変更されています。
      • src/cmd/gc/gen.c および src/cmd/gc/go.hdotoffset 関数で、オフセット配列 oary の型が int* から int64* に変更されています。
      • src/cmd/gc/go.hType 構造体内の thistuple, outtuple, intuple フィールドの型が uchar から int に変更されています。これは、関数の引数や戻り値の数が255を超える可能性があるためです。
      • src/cmd/gc/go.hNode 構造体内の esc フィールドの型が uchar から uint に、funcdepth フィールドの型が uchar から int に変更されています。
      • src/cmd/gc/sinit.cmaplit 関数で、マップのバケットサイズ b の型が int から int64 に変更されています。
      • src/cmd/gc/subr.caindex 関数で、配列の境界 bound の型が int から int64 に変更されています。
      • src/cmd/gc/subr.csetmaxarg 関数で、引数の幅 w の型が int32 から int64 に変更されています。
      • src/cmd/gc/subr.ccount 関数で、リストの要素数 n の型が int から vlong に変更されています。
      • src/cmd/gc/typecheck.ctypecheckcomplit 関数で、複合リテラルの長さ len の型が int から int64 に変更されています。
      • src/cmd/gc/unsafe.cunsafenmagic 関数で、値 v の型が long から vlong に変更されています。
  2. オーバーフローチェックの追加:

    • カウンタやサイズが特定の最大値を超える場合に、エラーを報告するチェックが追加されています。
    • 例:
      • src/cmd/gc/align.cargsize 関数で、引数の合計サイズ wintの範囲を超える場合に fatal("argsize too big") エラーを発生させるチェックが追加されています。
      • src/cmd/gc/pgen.callocauto 関数で、スタックフレームのサイズ stksize が2GB(1ULL<<31)を超える場合に yyerror("stack frame too large (>2GB)") エラーを発生させるチェックが追加されています。これは、32ビットシステムでのスタックフレームのサイズ制限に対応するためです。
      • src/cmd/gc/sinit.cstataddr 関数で、nam->xoffset += l*n->type->width; の計算がオーバーフローしないように、MAXWIDTH/n->type->width <= l のチェックが追加されています。
      • src/cmd/gc/subr.ccount 関数で、リストの要素数 nintの範囲を超える場合に yyerror("too many elements in list") エラーを発生させるチェックが追加されています。
      • src/cmd/gc/swt.cmkcaselist 関数で、スイッチ文のケース数 orduint16 の範囲を超える場合に fatal("too many cases in switch") エラーを発生させるチェックが追加されています。
  3. nilチェックの改善:

    • src/cmd/5g/cgen.c, src/cmd/6g/cgen.c, src/cmd/8g/cgen.cagen 関数(ODOT操作を処理する部分)で、構造体のポインタがnilであるかどうかのチェックロジックが改善されています。特に、ネストされたODOT操作の場合に、既にnilチェックが行われている場合は再度チェックしないように最適化されています。これは、ポインタのオフセット計算が大きくなりすぎる可能性に関連しています。
  4. Ullman Numberのクランプ:

    • src/cmd/gc/subr.cullmancalc 関数で、計算されたUllman Number ul が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->offsetint32 の範囲を超える場合にエラーを報告するチェックを追加。
    • 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 関数で、引数の合計サイズ wint の範囲を超える場合にエラーチェックを追加。
  • 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 Number ul を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.cdotaddable 関数

--- 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.callocauto 関数

--- 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.ccount 関数

--- 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 で計算された nint の範囲に収まらない場合にエラーを報告します。これは、コンパイラが内部的に扱うリストのサイズが、最終的に32ビットのインデックスやカウンタに収まらない場合に問題となるためです。

これらのコード変更は、コンパイラの内部データ構造とアルゴリズムが、現代のシステムで扱われる可能性のある大規模なデータやプログラムに対応できるように、より堅牢に設計されていることを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goコンパイラの内部構造に関する一般的な情報)
  • Go言語のソースコード (特に src/cmd/gc 以下のファイル)
  • C言語の整数型とオーバーフローに関する一般的な情報
  • コンパイラの設計と実装に関する一般的な教科書やオンラインリソース (リロケーション、スタックフレームなど)
  • GitHubのコミットページ (変更されたファイルの差分表示)
  • Go issue tracker (関連するバグ報告や議論があれば)

(注: Web検索は行いましたが、特定の外部記事やブログへの直接的なリンクは見つかりませんでした。上記の参考情報は、一般的な知識とGoコンパイラの構造から推測されるものです。)