[インデックス 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)
はx
をalign
の倍数に切り上げます。
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
)を返す原因となっていました。
修正後のコードでは、この問題に対処するために以下の変更が加えられました。
-
ret
変数の型変更:bool *ret;
からuintptr ret;
に変更されました。これにより、ret
がポインタとしてではなく、純粋なメモリアドレスを表す符号なし整数として扱えるようになり、ポインタ演算の制約を受けずにアラインメント調整が可能になります。 -
y
のアドレス計算の修正:y = x + t->size;
からy = x + ROUND(t->size, t->align);
に変更されました。これは、y
のアドレスを計算する際に、型t
のサイズだけでなく、そのアラインメント要件(t->align
)も考慮して、y
が適切にアラインメントされた位置に配置されるように丸めることを意味します。これにより、x
とy
の間に適切なパディングが確保されます。 -
ret
アドレスのアラインメント調整:ret = (uintptr)(y + t->size);
の後に、ret = ROUND(ret, Structrnd);
という行が追加されました。これは、ret
が指すアドレスをStructrnd
(構造体のアラインメント境界、通常は8バイト)に丸めることを意味します。これにより、runtime.equal
の戻り値が格納されるスタック上の位置が、常に適切なアラインメント境界に配置されることが保証されます。
これらの変更により、runtime.equal
が比較結果を書き込むアドレスが常に正しくアラインメントされ、GCが生成するコードがそのアドレスからブール値を読み取る際に、パディングバイト内の不定な値を誤って読み取ることがなくなりました。結果として、構造体や配列の等価性比較が常に正確に行われるようになります。
コアとなるコードの変更箇所
src/pkg/runtime/alg.c
のruntime·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言語のランタイムにおける等価性比較の核心部分です。
-
uintptr ret;
:- 元のコードでは
bool *ret;
として、比較結果を格納するブール値へのポインタとして宣言されていました。 - 修正後は
uintptr ret;
として、ポインタではなく、メモリアドレスを保持する符号なし整数型として宣言されています。これにより、ポインタ演算の制約を受けずに、より低レベルで柔軟なアドレス操作(特にアラインメント調整)が可能になります。
- 元のコードでは
-
y = x + ROUND(t->size, t->align);
:- 元のコードでは
y = x + t->size;
として、単純にx
の後に型t
のサイズ分だけ進んだ位置をy
のアドレスとしていました。 - 修正後は
ROUND(t->size, t->align)
を使用しています。これは、型t
のサイズを、その型のアラインメント要件(t->align
)の倍数に切り上げることを意味します。これにより、x
とy
の間に適切なパディングが挿入され、y
が常に適切にアラインメントされたアドレスに配置されることが保証されます。これは、x
とy
がメモリ上で隣接するデータブロックである場合に、その境界が正しくアラインメントされるようにするための重要なステップです。
- 元のコードでは
-
ret = ROUND(ret, Structrnd);
:- この行が新たに追加されました。
ret = (uintptr)(y + t->size);
で計算されたret
のアドレスを、さらにStructrnd
(構造体のアラインメント境界、通常は8バイト)に丸めます。- このステップは、
runtime.equal
の戻り値(比較結果のブール値)が格納されるスタック上の位置が、常にシステムのアラインメント要件を満たすようにするために不可欠です。これにより、GCが生成するコードがこのアドレスからブール値を読み取る際に、アラインメントエラーや、パディングバイト内の不定な値を誤って読み取ることを防ぎます。
これらの変更により、runtime.equal
は、比較対象のデータだけでなく、その戻り値が格納されるメモリ領域についても、厳密なアラインメント規則に従うようになりました。これにより、Goプログラムにおける等価性比較の信頼性と正確性が大幅に向上しました。
関連リンク
- Go Issue 3866: https://github.com/golang/go/issues/3866
- Go CL 6452046: https://golang.org/cl/6452046
参考にした情報源リンク
- Go issue 3866, titled "runtime.equal padding,"
- In Go, struct fields are aligned in memory to optimize access efficiency, which can result in the insertion of "padding" bytes between fields.
- The contents of these padding bytes are undefined and can contain arbitrary data.
- Go's struct equality is designed to be a field-by-field comparison, not a raw byte-by-byte comparison, precisely because of these padding bytes and the varying memory representations of different types (like strings and interfaces).