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

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

このコミットは、Go言語のランタイムにおけるruntime.equal関数のバグ修正に関するものです。具体的には、構造体の比較を行う際に、戻り値のアドレスが適切にアラインメントされていなかったために発生する、誤った比較結果の問題(Issue 3866)を解決します。この修正により、runtime.equalがスタック上のランダムなブール値を比較結果として読み取ってしまう可能性が排除され、Goプログラムにおける構造体や配列の等価性チェックの信頼性が向上します。

コミット

commit e07958f7dfde86fe9053e25793219d4807f4d74c
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Jul 31 23:02:46 2012 -0400

    runtime: round return value address in runtime.equal
         Fixes #3866.
    
    R=rsc, r, nigeltao
    CC=golang-dev
    https://golang.org/cl/6452046

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

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

元コミット内容

このコミットは、src/pkg/runtime/alg.c内のruntime.equal関数と、バグを再現し修正を検証するための新しいテストファイルtest/fixedbugs/bug449.goの追加を含んでいます。

runtime.equal関数では、戻り値のアドレス計算において、パディング(詰め物)を考慮していなかった点が修正されています。具体的には、ret変数の型がbool *からuintptrに変更され、ROUNDマクロとStructrnd定数を用いて、戻り値のアドレスが適切にアラインメントされるように変更されました。

テストファイルtest/fixedbugs/bug449.goは、runtime.equalが引数と戻り値の間のパディングを考慮しないために、特定のケースでGC(ガベージコレクタ)が生成したコードがスタックからランダムなブール値を比較結果として読み取ってしまう問題を捕捉するために作成されました。このテストは、多数の等価性テストを生成し、T型とTの基盤となる[]byteのスライスを比較することで、runtime.equalへの呼び出しをGCに生成させ、問題が修正されたことを検証します。

変更の背景

この変更は、Go言語のIssue 3866「runtime.equal failed to take padding between arguments and return values into account」を修正するために行われました。

Go言語では、構造体のフィールドはメモリ上で効率的なアクセスを可能にするためにアラインメントされます。これにより、フィールド間に「パディング(詰め物)」バイトが挿入されることがあります。これらのパディングバイトの内容は未定義であり、任意のデータが含まれる可能性があります。

runtime.equal関数は、2つの値を比較して等価性を判断する役割を担っています。Issue 3866のバグは、runtime.equalがこれらの未定義のパディングバイトを比較時に適切にスキップしていなかったことに起因します。その結果、本来は同一であるはずの2つの構造体が、パディングバイト内の任意データのために等しくないと判断される可能性がありました。

特に、GCが生成するコードがruntime.equalを呼び出す際に、引数と戻り値の間のスタック上のパディングを考慮しないと、比較結果としてスタック上のランダムなブール値を読み取ってしまうという問題が発生していました。この問題は、Goプログラムの正確性と信頼性に直接影響を与えるため、早急な修正が必要とされました。

前提知識の解説

1. runtime.equal関数

runtime.equalは、Go言語のランタイムが提供する内部関数で、主に構造体や配列などの複合型の等価性を比較するためにコンパイラによって呼び出されます。Go言語では、==演算子を使って構造体や配列を比較すると、コンパイラは適切な等価性チェックのコードを生成します。多くの場合、これはruntime.equalへの呼び出しに変換されます。この関数は、型情報(Type *t)、比較対象の2つの値(x, y)、そして比較結果を格納する場所(ret)を受け取ります。

2. メモリのアラインメントとパディング

コンピュータのアーキテクチャでは、データが特定のメモリアドレスに配置されると、CPUがそのデータに効率的にアクセスできるようになります。これを「メモリのアラインメント」と呼びます。例えば、4バイトの整数は4の倍数のアドレスに配置されると効率的です。

構造体(struct)を定義する際、そのフィールドはそれぞれ異なるサイズとアラインメント要件を持つことがあります。コンパイラは、これらの要件を満たすようにフィールドをメモリに配置しますが、その結果として、フィールド間に未使用のバイト(「パディング」または「詰め物」)が挿入されることがあります。

例:

type MyStruct struct {
    b byte   // 1 byte
    i int32  // 4 bytes, usually 4-byte aligned
    s string // string header, usually 8-byte aligned
}

この構造体がメモリに配置される際、bの後に3バイトのパディングが挿入され、iが4バイト境界に配置されることがあります。同様に、iの後にパディングが挿入され、sが8バイト境界に配置されることもあります。これらのパディングバイトの内容は、初期化されていない場合、不定な値(ガベージ)を含んでいます。

3. Goにおける構造体の等価性

Go言語における構造体の等価性比較は、フィールドごとの比較によって行われます。これは、生のバイト列比較ではありません。なぜなら、前述のパディングバイトが存在し、その内容が不定であるため、バイト列比較では誤った結果を招く可能性があるからです。コンパイラは、パディングバイトをスキップし、定義されたフィールドのみを比較するように、特定の等価性関数を生成します。

4. uintptr

uintptrは、Go言語における整数型の一つで、ポインタを保持するのに十分な大きさを持つ符号なし整数型です。これは、ポインタ演算を行う際や、ポインタを整数として扱う必要がある場合(例えば、アラインメント調整など)に利用されます。unsafeパッケージと組み合わせて使用されることが多いですが、このコミットのようにランタイム内部で低レベルなメモリ操作を行う際にも用いられます。

5. ROUNDマクロとStructrnd定数

ROUNDマクロは、与えられた値を特定のアラインメント境界に切り上げるために使用されます。例えば、ROUND(x, align)xalignの倍数に切り上げます。 Structrndは、構造体のアラインメントに関する定数であり、通常はシステムのアラインメント要件(例えば、8バイト境界)を示します。この定数を用いてアドレスを丸めることで、メモリ上のデータが適切に配置され、CPUが効率的にアクセスできるようになります。

