[インデックス 16625] ファイルの概要
このコミットは、Go言語の time
パッケージにおけるガベージコレクタ (GC) のエイリアシングバグを修正するものです。具体的には、time.Time
構造体のフィールドのメモリレイアウトが原因で、GCが誤ったポインタを認識し、問題を引き起こす可能性があった点を改善しています。
コミット
commit f21bc7920decb5b6f94d49a9e8eefcdb74960c24
Author: Russ Cox <rsc@golang.org>
Date: Mon Jun 24 14:49:35 2013 -0400
time: avoid garbage collector aliasing bug
Time is a tiny struct, so the compiler copies a Time by
copying each of the three fields.
The layout of a time on amd64 is [ptr int32 gap32 ptr].
Copying a Time onto a location that formerly held a pointer in the
second word changes only the low 32 bits, creating a different
but still plausible pointer. This confuses the garbage collector
when it appears in argument or result frames.
To avoid this problem, declare nsec as uintptr, so that there is
no gap on amd64 anymore, and therefore no partial pointers.
Note that rearranging the fields to put the int32 last still leaves
a gap - [ptr ptr int32 gap32] - because Time must have a total
size that is ptr-width aligned.
Update #5749
This CL is enough to fix the problem, but we should still do
the other actions listed in the initial report. We're not too far
from completely precise collection.
R=golang-dev, dvyukov, r
CC=golang-dev
https://golang.org/cl/10504043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f21bc7920decb5b6f94d49a9e8eefcdb74960c24
元コミット内容
time: avoid garbage collector aliasing bug
このコミットは、time.Time
構造体のコピー時に発生するガベージコレクタのエイリアシングバグを回避するためのものです。Time
構造体が小さいため、コンパイラは各フィールドをコピーすることで Time
をコピーします。
amd64
アーキテクチャにおける Time
のメモリレイアウトは [ptr int32 gap32 ptr]
となっていました。以前ポインタを保持していた2番目のワードに Time
をコピーすると、下位32ビットのみが変更され、異なるがもっともらしいポインタが生成されてしまいます。これが引数や結果フレームに現れると、ガベージコレクタを混乱させます。
この問題を回避するため、nsec
フィールドを uintptr
として宣言し、amd64
上でギャップがなくなり、部分的なポインタが生成されないようにします。
フィールドを再配置して int32
を最後に配置しても、Time
の合計サイズがポインタ幅にアラインされる必要があるため、[ptr ptr int32 gap32]
のようにギャップが残ることに注意してください。
この変更は Issue #5749 に関連しています。
この変更は問題を修正するのに十分ですが、初期レポートに記載されている他のアクションもまだ実行する必要があります。完全に正確なコレクションにはまだ遠くありません。
変更の背景
Go言語のガベージコレクタは、プログラムが使用しなくなったメモリを自動的に解放する重要な役割を担っています。GCが正しく機能するためには、メモリ上のどの値がポインタであり、どのポインタが有効なオブジェクトを指しているかを正確に識別する必要があります。しかし、特定の条件下で、GCがポインタではない値をポインタとして誤認識したり、無効なポインタを有効なポインタとして扱ったりする「エイリアシングバグ」が発生することがあります。
このコミットの背景にあるのは、time.Time
構造体のメモリレイアウトと、コンパイラによる構造体のコピー方法に起因するGCの誤動作です。time.Time
は、時刻を表すために内部的に秒数 (sec
) とナノ秒 (nsec
)、そしてタイムゾーン情報 (loc
) を保持しています。この構造体が非常に小さいため、Goコンパイラは最適化の一環として、構造体全体を一度にコピーするのではなく、個々のフィールドをコピーして新しい Time
値を生成することがあります。
問題は、amd64
(64ビット) システムにおける time.Time
構造体のメモリレイアウトにありました。元のレイアウトは [ptr int32 gap32 ptr]
のような形になっていました。ここで ptr
はポインタ、int32
は32ビット整数、gap32
は32ビットのパディング(アラインメントのために挿入される未使用領域)を意味します。この gap32
の部分が、以前にポインタを格納していたメモリ領域と重なる場合、Time
構造体のコピー時に int32
の値がそのギャップに書き込まれると、その int32
の下位32ビットが、以前のポインタの下位32ビットと結合されて、GCが「もっともらしい」が実際には無効なポインタとして認識してしまう可能性がありました。このような「部分的なポインタ」は、GCの正確性を損ない、メモリリークやクラッシュなどの深刻な問題を引き起こす可能性があります。
このバグは、特に引数や結果フレーム(関数呼び出しのスタックフレーム)で time.Time
値が扱われる際に顕在化し、GCがスタック上のポインタをスキャンする際に誤った判断を下す原因となっていました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンピュータサイエンスの基本的な概念を理解しておく必要があります。
-
Go言語のガベージコレクタ (GC):
- GoのGCは、主に並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えることを目指しています。
- GCは、メモリ上のオブジェクトがまだ参照されているかどうかを判断するために、ポインタを追跡します。ルート(グローバル変数、スタック上の変数など)から到達可能なオブジェクトは「生きている」と判断され、それ以外は「死んでいる」と判断されて解放されます。
- GCがポインタを正確に識別できないと、誤って生きているオブジェクトを解放したり(Use-After-Free)、死んでいるオブジェクトを解放しなかったり(メモリリーク)する問題が発生します。
-
ポインタとメモリレイアウト:
- ポインタは、メモリ上の特定のアドレスを指す変数です。64ビットシステムでは、ポインタは通常64ビット(8バイト)のサイズを持ちます。
- 構造体(
struct
)は、異なる型のフィールドをまとめた複合データ型です。構造体のフィールドは、メモリ上で連続して配置されますが、CPUの効率的なアクセスを可能にするために「アラインメント」という規則に従います。 - アラインメント: CPUは、特定のデータ型が特定のメモリアドレスの倍数に配置されている場合に、最も効率的にデータを読み書きできます。例えば、64ビット整数は8バイト境界に、32ビット整数は4バイト境界に配置されることが一般的です。
- パディング (Padding) / ギャップ (Gap): アラインメントの要件を満たすために、構造体のフィールド間に未使用のバイト(パディング)が挿入されることがあります。これにより、構造体の合計サイズが増加することがあります。例えば、
[ptr int32]
の後にptr
が続く場合、int32
の後に4バイトのパディングが挿入され、次のptr
が8バイト境界に配置されるように調整されることがあります。これがgap32
の正体です。
-
uintptr
型:- Go言語の
uintptr
型は、ポインタを保持するのに十分な大きさの符号なし整数型です。そのサイズは、システム上のポインタのサイズ(32ビットシステムでは32ビット、64ビットシステムでは64ビット)と同じです。 uintptr
は、ポインタ演算を行う際や、C言語との相互運用、または低レベルのメモリ操作を行う際に使用されます。- 重要なのは、
uintptr
は「ポインタ」としてGCに認識されないという点です。GCはuintptr
の値を単なる数値として扱い、それが指すメモリを追跡しません。これは、GCの正確性を確保するために意図された挙動です。開発者がuintptr
をポインタとして使用する場合は、GCにそのポインタを認識させるための特別なメカニズム(例:runtime.KeepAlive
)が必要になることがあります。
- Go言語の
-
エイリアシング (Aliasing):
- プログラミングにおいて、エイリアシングとは、同じメモリ位置が複数の異なる名前や参照によってアクセスされる状況を指します。
- このコミットの文脈では、
gap32
のメモリ領域が、以前は有効なポインタの一部であったが、int32
の値が書き込まれることで、そのポインタの下位ビットが変更され、GCが誤って新しい「もっともらしい」ポインタとして認識してしまう状況を指します。
技術的詳細
このバグの核心は、Goの time.Time
構造体が amd64
アーキテクチャ上でどのようにメモリに配置されるか、そしてGoコンパイラがその構造体をどのようにコピーするかという点にありました。
元の time.Time
構造体は、以下のようなフィールドを持っていました(簡略化):
type Time struct {
sec int64 // 秒数
nsec int32 // ナノ秒
loc *Location // タイムゾーン情報へのポインタ
}
amd64
システムでは、int64
は8バイト、int32
は4バイト、ポインタ (*Location
) は8バイトです。アラインメントの要件により、int32
の後にパディングが発生していました。
具体的なメモリレイアウトは以下のようになっていました:
sec
(int64
): 8バイトnsec
(int32
): 4バイト- パディング (
gap32
): 4バイト (次のloc
ポインタを8バイト境界にアラインするため) loc
(*Location
): 8バイト
したがって、Time
構造体のメモリレイアウトは概念的に [int64 | int32 | gap32 | *Location]
となっていました。
問題は、コンパイラが Time
構造体をコピーする際に、各フィールドを個別にコピーする挙動にありました。例えば、ある Time
変数 t1
を別の Time
変数 t2
に代入する t2 = t1
のような操作が行われると、コンパイラは t1.sec
を t2.sec
に、t1.nsec
を t2.nsec
に、t1.loc
を t2.loc
にそれぞれコピーします。
ここで、もし t2
が以前に別のポインタを保持していたメモリ領域に配置されており、そのポインタの下位32ビットが t2.nsec
の int32
フィールドが書き込まれる領域と重なっていた場合、t2.nsec
のコピーによって、そのポインタの下位32ビットが上書きされてしまいます。これにより、GCがスキャンする際に、元のポインタとは異なるが、依然として「もっともらしい」(つまり、有効なアドレス範囲内にあるように見える)ポインタが生成されてしまう可能性がありました。
GoのGCは、スタックやヒープ上のメモリをスキャンしてポインタを識別し、到達可能性を判断します。もしGCがこのような「部分的に変更されたポインタ」を有効なポインタとして誤認識してしまうと、実際には存在しないオブジェクトを追跡しようとしたり、誤ったメモリ領域を解放しようとしたりする可能性があります。これは、GCの正確性を損ない、プログラムの不安定性やクラッシュにつながります。
この問題を解決するために、コミットでは nsec
フィールドの型を int32
から uintptr
に変更しました。
type Time struct {
sec int64
nsec uintptr // 変更点: int32 から uintptr へ
loc *Location
}
uintptr
は64ビットシステムでは64ビット(8バイト)のサイズを持ちます。これにより、Time
構造体のメモリレイアウトは以下のように変化します:
sec
(int64
): 8バイトnsec
(uintptr
): 8バイトloc
(*Location
): 8バイト
新しいレイアウトは [int64 | uintptr | *Location]
となり、nsec
フィールドがポインタと同じ8バイトのサイズを持つため、int32
の後に発生していた4バイトのパディング (gap32
) がなくなります。
パディングがなくなることで、Time
構造体のコピー時に、以前ポインタを保持していたメモリ領域に nsec
の値が書き込まれても、その領域全体が uintptr
の値で上書きされるため、部分的なポインタが生成される可能性がなくなります。uintptr
はGCによってポインタとして扱われないため、GCが誤って追跡しようとすることもありません。これにより、GCの正確性が保たれ、エイリアシングバグが回避されます。
コミットメッセージでは、「フィールドを再配置して int32
を最後に配置してもギャップが残る」と述べられています。これは、[ptr ptr int32 gap32]
のように、構造体の合計サイズがポインタ幅(8バイト)にアラインされる必要があるため、最後の int32
の後にパディングが必要になることを意味します。uintptr
に変更することで、このアラインメントの問題も同時に解決され、構造体全体がポインタ幅の倍数となり、パディングが不要になります。
この修正は、GoのGCがまだ「完全に正確なコレクション」ではない時期に行われたものであり、GCの進化の過程における重要なステップを示しています。将来的にはGCがより正確になり、このような低レベルのメモリレイアウトの考慮が不要になることが期待されていますが、この時点では uintptr
を使用することが最も堅牢な解決策でした。
コアとなるコードの変更箇所
変更は src/pkg/time/time.go
ファイルに集中しています。
-
Time
構造体のnsec
フィールドの型変更:type Time struct { ... nsec int32 ... }
- から
type Time struct { ... nsec uintptr ... }
- へ変更されました。
-
nsec
フィールドへのアクセスと操作の修正:nsec
がuintptr
になったため、int32
として扱っていた箇所で型変換が必要になりました。Add
メソッド、Sub
メソッド、Now
関数、GobDecode
メソッド、Unix
関数、Date
関数、div
関数など、nsec
を直接操作するすべての箇所でint32
とuintptr
間の明示的な型変換が追加されています。
コアとなるコードの解説
以下に、主要な変更箇所のコードと解説を示します。
Time
構造体の定義
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -39,7 +39,14 @@ type Time struct {
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
- nsec int32
+ //
+ // It is declared as uintptr instead of int32 or uint32
+ // to avoid garbage collector aliasing in the case where
+ // on a 64-bit system the int32 or uint32 field is written
+ // over the low half of a pointer, creating another pointer.
+ // TODO(rsc): When the garbage collector is completely
+ // precise, change back to int32.
+ nsec uintptr
- 変更点:
nsec
フィールドの型がint32
からuintptr
に変更されました。 - 解説: これがこのコミットの最も重要な変更点です。
uintptr
はポインタと同じサイズ(64ビットシステムでは8バイト)を持つため、nsec
の後にパディングが発生しなくなります。これにより、Time
構造体のメモリレイアウトが密になり、GCが誤ったポインタを認識する原因となっていた「ギャップ」が解消されます。コメントには、この変更がGCのエイリアシング問題を回避するためであること、そして将来的にGCが完全に正確になればint32
に戻す可能性があることが明記されています。
Add
メソッド
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -605,14 +612,15 @@ func (d Duration) Hours() float64 {\
// Add returns the time t+d.
func (t Time) Add(d Duration) Time {\
t.sec += int64(d / 1e9)\
- t.nsec += int32(d % 1e9)\
- if t.nsec >= 1e9 {\
+ nsec := int32(t.nsec) + int32(d%1e9)\
+ if nsec >= 1e9 {\
\tt.sec++\
- \tt.nsec -= 1e9\
- } else if t.nsec < 0 {\
+ \tnsec -= 1e9\
+ } else if nsec < 0 {\
\tt.sec--\
- \tt.nsec += 1e9\
+ \tnsec += 1e9\
}\
+\tt.nsec = uintptr(nsec)\
return t\
}
- 変更点:
t.nsec
の直接的な加算が、一時変数nsec
を介した操作に変更され、最後にuintptr
に型変換してt.nsec
に代入されています。 - 解説:
t.nsec
がuintptr
型になったため、int32
型のd % 1e9
を直接加算することはできません。そのため、t.nsec
をint32
に型変換し、d % 1e9
を加算して一時変数nsec
に格納します。ナノ秒の範囲チェックと秒数への繰り上がり/繰り下がりを行った後、最終的なnsec
の値をuintptr
に型変換してt.nsec
に戻しています。これにより、nsec
の値がuintptr
として正しく保持されつつ、ナノ秒としての算術演算がint32
の精度で行われます。
Sub
メソッド
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -621,7 +629,7 @@ func (t Time) Add(d Duration) Time {\
// will be returned.\
// To compute t-d for a duration d, use t.Add(-d).\
func (t Time) Sub(u Time) Duration {\
- d := Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec)\
+ d := Duration(t.sec-u.sec)*Second + Duration(int32(t.nsec)-int32(u.nsec))\
// Check for overflow or underflow.\
switch {\
case u.Add(d).Equal(t):\
- 変更点:
t.nsec - u.nsec
の部分がint32(t.nsec) - int32(u.nsec)
に変更されました。 - 解説:
nsec
がuintptr
型であるため、直接減算するとuintptr
としての減算が行われてしまいます。ナノ秒の差を計算するためには、両方のnsec
をint32
に型変換してから減算する必要があります。これにより、ナノ秒の差が正しく計算され、Duration
型に変換されます。
Now
関数
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -776,7 +784,7 @@ func now() (sec int64, nsec int32)\
// Now returns the current local time.\
func Now() Time {\
sec, nsec := now()\
- return Time{sec + unixToInternal, nsec, Local}\
+ return Time{sec + unixToInternal, uintptr(nsec), Local}\
}
- 変更点:
Time
構造体の初期化時に、nsec
の値がuintptr(nsec)
と明示的に型変換されています。 - 解説:
now()
関数はnsec
をint32
として返しますが、Time
構造体のnsec
フィールドはuintptr
型であるため、ここで型変換が必要です。
同様の型変換は、GobDecode
、Unix
、Date
、div
といった nsec
を扱う他の関数やメソッドでも行われています。これらの変更はすべて、nsec
フィールドの型が int32
から uintptr
に変更されたことに伴う、型安全性を保ちつつ正しい算術演算を行うための修正です。
関連リンク
- Go Issue #5749: このコミットが修正したバグの元のIssue。詳細な議論や背景情報が含まれている可能性があります。
- https://code.google.com/p/go/issues/detail?id=5749 (古いGoプロジェクトのIssueトラッカーへのリンクですが、アーカイブされている可能性があります)
- Go CL 10504043: このコミットに対応するGoの変更リスト (Change List)。
参考にした情報源リンク
- Go言語の公式ドキュメント (特に
time
パッケージ、uintptr
型、メモリモデルに関する記述) - Go言語のガベージコレクタに関する技術記事や論文 (GoのGCの仕組み、ポインタの識別など)
amd64
アーキテクチャにおけるデータアラインメントとメモリレイアウトに関する一般的な情報- GoのIssueトラッカーやメーリングリストでの関連議論 (特にIssue 5749)
- Goのソースコード (特に
runtime
パッケージやsrc/pkg/time
パッケージ)
[インデックス 16625] ファイルの概要
このコミットは、Go言語の time
パッケージにおけるガベージコレクタ (GC) のエイリアシングバグを修正するものです。具体的には、time.Time
構造体のフィールドのメモリレイアウトが原因で、GCが誤ったポインタを認識し、問題を引き起こす可能性があった点を改善しています。
コミット
commit f21bc7920decb5b6f94d49a9e8eefcdb74960c24
Author: Russ Cox <rsc@golang.org>
Date: Mon Jun 24 14:49:35 2013 -0400
time: avoid garbage collector aliasing bug
Time is a tiny struct, so the compiler copies a Time by
copying each of the three fields.
The layout of a time on amd64 is [ptr int32 gap32 ptr].
Copying a Time onto a location that formerly held a pointer in the
second word changes only the low 32 bits, creating a different
but still plausible pointer. This confuses the garbage collector
when it appears in argument or result frames.
To avoid this problem, declare nsec as uintptr, so that there is
no gap on amd64 anymore, and therefore no partial pointers.
Note that rearranging the fields to put the int32 last still leaves
a gap - [ptr ptr int32 gap32] - because Time must have a total
size that is ptr-width aligned.
Update #5749
This CL is enough to fix the problem, but we should still do
the other actions listed in the initial report. We're not too far
from completely precise collection.
R=golang-dev, dvyukov, r
CC=golang-dev
https://golang.org/cl/10504043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f21bc7920decb5b6f94d49a9e8eefcdb74960c24
元コミット内容
time: avoid garbage collector aliasing bug
このコミットは、time.Time
構造体のコピー時に発生するガベージコレクタのエイリアシングバグを回避するためのものです。Time
構造体が小さいため、コンパイラは各フィールドをコピーすることで Time
をコピーします。
amd64
アーキテクチャにおける Time
のメモリレイアウトは [ptr int32 gap32 ptr]
となっていました。以前ポインタを保持していた2番目のワードに Time
をコピーすると、下位32ビットのみが変更され、異なるがもっともらしいポインタが生成されてしまいます。これが引数や結果フレームに現れると、ガベージコレクタを混乱させます。
この問題を回避するため、nsec
フィールドを uintptr
として宣言し、amd64
上でギャップがなくなり、部分的なポインタが生成されないようにします。
フィールドを再配置して int32
を最後に配置しても、Time
の合計サイズがポインタ幅にアラインされる必要があるため、[ptr ptr int32 gap32]
のようにギャップが残ることに注意してください。
この変更は Issue #5749 に関連しています。
この変更は問題を修正するのに十分ですが、初期レポートに記載されている他のアクションもまだ実行する必要があります。完全に正確なコレクションにはまだ遠くありません。
変更の背景
Go言語のガベージコレクタは、プログラムが使用しなくなったメモリを自動的に解放する重要な役割を担っています。GCが正しく機能するためには、メモリ上のどの値がポインタであり、どのポインタが有効なオブジェクトを指しているかを正確に識別する必要があります。しかし、特定の条件下で、GCがポインタではない値をポインタとして誤認識したり、無効なポインタを有効なポインタとして扱ったりする「エイリアシングバグ」が発生することがあります。
このコミットの背景にあるのは、time.Time
構造体のメモリレイアウトと、コンパイラによる構造体のコピー方法に起因するGCの誤動作です。time.Time
は、時刻を表すために内部的に秒数 (sec
) とナノ秒 (nsec
)、そしてタイムゾーン情報 (loc
) を保持しています。この構造体が非常に小さいため、Goコンパイラは最適化の一環として、構造体全体を一度にコピーするのではなく、個々のフィールドをコピーして新しい Time
値を生成することがあります。
問題は、amd64
(64ビット) システムにおける time.Time
構造体のメモリレイアウトにありました。元のレイアウトは [ptr int32 gap32 ptr]
のような形になっていました。ここで ptr
はポインタ、int32
は32ビット整数、gap32
は32ビットのパディング(アラインメントのために挿入される未使用領域)を意味します。この gap32
の部分が、以前にポインタを格納していたメモリ領域と重なる場合、Time
構造体のコピー時に int32
の値がそのギャップに書き込まれると、その int32
の下位32ビットが、以前のポインタの下位32ビットと結合されて、GCが「もっともらしい」が実際には無効なポインタとして認識してしまう可能性がありました。このような「部分的なポインタ」は、GCの正確性を損ない、メモリリークやクラッシュなどの深刻な問題を引き起こす可能性があります。
このバグは、特に引数や結果フレーム(関数呼び出しのスタックフレーム)で time.Time
値が扱われる際に顕在化し、GCがスタック上のポインタをスキャンする際に誤った判断を下す原因となっていました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンピュータサイエンスの基本的な概念を理解しておく必要があります。
-
Go言語のガベージコレクタ (GC):
- GoのGCは、主に並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えることを目指しています。
- GCは、メモリ上のオブジェクトがまだ参照されているかどうかを判断するために、ポインタを追跡します。ルート(グローバル変数、スタック上の変数など)から到達可能なオブジェクトは「生きている」と判断され、それ以外は「死んでいる」と判断されて解放されます。
- GCがポインタを正確に識別できないと、誤って生きているオブジェクトを解放したり(Use-After-Free)、死んでいるオブジェクトを解放しなかったり(メモリリーク)する問題が発生します。
-
ポインタとメモリレイアウト:
- ポインタは、メモリ上の特定のアドレスを指す変数です。64ビットシステムでは、ポインタは通常64ビット(8バイト)のサイズを持ちます。
- 構造体(
struct
)は、異なる型のフィールドをまとめた複合データ型です。構造体のフィールドは、メモリ上で連続して配置されますが、CPUの効率的なアクセスを可能にするために「アラインメント」という規則に従います。 - アラインメント: CPUは、特定のデータ型が特定のメモリアドレスの倍数に配置されている場合に、最も効率的にデータを読み書きできます。例えば、64ビット整数は8バイト境界に、32ビット整数は4バイト境界に配置されることが一般的です。
- パディング (Padding) / ギャップ (Gap): アラインメントの要件を満たすために、構造体のフィールド間に未使用のバイト(パディング)が挿入されることがあります。これにより、構造体の合計サイズが増加することがあります。例えば、
[ptr int32]
の後にptr
が続く場合、int32
の後に4バイトのパディングが挿入され、次のptr
が8バイト境界に配置されるように調整されることがあります。これがgap32
の正体です。
-
uintptr
型:- Go言語の
uintptr
型は、ポインタを保持するのに十分な大きさの符号なし整数型です。そのサイズは、システム上のポインタのサイズ(32ビットシステムでは32ビット、64ビットシステムでは64ビット)と同じです。 uintptr
は、ポインタ演算を行う際や、C言語との相互運用、または低レベルのメモリ操作を行う際に使用されます。- 重要なのは、
uintptr
は「ポインタ」としてGCに認識されないという点です。GCはuintptr
の値を単なる数値として扱い、それが指すメモリを追跡しません。これは、GCの正確性を確保するために意図された挙動です。開発者がuintptr
をポインタとして使用する場合は、GCにそのポインタを認識させるための特別なメカニズム(例:runtime.KeepAlive
)が必要になることがあります。
- Go言語の
-
エイリアシング (Aliasing):
- プログラミングにおいて、エイリアシングとは、同じメモリ位置が複数の異なる名前や参照によってアクセスされる状況を指します。
- このコミットの文脈では、
gap32
のメモリ領域が、以前は有効なポインタの一部であったが、int32
の値が書き込まれることで、そのポインタの下位ビットが変更され、GCが誤って新しい「もっともらしい」ポインタとして認識してしまう状況を指します。
技術的詳細
このバグの核心は、Goの time.Time
構造体が amd64
アーキテクチャ上でどのようにメモリに配置されるか、そしてGoコンパイラがその構造体をどのようにコピーするかという点にありました。
元の time.Time
構造体は、以下のようなフィールドを持っていました(簡略化):
type Time struct {
sec int64 // 秒数
nsec int32 // ナノ秒
loc *Location // タイムゾーン情報へのポインタ
}
amd64
システムでは、int64
は8バイト、int32
は4バイト、ポインタ (*Location
) は8バイトです。アラインメントの要件により、int32
の後にパディングが発生していました。
具体的なメモリレイアウトは以下のようになっていました:
sec
(int64
): 8バイトnsec
(int32
): 4バイト- パディング (
gap32
): 4バイト (次のloc
ポインタを8バイト境界にアラインするため) loc
(*Location
): 8バイト
したがって、Time
構造体のメモリレイアウトは概念的に [int64 | int32 | gap32 | *Location]
となっていました。
問題は、コンパイラが Time
構造体をコピーする際に、各フィールドを個別にコピーする挙動にありました。例えば、ある Time
変数 t1
を別の Time
変数 t2
に代入する t2 = t1
のような操作が行われると、コンパイラは t1.sec
を t2.sec
に、t1.nsec
を t2.nsec
に、t1.loc
を t2.loc
にそれぞれコピーします。
ここで、もし t2
が以前に別のポインタを保持していたメモリ領域に配置されており、そのポインタの下位32ビットが t2.nsec
の int32
フィールドが書き込まれる領域と重なっていた場合、t2.nsec
のコピーによって、そのポインタの下位32ビットが上書きされてしまいます。これにより、GCがスキャンする際に、元のポインタとは異なるが、依然として「もっともらしい」(つまり、有効なアドレス範囲内にあるように見える)ポインタが生成されてしまう可能性がありました。
GoのGCは、スタックやヒープ上のメモリをスキャンしてポインタを識別し、到達可能性を判断します。もしGCがこのような「部分的に変更されたポインタ」を有効なポインタとして誤認識してしまうと、実際には存在しないオブジェクトを追跡しようとしたり、誤ったメモリ領域を解放しようとしたりする可能性があります。これは、GCの正確性を損ない、プログラムの不安定性やクラッシュにつながります。
この問題を解決するために、コミットでは nsec
フィールドの型を int32
から uintptr
に変更しました。
type Time struct {
sec int64
nsec uintptr // 変更点: int32 から uintptr へ
loc *Location
}
uintptr
は64ビットシステムでは64ビット(8バイト)のサイズを持ちます。これにより、Time
構造体のメモリレイアウトは以下のように変化します:
sec
(int64
): 8バイトnsec
(uintptr
): 8バイトloc
(*Location
): 8バイト
新しいレイアウトは [int64 | uintptr | *Location]
となり、nsec
フィールドがポインタと同じ8バイトのサイズを持つため、int32
の後に発生していた4バイトのパディング (gap32
) がなくなります。
パディングがなくなることで、Time
構造体のコピー時に、以前ポインタを保持していたメモリ領域に nsec
の値が書き込まれても、その領域全体が uintptr
の値で上書きされるため、部分的なポインタが生成される可能性がなくなります。uintptr
はGCによってポインタとして扱われないため、GCが誤って追跡しようとすることもありません。これにより、GCの正確性が保たれ、エイリアシングバグが回避されます。
コミットメッセージでは、「フィールドを再配置して int32
を最後に配置してもギャップが残る」と述べられています。これは、[ptr ptr int32 gap32]
のように、構造体の合計サイズがポインタ幅(8バイト)にアラインされる必要があるため、最後の int32
の後にパディングが必要になることを意味します。uintptr
に変更することで、このアラインメントの問題も同時に解決され、構造体全体がポインタ幅の倍数となり、パディングが不要になります。
この修正は、GoのGCがまだ「完全に正確なコレクション」ではない時期に行われたものであり、GCの進化の過程における重要なステップを示しています。将来的にはGCがより正確になり、このような低レベルのメモリレイアウトの考慮が不要になることが期待されていますが、この時点では uintptr
を使用することが最も堅牢な解決策でした。
コアとなるコードの変更箇所
変更は src/pkg/time/time.go
ファイルに集中しています。
-
Time
構造体のnsec
フィールドの型変更:type Time struct { ... nsec int32 ... }
- から
type Time struct { ... nsec uintptr ... }
- へ変更されました。
-
nsec
フィールドへのアクセスと操作の修正:nsec
がuintptr
になったため、int32
として扱っていた箇所で型変換が必要になりました。Add
メソッド、Sub
メソッド、Now
関数、GobDecode
メソッド、Unix
関数、Date
関数、div
関数など、nsec
を直接操作するすべての箇所でint32
とuintptr
間の明示的な型変換が追加されています。
コアとなるコードの解説
以下に、主要な変更箇所のコードと解説を示します。
Time
構造体の定義
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -39,7 +39,14 @@ type Time struct {
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
- nsec int32
+ //
+ // It is declared as uintptr instead of int32 or uint32
+ // to avoid garbage collector aliasing in the case where
+ // on a 64-bit system the int32 or uint32 field is written
+ // over the low half of a pointer, creating another pointer.
+ // TODO(rsc): When the garbage collector is completely
+ // precise, change back to int32.
+ nsec uintptr
- 変更点:
nsec
フィールドの型がint32
からuintptr
に変更されました。 - 解説: これがこのコミットの最も重要な変更点です。
uintptr
はポインタと同じサイズ(64ビットシステムでは8バイト)を持つため、nsec
の後にパディングが発生しなくなります。これにより、Time
構造体のメモリレイアウトが密になり、GCが誤ったポインタを認識する原因となっていた「ギャップ」が解消されます。コメントには、この変更がGCのエイリアシング問題を回避するためであること、そして将来的にGCが完全に正確になればint32
に戻す可能性があることが明記されています。
Add
メソッド
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -605,14 +612,15 @@ func (d Duration) Hours() float64 {\
// Add returns the time t+d.
func (t Time) Add(d Duration) Time {\
t.sec += int64(d / 1e9)\
- t.nsec += int32(d % 1e9)\
- if t.nsec >= 1e9 {\
+ nsec := int32(t.nsec) + int32(d%1e9)\
+ if nsec >= 1e9 {\
\tt.sec++\
- \tt.nsec -= 1e9\
- } else if t.nsec < 0 {\
+ \tnsec -= 1e9\
+ } else if nsec < 0 {\
\tt.sec--\
- \tt.nsec += 1e9\
+ \tnsec += 1e9\
}\
+\tt.nsec = uintptr(nsec)\
return t\
}
- 変更点:
t.nsec
の直接的な加算が、一時変数nsec
を介した操作に変更され、最後にuintptr
に型変換してt.nsec
に代入されています。 - 解説:
t.nsec
がuintptr
型になったため、int32
型のd % 1e9
を直接加算することはできません。そのため、t.nsec
をint32
に型変換し、d % 1e9
を加算して一時変数nsec
に格納します。ナノ秒の範囲チェックと秒数への繰り上がり/繰り下がりを行った後、最終的なnsec
の値をuintptr
に型変換してt.nsec
に戻しています。これにより、nsec
の値がuintptr
として正しく保持されつつ、ナノ秒としての算術演算がint32
の精度で行われます。
Sub
メソッド
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -621,7 +629,7 @@ func (t Time) Add(d Duration) Time {\
// will be returned.\
// To compute t-d for a duration d, use t.Add(-d).\
func (t Time) Sub(u Time) Duration {\
- d := Duration(t.sec-u.sec)*Second + Duration(t.nsec-u.nsec)\
+ d := Duration(t.sec-u.sec)*Second + Duration(int32(t.nsec)-int32(u.nsec))\
// Check for overflow or underflow.\
switch {\
case u.Add(d).Equal(t):\
- 変更点:
t.nsec - u.nsec
の部分がint32(t.nsec) - int32(u.nsec)
に変更されました。 - 解説:
nsec
がuintptr
型であるため、直接減算するとuintptr
としての減算が行われてしまいます。ナノ秒の差を計算するためには、両方のnsec
をint32
に型変換してから減算する必要があります。これにより、ナノ秒の差が正しく計算され、Duration
型に変換されます。
Now
関数
--- a/src/pkg/time/time.go
+++ b/src/pkg/time/time.go
@@ -776,7 +784,7 @@ func now() (sec int64, nsec int32)\
// Now returns the current local time.\
func Now() Time {\
sec, nsec := now()\
- return Time{sec + unixToInternal, nsec, Local}\
+ return Time{sec + unixToInternal, uintptr(nsec), Local}\
}
- 変更点:
Time
構造体の初期化時に、nsec
の値がuintptr(nsec)
と明示的に型変換されています。 - 解説:
now()
関数はnsec
をint32
として返しますが、Time
構造体のnsec
フィールドはuintptr
型であるため、ここで型変換が必要です。
同様の型変換は、GobDecode
、Unix
、Date
、div
といった nsec
を扱う他の関数やメソッドでも行われています。これらの変更はすべて、nsec
フィールドの型が int32
から uintptr
に変更されたことに伴う、型安全性を保ちつつ正しい算術演算を行うための修正です。
関連リンク
- Go Issue #5749: このコミットが修正したバグの元のIssue。コミットメッセージに記載されている
https://code.google.com/p/go/issues/detail?id=5749
は、Goプロジェクトが以前使用していたGoogle CodeのIssueトラッカーへのリンクであり、現在はアーカイブされているか、別のIssueトラッカーに移行している可能性があります。現在の検索結果では、GoLand IDEのIssueトラッカー (JetBrains YouTrack) のGO-5749
がヒットしますが、これは本コミットとは無関係です。 - Go CL 10504043: このコミットに対応するGoの変更リスト (Change List)。
参考にした情報源リンク
- Go言語の公式ドキュメント (特に
time
パッケージ、uintptr
型、メモリモデルに関する記述) - Go言語のガベージコレクタに関する技術記事や論文 (GoのGCの仕組み、ポインタの識別など)
amd64
アーキテクチャにおけるデータアラインメントとメモリレイアウトに関する一般的な情報- GoのIssueトラッカーやメーリングリストでの関連議論 (特にIssue 5749)
- Goのソースコード (特に
runtime
パッケージやsrc/pkg/time
パッケージ)