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

[インデックス 16795] ファイルの概要

このコミットは、Goランタイムにおけるガベージコレクション (GC) ウォーク中のスタックフレームサイズの決定方法を改善することを目的としています。特に、関数の引数サイズに関する新しい情報を使用することで、GCがスタックを正確に辿る際に、以前のような「呼び出し元のスタックフレーム全体を仮定する」というフォールバックが不要になるように変更されています。これにより、GCの精度と効率が向上し、ランタイムの堅牢性が高まります。

コミット

commit a83748596c009db47bcd35a69531e485e2c7f924
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jul 17 12:47:18 2013 -0400

    runtime: use new frame argument size information
    
    With this CL, I believe the runtime always knows
    the frame size during the gc walk. There is no fallback
    to "assume entire stack frame of caller" anymore.
    
    R=golang-dev, khr, cshapiro, dvyukov
    CC=golang-dev
    https://golang.org/cl/11374044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a83748596c009db47bcd35a69531e485e2c7f924

元コミット内容

runtime: use new frame argument size information

With this CL, I believe the runtime always knows
the frame size during the gc walk. There is no fallback
to "assume entire stack frame of caller" anymore.

R=golang-dev, khr, cshapiro, dvyukov
CC=golang-dev
https://golang.org/cl/11374044

変更の背景

Go言語のランタイムは、ガベージコレクション(GC)を実行する際に、実行中のゴルーチン(goroutine)のスタックを正確に辿る必要があります。スタックフレームには、関数のローカル変数や引数などが格納されており、GCはこれらの情報に基づいて到達可能なオブジェクトを特定します。

以前のGoランタイムでは、特定の状況下で関数の引数サイズを正確に特定できない場合がありました。このような場合、GCは「呼び出し元のスタックフレーム全体を仮定する」という保守的なフォールバック戦略を用いていました。この戦略は安全ではありますが、不要なメモリ領域をスキャンしたり、GCの効率を低下させたりする可能性がありました。

このコミットの背景には、Goランタイムが常に正確なスタックフレームサイズ、特に引数サイズを把握できるようにすることで、GCの精度とパフォーマンスを向上させるという目的があります。これにより、GCがより効率的に動作し、ランタイム全体の堅牢性が高まります。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念に関する知識が必要です。

  • スタックフレーム (Stack Frame): 関数が呼び出されるたびに、その関数に必要な情報(ローカル変数、引数、戻りアドレスなど)を格納するためにスタック上に確保されるメモリ領域です。
  • ガベージコレクション (Garbage Collection, GC): プログラムが動的に確保したメモリのうち、もはや使用されていない(到達不可能になった)領域を自動的に解放するプロセスです。GoのGCは、並行マーク&スイープ方式を採用しています。
  • GCウォーク (GC Walk): GCが到達可能なオブジェクトを特定するために、プログラムのルート(グローバル変数、スタック、レジスタなど)からポインタを辿っていくプロセスです。この際、スタックフレームを正確に辿ることが重要になります。
  • PC (Program Counter): 現在実行中の命令のアドレスを指すレジスタです。
  • SP (Stack Pointer): 現在のスタックトップのアドレスを指すレジスタです。
  • FP (Frame Pointer): 現在のスタックフレームのベースアドレスを指すレジスタです。
  • Func 構造体: Goランタイム内部で関数に関するメタデータ(エントリポイント、引数サイズ、PC-SPデータなど)を保持する構造体です。
  • PC-SPデータ (PC-SP data): Goの関数は、プログラムカウンタ (PC) の値に基づいてスタックポインタ (SP) のオフセット情報を持つことがあります。これは、スタックトレースやGCがスタックを辿る際に利用されます。
  • 可変引数関数 (Variadic Functions): 引数の数が可変である関数(例: fmt.Println)。これらの関数の引数サイズは、コンパイル時に固定できないため、ランタイムで特別な処理が必要になります。
  • ... (Ellipsis) in C/Go Assembly: C言語の関数プロトタイプやGoの内部アセンブリコードで ... を使用すると、コンパイラがその関数の引数フレームサイズを決定しないように指示できます。これは、非常に特殊なランタイム関数(例: deferreturn)で、コンパイラが誤った引数サイズを推測するのを防ぐために使用されます。
  • PCQuantum: プログラムカウンタ (PC) の増分単位を示す定数です。アーキテクチャによって異なります。例えば、x86/amd64では1バイト、ARMでは4バイトです。これは、PC-SPデータなどのPCベースのオフセット計算に影響します。
  • PCDATA_ArgSize: Goランタイムの内部データで、関数の引数サイズに関する情報を示すためのインデックスです。

