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

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

このコミットは、Goランタイムにおけるスタックウォークの挙動を改善し、特にフォールト(例外発生)したスタックフレームの処理中に発生するクラッシュを修正することを目的としています。具体的には、「継続PC (continuation pc)」という概念を導入し、スタックフレームが実行を再開できる正確なプログラムカウンタ(PC)を特定することで、ガベージコレクション(GC)やスタックコピー時のライブネス情報(どの変数がまだ使用されているか)の誤った参照を防ぎます。

コミット

commit 14d2ee1d00b4fcaef569a84cb84888603405ca31
Author: Russ Cox <rsc@golang.org>
Date:   Sat May 31 10:10:12 2014 -0400

    runtime: make continuation pc available to stack walk
    
    The 'continuation pc' is where the frame will continue
    execution, if anywhere. For a frame that stopped execution
    due to a CALL instruction, the continuation pc is immediately
    after the CALL. But for a frame that stopped execution due to
    a fault, the continuation pc is the pc after the most recent CALL
    to deferproc in that frame, or else 0. That is where execution
    will continue, if anywhere.
    
    The liveness information is only recorded for CALL instructions.
    This change makes sure that we never look for liveness information
    except for CALL instructions.
    
    Using a valid PC fixes crashes when a garbage collection or
    stack copying tries to process a stack frame that has faulted.
    
    Record continuation pc in heapdump (format change).
    
    Fixes #8048.
    
    LGTM=iant, khr
    R=khr, iant, dvyukov
    CC=golang-codereviews, r
    https://golang.org/cl/100870044

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

https://github.com/golang/go/commit/14d2ee1d00b4fcaef569a84cb84888603405ca31

元コミット内容

このコミットは、Goランタイムのスタックウォーク機能に「継続PC (continuation pc)」の概念を導入します。継続PCは、スタックフレームが実行を再開する可能性のあるプログラムカウンタを示します。通常の関数呼び出し(CALL命令)による停止の場合、継続PCはCALL命令の直後になります。しかし、フォールト(例えば、nilポインタ参照など)によって実行が停止した場合、継続PCはそのフレーム内で最も新しいdeferprocへのCALL命令の直後、または0(実行が再開されない場合)となります。

この変更の主な目的は、ライブネス情報(ガベージコレクタがどの変数がまだ使用中であるかを判断するために使用する情報)がCALL命令に対してのみ記録されるという事実に対応することです。フォールトしたフレームをGCやスタックコピーが処理しようとした際に、誤ったPCでライブネス情報を参照することで発生していたクラッシュを修正します。また、ヒープダンプのフォーマットも変更され、継続PCが記録されるようになります。このコミットはIssue #8048を修正します。

変更の背景

Goランタイムは、ガベージコレクション(GC)やスタックのコピー(スタックの拡張など)を行う際に、実行中のゴルーチンのスタックをウォーク(走査)し、各スタックフレーム内の変数のライブネス情報(その変数が今後も使用される可能性があるか否か)を判断する必要があります。このライブネス情報は、コンパイラによって生成され、通常は関数呼び出し(CALL命令)の直後のPC(プログラムカウンタ)に関連付けられています。これは、CALL命令の直後が、関数が呼び出された時点での変数の状態を正確に反映しているためです。

しかし、プログラムが予期せぬフォールト(例えば、nilポインタ参照、配列の範囲外アクセスなど)によって停止した場合、その時点のPCは必ずしもCALL命令の直後ではありません。このような状況でGCやスタックコピーがスタックウォークを行うと、フォールト発生時のPCを使用してライブネス情報を参照しようとします。もしそのPCがCALL命令の直後ではない場合、誤ったライブネス情報が取得され、その結果、GCが誤ってまだ使用中のメモリを解放したり、スタックコピーが不正なメモリ領域を読み書きしたりして、ランタイムのクラッシュを引き起こす可能性がありました。

この問題は、特にpanicdeferが絡むシナリオで顕著でした。defer関数は、現在の関数の実行が終了する際に(正常終了かパニックかに関わらず)実行されるようにスケジュールされます。deferのメカニズムはランタイムのdeferproc関数によって管理されており、パニックが発生した場合、ランタイムはスタックを巻き戻し(unwind)、適切なdefer関数を実行しようとします。このプロセス中に、フォールトしたフレームのライブネス情報を正確に扱うことが重要でした。

このコミットは、このようなクラッシュを回避するために、スタックフレームが「実行を継続できる場所」を正確に特定する「継続PC」という概念を導入し、GCやスタックコピーが常に正しいPCでライブネス情報を参照できるようにすることで、ランタイムの堅牢性を向上させます。

