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

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

このコミットは、Goコンパイラツールチェーンの一部である cmd/dist における goc2c 関数が生成するコードに修正を加えるものです。具体的には、Go関数のエントリポイントで出力変数をゼロ初期化することで、ガベージコレクタ(GC)が未初期化の値をポインタとして誤認識する問題を軽減し、それによって発生する「不正なポインタ」エラーメッセージの数を減らすことを目的としています。

コミット

cmd/dist: zero output variables on entry to goc2c functions

Zeroing the outputs makes sure that during function calls
in those functions we do not let the garbage collector
treat uninitialized values as pointers.

The garbage collector may still see uninitialized values
if a preemption occurs during the function prologue,
before the zeroing has had a chance to run.

This reduces the number of 'bad pointer' messages when
that runtime check is enabled, but it doesn't fix all of them,
so the check is still disabled.

It will also avoid leaks, although I doubt any of these were
particularly serious.

LGTM=iant, khr
R=iant, khr
CC=golang-codereviews
https://golang.org/cl/80850044

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

https://github.com/golang/go/commit/f94bff7935d031f3114980715381af226ae3ac75

元コミット内容

commit f94bff7935d031f3114980715381af226ae3ac75
Author: Russ Cox <rsc@golang.org>
Date:   Thu Mar 27 14:05:31 2014 -0400

    cmd/dist: zero output variables on entry to goc2c functions
    
    Zeroing the outputs makes sure that during function calls
    in those functions we do not let the garbage collector
    treat uninitialized values as pointers.
    
    The garbage collector may still see uninitialized values
    if a preemption occurs during the function prologue,
    before the zeroing has had a chance to run.
    
    This reduces the number of 'bad pointer' messages when
    that runtime check is enabled, but it doesn't fix all of them,
    so the check is still disabled.
    
    It will also avoid leaks, although I doubt any of these were
    particularly serious.
    
    LGTM=iant, khr
    R=iant, khr
    CC=golang-codereviews
    https://golang.org/cl/80850044

変更の背景

Goのガベージコレクタ(GC)は、プログラムの実行中にメモリをスキャンし、到達可能なオブジェクトを特定して、到達不能なオブジェクトが占めるメモリを解放します。このプロセスにおいて、GCはスタックやレジスタ上の値をポインタとして解釈し、それが指すメモリ領域を追跡します。

問題は、関数が呼び出された直後、特にその関数の出力変数(戻り値やoutパラメータ)がまだ初期化されていない状態でGCが実行された場合に発生します。この未初期化のメモリ領域には、以前使用されていたデータ(ガベージ)が残っている可能性があり、その中にたまたま有効なポインタのように見えるビットパターンが含まれていることがあります。GCがこれを本物のポインタと誤認識すると、存在しないメモリ領域を追跡しようとしたり、本来解放されるべきメモリを解放しなかったり(メモリリーク)、あるいはクラッシュを引き起こしたりする可能性があります。

このコミットの背景には、Goランタイムのデバッグ時に「bad pointer」(不正なポインタ)というメッセージが頻繁に報告されていたという問題があります。これは、GCが未初期化のメモリをスキャンした際に、ポインタではない値をポインタとして誤って解釈したことを示唆していました。このコミットは、このような誤認識を減らし、ランタイムの安定性とGCの正確性を向上させることを目的としています。

前提知識の解説

  • Goのガベージコレクタ (GC): Goはトレース型GCを採用しており、プログラムが使用しているメモリ(到達可能なオブジェクト)を自動的に識別し、不要になったメモリを解放します。GCは、ルートセット(グローバル変数、スタック、レジスタなど)から開始して、ポインタをたどって到達可能なすべてのオブジェクトをマークします。
  • スタックとレジスタ: 関数が呼び出されると、そのローカル変数や引数、戻り値などはスタック上に割り当てられるか、CPUのレジスタに格納されます。これらの領域は、関数が開始された時点では必ずしもゼロ初期化されているわけではなく、以前の関数の実行によって残された「ガベージ」データが含まれていることがあります。
  • 未初期化の値とポインタ: Goでは、変数を宣言するとデフォルト値(数値型は0、ブール型はfalse、ポインタ型はnilなど)で初期化されます。しかし、コンパイラが生成する低レベルのコードでは、最適化のために必ずしもすべてのメモリが即座にゼロ初期化されるわけではありません。特に、関数のプロローグ(関数本体が実行される前の準備段階)では、出力変数のためのスタック領域が確保されるものの、その内容がまだクリアされていないことがあります。
  • cmd/dist: Goのビルドシステムの一部であり、Goのソースコードから実行可能ファイルを生成する過程で様々なツールを調整します。
  • goc2c.c: cmd/dist ディレクトリ内のC言語ソースファイルで、Goのコンパイラ(特に6g8gなど、当時のアーキテクチャ固有のコンパイラ)が使用するCコードを生成する役割を担っています。これは、Goランタイムの一部がC言語で書かれているため、GoのコンパイラがCコードを生成する際に利用されます。このファイルは、Goの関数ヘッダ(プロローグ)をC言語で記述する際に、戻り値の型に応じた処理を生成します。
  • 関数プロローグ: 関数が実際に処理を開始する前に実行される一連の命令。これには、スタックフレームのセットアップ、レジスタの保存、ローカル変数のためのメモリ割り当てなどが含まれます。
  • プリエンプション (Preemption): Goランタイムのスケジューラが、実行中のゴルーチンを一時停止させ、別のゴルーチンにCPUを割り当てること。これは、GCが実行されるタイミングや、長時間実行されるゴルーチンが他のゴルーチンをブロックするのを防ぐために行われます。関数プロローグ中にプリエンプションが発生すると、出力変数がゼロ初期化される前にGCが実行される可能性があります。