技術的詳細

このコミットの主要な技術的変更点は、Goランタイムがスタックフレーム、特に引数サイズをより正確に決定するためのメカニズムを導入したことです。

  1. PCQuantum の導入:

    • src/pkg/runtime/arch_386.h, src/pkg/runtime/arch_amd64.h, src/pkg/runtime/arch_arm.hPCQuantum 定数が追加されました。
    • これは、PC-SPデータなどのPCベースのオフセット計算において、PCの増分単位をアーキテクチャごとに正確に考慮するために使用されます。以前は、pcshift という変数で同様の目的を果たしていましたが、PCQuantum を直接使用することで、より明確で統一された方法が提供されます。
  2. pcvalue 関数の改善:

    • src/pkg/runtime/symtab.cpcvalue 関数は、PC-SPデータなどのPCベースのテーブルから値を取得するために使用されます。
    • この関数に strict という新しいブール引数が追加されました。stricttrue の場合、PCがテーブルの範囲外であるとランタイムエラーをスローします。false の場合、エラーをスローせずに -1 を返します。これにより、pcvalue の呼び出し元が、PCが有効な範囲内にあるかどうかをより柔軟に制御できるようになります。
    • pcdelta の計算が readvarint(&p) << pcshift; から readvarint(&p) * PCQuantum; に変更されました。これにより、アーキテクチャ固有のPCの量子化が正確に適用されます。
  3. runtime·deferreturn および runtime·_sfloat2 の引数処理:

    • src/pkg/runtime/panic.cruntime·deferreturnsrc/pkg/runtime/softfloat_arm.cruntime·_sfloat2 の関数シグネチャに ... が追加されました。
    • これは、これらの関数が非常に特殊なランタイム関数であり、コンパイラがこれらの関数の引数フレームサイズを誤って推測するのを防ぐためのものです。これにより、GCウォーク中にこれらの関数のスタックフレームが正しく解釈されるようになります。
  4. runtime·topofstack 関数の導入:

    • src/pkg/runtime/proc.cruntime·topofstack(Func *f) という新しい関数が追加されました。
    • この関数は、与えられた Func がゴルーチンのスタックの最上位(例: runtime·goexit, runtime·mstart, runtime·mcall, _rt0_go)を示すかどうかを判断します。
    • これは、スタックトレースやGCウォーク中に、スタックの終端を正確に識別するために使用されます。
  5. runtime·gentraceback の引数サイズ決定ロジックの改善:

    • src/pkg/runtime/traceback_arm.csrc/pkg/runtime/traceback_x86.cruntime·gentraceback 関数は、スタックトレースを生成する際にスタックフレームを辿る主要な関数です。
    • この関数における引数サイズの決定ロジックが大幅に改善されました。
    • 以前は、f->argsruntime·haszeroargsruntime·lessstackdeferproc/newproc、そして「呼び出し元のスタックフレーム全体を仮定する」という保守的なフォールバックなど、複数の条件分岐とフォールバックがありました。
    • 新しいロジックでは、f->args(固定引数サイズ)、flr == nil(スタックの最上位)、runtime·lessstack(スタック分割)、そして runtime·funcarglen(flr, frame.lr)(呼び出し元からの引数サイズ情報)を優先的に使用します。
    • 特に重要なのは、runtime·funcarglenPCDATA_ArgSize を使用して、呼び出し元が記録した引数サイズ情報を取得するようになった点です。これにより、可変引数関数など、コンパイル時に引数サイズが確定しない場合でも、正確なサイズをランタイムで取得できるようになりました。
    • これにより、「呼び出し元のスタックフレーム全体を仮定する」というフォールバックが不要になり、GCウォークの精度が向上します。