前提知識の解説

このコミットの理解には、以下のGoランタイムの概念に関する知識が役立ちます。

  1. プログラムカウンタ (PC): CPUが次に実行する命令のアドレスを指すレジスタです。Goランタイムでは、スタックトレースやデバッグにおいて、どのコードが実行されていたかを特定するためにPCが使用されます。
  2. スタックフレーム: 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどがスタック上に確保される領域です。これがスタックフレームを構成します。スタックウォークは、これらのフレームを順に辿っていくプロセスです。
  3. スタックウォーク (Stack Walk): 実行中のゴルーチンのスタックを、最新の関数呼び出しから遡って(呼び出し元へ向かって)走査するプロセスです。デバッガでのスタックトレース表示、GCでのライブネス情報収集、スタックの拡張・移動などで利用されます。
  4. ライブネス情報 (Liveness Information): ガベージコレクタが、特定の時点においてどの変数がまだ「生きている」(今後プログラムによってアクセスされる可能性がある)かを判断するために使用するメタデータです。Goコンパイラは、各関数呼び出しの直後(CALL命令の直後)のPCに対して、その時点でのライブな変数の情報を生成します。これにより、GCは不要になったメモリを安全に解放できます。
  5. ガベージコレクション (GC): Goの自動メモリ管理システムです。プログラムが動的に確保したメモリのうち、もはや到達不可能(どの変数からも参照されていない)になったものを自動的に解放し、メモリリークを防ぎます。GCは、スタックウォークを通じてライブなオブジェクトを特定します。
  6. panicrecover: Goのエラーハンドリングメカニズムの一つです。panicはプログラムの異常終了を引き起こし、現在のゴルーチンのスタックを巻き戻していきます。この巻き戻し中にdefer関数が実行されます。recoverは、defer関数内でpanicからの回復を試み、プログラムの異常終了を防ぐために使用されます。
  7. deferdeferproc: deferステートメントは、現在の関数がリターンする直前(またはpanicによってスタックが巻き戻される際)に実行される関数をスケジュールします。ランタイム内部では、defer関数はdeferprocというランタイム関数によって管理・実行されます。deferprocは、deferされた関数の引数やPCなどの情報をスタック上に保存します。

技術的詳細

このコミットの核心は、Stkframe構造体にcontinpcフィールドを追加し、スタックウォーク時にこのcontinpcを正確に計算・利用することです。

Stkframe構造体の変更

src/pkg/runtime/runtime.hにおいて、スタックフレームの情報を保持するStkframe構造体にuintptr continpc;が追加されました。

struct Stkframe
{
	Func*	fn;	// function being run
	uintptr	pc;	// program counter within fn
	uintptr	continpc;	// program counter where execution can continue, or 0 if not
	uintptr	lr;	// program counter at caller aka link register
	uintptr	sp;	// stack pointer at pc
	uintptr	fp;	// stack pointer at caller aka frame pointer
};

continpcは、そのフレームが実行を継続できるプログラムカウンタを示します。これが0の場合、そのフレームは「デッド」(実行を継続できない)と見なされます。

gentraceback関数におけるcontinpcの計算

src/pkg/runtime/traceback_arm.csrc/pkg/runtime/traceback_x86.cruntime·gentraceback関数(スタックトレースを生成する主要な関数)が変更され、各スタックフレームのcontinpcを計算するロジックが追加されました。

  • 通常の実行パス: 通常の関数呼び出しの場合、frame.continpcframe.pc(現在のPC)に設定されます。これは、CALL命令の直後が継続点となるためです。
  • フォールト発生時 (waspanicがtrueの場合):
    • sigpanic(シグナルハンドラから呼ばれるパニック処理)の直下にあるフレームの場合、frame.pcはライブネス情報を参照するのに安全な点ではない可能性があります。
    • この場合、ランタイムは、そのフレーム内で最も新しくdeferprocが呼び出された場所のPCをcontinpcとして使用しようとします。これは、deferprocが呼び出された時点でのライブネス情報が、パニック発生時にも有効であると仮定できるためです。
    • 具体的には、現在のゴルーチン(gp)のpanicおよびdeferスタックを走査し、現在のフレームのスタックポインタ(sparg)に対応するdeferエントリを見つけます。そのdeferエントリに記録されているPCがcontinpcとして設定されます。
    • もし対応するdeferエントリが見つからない場合、continpcは0に設定され、そのフレームは「デッド」と見なされます。

