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

[インデックス 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の基本的なプロセスは以下の通りです。

  1. Markフェーズ: GCは、プログラムの「ルート」(グローバル変数、スタック上の変数、レジスタなど)から到達可能なすべてのオブジェクトを「生きている(live)」とマークします。この際、スタック上のポインタを正確に識別することが非常に重要です。
  2. 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)に対する変更です。

  1. 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であることを示します。
  2. 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は、これらの関数が実行されている際に、このメタデータを使用してスタックを正確にスキャンできるようになります。

  3. 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 の変更

  1. 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はこれを見て、スタック上のどの位置にポインタが存在し、どの位置がスカラー値であるかを正確に判断できます。
  2. gclocals_reflectcall<> の定義: このデータセクションは、reflect.callXXフレーム内のローカル変数のポインタ情報をGCに伝えます。

    • $1 (0x00オフセット): スタックマップの数を指定します。
    • $0 (0x04オフセット): ローカル変数の数を指定します。reflect.callXX関数は、その性質上、独自のローカル変数をほとんど持たないため、ここでは0が設定されています。これにより、GCはこれらのフレームでローカル変数をスキャンする必要がないことを認識します。
  3. FUNCDATA ディレクティブの追加: CALLFNマクロ内にFUNCDATA $FUNCDATA_ArgsPointerMaps,gcargs_reflectcall<>(SB)FUNCDATA $FUNCDATA_LocalsPointerMaps,gclocals_reflectcall<>(SB)が追加されたことで、これらのGCマップがreflect.callXX関数に明示的に関連付けられました。これにより、Goランタイムは、これらの関数が実行されている間、GCがスタックをスキャンするために必要なすべてのポインタ情報を利用できるようになります。

  4. PCDATA ディレクティブの追加: PCDATA $PCDATA_StackMapIndex, $0は、CALL命令の直前でスタックマップのインデックスを0に設定します。これは、GCがスタックをスキャンする際に、特定のプログラムカウンタ(PC)値におけるスタックの状態(どのスタックマップを使用すべきか)を正確に判断できるようにするための重要な情報です。これにより、GCは動的な関数呼び出しの途中でスタックをスキャンする際にも、ポインタの正確な位置を把握できます。

これらの変更は、GoのGCがreflectパッケージを介した動的な関数呼び出しのスタックフレームをより正確に処理できるようにするための基盤を確立し、Goプログラムのメモリ安全性と安定性を向上させます。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント
  • Goのソースコード(特にsrc/pkg/runtimeディレクトリ)
  • Goのガベージコレクションに関する技術記事やブログ
  • Goのアセンブリに関する解説記事
  • GoのIssueトラッカー(ただし、#8030は直接見つからず、コミットメッセージからの推測)
  • Web検索 (例: "Go reflect.callXX gc maps", "Go FUNCDATA PCDATA")