これらの変更により、Goランタイムはスタックフレームの構造、特に引数サイズをより正確に把握できるようになり、ガベージコレクションの効率と信頼性が向上しました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/pkg/runtime/arch_*.h (386, amd64, arm):

    • PCQuantum 定数の追加。
      --- a/src/pkg/runtime/arch_386.h
      +++ b/src/pkg/runtime/arch_386.h
      @@ -6,5 +6,6 @@ enum {
       	thechar = '8',
       	BigEndian = 0,
       	CacheLineSize = 64,
      -	appendCrossover = 16
      +	appendCrossover = 16,
      +	PCQuantum = 1
       };
      
      (amd64も同様に PCQuantum = 1、armは PCQuantum = 4)
  2. src/pkg/runtime/symtab.c:

    • pcvalue 関数のシグネチャ変更と PCQuantum の使用。
    • runtime·funcarglenPCDATA_ArgSize を使用。
      --- a/src/pkg/runtime/symtab.c
      +++ b/src/pkg/runtime/symtab.c
      @@ -81,26 +82,17 @@ funcdata(Func *f, int32 i)
       // Return associated data value for targetpc in func f.
       // (Source file is f->src.)
       static int32
      -pcvalue(Func *f, int32 off, uintptr targetpc)
      +pcvalue(Func *f, int32 off, uintptr targetpc, bool strict)
       {
       	byte *p;
       	uintptr pc;
      -	int32 value, vdelta, pcshift;
      +	int32 value, vdelta;
       	uint32 uvdelta, pcdelta;
       
       	enum {
       		debug = 0
       	};
       
      -	switch(thechar) {
      -	case '5':
      -		pcshift = 2;
      -		break;
      -	default:	// 6, 8
      -		pcshift = 0;
      -		break;
      -	}
      -
       	// The table is a delta-encoded sequence of (value, pc) pairs.
       	// Each pair states the given value is in effect up to pc.
       	// The value deltas are signed, zig-zag encoded.
      @@ -126,7 +118,7 @@ pcvalue(Func *f, int32 off, uintptr targetpc)
       		else
       			uvdelta >>= 1;
       		vdelta = (int32)uvdelta;
      -		pcdelta = readvarint(&p) << pcshift;
      +		pcdelta = readvarint(&p) * PCQuantum;
       		value += vdelta;
       		pc += pcdelta;
       		if(debug)
      @@ -137,23 +129,43 @@ pcvalue(Func *f, int32 off, uintptr targetpc)
       	
       	// If there was a table, it should have covered all program counters.
       	// If not, something is wrong.
      +	if(runtime·panicking || !strict)
      +		return -1;
       	runtime·printf("runtime: invalid pc-encoded table f=%S pc=%p targetpc=%p tab=%p\n",
       		*f->name, pc, targetpc, p);
      +	p = (byte*)f + off;
      +	pc = f->entry;
      +	value = -1;
      +	for(;;) {
      +		uvdelta = readvarint(&p);
      +		if(uvdelta == 0 && pc != f->entry)
      +			break;
      +		if(uvdelta&1)
      +			uvdelta = ~(uvdelta>>1);
      +		else
      +			uvdelta >>= 1;
      +		vdelta = (int32)uvdelta;
      +		pcdelta = readvarint(&p) * PCQuantum;
      +		value += vdelta;
      +		pc += pcdelta;
      +		runtime·printf("\tvalue=%d until pc=%p\n", value, pc);
      +	}
      +	
       	runtime·throw("invalid runtime symbol table");
       	return -1;
       }
       
       static String unknown = { (uint8*)"?", 1 };
       
      -int32
      -runtime·funcline(Func *f, uintptr targetpc, String *file)
      +static int32
      +funcline(Func *f, uintptr targetpc, String *file, bool strict)
       {
       	int32 line;
       	int32 fileno;
       
       	*file = unknown;
      -	fileno = pcvalue(f, f->pcfile, targetpc);
      -	line = pcvalue(f, f->pcln, targetpc);
      +	fileno = pcvalue(f, f->pcfile, targetpc, strict);
      +	line = pcvalue(f, f->pcln, targetpc, strict);
       	if(fileno == -1 || line == -1 || fileno >= nfiletab) {
       		// runtime·printf("looking for %p in %S got file=%d line=%d\n", targetpc, *f->name, fileno, line);
       		return 0;
      @@ -162,12 +174,18 @@ runtime·funcline(Func *f, uintptr targetpc, String *file)
       	return line;
       }
       
      +int32
      +runtime·funcline(Func *f, uintptr targetpc, String *file)
      +{
      +	return funcline(f, targetpc, file, true);
      +}
      +
       int32
       runtime·funcspdelta(Func *f, uintptr targetpc)
       {
       	int32 x;
       	
      -	x = pcvalue(f, f->pcsp, targetpc);
      +	x = pcvalue(f, f->pcsp, targetpc, true);
       	if(x&(sizeof(void*)-1))
       		runtime·printf("invalid spdelta %d %d\n", f->pcsp, x);
       	return x;
      @@ -178,19 +196,23 @@ pcdatavalue(Func *f, int32 table, uintptr targetpc)
       {
       	if(table < 0 || table >= f->npcdata)
       		return -1;
      -	return pcvalue(f, (&f->nfuncdata)[1+table], targetpc);
      +	return pcvalue(f, (&f->nfuncdata)[1+table], targetpc, true);
       }
       
       int32
       runtime·funcarglen(Func *f, uintptr targetpc)
       {
      -	return pcdatavalue(f, 0, targetpc);
      +	if(targetpc == f->entry)
      +		return 0;
      +	return pcdatavalue(f, PCDATA_ArgSize, targetpc-PCQuantum);
       }
       
       void
       runtime·funcline_go(Func *f, uintptr targetpc, String retfile, intgo retline)
       {
      -	retline = runtime·funcline(f, targetpc, &retfile);
      +	// Pass strict=false here, because anyone can call this function,
      +	// and they might just be wrong about targetpc belonging to f.
      +	retline = funcline(f, targetpc, &retfile, false);
       	FLUSH(&retline);
       }
      
  3. src/pkg/runtime/proc.c:

    • runtime·topofstack 関数の追加。
      --- a/src/pkg/runtime/proc.c
      +++ b/src/pkg/runtime/proc.c
      @@ -2496,3 +2496,12 @@ runtime·haszeroargs(uintptr pc)
       	pc == (uintptr)_rt0_go;
       }
       
      +// Does f mark the top of a goroutine stack?
      +bool
      +runtime·topofstack(Func *f)
      +{
      +	return f->entry == (uintptr)runtime·goexit ||
      +		f->entry == (uintptr)runtime·mstart ||
      +		f->entry == (uintptr)runtime·mcall ||
      +		f->entry == (uintptr)_rt0_go;
      +}
      
  4. src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c:

    • runtime·gentraceback における引数サイズ決定ロジックの変更と runtime·topofstack の使用。
      --- a/src/pkg/runtime/traceback_arm.c
      +++ b/src/pkg/runtime/traceback_arm.c
      @@ -64,41 +65,57 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
       	if(printing && runtime·showframe(nil, gp))
       		runtime·printf("----- stack segment boundary -----\n");
       	stk = (Stktop*)stk->stackbase;
      -	continue;
      -	}
      -	
      -	if(frame.pc <= 0x1000 || (frame.fn = f = runtime·findfunc(frame.pc)) == nil) {
      -	if(callback != nil) {
      -		runtime·printf("runtime: unknown pc %p at frame %d\n", frame.pc, skip0-skip+n);
      -		runtime·throw("invalid stack");
      +	
      +	f = runtime·findfunc(frame.pc);
      +	if(f == nil) {
      +		runtime·printf("runtime: unknown pc %p after stack split\n", frame.pc);
      +		runtime·throw("unknown pc");
       	}
      -	break;
      +	frame.fn = f;
      +	continue;
       	}
      +	f = frame.fn;
       	
       	// Found an actual function.
       	// Derive frame pointer and link register.
      -	if(frame.lr == 0)
      -		frame.lr = *(uintptr*)frame.sp;
       	if(frame.fp == 0)
       		frame.fp = frame.sp + runtime·funcspdelta(f, frame.pc);
      -
      +	if(runtime·topofstack(f)) {
      +		frame.lr = 0;
      +		flr = nil;
      +	} else {
      +		if(frame.lr == 0)
      +			frame.lr = *(uintptr*)frame.sp;
      +		flr = runtime·findfunc(frame.lr);
      +		if(flr == nil) {
      +			runtime·printf("runtime: unexpected return pc for %S called from %p", *f->name, frame.lr);
      +			runtime·throw("unknown caller pc");
      +		}
      +	}
      +	
       	// Derive size of arguments.
      -	frame.argp = (byte*)frame.fp + sizeof(uintptr);
      -	frame.arglen = 0;
      -	if(f->args != ArgsSizeUnknown)
      -		frame.arglen = f->args;
      -	else if(runtime·haszeroargs(f->entry))
      -		frame.arglen = 0;
      -	else if(frame.lr == (uintptr)runtime·lessstack)
      -		frame.arglen = stk->argsize;
      -	else if(f->entry == (uintptr)runtime·deferproc || f->entry == (uintptr)runtime·newproc)
      -		frame.arglen = 3*sizeof(uintptr) + *(int32*)frame.argp;
      -	else if((f2 = runtime·findfunc(frame.lr)) != nil && f2->frame >= sizeof(uintptr))
      -		frame.arglen = f2->frame; // conservative overestimate
      -	else {
      -		runtime·printf("runtime: unknown argument frame size for %S\n", *f->name);
      -		if(!printing)
      -			runtime·throw("invalid stack");
      +	// Most functions have a fixed-size argument block,
      +	// so we can use metadata about the function f.
      +	// Not all, though: there are some variadic functions
      +	// in package runtime, and for those we use call-specific
      +	// metadata recorded by f's caller.
      +	if(callback != nil || printing) {
      +		frame.argp = (byte*)frame.fp + sizeof(uintptr);
      +		if(f->args != ArgsSizeUnknown)
      +			frame.arglen = f->args;
      +		else if(flr == nil)
      +			frame.arglen = 0;
      +		else if(frame.lr == (uintptr)runtime·lessstack)
      +			frame.arglen = stk->argsize;
      +		else if((i = runtime·funcarglen(flr, frame.lr)) >= 0)
      +			frame.arglen = i;
      +		else {
      +			runtime·printf("runtime: unknown argument frame size for %S called from %p [%S]\n",
      +				*f->name, frame.lr, flr ? *flr->name : unknown);
      +			if(!printing)
      +				runtime·throw("invalid stack");
      +			frame.arglen = 0;
      +		}
       	}
       
       	// Derive location and size of local variables.
      @@ -165,11 +182,12 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
       	waspanic = f->entry == (uintptr)runtime·sigpanic;
       
       	// Do not unwind past the bottom of the stack.
      -	if(f->entry == (uintptr)runtime·goexit || f->entry == (uintptr)runtime·mstart || f->entry == (uintptr)runtime·mcall || f->entry == (uintptr)_rt0_go)
      +	if(flr == nil)
       		break;
       
       	// Unwind to next frame.
       	frame.pc = frame.lr;
      +	frame.fn = flr;
       	frame.lr = 0;
       	frame.sp = frame.fp;
       	frame.fp = 0;
      
      (x86も同様の変更)

コアとなるコードの解説

このコミットの核心は、Goランタイムがスタックフレームの引数サイズをより正確に、かつ常に把握できるようにすることです。

  1. PCQuantum の役割:

    • PCQuantum は、プログラムカウンタ (PC) の最小単位を表します。例えば、x86/amd64では命令がバイト単位で配置されるため 1、ARMでは命令が4バイトアラインされるため 4 となります。
    • symtab.cpcvalue 関数では、PCベースのデータ(PC-SPデータなど)を読み取る際に、この PCQuantum を乗算することで、PCのオフセットを正確に計算します。これにより、異なるアーキテクチャ間でのPCデータの解釈の正確性が保証されます。
  2. pcvaluestrict 引数:

    • pcvalue 関数は、特定のPCに対応する値(例: ファイル番号、行番号、SPオフセット)をルックアップするために使用されます。
    • strict 引数は、ルックアップが失敗した場合のランタイムの挙動を制御します。true の場合、失敗は致命的なエラーと見なされ、ランタイムパニックを引き起こします。false の場合、エラーは抑制され、-1 が返されます。
    • runtime·funcline_go のように、ユーザーコードから呼び出される可能性のある関数では strict=false を使用し、ランタイム内部の厳密な処理では strict=true を使用することで、堅牢性と柔軟性を両立させています。
  3. runtime·deferreturnruntime·_sfloat2...:

    • これらの関数は、Goランタイムの非常に低レベルな部分であり、コンパイラが通常の関数呼び出し規約に従って引数フレームサイズを推測すると問題が発生する可能性があります。
    • ... を使用することで、コンパイラはこれらの関数の引数フレームサイズを決定せず、ランタイムが手動で管理することを許可します。これにより、GCウォーク中にこれらの特殊なスタックフレームが正しく処理されることが保証されます。
  4. runtime·topofstack の導入:

    • この関数は、スタックトレースの終端条件を明確に定義するために導入されました。
    • runtime·goexit (ゴルーチンの終了)、runtime·mstart (Mの開始)、runtime·mcall (Mからの呼び出し)、_rt0_go (初期化ルーチン) などは、ゴルーチンのスタックの最上位を示す特別なエントリポイントです。
    • runtime·gentraceback は、この関数を使用して、スタックの最上位に到達したかどうかを判断し、それ以上スタックをアンワインドしないようにします。これにより、無限ループや不正なメモリアクセスを防ぎます。
  5. runtime·gentraceback における引数サイズ決定の改善:

    • これがこのコミットの最も重要な部分です。以前は、引数サイズの決定が複雑で、場合によっては「呼び出し元のスタックフレーム全体を仮定する」という保守的な推測に頼っていました。
    • 新しいロジックでは、以下の優先順位で引数サイズを決定します。
      • f->args: 関数のメタデータに固定の引数サイズが記録されている場合、それを使用します。これは、ほとんどのGo関数に適用されます。
      • flr == nil: 呼び出し元関数が存在しない場合(つまり、現在のフレームがスタックの最上位である場合)、引数サイズは 0 となります。これは runtime·topofstack と連携して機能します。
      • frame.lr == (uintptr)runtime·lessstack: スタック分割が発生した場合、stk->argsize を使用して引数サイズを取得します。
      • runtime·funcarglen(flr, frame.lr): これが最も重要な改善点です。runtime·funcarglen は、呼び出し元関数 (flr) のPC (frame.lr) に基づいて、PCDATA_ArgSize データから引数サイズを取得します。これにより、可変引数関数など、コンパイル時に引数サイズが確定しない場合でも、呼び出し元が記録した正確な引数サイズ情報を利用できるようになります。
    • この新しいアプローチにより、ランタイムは常に正確な引数サイズを把握できるようになり、GCウォーク中のスタックの正確なスキャンが可能になります。これにより、GCの効率が向上し、不要なメモリ領域のスキャンが削減されます。

これらの変更は、Goランタイムのスタック管理とガベージコレクションの基盤を強化し、より堅牢で効率的な実行環境を提供することに貢献しています。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事
  • Goランタイムのソースコード(特に src/runtime ディレクトリ)
  • Goのコンパイラとアセンブリに関するドキュメント

参考にした情報源リンク