技術的詳細

このコミットの核心は、src/cmd/dist/goc2c.c ファイル内の write_6g_func_header 関数に、Go関数の出力変数を明示的にゼロ初期化するコードを追加することです。

Goのガベージコレクタは、スタックをスキャンしてポインタを探します。関数が呼び出され、その戻り値やoutパラメータがスタック上に割り当てられた直後、これらのメモリ領域には不定な値(以前の関数の実行によって残されたデータ)が含まれている可能性があります。もしこれらの不定な値がたまたま有効なポインタのように見える場合、GCはそれをポインタとして追跡しようとします。これにより、GCが不正なメモリにアクセスしたり、本来到達不能であるべきオブジェクトを到達可能と誤認識してメモリリークを引き起こしたりする問題が発生します。

このコミットは、write_6g_func_header 関数が生成するCコードに、関数のエントリポイントで出力変数をゼロ初期化するループを追加します。これにより、関数本体が実行される前に、出力変数のメモリ領域が既知のゼロ値で埋められます。

具体的には、以下のGoの組み込み型に対して、その構造体フィールドを個別にゼロ初期化します。

  • Slice: array, len, cap フィールドをゼロに設定。
  • String: str, len フィールドをゼロに設定。
  • Eface (empty interface): type, data フィールドをゼロに設定。
  • Iface (interface with methods): tab, data フィールドをゼロに設定。
  • Complex128: real, imag フィールドをゼロに設定。
  • その他の型: 単純に変数全体をゼロに設定。

このゼロ初期化により、GCがスキャンする際に未初期化の「ガベージ」データがポインタとして誤認識される可能性が大幅に減少します。

ただし、コミットメッセージにも記載されているように、この変更はすべての「不正なポインタ」の問題を解決するわけではありません。特に、関数のプロローグが完了し、出力変数がゼロ初期化される前にプリエンプション(Goスケジューラによるゴルーチンの切り替え)が発生した場合、GCは依然として未初期化の値をスキャンする可能性があります。これは、ゼロ初期化のコード自体が実行される前にGCが介入する可能性があるためです。

それでも、この変更は「不正なポインタ」メッセージの数を減らし、GCの堅牢性を向上させます。また、未初期化の値がポインタとして誤認識されることによる潜在的なメモリリークも回避できます。コミットメッセージでは、これらのリークが「特に深刻なものではなかった」と述べられていますが、それでも改善は重要です。

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

変更は src/cmd/dist/goc2c.c ファイルの write_6g_func_header 関数内で行われています。