技術的詳細

このコミットの技術的詳細の核心は、runtime.equal関数が、比較結果を格納するスタック上のアドレスを計算する際に、適切なメモリのアラインメントを保証するように変更された点にあります。

元のコードでは、runtime.equalの戻り値(ret)のアドレスは、比較対象の2つの値(x, y)のメモリ領域の直後に配置されると仮定されていました。具体的には、yのアドレスにt->size(型tのサイズ)を加算した位置をretのアドレスとしていました。

// Original code snippet from runtime.equal
x = (byte*)(&t+1);
y = x + t->size;
ret = (bool*)(y + t->size); // Problematic line
t->alg->equal(ret, t->size, x, y);

この計算方法では、y + t->sizeが必ずしも適切なアラインメント境界に位置するとは限りませんでした。特に、スタックフレームのレイアウトや、前の引数や値のサイズによっては、retがパディングバイトの途中に配置されたり、アラインメントされていないアドレスになったりする可能性がありました。Goのコンパイラが生成するコードは、アラインメントされたアドレスを期待することが多いため、アラインメントされていないアドレスからブール値を読み取ろうとすると、意図しないメモリ領域(例えば、パディングバイト内のガベージデータ)を読み取ってしまい、誤った比較結果(ランダムなtrueまたはfalse)を返す原因となっていました。

修正後のコードでは、この問題に対処するために以下の変更が加えられました。

  1. ret変数の型変更: bool *ret;からuintptr ret;に変更されました。これにより、retがポインタとしてではなく、純粋なメモリアドレスを表す符号なし整数として扱えるようになり、ポインタ演算の制約を受けずにアラインメント調整が可能になります。

  2. yのアドレス計算の修正: y = x + t->size;からy = x + ROUND(t->size, t->align);に変更されました。これは、yのアドレスを計算する際に、型tのサイズだけでなく、そのアラインメント要件(t->align)も考慮して、yが適切にアラインメントされた位置に配置されるように丸めることを意味します。これにより、xyの間に適切なパディングが確保されます。

  3. retアドレスのアラインメント調整: ret = (uintptr)(y + t->size);の後に、ret = ROUND(ret, Structrnd);という行が追加されました。これは、retが指すアドレスをStructrnd(構造体のアラインメント境界、通常は8バイト)に丸めることを意味します。これにより、runtime.equalの戻り値が格納されるスタック上の位置が、常に適切なアラインメント境界に配置されることが保証されます。

これらの変更により、runtime.equalが比較結果を書き込むアドレスが常に正しくアラインメントされ、GCが生成するコードがそのアドレスからブール値を読み取る際に、パディングバイト内の不定な値を誤って読み取ることがなくなりました。結果として、構造体や配列の等価性比較が常に正確に行われるようになります。

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

src/pkg/runtime/alg.cruntime·equal関数における変更点です。

--- a/src/pkg/runtime/alg.c
+++ b/src/pkg/runtime/alg.c
@@ -469,10 +469,11 @@ void
 runtime·equal(Type *t, ...)
 {
 	byte *x, *y;
-	bool *ret;
+	uintptr ret;
 	
 	x = (byte*)(&t+1);
-	y = x + t->size;
-	ret = (bool*)(y + t->size);
-	t->alg->equal(ret, t->size, x, y);
+	y = x + ROUND(t->size, t->align);
+	ret = (uintptr)(y + t->size);
+	ret = ROUND(ret, Structrnd);
+	t->alg->equal((bool*)ret, t->size, x, y);
 }

コアとなるコードの解説

変更されたruntime·equal関数は、Go言語のランタイムにおける等価性比較の核心部分です。

  1. uintptr ret;:

    • 元のコードではbool *ret;として、比較結果を格納するブール値へのポインタとして宣言されていました。
    • 修正後はuintptr ret;として、ポインタではなく、メモリアドレスを保持する符号なし整数型として宣言されています。これにより、ポインタ演算の制約を受けずに、より低レベルで柔軟なアドレス操作(特にアラインメント調整)が可能になります。
  2. y = x + ROUND(t->size, t->align);:

    • 元のコードではy = x + t->size;として、単純にxの後に型tのサイズ分だけ進んだ位置をyのアドレスとしていました。
    • 修正後はROUND(t->size, t->align)を使用しています。これは、型tのサイズを、その型のアラインメント要件(t->align)の倍数に切り上げることを意味します。これにより、xyの間に適切なパディングが挿入され、yが常に適切にアラインメントされたアドレスに配置されることが保証されます。これは、xyがメモリ上で隣接するデータブロックである場合に、その境界が正しくアラインメントされるようにするための重要なステップです。
  3. ret = ROUND(ret, Structrnd);:

    • この行が新たに追加されました。
    • ret = (uintptr)(y + t->size);で計算されたretのアドレスを、さらにStructrnd(構造体のアラインメント境界、通常は8バイト)に丸めます。
    • このステップは、runtime.equalの戻り値(比較結果のブール値)が格納されるスタック上の位置が、常にシステムのアラインメント要件を満たすようにするために不可欠です。これにより、GCが生成するコードがこのアドレスからブール値を読み取る際に、アラインメントエラーや、パディングバイト内の不定な値を誤って読み取ることを防ぎます。

これらの変更により、runtime.equalは、比較対象のデータだけでなく、その戻り値が格納されるメモリ領域についても、厳密なアラインメント規則に従うようになりました。これにより、Goプログラムにおける等価性比較の信頼性と正確性が大幅に向上しました。

関連リンク

参考にした情報源リンク