mgc0.cstack.cにおけるcontinpcの利用

  • scanframe関数 (src/pkg/runtime/mgc0.c): GCがスタックフレームをスキャンしてライブネス情報を取得する際に、frame->pcの代わりにframe->continpcを使用するように変更されました。

    -	targetpc = frame->pc;
    +	targetpc = frame->continpc;
    +	if(targetpc == 0) {
    +		// Frame is dead.
    +		return true;
    +	}
    

    これにより、GCは常にライブネス情報が正確に記録されているPC(CALL命令の直後)を参照するようになります。continpcが0の場合は、そのフレームはライブなオブジェクトを含まないと判断され、スキャンがスキップされます。

  • adjustframe関数 (src/pkg/runtime/stack.c): スタックのコピーや移動を行う際にフレームを調整するこの関数も、targetpcの決定にframe->continpcを使用するように変更されました。これにより、スタックコピー時にも正しいライブネス情報に基づいてポインタの調整が行われるようになります。

NoArgsマクロの導入

src/pkg/runtime/runtime.hでは、Defer構造体において引数がない場合にargpフィールドに設定される特殊な値としてNoArgsマクロが導入されました。これは以前-1が使用されていた箇所を置き換えるもので、コードの可読性と意図を明確にします。

ヒープダンプの変更

src/pkg/runtime/heapdump.cでは、ヒープダンプの際にスタックフレーム情報にcontinpcも記録されるようになりました。これにより、デバッグやプロファイリングの際に、フォールトしたフレームの継続PCを分析できるようになります。

テストケースの追加

test/fixedbugs/issue8048.goに新しいテストケースが追加されました。このテストは、nilポインタ参照によるパニックを意図的に発生させ、その後にruntime.GC()を呼び出すことで、フォールトしたスタックフレームのGC処理がクラッシュしないことを検証します。特に、deferの有無や、GCが呼び出されるタイミングを変えることで、様々なシナリオでの堅牢性を確認しています。

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

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

  1. src/pkg/runtime/runtime.h:
    • Stkframe構造体にcontinpcフィールドを追加。
    • NoArgsマクロを定義。
  2. src/pkg/runtime/traceback_arm.c / src/pkg/runtime/traceback_x86.c:
    • runtime·gentraceback関数内でStkframe.continpcを計算し設定するロジックを追加。特にパニック発生時のcontinpcの決定ロジックが重要。
    • panicdeferのスタックを走査し、continpcを決定する部分。
  3. src/pkg/runtime/mgc0.c:
    • scanframe関数で、ライブネス情報取得のためにframe->pcの代わりにframe->continpcを使用するように変更。
  4. src/pkg/runtime/stack.c:
    • adjustframe関数で、スタック調整のためにtargetpcの決定にframe->continpcを使用するように変更。
  5. src/pkg/runtime/heapdump.c:
    • dumpframe関数で、ヒープダンプにs->continpcを追加。
  6. src/pkg/runtime/cgocall.c, src/pkg/runtime/panic.c, src/pkg/runtime/proc.c:
    • Defer構造体のargpフィールドに-1を直接設定していた箇所をNoArgsマクロに置き換え。
  7. test/fixedbugs/issue8048.go:
    • フォールトしたフレームでのGC処理の堅牢性を検証する新しいテストケースを追加。

コアとなるコードの解説

src/pkg/runtime/runtime.h

// Stkframe構造体へのcontinpcの追加
struct Stkframe
{
	Func*	fn;	// function being run
	uintptr	pc;	// program counter within fn
	uintptr	continpc;	// program counter where execution can continue, or 0 if not
	uintptr	lr;	// program counter at caller aka link register
	uintptr	sp;	// stack pointer at pc
	uintptr	fp;	// stack pointer at caller aka frame pointer
};

// NoArgsマクロの定義
#define NoArgs ((byte*)-1)

Stkframecontinpcが追加されたことで、各スタックフレームが「どこから実行を再開できるか」という情報を持つことができるようになりました。これは、特にフォールト発生時など、現在のpcがライブネス情報を参照するのに適さない場合に、GCやスタックコピーが正しい参照点を見つけるために不可欠です。NoArgsは、defer構造体で引数がない場合のargpの特殊な値として導入され、コードの意図を明確にしています。

src/pkg/runtime/traceback_arm.c / src/pkg/runtime/traceback_x86.c

これらのファイルでは、runtime·gentraceback関数内でframe.continpcが計算されます。