--- a/src/cmd/dist/goc2c.c
+++ b/src/cmd/dist/goc2c.c
@@ -524,6 +524,7 @@ write_6g_func_header(char *package, char *name, struct params *params,\n \t\t     int paramwid, struct params *rets)\n {\n \tint first, n;\n+\tstruct params *p;\n \n \tbwritef(output, \"void\\n\");\n \tif(!contains(name, \"·\"))\n@@ -546,6 +547,24 @@ write_6g_func_header(char *package, char *name, struct params *params,\n \n \twrite_params(rets, &first);\n \tbwritef(output, \")\\n{\\n\");\n+\t\n+\tfor (p = rets; p != nil; p = p->next) {\n+\t\tif(streq(p->name, \"...\"))\n+\t\t\tcontinue;\n+\t\tif(streq(p->type, \"Slice\"))\n+\t\t\tbwritef(output, \"\\t%s.array = 0;\\n\\t%s.len = 0;\\n\\t%s.cap = 0;\\n\", p->name, p->name, p->name);\n+\t\telse if(streq(p->type, \"String\"))\n+\t\t\tbwritef(output, \"\\t%s.str = 0;\\n\\t%s.len = 0;\\n\", p->name, p->name);\n+\t\telse if(streq(p->type, \"Eface\"))\n+\t\t\tbwritef(output, \"\\t%s.type = 0;\\n\\t%s.data = 0;\\n\", p->name, p->name);\n+\t\telse if(streq(p->type, \"Iface\"))\n+\t\t\tbwritef(output, \"\\t%s.tab = 0;\\n\\t%s.data = 0;\\n\", p->name, p->name);\n+\t\telse if(streq(p->type, \"Complex128\"))\n+\t\t\tbwritef(output, \"\\t%s.real = 0;\\n\\t%s.imag = 0;\\n\", p->name, p->name);\n+\t\telse\n+\t\t\tbwritef(output, \"\\t%s = 0;\\n\", p->name);\n+\t\tbwritef(output, \"\\tFLUSH(&%s);\\n\", p->name);\n+\t}\n }\n \n /* Write a 6g function trailer.  */\n```

## コアとなるコードの解説

追加されたコードは、`write_6g_func_header` 関数が生成するCコードの関数本体の開始部分に挿入されます。

1.  **`struct params *p;`**: 戻り値のパラメータをイテレートするためのポインタ `p` が宣言されています。
2.  **`for (p = rets; p != nil; p = p->next)`**: `rets` は関数の戻り値(出力変数)のリストを表す `struct params` 型のポインタです。このループは、すべての戻り値パラメータを順に処理します。
3.  **`if(streq(p->name, "...")) continue;`**: 可変引数リストを示す `...` はスキップされます。
4.  **型に応じたゼロ初期化**:
    *   `Slice` 型の場合: `array`, `len`, `cap` の各フィールドを `0` に設定するCコードを生成します。Goのスライスは内部的にポインタ(array)、長さ(len)、容量(cap)を持つ構造体です。
    *   `String` 型の場合: `str`, `len` の各フィールドを `0` に設定するCコードを生成します。Goの文字列は内部的にバイト配列へのポインタ(str)と長さ(len)を持つ構造体です。
    *   `Eface` (empty interface) 型の場合: `type`, `data` の各フィールドを `0` に設定するCコードを生成します。空インターフェースは、型情報とデータポインタのペアで構成されます。
    *   `Iface` (interface with methods) 型の場合: `tab`, `data` の各フィールドを `0` に設定するCコードを生成します。メソッドを持つインターフェースは、メソッドテーブルへのポインタ(tab)とデータポインタのペアで構成されます。
    *   `Complex128` 型の場合: `real`, `imag` の各フィールドを `0` に設定するCコードを生成します。複素数型は実部と虚部で構成されます。
    *   `else`: 上記以外の単純な型(整数、浮動小数点数、ポインタなど)の場合、変数全体を `0` に設定するCコードを生成します。
5.  **`bwritef(output, "\\tFLUSH(&%s);\\n", p->name);`**: 各変数のゼロ初期化後、`FLUSH` マクロが呼び出されます。この `FLUSH` は、コンパイラの最適化によってゼロ初期化が削除されないようにするためのものです。特に、メモリに書き込まれた値がすぐに読み取られない場合、コンパイラは「デッドストア」としてその書き込みを最適化で削除してしまう可能性があります。`FLUSH` は、この書き込みがGCによって「見える」ように、コンパイラに最適化を抑制させる役割を果たします。これは通常、揮発性(volatile)アクセスや、メモリバリアのような効果を持つ命令を生成することで実現されます。

この変更により、Go関数が呼び出された直後に、その戻り値が格納されるメモリ領域が確実にゼロ初期化されるようになります。これにより、GCが未初期化のメモリをスキャンする際に、誤ってポインタと解釈するリスクが大幅に低減されます。

## 関連リンク

*   Go Gerrit Changelist: [https://go.dev/cl/80850044](https://go.dev/cl/80850044)

## 参考にした情報源リンク

*   GitHub Commit: [https://github.com/golang/go/commit/f94bff7935d031f3114980715381af226ae3ac75](https://github.com/golang/go/commit/f94bff7935d031f3114980715381af226ae3ac75)
*   Go Gerrit Changelist: [https://go.dev/cl/80850044](https://go.dev/cl/80850044)