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

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

このコミットは、Goコンパイラのバックエンドの一部であるsrc/cmd/6g/cgen.csrc/cmd/6g/gsubr.cの2つのファイルに対する変更を含んでいます。

  • src/cmd/6g/cgen.c: Go言語のソースコードから中間表現(AST)を生成し、それをターゲットアーキテクチャ(この場合は64ビットシステム)の機械語に変換するコード生成(Code Generation)に関連するファイルです。レジスタ割り当てや命令の生成といった低レベルな処理を扱います。
  • src/cmd/6g/gsubr.c: Goコンパイラのバックエンドにおける汎用的なサブルーチン(General Subroutines)を含むファイルです。アドレスの操作や命令の構築など、コード生成プロセスで共通して使用されるユーティリティ関数が定義されています。

コミット

commit 7b6bdfb7350e3148e8ea931a7e267f4bc804000a
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jan 30 15:11:46 2009 -0800

    two uses of uninitialized memory,
    picked up by valgrind.
    fixes test/escape.go on linux.
    
    R=ken
    OCL=23971
    CL=23971

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

https://github.com/golang/go/commit/7b6bdfb7350e3148e8ea931a7e267f4bc804000a

元コミット内容

diff --git a/src/cmd/6g/cgen.c b/src/cmd/6g/cgen.c
index ba1427d014..4a7404e8ca 100644
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -118,7 +118,7 @@ cgen(Node *n, Node *res)
 		\tp1 = gins(a, N, res);
 		\tp1->from = addr;
 		} else {
-\t\t\tregalloc(&n2, n->type, &n1);\n+\t\t\tregalloc(&n2, n->type, N);\n \t\t\tp1 = gins(a, N, &n2);\n \t\t\tp1->from = addr;\n \t\t\tgins(a, &n2, res);\ndiff --git a/src/cmd/6g/gsubr.c b/src/cmd/6g/gsubr.c
index 0f71c104c8..67def99527 100644
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -1006,7 +1006,8 @@ gins(int as, Node *f, Node *t)\n void\n naddr(Node *n, Addr *a)\n {\n-\n+\ta->scale = 0;\n+\ta->index = D_NONE;\n \ta->type = D_NONE;\n \tif(n == N)\n \t\treturn;\n```

## 変更の背景

このコミットは、Goコンパイラ(具体的には`6g`、64ビットアーキテクチャ向けのコンパイラ)における未初期化メモリの使用という深刻なバグを修正するものです。この問題は、メモリデバッグツールであるValgrindによって検出されました。未初期化メモリの使用は、プログラムの動作が予測不能になる「未定義動作(Undefined Behavior)」を引き起こす可能性があり、クラッシュ、誤った計算結果、セキュリティ脆弱性など、様々な問題の原因となります。

特に、`test/escape.go`というテストケースがLinux環境で失敗していたことが、このバグの具体的な影響として挙げられています。これは、コンパイラが生成するコードが、特定の条件下で正しくないメモリ操作を行っていたことを示唆しています。

## 前提知識の解説

### 未初期化メモリ (Uninitialized Memory)

未初期化メモリとは、プログラムがメモリ領域を確保したものの、その領域に明示的に値を書き込む前に読み出そうとすることによって発生する状態を指します。CやC++のような言語では、ローカル変数や動的に確保されたメモリは、初期化されない限り、その時点でのメモリの内容(以前にそのメモリを使用していたプログラムの残骸など)を保持しています。これを「ガベージ値」と呼びます。

未初期化メモリを読み出すと、プログラムは予測不能な動作(未定義動作)を引き起こします。これは、以下のような問題につながる可能性があります。

*   **クラッシュ**: 不正なアドレスへのアクセスや、無効な値を使った演算が原因でプログラムが異常終了する。
*   **誤った結果**: ガベージ値が計算に使われ、論理的に誤った結果が生成される。
*   **セキュリティ脆弱性**: 以前のデータがメモリに残っている場合、それが機密情報であれば、攻撃者によって読み取られる可能性がある。

### 未定義動作 (Undefined Behavior)

未定義動作は、CやC++などの言語仕様において、特定の操作の結果が規定されていない状態を指します。コンパイラは未定義動作を含むコードに対して、どのようなコードを生成してもよいとされています。そのため、同じコードでもコンパイラのバージョン、最適化レベル、実行環境などによって動作が変わる可能性があり、デバッグが非常に困難になります。未初期化メモリの読み出しは、典型的な未定義動作の一つです。

### Valgrind

Valgrindは、主にLinux上で動作するオープンソースのインストゥルメンテーションフレームワークです。プログラムの実行時に、メモリ管理やスレッドのバグを検出するために使用されます。Valgrindは、プログラムのバイナリコードを動的に書き換え(インストゥルメント)、メモリの読み書き、ヒープ操作、システムコールなどを監視します。

Valgrindの最も有名なツールはMemcheckで、以下のようなメモリ関連のバグを検出できます。

*   未初期化メモリの使用
*   ヒープオーバーフロー/アンダーフロー
*   解放済みメモリへのアクセス (use-after-free)
*   二重解放 (double-free)
*   メモリリーク

Valgrindは、開発者がメモリ関連の深刻なバグを特定し、修正する上で非常に強力なツールです。このコミットの背景にあるように、コンパイラのような低レベルなソフトウェアの開発において、Valgrindは品質保証に不可欠な役割を果たします。

### Goコンパイラ (6g)

Go言語の初期のコンパイラは、`gc`(Go Compiler)ツールチェーンの一部として提供されていました。`6g`は、その中でもAMD64(x86-64)アーキテクチャ向けのコンパイラを指します。Goコンパイラは、Goのソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語にコンパイルします。このプロセスには、構文解析、型チェック、最適化、コード生成、レジスタ割り当てなどが含まれます。

`cgen.c`や`gsubr.c`のようなファイルは、コンパイラのバックエンド、特にコード生成とレジスタ割り当てのフェーズを担当していました。これらのファイルはC言語で書かれており、Go言語自体がまだ成熟していなかった初期のGoコンパイラの開発における一般的なプラクティスでした。

## 技術的詳細

このコミットは、`regalloc`関数と`naddr`関数における未初期化メモリの使用を修正しています。

### `src/cmd/6g/cgen.c`の変更

`cgen`関数は、Goの抽象構文木(AST)ノードを処理し、対応する機械語命令を生成する役割を担っています。この関数内で、`regalloc`というレジスタ割り当てを行う関数が呼び出されています。

変更前のコード:
```c
regalloc(&n2, n->type, &n1);

変更後のコード:

regalloc(&n2, n->type, N);

regalloc関数の3番目の引数は、レジスタ割り当てのヒントとして使用されるNode型へのポインタです。変更前は&n1が渡されていましたが、n1regallocが呼び出される時点で初期化されていない可能性がありました。n1が未初期化の場合、そのアドレスをregallocに渡すことは未定義動作を引き起こす可能性があります。

修正では、この引数をN(GoコンパイラにおけるNULLポインタに相当するマクロ)に変更しています。これにより、regalloc関数はヒントとして未初期化メモリを参照する代わりに、NULLポインタを受け取るようになります。これは、ヒントが不要な場合や、安全にヒントを提供できない場合に推奨されるプラクティスです。この変更により、regallocが未初期化のn1の値を読み取ろうとすることがなくなり、未定義動作が回避されます。

src/cmd/6g/gsubr.cの変更

naddr関数は、Node構造体からAddr構造体(アドレスを表現する構造体)を構築する役割を担っています。

変更前のコード:

void
naddr(Node *n, Addr *a)
{

    a->type = D_NONE;
    if(n == N)
        return;
    // ...
}

変更後のコード:

void
naddr(Node *n, Addr *a)
{
    a->scale = 0;
    a->index = D_NONE;
    a->type = D_NONE;
    if(n == N)
        return;
    // ...
}

Addr構造体には、typescaleindexなどのフィールドが含まれています。変更前はa->typeのみが初期化されており、a->scalea->indexは初期化されていませんでした。naddr関数が呼び出された際に、これらのフィールドが後続の処理で読み出されると、未初期化メモリの読み出しが発生し、未定義動作につながる可能性がありました。

修正では、a->scale0に、a->indexD_NONEに明示的に初期化しています。D_NONEは、Goコンパイラでアドレスのインデックスレジスタがないことを示す定数です。これにより、Addr構造体のすべての関連フィールドが、関数が終了する前に既知の安全な値で初期化されることが保証されます。

これらの修正は、Valgrindのようなツールが検出する典型的な未初期化メモリのバグパターンに合致しており、コンパイラの堅牢性と信頼性を向上させる上で非常に重要です。

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

src/cmd/6g/cgen.c

--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -118,7 +118,7 @@ cgen(Node *n, Node *res)
 		\tp1 = gins(a, N, res);
 		\tp1->from = addr;
 		} else {
-\t\t\tregalloc(&n2, n->type, &n1);\n+\t\t\tregalloc(&n2, n->type, N);\n \t\t\tp1 = gins(a, N, &n2);\n \t\t\tp1->from = addr;\n \t\t\tgins(a, &n2, res);\

src/cmd/6g/gsubr.c

--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -1006,7 +1006,8 @@ gins(int as, Node *f, Node *t)\n void\n naddr(Node *n, Addr *a)\n {\n-\n+\ta->scale = 0;\n+\ta->index = D_NONE;\n \ta->type = D_NONE;\n \tif(n == N)\n \t\treturn;\

コアとなるコードの解説

src/cmd/6g/cgen.cの変更点

  • 変更前: regalloc(&n2, n->type, &n1);

    • regalloc関数は、レジスタを割り当てるための関数です。
    • 3番目の引数&n1は、レジスタ割り当てのヒントとして使用されるNode型へのポインタです。
    • 問題は、このn1変数がregallocが呼び出される時点で初期化されていない可能性があったことです。未初期化のポインタを渡すと、regalloc関数内でそのポインタが参照するメモリの内容が読み取られた場合に、未定義動作が発生します。
  • 変更後: regalloc(&n2, n->type, N);

    • 3番目の引数をN(NULLポインタ)に変更しました。
    • これにより、regalloc関数は有効なヒントを受け取らないことを明示的に示します。
    • この修正により、未初期化のn1が参照されることがなくなり、未定義動作が回避されます。これは、ヒントが不要な場合や、安全にヒントを提供できない場合に、NULLポインタを渡すという安全なプラクティスに従ったものです。

src/cmd/6g/gsubr.cの変更点

  • 変更前:

    void
    naddr(Node *n, Addr *a)
    {
    
        a->type = D_NONE;
        if(n == N)
            return;
        // ...
    }
    
    • naddr関数は、NodeからAddr構造体を構築します。
    • Addr構造体にはscaleindextypeなどのフィールドがあります。
    • 変更前はa->typeのみがD_NONEで初期化されており、a->scalea->indexは初期化されていませんでした。
    • これらの未初期化フィールドが後続のコードで読み取られると、未定義動作が発生する可能性があります。
  • 変更後:

    void
    naddr(Node *n, Addr *a)
    {
        a->scale = 0;
        a->index = D_NONE;
        a->type = D_NONE;
        if(n == N)
            return;
        // ...
    }
    
    • a->scale0に、a->indexD_NONEに明示的に初期化する行が追加されました。
    • D_NONEは、インデックスレジスタがないことを示す定数です。
    • この修正により、Addr構造体のすべての関連フィールドが、関数が終了する前に既知の安全な値で初期化されることが保証されます。これにより、未初期化メモリの読み出しによる未定義動作が防止されます。

これらの変更は、Goコンパイラの堅牢性を高め、Valgrindのようなメモリデバッグツールによって検出されるような、潜在的に危険な未初期化メモリの使用を排除することを目的としています。

関連リンク

参考にした情報源リンク