// ... (前略) ...
	// Determine frame's 'continuation PC', where it can continue.
	// Normally this is the return address on the stack, but if sigpanic
	// is immediately below this function on the stack, then the frame
	// stopped executing due to a trap, and frame.pc is probably not
	// a safe point for looking up liveness information. In this panicking case,
	// the function either doesn't return at all (if it has no defers or if the
	// defers do not recover) or it returns from one of the calls to 
	// deferproc a second time (if the corresponding deferred func recovers).
	// It suffices to assume that the most recent deferproc is the one that
	// returns; everything live at earlier deferprocs is still live at that one.
	frame.continpc = frame.pc; // デフォルトは現在のPC
	if(waspanic) { // パニックが発生している場合
		if(panic != nil && panic->defer->argp == (byte*)sparg)
			frame.continpc = (uintptr)panic->defer->pc; // パニックに関連するdeferのPC
		else if(defer != nil && defer->argp == (byte*)sparg)
			frame.continpc = (uintptr)defer->pc; // 通常のdeferのPC
		else
			frame.continpc = 0; // 継続点がない場合
	}
// ... (後略) ...

このロジックは、continpcの決定がGoのpanicdeferのセマンティクスに深く関連していることを示しています。パニックが発生した場合、スタックフレームの実行は中断され、通常のpcはライブネス情報を正確に反映しない可能性があります。そのため、deferprocが呼び出された場所のPCを「継続点」として利用することで、GCが安全にライブネス情報を取得できるようにしています。これは、deferが実行される際には、その時点でのライブな変数が適切に管理されているという前提に基づいています。

src/pkg/runtime/mgc0.c

// scanframe関数における変更
// ... (前略) ...
	f = frame->fn;
	targetpc = frame->continpc; // frame->pc の代わりに frame->continpc を使用
	if(targetpc == 0) {
		// Frame is dead.
		return true;
	}
	if(targetpc != f->entry)
		targetpc--;
	pcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, targetpc);
// ... (後略) ...

scanframeはGCの重要な部分であり、スタックフレーム内のポインタをスキャンしてライブなオブジェクトを特定します。targetpcframe->continpcに変更することで、GCは常にライブネス情報が正確に記録されているPC(通常はCALL命令の直後)を参照するようになります。これにより、フォールトしたフレームであっても、GCが誤ったライブネス情報に基づいてメモリを解放してしまうことを防ぎ、クラッシュを回避します。targetpc == 0のチェックは、フレームが実行を継続できない状態であることを示し、そのフレームのスキャンをスキップすることで効率化と安全性を確保します。

test/fixedbugs/issue8048.go

// test1f関数の一部
func test1f() {
	// ... (前略) ...
	var x *int
	var b bool
	if b { // bはfalseなので、このifブロックは実行されない
		y := make([]int, 1)
		runtime.GC()
		x = &y[0]
	}
	println(*x) // xはnilなので、ここでパニックが発生する
}

このテストケースは、test1f関数内でnilポインタ参照によるパニックを意図的に発生させます。if bブロックは実行されないため、xnilのままです。println(*x)が実行されると、nilポインタデリファレンスによりパニックが発生します。このパニック発生時に、test1関数で設定されたdefer内のruntime.GC()が呼び出されます。このGCが、フォールトしたtest1fのスタックフレームを安全に処理できるかどうかが検証されます。以前は、このようなシナリオでGCがクラッシュする可能性がありましたが、continpcの導入により、この問題が解決されたことを確認します。

関連リンク

  • Go Change-ID: https://golang.org/cl/100870044 (このコミットに関連するGerritの変更リスト)
  • Go Issue: Fixes #8048 (このコミットが修正したGoのバグトラッカーの課題番号。ただし、現在の公開リポジトリでは直接この番号の課題は見つかりませんでした。これは古い課題番号であるか、内部的な課題管理システムのものである可能性があります。)

