[インデックス 19436] ファイルの概要
このコミットは、Goランタイムにおけるreflect.callXX
フレームのガベージコレクション(GC)マップを提供することで、リフレクションを用いた動的な関数呼び出しにおけるGCの正確性を向上させるものです。具体的には、reflect
パッケージが内部的に使用するアセンブリ関数群(call386
, callamd64
, callarm
など)のスタックフレームに対して、GCがポインタを正しく識別できるようにするためのメタデータ(GCマップ)を追加しています。これにより、これらのフレームがアクティブな間にスタック上に存在するポインタがGCによって誤って解放されたり、逆に解放されるべきメモリが保持され続けたりする問題を解決します。
コミット
commit cee8bcabfaecc064b033b8b19aa36f625760f33f
Author: Keith Randall <khr@golang.org>
Date: Wed May 21 14:28:34 2014 -0700
runtime: provide gc maps for the reflect.callXX frames.
Update #8030
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/100620045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cee8bcabfaecc064b033b8b19aa36f625760f33f
元コミット内容
runtime: provide gc maps for the reflect.callXX frames.
Update #8030
変更の背景
Goのガベージコレクタ(GC)は、プログラムの実行中に不要になったメモリを自動的に解放する役割を担っています。GCが正しく機能するためには、スタック上にあるポインタを正確に識別し、それらが参照しているオブジェクトがまだ「生きている」かどうかを判断する必要があります。もしGCがスタック上のポインタを認識できない場合、そのポインタが参照しているオブジェクトを誤って解放してしまう(Use-After-Free)か、あるいは本来解放されるべきオブジェクトを解放せずにメモリリークを引き起こす可能性があります。
reflect
パッケージは、Goプログラムが実行時に自身の構造を検査し、動的に関数を呼び出すことを可能にする強力な機能を提供します。reflect.Call
のような関数は、内部的にreflect.callXX
というアセンブリで書かれたランタイム関数を呼び出します。これらのcallXX
関数は、動的に渡される引数をスタック上に配置し、対象の関数を呼び出すためのスタックフレームを構築します。
このコミット以前は、これらのreflect.callXX
フレームにはGCマップが適切に提供されていませんでした。そのため、GCがスタックをスキャンする際に、reflect.callXX
フレーム内のスタック上のポインタを正しく識別できず、結果としてGCの正確性に問題が生じる可能性がありました。コミットメッセージにあるUpdate #8030
は、この問題が内部的に認識され、追跡されていたことを示唆しています。この変更は、reflect.Call
の堅牢性とGoランタイム全体の安定性を確保するために不可欠でした。
前提知識の解説
Goのガベージコレクション (Go's Garbage Collection)
GoのGCは、主に「Mark-and-Sweep」アルゴリズムをベースにしています。Go 1.5以降は、より低レイテンシを実現するために「Concurrent GC」が導入されています。GCの基本的なプロセスは以下の通りです。
- Markフェーズ: GCは、プログラムの「ルート」(グローバル変数、スタック上の変数、レジスタなど)から到達可能なすべてのオブジェクトを「生きている(live)」とマークします。この際、スタック上のポインタを正確に識別することが非常に重要です。
- Sweepフェーズ: マークされなかった(到達不可能な)オブジェクトは「死んでいる(dead)」と判断され、そのメモリが再利用のために解放されます。
スタックスキャン (Stack Scanning): GCがスタックをスキャンする際、どのメモリ領域がポインタであり、どのメモリ領域が単なるスカラー値(整数、浮動小数点数など)であるかを区別する必要があります。誤ってスカラー値をポインタと解釈したり、その逆を行ったりすると、GCの誤動作につながります。
GCマップ (GC Maps): この問題を解決するために、Goコンパイラは各関数のスタックフレームに関するメタデータ、すなわちGCマップを生成します。GCマップは、特定のスタックフレーム内のどのオフセットにポインタが存在するかを示すビットマップのような情報です。これにより、GCはスタックをスキャンする際に、どのメモリ位置をポインタとして追跡すべきかを正確に判断できます。
Goのreflect
パッケージ (Go's reflect
Package)
reflect
パッケージは、Goプログラムが実行時に型情報にアクセスし、値を操作するための機能を提供します。これにより、ジェネリックなコードや、実行時に型が決定されるような柔軟なプログラムを作成できます。
reflect.Call
の仕組み:reflect.Call
は、reflect.Value
として表現された関数を、同じくreflect.Value
のスライスとして渡された引数で動的に呼び出すために使用されます。この動的な呼び出しは、コンパイル時には関数のシグネチャが不明な場合や、引数の数が可変である場合などに特に有用です。reflect.callXX
関数:reflect.Call
が実際に動的な関数呼び出しを実行する際には、内部的にGoランタイムのアセンブリコードで実装されたreflect.callXX
関数群(例:reflect.call386
,reflect.callamd64
,reflect.callarm
など)が利用されます。これらの関数は、アーキテクチャ固有のスタックフレームを構築し、引数を配置し、対象の関数にジャンプする役割を担います。
Goのアセンブリ (Go Assembly)
Goは、一部の低レベルなランタイム機能やパフォーマンスが重要な部分でアセンブリ言語を使用します。Goのアセンブリは、一般的なアセンブリ言語とは異なる独自の構文を持っています。
TEXT
: 関数の定義を開始します。DATA
: データセクションを定義し、初期化されたデータを配置します。GLOBL
: シンボルをグローバルに宣言します。FUNCDATA
: 関数に関するメタデータを定義するために使用されます。特に、GCマップの情報をランタイムに提供するために使われます。$FUNCDATA_ArgsPointerMaps
: 関数の引数に関するポインタマップ。$FUNCDATA_LocalsPointerMaps
: 関数のローカル変数に関するポインタマップ。
PCDATA
: プログラムカウンタ(PC)の値に基づいて変化するデータ(例えば、スタックマップのインデックス)を定義するために使用されます。GCがスタックをスキャンする際に、特定のPC値におけるスタックの状態(どのスタックマップを使用すべきか)を判断するのに役立ちます。const_BitsPointer
,const_BitsScalar
: これらはGCマップ内で使用される定数で、それぞれメモリ位置がポインタであるか、スカラー値であるかを示します。これらの定数をビットシフトや論理和で組み合わせることで、複数の引数やローカル変数のポインタ情報をコンパクトに表現します。
技術的詳細
このコミットの主要な目的は、reflect.callXX
フレームがGCマップを持つようにすることです。これにより、GCがこれらのフレームをスキャンする際に、スタック上のポインタを正確に識別できるようになります。
src/cmd/5a/a.y
および src/cmd/5a/y.tab.c
の変更
これらのファイルは、Goのアセンブラ(5a
は386アーキテクチャ用のアセンブラ)のパーサと字句解析器に関連するソースコードです。変更内容は、FUNCDATA
ディレクティブの第4引数(値)が、既存のD_EXTERN
(外部シンボル)やD_STATIC
(静的シンボル)に加えて、新たにD_OREG
(オフセットレジスタ)型も受け入れるように拡張されたことです。
これは、アセンブリコード内でFUNCDATA
がレジスタオフセットを参照するような新しいパターンに対応するために行われました。具体的には、gcargs_reflectcall<>
やgclocals_reflectcall<>
のようなGCマップデータが、アセンブリ内でレジスタオフセットとして参照される可能性があるため、パーサがこれを正しく解釈できるようにするための変更です。
src/pkg/runtime/asm_*.s
の変更
このコミットの核心は、386、AMD64、ARMアーキテクチャ向けのアセンブリファイル(asm_386.s
, asm_amd64.s
, asm_arm.s
)に対する変更です。
-
GCマップデータの定義:
gcargs_reflectcall<>
とgclocals_reflectcall<>
という2つの新しいデータセクションが追加されました。これらは、reflect.callXX
フレームの引数とローカル変数のGCマップを定義します。-
gcargs_reflectcall<>
:DATA gcargs_reflectcall<>+0x00(SB)/4, $1
: 1つのスタックマップが存在することを示します。DATA gcargs_reflectcall<>+0x04(SB)/4, $6
: 3つの引数があることを示します(6
はビットマップの長さを示唆)。DATA gcargs_reflectcall<>+0x08(SB)/4, $(const_BitsPointer+(const_BitsPointer<<2)+(const_BitsScalar<<4))
: これが実際のGCマップのビットパターンです。const_BitsPointer
: ポインタであることを示すビット。const_BitsPointer<<2
: 2ビット左シフト。これは、2番目の引数がポインタであることを示します。const_BitsScalar<<4
: 4ビット左シフト。これは、3番目の引数がスカラーであることを示します。 この組み合わせにより、3つの引数のうち、1番目と2番目がポインタであり、3番目がスカラーであることをGCに伝えます。これは、reflect.Call
が通常、関数ポインタ、引数データへのポインタ、引数サイズという3つの主要な引数を扱うことに対応しています。
-
gclocals_reflectcall<>
:DATA gclocals_reflectcall<>+0x00(SB)/4, $1
: 1つのスタックマップが存在することを示します。DATA gclocals_reflectcall<>+0x04(SB)/4, $0
:reflect.callXX
フレームにはローカル変数が存在しないため、ローカル変数の数は0であることを示します。
-
-
FUNCDATA
ディレクティブの追加:CALLFN
マクロ(reflect.callXX
関数を定義するために使用されるマクロ)内に、以下のFUNCDATA
ディレクティブが追加されました。FUNCDATA $FUNCDATA_ArgsPointerMaps,gcargs_reflectcall<>(SB); FUNCDATA $FUNCDATA_LocalsPointerMaps,gclocals_reflectcall<>(SB);
これにより、先ほど定義した
gcargs_reflectcall<>
とgclocals_reflectcall<>
のGCマップが、reflect.callXX
関数に関連付けられます。GCは、これらの関数が実行されている際に、このメタデータを使用してスタックを正確にスキャンできるようになります。 -
PCDATA
ディレクティブの追加:CALLFN
マクロ内のCALL
命令の直前に、以下のPCDATA
ディレクティブが追加されました。PCDATA $PCDATA_StackMapIndex, $0;
これは、プログラムカウンタ(PC)がこの位置にあるときに、GCが使用すべきスタックマップのインデックスを
0
に設定することを示します。これにより、GCはCALL
命令が実行される直前のスタックの状態を正確に把握し、適切なGCマップを適用してポインタを追跡できます。
これらの変更により、reflect.callXX
関数がスタックフレームを構築する際に、GCが必要とするすべてのポインタ情報がランタイムに提供されるようになり、リフレクションを用いた動的な関数呼び出しにおけるGCの正確性と安全性が大幅に向上しました。
コアとなるコードの変更箇所
src/cmd/5a/a.y
および src/cmd/5a/y.tab.c
--- a/src/cmd/5a/a.y
+++ b/src/cmd/5a/a.y
@@ -336,7 +336,7 @@ inst:
{
if($2.type != D_CONST)
yyerror("index for FUNCDATA must be integer constant");
- if($4.type != D_EXTERN && $4.type != D_STATIC)
+ if($4.type != D_EXTERN && $4.type != D_STATIC && $4.type != D_OREG)
yyerror("value for FUNCDATA must be symbol reference");
outcode($1, Always, &$2, NREG, &$4);
}
FUNCDATA
の第4引数の型チェックにD_OREG
が追加されました。
src/pkg/runtime/asm_386.s
, src/pkg/runtime/asm_amd64.s
, src/pkg/runtime/asm_arm.s
これらのファイルは同様の変更が加えられています。以下はasm_386.s
の例です。
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -343,8 +343,22 @@ TEXT reflect·call(SB), NOSPLIT, $0-16
MOVL $runtime·badreflectcall(SB), AX
JMP AX
+// Argument map for the callXX frames. Each has one
+// stack map (for the single call) with 3 arguments.
+DATA gcargs_reflectcall<>+0x00(SB)/4, $1 // 1 stackmap
+DATA gcargs_reflectcall<>+0x04(SB)/4, $6 // 3 args
+DATA gcargs_reflectcall<>+0x08(SB)/4, $(const_BitsPointer+(const_BitsPointer<<2)+(const_BitsScalar<<4))\
+GLOBL gcargs_reflectcall<>(SB),RODATA,$12
+
+// callXX frames have no locals
+DATA gclocals_reflectcall<>+0x00(SB)/4, $1 // 1 stackmap
+DATA gclocals_reflectcall<>+0x04(SB)/4, $0 // 0 locals
+GLOBL gclocals_reflectcall<>(SB),RODATA,$8
+
#define CALLFN(NAME,MAXSIZE) \
TEXT runtime·NAME(SB), WRAPPER, $MAXSIZE-16;\ \
+\ FUNCDATA $FUNCDATA_ArgsPointerMaps,gcargs_reflectcall<>(SB);\ \
+\ FUNCDATA $FUNCDATA_LocalsPointerMaps,gclocals_reflectcall<>(SB);\\\
/* copy arguments to stack */ \
MOVL argptr+4(FP), SI; \
MOVL argsize+8(FP), CX; \
@@ -353,6 +367,7 @@ TEXT runtime·NAME(SB), WRAPPER, $MAXSIZE-16;\ \
/* call function */ \
MOVL f+0(FP), DX; \
MOVL (DX), AX; \
+\ PCDATA $PCDATA_StackMapIndex, $0;\ \
CALL AX; \
/* copy return values back */ \
MOVL argptr+4(FP), DI; \
コアとなるコードの解説
src/cmd/5a/a.y
および src/cmd/5a/y.tab.c
の変更
これらの変更は、GoのアセンブラがFUNCDATA
ディレクティブの引数として、レジスタオフセット(D_OREG
)を正しく認識できるようにするためのものです。これは、アセンブリコード内でGCマップのデータがレジスタオフセットとして参照される新しいパターンに対応するために必要でした。この変更がなければ、アセンブラは新しいFUNCDATA
の記述を構文エラーとして処理してしまいます。
src/pkg/runtime/asm_*.s
の変更
-
gcargs_reflectcall<>
の定義: このデータセクションは、reflect.callXX
関数に渡される引数のポインタ情報をGCに伝えます。$1
(0x00オフセット): スタックマップの数を指定します。ここでは1つのスタックマップが定義されています。$6
(0x04オフセット): 引数のビットマップの長さを指定します。これは、3つの引数(各2ビットで表現されるため、3 * 2 = 6ビット)に対応します。$(const_BitsPointer+(const_BitsPointer<<2)+(const_BitsScalar<<4))
(0x08オフセット): これが引数のポインタビットマップの具体的な値です。const_BitsPointer
: 最初の引数がポインタであることを示します。const_BitsPointer<<2
: 2ビット左シフトすることで、2番目の引数がポインタであることを示します。const_BitsScalar<<4
: 4ビット左シフトすることで、3番目の引数がスカラー(ポインタではない)であることを示します。 このビットマップは、reflect.Call
が内部的に扱う引数(通常、関数ポインタ、引数データへのポインタ、引数サイズ)の型情報と一致するように設計されています。GCはこれを見て、スタック上のどの位置にポインタが存在し、どの位置がスカラー値であるかを正確に判断できます。
-
gclocals_reflectcall<>
の定義: このデータセクションは、reflect.callXX
フレーム内のローカル変数のポインタ情報をGCに伝えます。$1
(0x00オフセット): スタックマップの数を指定します。$0
(0x04オフセット): ローカル変数の数を指定します。reflect.callXX
関数は、その性質上、独自のローカル変数をほとんど持たないため、ここでは0
が設定されています。これにより、GCはこれらのフレームでローカル変数をスキャンする必要がないことを認識します。
-
FUNCDATA
ディレクティブの追加:CALLFN
マクロ内にFUNCDATA $FUNCDATA_ArgsPointerMaps,gcargs_reflectcall<>(SB)
とFUNCDATA $FUNCDATA_LocalsPointerMaps,gclocals_reflectcall<>(SB)
が追加されたことで、これらのGCマップがreflect.callXX
関数に明示的に関連付けられました。これにより、Goランタイムは、これらの関数が実行されている間、GCがスタックをスキャンするために必要なすべてのポインタ情報を利用できるようになります。 -
PCDATA
ディレクティブの追加:PCDATA $PCDATA_StackMapIndex, $0
は、CALL
命令の直前でスタックマップのインデックスを0
に設定します。これは、GCがスタックをスキャンする際に、特定のプログラムカウンタ(PC)値におけるスタックの状態(どのスタックマップを使用すべきか)を正確に判断できるようにするための重要な情報です。これにより、GCは動的な関数呼び出しの途中でスタックをスキャンする際にも、ポインタの正確な位置を把握できます。
これらの変更は、GoのGCがreflect
パッケージを介した動的な関数呼び出しのスタックフレームをより正確に処理できるようにするための基盤を確立し、Goプログラムのメモリ安全性と安定性を向上させます。
関連リンク
- Go
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - Go アセンブリのドキュメント (Go 1.4のドキュメントですが、基本的な概念は共通): https://go.dev/doc/asm
- Goのガベージコレクションに関するブログ記事 (Go 1.5以降のGCについて): https://go.dev/blog/go15gc
参考にした情報源リンク
- Goの公式ドキュメント
- Goのソースコード(特に
src/pkg/runtime
ディレクトリ) - Goのガベージコレクションに関する技術記事やブログ
- Goのアセンブリに関する解説記事
- GoのIssueトラッカー(ただし、#8030は直接見つからず、コミットメッセージからの推測)
- Web検索 (例: "Go reflect.callXX gc maps", "Go FUNCDATA PCDATA")