[インデックス 19367] ファイルの概要
このコミットは、Goランタイムにおけるガベージコレクション(GC)の挙動を修正し、特にインターフェース内のポインタのスキャン方法を通常のポインタのスキャン方法と一致させることを目的としています。これにより、reflect.SliceHeader
の特定の利用シナリオで発生していたクラッシュが解決されました。
コミット
commit 68aaf2ccda2dff72ff9a0b368995f1f5614a0924
Author: Russ Cox <rsc@golang.org>
Date: Thu May 15 15:53:36 2014 -0400
runtime: make scan of pointer-in-interface same as scan of pointer
The GC program describing a data structure sometimes trusts the
pointer base type and other times does not (if not, the garbage collector
must fall back on per-allocation type information stored in the heap).
Make the scanning of a pointer in an interface do the same.
This fixes a crash in a particular use of reflect.SliceHeader.
Fixes #8004.
LGTM=khr
R=golang-codereviews, khr
CC=0xe2.0x9a.0x9b, golang-codereviews, iant, r
https://golang.org/cl/100470045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/68aaf2ccda2dff72ff9a0b368995f1f5614a0924
元コミット内容
このコミットは、Goランタイムのガベージコレクタ(GC)が、インターフェース内に含まれるポインタをスキャンする際の挙動を、通常のポインタのスキャン挙動と統一することを目的としています。
GoのGCは、データ構造を記述するGCプログラム(GCメタデータ)を利用して、ヒープ上のポインタを識別し、到達可能性を判断します。このGCプログラムは、ポインタの基底型を信頼する場合と、信頼しない場合があります。信頼しない場合、GCはヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行います。
このコミット以前は、インターフェース内のポインタのスキャンが、通常のポインタのスキャンと異なる挙動を示すことがありました。特に、reflect.SliceHeader
を特定の形で利用した場合に、GCが誤ったスキャンを行い、クラッシュを引き起こす問題(Issue 8004)が発生していました。この変更により、インターフェース内のポインタのスキャンも、ポインタの基底型を信頼しない場合の挙動に合わせることで、このクラッシュが修正されました。
変更の背景
この変更の背景には、Goのガベージコレクタが型情報をどのように利用してヒープをスキャンするかという複雑な問題があります。特に、reflect.SliceHeader
のような低レベルなリフレクション構造体を使用する際に、GCが誤った判断を下す可能性がありました。
reflect.SliceHeader
は、Goのスライスの内部表現(データポインタ、長さ、容量)を直接操作するための構造体です。unsafe.Pointer
と組み合わせて使用することで、Goの型システムを迂回してメモリを直接操作することが可能になります。しかし、このような低レベルな操作は、GCの型推論と競合する可能性があり、GCがオブジェクトの実際の型を正確に判断できない場合に問題を引き起こすことがあります。
Issue 8004は、まさにこの問題に起因するクラッシュでした。具体的には、*[]int
のようなポインタ型と、そのポインタが指すメモリを*reflect.SliceHeader
として扱う場合、GCが*reflect.SliceHeader
の型情報に基づいてスキャンを行うと、スライス内部の要素(この場合はint
)がポインタを含まないため、GCは「ポインタがない」と判断し、そのメモリブロックのスキャンを終了してしまいます。しかし、実際には同じメモリブロックを指す*[]int
は、スライス要素がポインタではないとしても、スライス自体はポインタであり、そのポインタが指すデータはGCの対象となるべきです。このGCの誤った判断が、メモリの不正な解放や、到達可能なオブジェクトが回収されてしまうといった問題を引き起こし、最終的にクラッシュに至っていました。
このコミットは、インターフェース内のポインタのスキャンにおいて、GCがより保守的なアプローチを取るように変更することで、この問題を解決しています。つまり、GCプログラムがポインタの基底型を信頼しない場合と同様に、ヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行うようにしたのです。
前提知識の解説
このコミットを理解するためには、以下のGoのランタイムとガベージコレクションに関する前提知識が必要です。
-
Goのガベージコレクション(GC)の基本: GoのGCは、並行マーク&スイープ方式を採用しています。GCは、プログラムが実行中に到達可能なオブジェクトを特定し、到達不可能なオブジェクトをメモリから解放します。この「到達可能」の判断は、ルートセット(グローバル変数、スタック上の変数、レジスタなど)からポインタをたどっていくことで行われます。
-
GCプログラムとGCメタデータ: Goのコンパイラ(
cmd/gc
)は、各データ型について、GCがその型内のどこにポインタが存在するかを識別するための「GCプログラム」または「GCメタデータ」を生成します。これは、型がポインタを含むかどうか、そしてポインタが含まれる場合、そのオフセットはどこか、といった情報を含みます。このメタデータは、GCがヒープ上のオブジェクトを効率的にスキャンするために使用されます。 -
ポインタの基底型とヒープ上の型情報: GCがオブジェクトをスキャンする際、コンパイラが生成したGCプログラム(ポインタの基底型に基づく)を信頼してスキャンを行う場合と、信頼せずにヒープに保存されているアロケーションごとの型情報にフォールバックする場合があります。
- ポインタの基底型を信頼する場合: コンパイラが生成したGCプログラムが、その型のポインタ構造を完全に記述していると判断できる場合です。GCはこの情報に基づいて効率的にスキャンを行います。
- ヒープ上の型情報にフォールバックする場合: コンパイラが生成したGCプログラムだけでは不十分、または信頼できないと判断される場合です。この場合、GCはメモリブロックがヒープに割り当てられた際に記録された、より正確な型情報(アロケーションごとの型情報)を参照してスキャンを行います。これは、より安全ですが、オーバーヘッドが大きくなる可能性があります。
-
インターフェースの内部構造: Goのインターフェースは、内部的に2つのワードで構成されています。
- 型情報(itab/type descriptor): インターフェースに格納されている値の動的な型に関する情報(メソッドセット、GCメタデータなど)を指すポインタ。
- データポインタ: インターフェースに格納されている実際の値(データ)を指すポインタ。値がポインタ型の場合、このデータポインタはさらにそのポインタが指すデータへのポインタとなります。
-
reflect.SliceHeader
とunsafe.Pointer
:reflect.SliceHeader
: Goのスライスの内部表現(データポインタ、長さ、容量)を定義する構造体です。type SliceHeader struct { Data uintptr Len int Cap int }
unsafe.Pointer
: 任意のGoのポインタ型とuintptr
の間で変換できる特殊なポインタ型です。Goの型システムを迂回してメモリを直接操作することを可能にしますが、非常に危険であり、GCの挙動に影響を与える可能性があります。
これらの知識が、コミットが解決しようとしている問題と、その解決策を理解する上で不可欠です。特に、GCが型情報をどのように利用し、いつフォールバックするのか、そしてインターフェースやunsafe.Pointer
がそのプロセスにどのように影響を与えるのかが重要です。
技術的詳細
このコミットの技術的詳細は、主にGoコンパイラのreflect.c
とGoランタイムのmgc0.c
における変更に集約されます。これらの変更は、インターフェース内のポインタのスキャン挙動を統一し、reflect.SliceHeader
に関連するGCのクラッシュを修正することを目的としています。
src/cmd/gc/reflect.c
の変更
このファイルは、Goコンパイラの一部であり、Goの型システムがどのようにGCプログラム(GCメタデータ)を生成するかを扱っています。特に、ポインタ型(TPTR32
/TPTR64
)のGCメタデータ生成ロジックが変更されています。
変更前は、ポインタが指す型(要素型)がポインタを含まない場合(!haspointers(t->type) || t->type->etype == TUINT8
)、GC_APTRという特別なGCメタデータが生成されていました。このGC_APTRは、GCに対して「このポインタが指すメモリの型情報は信頼できないので、ヒープに保存されているアロケーションごとの型情報を見てスキャンしてください」と指示するものです。
変更後、条件が!haspointers(t->type)
に簡略化されました。これは、ポインタが指す型がポインタを含まない場合に、常にGC_APTRを生成することを意味します。
変更の意図: コミットメッセージのコメントに詳細な説明があります。
// NOTE(rsc): Emitting GC_APTR here for *nonptrtype
// (pointer to non-pointer-containing type) means that
// we do not record 'nonptrtype' and instead tell the
// garbage collector to look up the type of the memory in
// type information stored in the heap. In effect we are telling
// the collector "we don't trust our information - use yours".
// It's not completely clear why we want to do this.
// It does have the effect that if you have a *SliceHeader and a *[]int
// pointing at the same actual slice header, *SliceHeader will not be
// used as an authoritative type for the memory, which is good:
// if the collector scanned the memory as type *SliceHeader, it would
// see no pointers inside but mark the block as scanned, preventing
// the seeing of pointers when we followed the *[]int pointer.
// Perhaps that kind of situation is the rationale.
このコメントは、*nonptrtype
(ポインタを含まない型へのポインタ)に対してGC_APTRを生成する理由を説明しています。これは、コンパイラが生成するGCプログラムが、そのポインタが指すメモリの型情報を「信頼しない」ことをGCに伝えるためです。これにより、GCはヒープに保存されているアロケーションごとの型情報にフォールバックします。
特に、*reflect.SliceHeader
と*[]int
が同じ実際のスライスヘッダを指しているような状況で、この挙動が重要になります。もしGCが*reflect.SliceHeader
の型情報に基づいてメモリをスキャンした場合、SliceHeader
自体はポインタを含まないため、GCはポインタを見つけずにブロックをスキャン済みとしてマークしてしまいます。しかし、同じメモリを*[]int
として参照している場合、GCはスライス内の要素(たとえそれがポインタでなくても)を正しくスキャンする必要があります。GC_APTRを生成することで、*SliceHeader
がメモリの「権威ある型」として使用されなくなり、GCが*[]int
ポインタをたどったときにポインタを見逃すことを防ぎます。
src/pkg/runtime/mgc0.c
の変更
このファイルは、Goランタイムのガベージコレクタの主要なロジックを含んでいます。特に、インターフェース(eface
とiface
)内のデータポインタをスキャンする部分が変更されています。
変更前は、インターフェース内のデータがポインタ型(t->kind & ~KindNoPointers) == KindPtr
)である場合、その要素型(((PtrType*)t)->elem
)のGCメタデータ(gc
フィールド)を直接objti
として使用していました。
変更後、このロジックに条件が追加されました。ポインタ型であることに加えて、その要素型がポインタを含む型でない場合(!(et->kind & KindNoPointers)
)にのみ、要素型のGCメタデータを使用するように変更されました。
変更の意図:
この変更は、reflect.c
でのGCプログラム生成の変更と連携しています。コメントに「This matches the GC programs written by cmd/gc/reflect.c's dgcsym1 in case TPTR32/case TPTR64. See rationale there.」とあるように、reflect.c
でGC_APTR
が生成されるケースと、ランタイムのGCスキャンロジックを一致させています。
つまり、インターフェース内のポインタが指す型がポインタを含まない場合、GCはコンパイラが生成したGCプログラム(gc
フィールド)を直接信頼するのではなく、reflect.c
でGC_APTRが生成されるのと同じように、ヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行うように挙動を統一したのです。これにより、reflect.SliceHeader
のように、GCプログラムがポインタを含まないと誤って判断する可能性がある型であっても、GCがヒープ上の実際の型情報に基づいて正しくスキャンできるようになります。
test/fixedbugs/issue8004.go
の追加
この新しいテストファイルは、このコミットが修正する特定のバグを再現し、修正を検証するためのものです。
test1()
関数とtest2()
関数は、どちらもnew([]int)
でスライスへのポインタを作成し、そのスライスにデータを追加しています。そして、unsafe.Pointer
を使ってこのスライスへのポインタを*reflect.SliceHeader
に変換し、all
という[]interface{}
または[]T
(T
はreflect.SliceHeader
と[]int
のポインタを含む構造体)に格納しています。
その後、runtime.GC()
を呼び出してGCを強制実行し、GC後にスライスの内容が正しく保持されているかを確認しています。
テストの目的:
このテストは、*[]int
と*reflect.SliceHeader
が同じメモリを指している状況で、GCが正しく動作するかを検証します。もしGCが*reflect.SliceHeader
の型情報に基づいて誤ったスキャンを行った場合、スライス内のデータがGCによって不正に回収され、テストが失敗(クラッシュまたはデータ破損)するはずです。このコミットの修正により、GCは正しくスキャンを行い、テストは成功するようになります。
コアとなるコードの変更箇所
src/cmd/gc/reflect.c
--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -1322,7 +1322,22 @@ dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size)
// NOTE: Any changes here need to be made to reflect.PtrTo as well.
if(*off % widthptr != 0)
fatal("dgcsym1: invalid alignment, %T", t);
- if(!haspointers(t->type) || t->type->etype == TUINT8) {
+
+ // NOTE(rsc): Emitting GC_APTR here for *nonptrtype
+ // (pointer to non-pointer-containing type) means that
+ // we do not record 'nonptrtype' and instead tell the
+ // garbage collector to look up the type of the memory in
+ // type information stored in the heap. In effect we are telling
+ // the collector "we don't trust our information - use yours".
+ // It's not completely clear why we want to do this.
+ // It does have the effect that if you have a *SliceHeader and a *[]int
+ // pointing at the same actual slice header, *SliceHeader will not be
+ // used as an authoritative type for the memory, which is good:
+ // if the collector scanned the memory as type *SliceHeader, it would
+ // see no pointers inside but mark the block as scanned, preventing
+ // the seeing of pointers when we followed the *[]int pointer.
+ // Perhaps that kind of situation is the rationale.
+ if(!haspointers(t->type)) {
ot = duintptr(s, ot, GC_APTR);
ot = duintptr(s, ot, *off);
} else {
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -724,7 +724,7 @@ scanblock(Workbuf *wbuf, bool keepworking)
uintptr *pc, precise_type, nominal_size;
uintptr *chan_ret, chancap;
void *obj;
- Type *t;
+ Type *t, *et;
Slice *sliceptr;
String *stringptr;
Frame *stack_ptr, stack_top, stack[GC_STACK_CAPACITY+4];
@@ -941,8 +941,14 @@ scanblock(Workbuf *wbuf, bool keepworking)
continue;
obj = eface->data;
- if((t->kind & ~KindNoPointers) == KindPtr)
- objti = (uintptr)((PtrType*)t)->elem->gc;
+ if((t->kind & ~KindNoPointers) == KindPtr) {
+ // Only use type information if it is a pointer-containing type.
+ // This matches the GC programs written by cmd/gc/reflect.c's
+ // dgcsym1 in case TPTR32/case TPTR64. See rationale there.
+ et = ((PtrType*)t)->elem;
+ if(!(et->kind & KindNoPointers))
+ objti = (uintptr)((PtrType*)t)->elem->gc;
+ }
} else {
obj = eface->data;
objti = (uintptr)t->gc;
@@ -973,8 +979,14 @@ scanblock(Workbuf *wbuf, bool keepworking)
continue;
obj = iface->data;
- if((t->kind & ~KindNoPointers) == KindPtr)
- objti = (uintptr)((PtrType*)t)->elem->gc;
+ if((t->kind & ~KindNoPointers) == KindPtr) {
+ // Only use type information if it is a pointer-containing type.
+ // This matches the GC programs written by cmd/gc/reflect.c's
+ // dgcsym1 in case TPTR32/case TPTR64. See rationale there.
+ et = ((PtrType*)t)->elem;
+ if(!(et->kind & KindNoPointers))
+ objti = (uintptr)((PtrType*)t)->elem->gc;
+ }
} else {
obj = iface->data;
objti = (uintptr)t->gc;
コアとなるコードの解説
src/cmd/gc/reflect.c
の変更点
- 変更前:
if(!haspointers(t->type) || t->type->etype == TUINT8)
- 変更後:
if(!haspointers(t->type))
この変更は、Goコンパイラがポインタ型(TPTR32
/TPTR64
)のGCメタデータを生成する際の条件を簡略化しました。
以前は、ポインタが指す型がポインタを含まない場合、またはその型がTUINT8
(uint8
)である場合にGC_APTR
という特別なGCメタデータが生成されていました。GC_APTR
は、GCに対して「このポインタが指すメモリの型情報は信頼できないので、ヒープに保存されているアロケーションごとの型情報を見てスキャンしてください」と指示するものです。
変更後は、ポインタが指す型がポインタを含まない場合に、常にGC_APTR
を生成するようにしました。これにより、reflect.SliceHeader
のように、コンパイラが生成するGCプログラムがポインタを含まないと誤って判断する可能性がある型であっても、GCがヒープ上の実際の型情報にフォールバックしてスキャンを行うようになります。これは、GCがより安全に、かつ正確にメモリをスキャンするための重要な変更です。
src/pkg/runtime/mgc0.c
の変更点
このファイルでは、scanblock
関数内のインターフェース(eface
とiface
)のデータポインタをスキャンするロジックが変更されました。
- 変更前: インターフェース内のデータがポインタ型である場合、その要素型(
((PtrType*)t)->elem
)のGCメタデータ(gc
フィールド)を直接objti
として使用していました。 - 変更後: インターフェース内のデータがポインタ型であり、かつその要素型がポインタを含む型でない場合(
!(et->kind & KindNoPointers)
)にのみ、要素型のGCメタデータを使用するように条件が追加されました。
この変更は、reflect.c
でのGCプログラム生成の変更と密接に連携しています。ランタイムのGCスキャンロジックを、コンパイラがGC_APTR
を生成するケースと一致させることで、インターフェース内のポインタが指す型がポインタを含まない場合、GCはコンパイラが生成したGCプログラムを直接信頼するのではなく、ヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行うように挙動を統一しました。これにより、reflect.SliceHeader
のようなケースで、GCが誤った型情報に基づいてスキャンを行い、クラッシュを引き起こすことを防ぎます。
関連リンク
- Fixes #8004: https://github.com/golang/go/issues/8004
- Go CL 100470045: https://golang.org/cl/100470045
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (一般的なGCの理解のため)
- Goの
reflect
パッケージに関する公式ドキュメント (特にSliceHeader
の理解のため) - Goのソースコード内のコメント (特に
src/cmd/gc/reflect.c
の変更点に関するコメント) - Issue 8004の議論 (問題の具体的な再現シナリオと背景の理解のため)
- Goのインターフェースの内部構造に関する資料 (インターフェースがGCにどのように影響するかを理解するため)
unsafe.Pointer
に関するGoのドキュメント (その危険性とGCへの影響を理解するため)
# [インデックス 19367] ファイルの概要
このコミットは、Goランタイムにおけるガベージコレクション(GC)の挙動を修正し、特にインターフェース内のポインタのスキャン方法を通常のポインタのスキャン方法と一致させることを目的としています。これにより、`reflect.SliceHeader`の特定の利用シナリオで発生していたクラッシュが解決されました。
## コミット
commit 68aaf2ccda2dff72ff9a0b368995f1f5614a0924 Author: Russ Cox rsc@golang.org Date: Thu May 15 15:53:36 2014 -0400
runtime: make scan of pointer-in-interface same as scan of pointer
The GC program describing a data structure sometimes trusts the
pointer base type and other times does not (if not, the garbage collector
must fall back on per-allocation type information stored in the heap).
Make the scanning of a pointer in an interface do the same.
This fixes a crash in a particular use of reflect.SliceHeader.
Fixes #8004.
LGTM=khr
R=golang-codereviews, khr
CC=0xe2.0x9a.0x9b, golang-codereviews, iant, r
https://golang.org/cl/100470045
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/68aaf2ccda2dff72ff9a0b368995f1f5614a0924](https://github.com/golang/go/commit/68aaf2ccda2dff72ff9a0b368995f1f5614a0924)
## 元コミット内容
このコミットは、Goランタイムのガベージコレクタ(GC)が、インターフェース内に含まれるポインタをスキャンする際の挙動を、通常のポインタのスキャン挙動と統一することを目的としています。
GoのGCは、データ構造を記述するGCプログラム(GCメタデータ)を利用して、ヒープ上のポインタを識別し、到達可能性を判断します。このGCプログラムは、ポインタの基底型を信頼する場合と、信頼しない場合があります。信頼しない場合、GCはヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行います。
このコミット以前は、インターフェース内のポインタのスキャンが、通常のポインタのスキャンと異なる挙動を示すことがありました。特に、`reflect.SliceHeader`を特定の形で利用した場合に、GCが誤ったスキャンを行い、クラッシュを引き起こす問題(Issue 8004)が発生していました。この変更により、インターフェース内のポインタのスキャンも、ポインタの基底型を信頼しない場合の挙動に合わせることで、このクラッシュが修正されました。
## 変更の背景
この変更の背景には、Goのガベージコレクタが型情報をどのように利用してヒープをスキャンするかという複雑な問題があります。特に、`reflect.SliceHeader`のような低レベルなリフレクション構造体を使用する際に、GCが誤った判断を下す可能性がありました。
`reflect.SliceHeader`は、Goのスライスの内部表現(データポインタ、長さ、容量)を直接操作するための構造体です。`unsafe.Pointer`と組み合わせて使用することで、Goの型システムを迂回してメモリを直接操作することが可能になります。しかし、このような低レベルな操作は、GCの型推論と競合する可能性があり、GCがオブジェクトの実際の型を正確に判断できない場合に問題を引き起こすことがあります。
Issue 8004は、まさにこの問題に起因するクラッシュでした。具体的には、`*[]int`のようなポインタ型と、そのポインタが指すメモリを`*reflect.SliceHeader`として扱う場合、GCが`*reflect.SliceHeader`の型情報に基づいてスキャンを行うと、スライス内部の要素(この場合は`int`)がポインタを含まないため、GCは「ポインタがない」と判断し、そのメモリブロックのスキャンを終了してしまいます。しかし、実際には同じメモリブロックを指す`*[]int`は、スライス要素がポインタではないとしても、スライス自体はポインタであり、そのポインタが指すデータはGCの対象となるべきです。このGCの誤った判断が、メモリの不正な解放や、到達可能なオブジェクトが回収されてしまうといった問題を引き起こし、最終的にクラッシュに至っていました。
このコミットは、インターフェース内のポインタのスキャンにおいて、GCがより保守的なアプローチを取るように変更することで、この問題を解決しています。つまり、GCプログラムがポインタの基底型を信頼しない場合と同様に、ヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行うようにしたのです。
## 前提知識の解説
このコミットを理解するためには、以下のGoのランタイムとガベージコレクションに関する前提知識が必要です。
1. **Goのガベージコレクション(GC)の基本**:
GoのGCは、並行マーク&スイープ方式を採用しています。GCは、プログラムが実行中に到達可能なオブジェクトを特定し、到達不可能なオブジェクトをメモリから解放します。この「到達可能」の判断は、ルートセット(グローバル変数、スタック上の変数、レジスタなど)からポインタをたどっていくことで行われます。
2. **GCプログラムとGCメタデータ**:
Goのコンパイラ(`cmd/gc`)は、各データ型について、GCがその型内のどこにポインタが存在するかを識別するための「GCプログラム」または「GCメタデータ」を生成します。これは、型がポインタを含むかどうか、そしてポインタが含まれる場合、そのオフセットはどこか、といった情報を含みます。このメタデータは、GCがヒープ上のオブジェクトを効率的にスキャンするために使用されます。
3. **ポインタの基底型とヒープ上の型情報**:
GCがオブジェクトをスキャンする際、コンパイラが生成したGCプログラム(ポインタの基底型に基づく)を信頼してスキャンを行う場合と、信頼せずにヒープに保存されているアロケーションごとの型情報にフォールバックする場合があります。
* **ポインタの基底型を信頼する場合**: コンパイラが生成したGCプログラムが、その型のポインタ構造を完全に記述していると判断できる場合です。GCはこの情報に基づいて効率的にスキャンを行います。
* **ヒープ上の型情報にフォールバックする場合**: コンパイラが生成したGCプログラムだけでは不十分、または信頼できないと判断される場合です。この場合、GCはメモリブロックがヒープに割り当てられた際に記録された、より正確な型情報(アロケーションごとの型情報)を参照してスキャンを行います。これは、より安全ですが、オーバーヘッドが大きくなる可能性があります。
4. **インターフェースの内部構造**:
Goのインターフェースは、内部的に2つのワードで構成されています。
* **型情報(itab/type descriptor)**: インターフェースに格納されている値の動的な型に関する情報(メソッドセット、GCメタデータなど)を指すポインタ。
* **データポインタ**: インターフェースに格納されている実際の値(データ)を指すポインタ。値がポインタ型の場合、このデータポインタはさらにそのポインタが指すデータへのポインタとなります。
5. **`reflect.SliceHeader`と`unsafe.Pointer`**:
* `reflect.SliceHeader`: Goのスライスの内部表現(データポインタ、長さ、容量)を定義する構造体です。
```go
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
```
* `unsafe.Pointer`: 任意のGoのポインタ型と`uintptr`の間で変換できる特殊なポインタ型です。Goの型システムを迂回してメモリを直接操作することを可能にしますが、非常に危険であり、GCの挙動に影響を与える可能性があります。
これらの知識が、コミットが解決しようとしている問題と、その解決策を理解する上で不可欠です。特に、GCが型情報をどのように利用し、いつフォールバックするのか、そしてインターフェースや`unsafe.Pointer`がそのプロセスにどのように影響を与えるのかが重要です。
## 技術的詳細
このコミットの技術的詳細は、主にGoコンパイラの`reflect.c`とGoランタイムの`mgc0.c`における変更に集約されます。これらの変更は、インターフェース内のポインタのスキャン挙動を統一し、`reflect.SliceHeader`に関連するGCのクラッシュを修正することを目的としています。
### `src/cmd/gc/reflect.c` の変更
このファイルは、Goコンパイラの一部であり、Goの型システムがどのようにGCプログラム(GCメタデータ)を生成するかを扱っています。特に、ポインタ型(`TPTR32`/`TPTR64`)のGCメタデータ生成ロジックが変更されています。
変更前は、ポインタが指す型(要素型)がポインタを含まない場合(`!haspointers(t->type) || t->type->etype == TUINT8`)、GC_APTRという特別なGCメタデータが生成されていました。このGC_APTRは、GCに対して「このポインタが指すメモリの型情報は信頼できないので、ヒープに保存されているアロケーションごとの型情報を見てスキャンしてください」と指示するものです。
変更後、条件が`!haspointers(t->type)`に簡略化されました。これは、ポインタが指す型がポインタを含まない場合に、常にGC_APTRを生成することを意味します。
**変更の意図**:
コミットメッセージのコメントに詳細な説明があります。
```c
// NOTE(rsc): Emitting GC_APTR here for *nonptrtype
// (pointer to non-pointer-containing type) means that
// we do not record 'nonptrtype' and instead tell the
// garbage collector to look up the type of the memory in
// type information stored in the heap. In effect we are telling
// the collector "we don't trust our information - use yours".
// It's not completely clear why we want to do this.
// It does have the effect that you have a *SliceHeader and a *[]int
// pointing at the same actual slice header, *SliceHeader will not be
// used as an authoritative type for the memory, which is good:
// if the collector scanned the memory as type *SliceHeader, it would
// see no pointers inside but mark the block as scanned, preventing
// the seeing of pointers when we followed the *[]int pointer.
// Perhaps that kind of situation is the rationale.
このコメントは、*nonptrtype
(ポインタを含まない型へのポインタ)に対してGC_APTRを生成する理由を説明しています。これは、コンパイラが生成するGCプログラムが、そのポインタが指すメモリの型情報を「信頼しない」ことをGCに伝えるためです。これにより、GCはヒープに保存されているアロケーションごとの型情報にフォールバックします。
特に、*reflect.SliceHeader
と*[]int
が同じ実際のスライスヘッダを指しているような状況で、この挙動が重要になります。もしGCが*reflect.SliceHeader
の型情報に基づいてメモリをスキャンした場合、SliceHeader
自体はポインタを含まないため、GCはポインタを見つけずにブロックをスキャン済みとしてマークしてしまいます。しかし、同じメモリを*[]int
として参照している場合、GCはスライス内の要素(たとえそれがポインタでなくても)を正しくスキャンする必要があります。GC_APTRを生成することで、*SliceHeader
がメモリの「権威ある型」として使用されなくなり、GCが*[]int
ポインタをたどったときにポインタを見逃すことを防ぎます。
src/pkg/runtime/mgc0.c
の変更
このファイルは、Goランタイムのガベージコレクタの主要なロジックを含んでいます。特に、インターフェース(eface
とiface
)内のデータポインタをスキャンする部分が変更されています。
変更前は、インターフェース内のデータがポインタ型(t->kind & ~KindNoPointers) == KindPtr
)である場合、その要素型(((PtrType*)t)->elem
)のGCメタデータ(gc
フィールド)を直接objti
として使用していました。
変更後、このロジックに条件が追加されました。ポインタ型であることに加えて、その要素型がポインタを含む型でない場合(!(et->kind & KindNoPointers)
)にのみ、要素型のGCメタデータを使用するように変更されました。
変更の意図:
この変更は、reflect.c
でのGCプログラム生成の変更と連携しています。コメントに「This matches the GC programs written by cmd/gc/reflect.c's dgcsym1 in case TPTR32/case TPTR64. See rationale there.」とあるように、reflect.c
でGC_APTR
が生成されるケースと、ランタイムのGCスキャンロジックを一致させています。
つまり、インターフェース内のポインタが指す型がポインタを含まない場合、GCはコンパイラが生成したGCプログラム(gc
フィールド)を直接信頼するのではなく、reflect.c
でGC_APTRが生成されるのと同じように、ヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行うように挙動を統一したのです。これにより、reflect.SliceHeader
のように、GCプログラムがポインタを含まないと誤って判断する可能性がある型であっても、GCがヒープ上の実際の型情報に基づいて正しくスキャンできるようになります。
test/fixedbugs/issue8004.go
の追加
この新しいテストファイルは、このコミットが修正する特定のバグを再現し、修正を検証するためのものです。
test1()
関数とtest2()
関数は、どちらもnew([]int)
でスライスへのポインタを作成し、そのスライスにデータを追加しています。そして、unsafe.Pointer
を使ってこのスライスへのポインタを*reflect.SliceHeader
に変換し、all
という[]interface{}
または[]T
(T
はreflect.SliceHeader
と[]int
のポインタを含む構造体)に格納しています。
その後、runtime.GC()
を呼び出してGCを強制実行し、GC後にスライスの内容が正しく保持されているかを確認しています。
テストの目的:
このテストは、*[]int
と*reflect.SliceHeader
が同じメモリを指している状況で、GCが正しく動作するかを検証します。もしGCが*reflect.SliceHeader
の型情報に基づいて誤ったスキャンを行った場合、スライス内のデータがGCによって不正に回収され、テストが失敗(クラッシュまたはデータ破損)するはずです。このコミットの修正により、GCは正しくスキャンを行い、テストは成功するようになります。
コアとなるコードの変更箇所
src/cmd/gc/reflect.c
--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -1322,7 +1322,22 @@ dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size)
// NOTE: Any changes here need to be made to reflect.PtrTo as well.
if(*off % widthptr != 0)
fatal("dgcsym1: invalid alignment, %T", t);
- if(!haspointers(t->type) || t->type->etype == TUINT8) {
+
+ // NOTE(rsc): Emitting GC_APTR here for *nonptrtype
+ // (pointer to non-pointer-containing type) means that
+ // we do not record 'nonptrtype' and instead tell the
+ // garbage collector to look up the type of the memory in
+ // type information stored in the heap. In effect we are telling
+ // the collector "we don't trust our information - use yours".
+ // It's not completely clear why we want to do this.
+ // It does have the effect that you have a *SliceHeader and a *[]int
+ // pointing at the same actual slice header, *SliceHeader will not be
+ // used as an authoritative type for the memory, which is good:
+ // if the collector scanned the memory as type *SliceHeader, it would
+ // see no pointers inside but mark the block as scanned, preventing
+ // the seeing of pointers when we followed the *[]int pointer.
+ // Perhaps that kind of situation is the rationale.
+ if(!haspointers(t->type)) {
ot = duintptr(s, ot, GC_APTR);
ot = duintptr(s, ot, *off);
} else {
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -724,7 +724,7 @@ scanblock(Workbuf *wbuf, bool keepworking)
uintptr *pc, precise_type, nominal_size;
uintptr *chan_ret, chancap;
void *obj;
- Type *t;
+ Type *t, *et;
Slice *sliceptr;
String *stringptr;
Frame *stack_ptr, stack_top, stack[GC_STACK_CAPACITY+4];
@@ -941,8 +941,14 @@ scanblock(Workbuf *wbuf, bool keepworking)
continue;
obj = eface->data;
- if((t->kind & ~KindNoPointers) == KindPtr)
- objti = (uintptr)((PtrType*)t)->elem->gc;
+ if((t->kind & ~KindNoPointers) == KindPtr) {
+ // Only use type information if it is a pointer-containing type.
+ // This matches the GC programs written by cmd/gc/reflect.c's
+ // dgcsym1 in case TPTR32/case TPTR64. See rationale there.
+ et = ((PtrType*)t)->elem;
+ if(!(et->kind & KindNoPointers))
+ objti = (uintptr)((PtrType*)t)->elem->gc;
+ }
} else {
obj = eface->data;
objti = (uintptr)t->gc;
@@ -973,8 +979,14 @@ scanblock(Workbuf *wbuf, bool keepworking)
continue;
obj = iface->data;
- if((t->kind & ~KindNoPointers) == KindPtr)
- objti = (uintptr)((PtrType*)t)->elem->gc;
+ if((t->kind & ~KindNoPointers) == KindPtr) {
+ // Only use type information if it is a pointer-containing type.
+ // This matches the GC programs written by cmd/gc/reflect.c's
+ // dgcsym1 in case TPTR32/case TPTR64. See rationale there.
+ et = ((PtrType*)t)->elem;
+ if(!(et->kind & KindNoPointers))
+ objti = (uintptr)((PtrType*)t)->elem->gc;
+ }
} else {
obj = iface->data;
objti = (uintptr)t->gc;
コアとなるコードの解説
src/cmd/gc/reflect.c
の変更点
- 変更前:
if(!haspointers(t->type) || t->type->etype == TUINT8)
- 変更後:
if(!haspointers(t->type))
この変更は、Goコンパイラがポインタ型(TPTR32
/TPTR64
)のGCメタデータを生成する際の条件を簡略化しました。
以前は、ポインタが指す型がポインタを含まない場合、またはその型がTUINT8
(uint8
)である場合にGC_APTR
という特別なGCメタデータが生成されていました。GC_APTR
は、GCに対して「このポインタが指すメモリの型情報は信頼できないので、ヒープに保存されているアロケーションごとの型情報を見てスキャンしてください」と指示するものです。
変更後は、ポインタが指す型がポインタを含まない場合に、常にGC_APTR
を生成するようにしました。これにより、reflect.SliceHeader
のように、コンパイラが生成するGCプログラムがポインタを含まないと誤って判断する可能性がある型であっても、GCがヒープ上の実際の型情報にフォールバックしてスキャンを行うようになります。これは、GCがより安全に、かつ正確にメモリをスキャンするための重要な変更です。
src/pkg/runtime/mgc0.c
の変更点
このファイルでは、scanblock
関数内のインターフェース(eface
とiface
)のデータポインタをスキャンするロジックが変更されました。
- 変更前: インターフェース内のデータがポインタ型である場合、その要素型(
((PtrType*)t)->elem
)のGCメタデータ(gc
フィールド)を直接objti
として使用していました。 - 変更後: インターフェース内のデータがポインタ型であり、かつその要素型がポインタを含む型でない場合(
!(et->kind & KindNoPointers)
)にのみ、要素型のGCメタデータを使用するように条件が追加されました。
この変更は、reflect.c
でのGCプログラム生成の変更と密接に連携しています。ランタイムのGCスキャンロジックを、コンパイラがGC_APTR
を生成するケースと一致させることで、インターフェース内のポインタが指す型がポインタを含まない場合、GCはコンパイラが生成したGCプログラムを直接信頼するのではなく、ヒープに保存されているアロケーションごとの型情報にフォールバックしてスキャンを行うように挙動を統一しました。これにより、reflect.SliceHeader
のようなケースで、GCが誤った型情報に基づいてスキャンを行い、クラッシュを引き起こすことを防ぎます。
関連リンク
- Fixes #8004: https://github.com/golang/go/issues/8004
- Go CL 100470045: https://golang.org/cl/100470045
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (一般的なGCの理解のため)
- Goの
reflect
パッケージに関する公式ドキュメント (特にSliceHeader
の理解のため) - Goのソースコード内のコメント (特に
src/cmd/gc/reflect.c
の変更点に関するコメント) - Issue 8004の議論 (問題の具体的な再現シナリオと背景の理解のため)
- Goのインターフェースの内部構造に関する資料 (インターフェースがGCにどのように影響するかを理解するため)
unsafe.Pointer
に関するGoのドキュメント (その危険性とGCへの影響を理解するため)