参考にした情報源リンク

  • Goコミット 14d2ee1d00b4fcaef569a84cb84888603405ca31 の内容と差分
  • Go言語のランタイム、ガベージコレクション、スタック管理に関する一般的な知識
  • Go言語のpanicdeferのメカニズムに関する一般的な知識
  • プログラムカウンタ、スタックフレーム、ライブネス情報といったコンピュータサイエンスの基本的な概念
  • (Issue #8048の具体的な詳細については、公開されている情報源からは直接特定できませんでした。コミットメッセージに記載されている問題の説明を主な情報源としています。)I have generated the detailed explanation of the commit as requested, following all the specified instructions and chapter structure. The explanation is in Japanese, maximally detailed, and covers the background, prerequisite knowledge, technical details, core code changes, and their explanations. I also included the relevant links and acknowledged the difficulty in finding the specific bug report for Issue #8048.

I will now output the explanation to standard output.

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

このコミットは、Goランタイムにおけるスタックウォークの挙動を改善し、特にフォールト(例外発生)したスタックフレームの処理中に発生するクラッシュを修正することを目的としています。具体的には、「継続PC (continuation pc)」という概念を導入し、スタックフレームが実行を再開できる正確なプログラムカウンタ(PC)を特定することで、ガベージコレクション(GC)やスタックコピー時のライブネス情報(どの変数がまだ使用されているか)の誤った参照を防ぎます。

## コミット

commit 14d2ee1d00b4fcaef569a84cb84888603405ca31 Author: Russ Cox rsc@golang.org Date: Sat May 31 10:10:12 2014 -0400

runtime: make continuation pc available to stack walk

The 'continuation pc' is where the frame will continue
execution, if anywhere. For a frame that stopped execution
due to a CALL instruction, the continuation pc is immediately
after the CALL. But for a frame that stopped execution due to
a fault, the continuation pc is the pc after the most recent CALL
to deferproc in that frame, or else 0. That is where execution
will continue, if anywhere.

The liveness information is only recorded for CALL instructions.
This change makes sure that we never look for liveness information
except for CALL instructions.

Using a valid PC fixes crashes when a garbage collection or
stack copying tries to process a stack frame that has faulted.

Record continuation pc in heapdump (format change).

Fixes #8048.

LGTM=iant, khr
R=khr, iant, dvyukov
CC=golang-codereviews, r
https://golang.org/cl/100870044

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

[https://github.com/golang/go/commit/14d2ee1d00b4fcaef569a84cb84888603405ca31](https://github.com/golang/go/commit/14d2ee1d00b4fcaef569a84cb84888603405ca31)

## 元コミット内容

このコミットは、Goランタイムのスタックウォーク機能に「継続PC (continuation pc)」の概念を導入します。継続PCは、スタックフレームが実行を再開する可能性のあるプログラムカウンタを示します。通常の関数呼び出し(`CALL`命令)による停止の場合、継続PCは`CALL`命令の直後になります。しかし、フォールト(例えば、nilポインタ参照など)によって実行が停止した場合、継続PCはそのフレーム内で最も新しい`deferproc`への`CALL`命令の直後、または0(実行が再開されない場合)となります。

この変更の主な目的は、ライブネス情報(ガベージコレクタがどの変数がまだ使用されているか判断するために使用する情報)が`CALL`命令に対してのみ記録されるという事実に対応することです。フォールトしたフレームをGCやスタックコピーが処理しようとした際に、誤ったPCでライブネス情報を参照することで発生していたクラッシュを修正します。また、ヒープダンプのフォーマットも変更され、継続PCが記録されるようになります。このコミットはIssue #8048を修正します。

## 変更の背景

Goランタイムは、ガベージコレクション(GC)やスタックのコピー(スタックの拡張など)を行う際に、実行中のゴルーチンのスタックをウォーク(走査)し、各スタックフレーム内の変数のライブネス情報(その変数が今後も使用される可能性があるか否か)を判断する必要があります。このライブネス情報は、コンパイラによって生成され、通常は関数呼び出し(`CALL`命令)の直後のPC(プログラムカウンタ)に関連付けられています。これは、`CALL`命令の直後が、関数が呼び出された時点での変数の状態を正確に反映しているためです。

しかし、プログラムが予期せぬフォールト(例えば、nilポインタ参照、配列の範囲外アクセスなど)によって停止した場合、その時点のPCは必ずしも`CALL`命令の直後ではありません。このような状況でGCやスタックコピーがスタックウォークを行うと、フォールト発生時のPCを使用してライブネス情報を参照しようとします。もしそのPCが`CALL`命令の直後ではない場合、誤ったライブネス情報が取得され、その結果、GCが誤ってまだ使用中のメモリを解放したり、スタックコピーが不正なメモリ領域を読み書きしたりして、ランタイムのクラッシュを引き起こす可能性がありました。

この問題は、特に`panic`や`defer`が絡むシナリオで顕著でした。`defer`関数は、現在の関数の実行が終了する際に(正常終了かパニックかに関わらず)実行されるようにスケジュールされます。パニックが発生した場合、ランタイムはスタックを巻き戻し(unwind)、適切な`defer`関数を実行しようとします。このプロセス中に、フォールトしたフレームのライブネス情報を正確に扱うことが重要でした。

このコミットは、このようなクラッシュを回避するために、スタックフレームが「実行を継続できる場所」を正確に特定する「継続PC」という概念を導入し、GCやスタックコピーが常に正しいPCでライブネス情報を参照できるようにすることで、ランタイムの堅牢性を向上させます。

## 前提知識の解説

このコミットの理解には、以下のGoランタイムの概念に関する知識が役立ちます。

1.  **プログラムカウンタ (PC)**: CPUが次に実行する命令のアドレスを指すレジスタです。Goランタイムでは、スタックトレースやデバッグにおいて、どのコードが実行されていたかを特定するためにPCが使用されます。
2.  **スタックフレーム**: 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどがスタック上に確保される領域です。これがスタックフレームを構成します。スタックウォークは、これらのフレームを順に辿っていくプロセスです。
3.  **スタックウォーク (Stack Walk)**: 実行中のゴルーチンのスタックを、最新の関数呼び出しから遡って(呼び出し元へ向かって)走査するプロセスです。デバッガでのスタックトレース表示、GCでのライブネス情報収集、スタックの拡張・移動などで利用されます。
4.  **ライブネス情報 (Liveness Information)**: ガベージコレクタが、特定の時点においてどの変数がまだ「生きている」(今後プログラムによってアクセスされる可能性がある)かを判断するために使用するメタデータです。Goコンパイラは、各関数呼び出しの直後(`CALL`命令の直後)のPCに対して、その時点でのライブな変数の情報を生成します。これにより、GCは不要になったメモリを安全に解放できます。
5.  **ガベージコレクション (GC)**: Goの自動メモリ管理システムです。プログラムが動的に確保したメモリのうち、もはや到達不可能(どの変数からも参照されていない)になったものを自動的に解放し、メモリリークを防ぎます。GCは、スタックウォークを通じてライブなオブジェクトを特定します。
6.  **`panic`と`recover`**: Goのエラーハンドリングメカニズムの一つです。`panic`はプログラムの異常終了を引き起こし、現在のゴルーチンのスタックを巻き戻していきます。この巻き戻し中に`defer`関数が実行されます。`recover`は、`defer`関数内で`panic`からの回復を試み、プログラムの異常終了を防ぐために使用されます。
7.  **`defer`と`deferproc`**: `defer`ステートメントは、現在の関数がリターンする直前(または`panic`によってスタックが巻き戻される際)に実行される関数をスケジュールします。ランタイム内部では、`defer`関数は`deferproc`というランタイム関数によって管理・実行されます。`deferproc`は、`defer`された関数の引数やPCなどの情報をスタック上に保存します。

## 技術的詳細

このコミットの核心は、`Stkframe`構造体に`continpc`フィールドを追加し、スタックウォーク時にこの`continpc`を正確に計算・利用することです。

### `Stkframe`構造体の変更

`src/pkg/runtime/runtime.h`において、スタックフレームの情報を保持する`Stkframe`構造体に`uintptr continpc;`が追加されました。
```c
struct Stkframe
{
	Func*	fn;	// function being run
	uintptr	pc;	// program counter within fn
	uintptr	continpc;	// program counter where execution can continue, or 0 if not
	uintptr	lr;	// program counter at caller aka link register
	uintptr	sp;	// stack pointer at pc
	uintptr	fp;	// stack pointer at caller aka frame pointer
};

continpcは、そのフレームが実行を継続できるプログラムカウンタを示します。これが0の場合、そのフレームは「デッド」(実行を継続できない)と見なされます。

gentraceback関数におけるcontinpcの計算

src/pkg/runtime/traceback_arm.csrc/pkg/runtime/traceback_x86.cruntime·gentraceback関数(スタックトレースを生成する主要な関数)が変更され、各スタックフレームのcontinpcを計算するロジックが追加されました。

  • 通常の実行パス: 通常の関数呼び出しの場合、frame.continpcframe.pc(現在のPC)に設定されます。これは、CALL命令の直後が継続点となるためです。
  • フォールト発生時 (waspanicがtrueの場合):
    • sigpanic(シグナルハンドラから呼ばれるパニック処理)の直下にあるフレームの場合、frame.pcはライブネス情報を参照するのに安全な点ではない可能性があります。
    • この場合、ランタイムは、そのフレーム内で最も新しくdeferprocが呼び出された場所のPCをcontinpcとして使用しようとします。これは、deferprocが呼び出された時点でのライブネス情報が、パニック発生時にも有効であると仮定できるためです。
    • 具体的には、現在のゴルーチン(gp)のpanicおよびdeferスタックを走査し、現在のフレームのスタックポインタ(sparg)に対応するdeferエントリを見つけます。そのdeferエントリに記録されているPCがcontinpcとして設定されます。
    • もし対応するdeferエントリが見つからない場合、continpcは0に設定され、そのフレームは「デッド」と見なされます。

mgc0.cstack.cにおけるcontinpcの利用

  • scanframe関数 (src/pkg/runtime/mgc0.c): GCがスタックフレームをスキャンしてライブネス情報を取得する際に、frame->pcの代わりにframe->continpcを使用するように変更されました。

    -	targetpc = frame->pc;
    +	targetpc = frame->continpc;
    +	if(targetpc == 0) {
    +		// Frame is dead.
    +		return true;
    +	}
    

    これにより、GCは常にライブネス情報が正確に記録されているPC(CALL命令の直後)を参照するようになります。continpcが0の場合は、そのフレームはライブなオブジェクトを含まないと判断され、スキャンがスキップされます。

  • adjustframe関数 (src/pkg/runtime/stack.c): スタックのコピーや移動を行う際にフレームを調整するこの関数も、targetpcの決定にframe->continpcを使用するように変更されました。これにより、スタックコピー時にも正しいライブネス情報に基づいてポインタの調整が行われるようになります。

NoArgsマクロの導入

src/pkg/runtime/runtime.hでは、Defer構造体において引数がない場合にargpフィールドに設定される特殊な値としてNoArgsマクロが導入されました。これは以前-1が使用されていた箇所を置き換えるもので、コードの可読性と意図を明確にします。

ヒープダンプの変更

src/pkg/runtime/heapdump.cでは、ヒープダンプの際にスタックフレーム情報にcontinpcも記録されるようになりました。これにより、デバッグやプロファイリングの際に、フォールトしたフレームの継続PCを分析できるようになります。

テストケースの追加

test/fixedbugs/issue8048.goに新しいテストケースが追加されました。このテストは、nilポインタ参照によるパニックを意図的に発生させ、その後にruntime.GC()を呼び出すことで、フォールトしたスタックフレームのGC処理がクラッシュしないことを検証します。特に、deferの有無や、GCが呼び出されるタイミングを変えることで、様々なシナリオでの堅牢性を確認しています。

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

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

  1. src/pkg/runtime/runtime.h:
    • Stkframe構造体にcontinpcフィールドを追加。
    • NoArgsマクロを定義。
  2. src/pkg/runtime/traceback_arm.c / src/pkg/runtime/traceback_x86.c:
    • runtime·gentraceback関数内でStkframe.continpcを計算し設定するロジックを追加。特にパニック発生時のcontinpcの決定ロジックが重要。
    • panicdeferのスタックを走査し、continpcを決定する部分。
  3. src/pkg/runtime/mgc0.c:
    • scanframe関数で、ライブネス情報取得のためにframe->pcの代わりにframe->continpcを使用するように変更。
  4. src/pkg/runtime/stack.c:
    • adjustframe関数で、スタック調整のためにtargetpcの決定にframe->continpcを使用するように変更。
  5. src/pkg/runtime/heapdump.c:
    • dumpframe関数で、ヒープダンプにs->continpcを追加。
  6. src/pkg/runtime/cgocall.c, src/pkg/runtime/panic.c, src/pkg/runtime/proc.c:
    • Defer構造体のargpフィールドに-1を直接設定していた箇所をNoArgsマクロに置き換え。
  7. test/fixedbugs/issue8048.go:
    • フォールトしたフレームでのGC処理の堅牢性を検証する新しいテストケースを追加。

コアとなるコードの解説

src/pkg/runtime/runtime.h

// Stkframe構造体へのcontinpcの追加
struct Stkframe
{
	Func*	fn;	// function being run
	uintptr	pc;	// program counter within fn
	uintptr	continpc;	// program counter where execution can continue, or 0 if not
	uintptr	lr;	// program counter at caller aka link register
	uintptr	sp;	// stack pointer at pc
	uintptr	fp;	// stack pointer at caller aka frame pointer
};

// NoArgsマクロの定義
#define NoArgs ((byte*)-1)

Stkframecontinpcが追加されたことで、各スタックフレームが「どこから実行を再開できるか」という情報を持つことができるようになりました。これは、特にフォールト発生時など、現在のpcがライブネス情報を参照するのに適さない場合に、GCやスタックコピーが正しい参照点を見つけるために不可欠です。NoArgsは、defer構造体で引数がない場合のargpの特殊な値として導入され、コードの意図を明確にしています。

src/pkg/runtime/traceback_arm.c / src/pkg/runtime/traceback_x86.c

これらのファイルでは、runtime·gentraceback関数内でframe.continpcが計算されます。

// ... (前略) ...
	// Determine frame's 'continuation PC', where it can continue.
	// Normally this is the return address on the stack, but if sigpanic
	// is immediately below this function on the stack, then the frame
	// stopped executing due to a trap, and frame.pc is probably not
	// a safe point for looking up liveness information. In this panicking case,
	// the function either doesn't return at all (if it has no defers or if the
	// defers do not recover) or it returns from one of the calls to 
	// deferproc a second time (if the corresponding deferred func recovers).
	// It suffices to assume that the most recent deferproc is the one that
	// returns; everything live at earlier deferprocs is still live at that one.
	frame.continpc = frame.pc; // デフォルトは現在のPC
	if(waspanic) { // パニックが発生している場合
		if(panic != nil && panic->defer->argp == (byte*)sparg)
			frame.continpc = (uintptr)panic->defer->pc; // パニックに関連するdeferのPC
		else if(defer != nil && defer->argp == (byte*)sparg)
			frame.continpc = (uintptr)defer->pc; // 通常のdeferのPC
		else
			frame.continpc = 0; // 継続点がない場合
	}
// ... (後略) ...

このロジックは、continpcの決定がGoのpanicdeferのセマンティクスに深く関連していることを示しています。パニックが発生した場合、スタックフレームの実行は中断され、通常のpcはライブネス情報を正確に反映しない可能性があります。そのため、deferprocが呼び出された場所のPCを「継続点」として利用することで、GCが安全にライブネス情報を取得できるようにしています。これは、deferが実行される際には、その時点でのライブな変数が適切に管理されているという前提に基づいています。

src/pkg/runtime/mgc0.c

// scanframe関数における変更
// ... (前略) ...
	f = frame->fn;
	targetpc = frame->continpc; // frame->pc の代わりに frame->continpc を使用
	if(targetpc == 0) {
		// Frame is dead.
		return true;
	}
	if(targetpc != f->entry)
		targetpc--;
	pcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, targetpc);
// ... (後略) ...

scanframeはGCの重要な部分であり、スタックフレーム内のポインタをスキャンしてライブなオブジェクトを特定します。targetpcframe->continpcに変更することで、GCは常にライブネス情報が正確に記録されているPC(通常はCALL命令の直後)を参照するようになります。これにより、フォールトしたフレームであっても、GCが誤ったライブネス情報に基づいてメモリを解放してしまうことを防ぎ、クラッシュを回避します。targetpc == 0のチェックは、フレームが実行を継続できない状態であることを示し、そのフレームのスキャンをスキップすることで効率化と安全性を確保します。

test/fixedbugs/issue8048.go

// test1f関数の一部
func test1f() {
	// ... (前略) ...
	var x *int
	var b bool
	if b { // bはfalseなので、このifブロックは実行されない
		y := make([]int, 1)
		runtime.GC()
		x = &y[0]
	}
	println(*x) // xはnilなので、ここでパニックが発生する
}

このテストケースは、test1f関数内でnilポインタ参照によるパニックを意図的に発生させます。if bブロックは実行されないため、xnilのままです。println(*x)が実行されると、nilポインタデリファレンスによりパニックが発生します。このパニック発生時に、test1関数で設定されたdefer内のruntime.GC()が呼び出されます。このGCが、フォールトしたtest1fのスタックフレームを安全に処理できるかどうかが検証されます。以前は、このようなシナリオでGCがクラッシュする可能性がありましたが、continpcの導入により、この問題が解決されたことを確認します。

関連リンク

  • Go Change-ID: https://golang.org/cl/100870044 (このコミットに関連するGerritの変更リスト)
  • Go Issue: Fixes #8048 (このコミットが修正したGoのバグトラッカーの課題番号。ただし、現在の公開リポジトリでは直接この番号の課題は見つかりませんでした。これは古い課題番号であるか、内部的な課題管理システムのものである可能性があります。)

参考にした情報源リンク

  • Goコミット 14d2ee1d00b4fcaef569a84cb84888603405ca31 の内容と差分
  • Go言語のランタイム、ガベージコレクション、スタック管理に関する一般的な知識
  • Go言語のpanicdeferのメカニズムに関する一般的な知識
  • プログラムカウンタ、スタックフレーム、ライブネス情報といったコンピュータサイエンスの基本的な概念
  • (Issue #8048の具体的な詳細については、公開されている情報源からは直接特定できませんでした。コミットメッセージに記載されている問題の説明を主な情報源としています。)