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

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

このコミットは、GoランタイムにSetPanicOnFault関数を追加し、予期せぬメモリアクセス違反(メモリフォールト)が発生した際に、プログラムをクラッシュさせる代わりにGoのパニック機構をトリガーするように変更するものです。これにより、特定の高度なユースケース(メモリマップドファイルの使用やアドレス空間のプロービングなど)において、メモリアクセス違反からの回復を可能にします。

変更されたファイルは以下の通りです。

  • doc/go1.3.txt: Go 1.3のリリースノートにSetPanicOnFaultの追加を記載。
  • src/pkg/runtime/debug/garbage.go: debugパッケージにSetPanicOnFault関数の宣言を追加。
  • src/pkg/runtime/os_darwin.c, src/pkg/runtime/os_dragonfly.c, src/pkg/runtime/os_freebsd.c, src/pkg/runtime/os_linux.c, src/pkg/runtime/os_netbsd.c, src/pkg/runtime/os_openbsd.c, src/pkg/runtime/os_solaris.c, src/pkg/runtime/os_windows.c: 各OS固有のシグナルハンドリングロジックを修正し、paniconfaultフラグが設定されている場合にパニックをトリガーするように変更。
  • src/pkg/runtime/proc.c: ゴルーチン終了時にpaniconfaultフラグをリセットする処理を追加。
  • src/pkg/runtime/rdebug.goc: SetPanicOnFault関数の実装(GoからCコードへのブリッジ)。
  • src/pkg/runtime/runtime.h: G (ゴルーチン) 構造体にpaniconfaultフィールドを追加。
  • src/pkg/runtime/runtime_test.go: SetPanicOnFaultの動作を検証するテストケースを追加。

コミット

commit e56c6e75353d32a97a301d4890b58a4e10963d82
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 20 16:18:05 2014 -0500

    runtime/debug: add SetPanicOnFault
    
    SetPanicOnFault allows recovery from unexpected memory faults.
    This can be useful if you are using a memory-mapped file
    or probing the address space of the current program.
    
    LGTM=r
    R=r
    CC=golang-codereviews
    https://golang.org/cl/66590044

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

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

元コミット内容

runtime/debug: add SetPanicOnFault

SetPanicOnFault allows recovery from unexpected memory faults.
This can be useful if you are using a memory-mapped file
or probing the address space of the current program.

変更の背景

Goプログラムが不正なメモリアドレスにアクセスしようとすると、通常、オペレーティングシステム(OS)はシグナル(Unix系OSではSIGSEGVSIGBUS、WindowsではEXCEPTION_ACCESS_VIOLATIONなど)をプロセスに送信します。Goランタイムはこれらのシグナルを捕捉し、通常はプログラムを即座にクラッシュさせます。これは、メモリアクセス違反がプログラムの深刻なバグ(例えば、nilポインタのデリファレンスや解放済みメモリへのアクセス)を示しているため、安全策として妥当な挙動です。

しかし、特定の高度なプログラミングシナリオでは、意図的に不正なメモリアドレスへのアクセスを試み、その結果として発生するメモリフォールトを捕捉して処理したい場合があります。例えば、以下のようなケースが挙げられます。

  1. メモリマップドファイル (Memory-mapped files): 非常に大きなファイルをメモリにマップし、その一部を読み書きする際に、マップされていない領域にアクセスするとメモリフォールトが発生します。これを捕捉して、必要に応じて追加のページをマップしたり、エラーを適切に処理したりしたい場合があります。
  2. アドレス空間のプロービング (Probing the address space): プログラム自身のアドレス空間の特定の領域がアクセス可能かどうかをテストするために、意図的にアクセスを試みることがあります。これは、カスタムアロケータの実装や、特定のメモリ領域の存在確認などに利用されます。

これらのシナリオでは、メモリフォールトは必ずしもプログラムのバグを示すものではなく、むしろ予期されたイベントとして扱われるべきです。しかし、従来のGoランタイムの挙動では、このようなフォールトが発生するとプログラムがクラッシュしてしまい、回復の機会がありませんでした。

このコミットは、このような特殊なケースに対応するため、メモリフォールトが発生した際にプログラムをクラッシュさせる代わりに、Goのパニック機構をトリガーする機能を提供します。これにより、Goのdeferrecoverメカニズムを使用して、メモリアクセス違反から回復し、エラーを適切に処理することが可能になります。

前提知識の解説

このコミットの理解には、以下の概念が重要です。

1. メモリフォールト (Memory Fault) / セグメンテーション違反 (Segmentation Fault) / バスエラー (Bus Error)

  • メモリフォールト: プログラムがアクセス権のないメモリ領域にアクセスしようとしたり、存在しないメモリ領域にアクセスしようとしたりする際に発生するエラーです。OSがこれを検知し、プロセスにシグナルを送信します。
  • セグメンテーション違反 (SIGSEGV): 主に、プログラムがアクセスを許可されていないメモリ領域(例: 読み取り専用のコードセグメントへの書き込み、OSが予約している領域へのアクセス)にアクセスしようとした場合に発生します。また、nilポインタのデリファレンスもこれに該当することが多いです。
  • バスエラー (SIGBUS): 主に、存在しない物理アドレスにアクセスしようとしたり、アラインメントされていないメモリアクセスを行ったりした場合に発生します。メモリマップドファイルで、マップされていない領域にアクセスしようとした場合もこれに該当することがあります。
  • WindowsにおけるEXCEPTION_ACCESS_VIOLATION: Windows OSにおけるメモリアクセス違反の例外です。Unix系のSIGSEGVSIGBUSに相当します。

これらのエラーは、通常、プログラムの深刻なバグを示しており、OSはプロセスを強制終了(クラッシュ)させることが一般的です。

2. Goのパニック (Panic) と回復 (Recover)

Goには、プログラムの異常な状態を処理するための組み込みメカニズムとして「パニック」と「回復」があります。

  • パニック: プログラムが回復不能なエラー状態に陥ったことを示すものです。Goランタイムがパニックを検知すると、現在のゴルーチンの実行を停止し、遅延関数(deferで登録された関数)を順に実行しながらスタックを巻き戻します。もしスタックの巻き戻し中にrecoverが呼び出されなければ、プログラム全体がクラッシュします。
  • 回復 (recover): defer関数内で呼び出すことで、パニックを捕捉し、パニック状態から回復することができます。recoverが呼び出されると、パニックの原因となった値が返され、プログラムの実行はrecoverが呼び出されたdefer関数の直後から再開されます。これにより、プログラムのクラッシュを防ぎ、エラーを適切に処理することが可能になります。

通常、OSが発行するメモリフォールトシグナルは、Goランタイムによって捕捉され、直接プログラムのクラッシュに繋がります。SetPanicOnFaultは、この挙動を変更し、OSシグナルをGoのパニックに変換することで、recoverによる回復の機会を提供します。

3. ゴルーチン (Goroutine)

Goの軽量な並行処理の単位です。Goプログラムは多数のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ち、独立して実行されます。SetPanicOnFaultは、呼び出したゴルーチンにのみ影響を与え、他のゴルーチンの挙動には影響しません。

技術的詳細

このコミットの技術的な核心は、GoランタイムがOSからのメモリフォールトシグナルを処理する方法を変更することにあります。

Goランタイムは、OSから送られてくるSIGSEGVSIGBUS(Unix系)、EXCEPTION_ACCESS_VIOLATION(Windows)といったシグナルを捕捉するための独自のシグナルハンドラを持っています。通常、これらのシグナルは、nilポインタのデリファレンスなど、Goのコード内で発生した特定の既知のメモリアクセス違反をGoのパニックに変換します。しかし、それ以外の「予期せぬ」メモリアクセス違反(例えば、0x1000以上の高アドレスでのフォールト)は、ランタイムによって「回復不能なエラー」と判断され、プログラムが即座にクラッシュする原因となっていました。

SetPanicOnFaulttrueに設定されると、このランタイムのシグナルハンドラの挙動が変更されます。具体的には、OSからメモリフォールトシグナルが送られてきた際に、それが通常のnilポインタデリファレンスなどによるものではなく、かつpaniconfaultフラグが設定されている場合、ランタイムはプログラムをクラッシュさせる代わりに、Goのパニックをトリガーします。これにより、アプリケーションコードはdeferrecoverを使ってこのパニックを捕捉し、エラー処理を行うことができるようになります。

この機能は、ゴルーチンごとに有効/無効を設定できます。これは、G(ゴルーチン)構造体にpaniconfaultという新しいブール型フィールドが追加されたことからもわかります。SetPanicOnFault関数が呼び出されると、現在のゴルーチンのG構造体にあるpaniconfaultフィールドが設定されます。

OS固有のシグナルハンドラ(os_darwin.c, os_linux.cなど)では、sigpanic関数内でシグナルを処理する際に、従来の条件(例: g->sigcode1 < 0x1000、つまり低アドレスでのフォールト)に加えて、g->paniconfaulttrueであるかどうかがチェックされます。この条件が追加されたことで、paniconfaultが有効な場合は、高アドレスでの予期せぬフォールトであってもパニックに変換されるようになります。

また、ゴルーチンが終了する際には、proc.c内のgoexit0関数でg->paniconfault0(false)にリセットされます。これにより、SetPanicOnFaultの設定が他のゴルーチンに漏れたり、再利用されたゴルーチンに意図せず引き継がれたりするのを防ぎます。

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

1. src/pkg/runtime/debug/garbage.go (現在の src/runtime/debug/debug.go)

SetPanicOnFault関数の宣言が追加されています。

// SetPanicOnFault controls the runtime's behavior when a program faults
// at an unexpected (non-nil) address. Such faults are typically caused by
// bugs such as runtime memory corruption, so the default response is to crash
// the program. Programs working with memory-mapped files or unsafe
// manipulation of memory may cause faults at non-nil addresses in less
// dramatic situations; SetPanicOnFault allows such programs to request
// that the runtime trigger only a panic, not a crash.
// SetPanicOnFault applies only to the current goroutine.
// It returns the previous setting.
func SetPanicOnFault(enabled bool) bool

2. src/pkg/runtime/runtime.h

G (ゴルーチン) 構造体にpaniconfaultフィールドが追加されています。

struct	G
{
	// ... 既存のフィールド ...
	bool	issystem;	// do not output in stack dump
	bool	isbackground;	// ignore in deadlock detector
	bool	preempt;	// preemption signal, duplicates stackguard0 = StackPreempt
	bool	paniconfault;	// panic (instead of crash) on unexpected fault address
	int8	traceignore;	// ignore race detection events
	M*	m;		// for debuggers, but offset not hard-coded
	M*	lockedm;
	// ... 既存のフィールド ...
};

3. src/pkg/runtime/rdebug.goc (現在の src/runtime/debug.go のCgo部分)

SetPanicOnFault関数の実装が追加されています。これはGoからCランタイムへのブリッジです。

func SetPanicOnFault(enabled bool) (old bool) {
	old = g->paniconfault;
	g->paniconfault = enabled;
}

4. src/pkg/runtime/os_*.c (例: src/pkg/runtime/os_linux.c)

各OS固有のシグナルハンドラ(runtime·sigpanic関数)内で、SIGBUSおよびSIGSEGV(WindowsではEXCEPTION_ACCESS_VIOLATION)の処理ロジックにg->paniconfaultのチェックが追加されています。

// SIGBUS のケース
	case SIGBUS:
		if(g->sigcode0 == BUS_ADRERR && g->sigcode1 < 0x1000 || g->paniconfault) { // ここが変更点
			if(g->sigpc == 0)
				runtime·panicstring("call of nil func value");
			runtime·panicstring("invalid memory address or nil pointer dereference");
		}
		runtime·printf("unexpected fault address %p\n", g->sigcode1);
		runtime·throw("fault");
// SIGSEGV のケース
	case SIGSEGV:
		if((g->sigcode0 == 0 || g->sigcode0 == SEGV_MAPERR || g->sigcode0 == SEGV_ACCERR) && g->sigcode1 < 0x1000 || g->paniconfault) { // ここが変更点
			if(g->sigpc == 0)
				runtime·panicstring("call of nil func value");
			runtime·panicstring("invalid memory address or nil pointer dereference");
		}

5. src/pkg/runtime/proc.c

ゴルーチンが終了する際に、paniconfaultフラグをリセットする処理が追加されています。

void
goexit0(G *gp)
{
	// ... 既存の処理 ...
	gp->status = Gdead;
	gp->m = nil;
	gp->lockedm = nil;
	gp->paniconfault = 0; // ここが変更点
	m->curg = nil;
	m->lockedg = nil;
	if(m->locked & ~LockExternal) {
	// ... 既存の処理 ...
}

6. src/pkg/runtime/runtime_test.go

TestSetPanicOnFaultというテスト関数が追加され、SetPanicOnFaultが正しく動作し、意図的に不正なメモリアクセスを発生させた際にパニックが捕捉されることを確認しています。

func TestSetPanicOnFault(t *testing.T) {
	old := debug.SetPanicOnFault(true)
	defer debug.SetPanicOnFault(old)

	defer func() {
		if err := recover(); err == nil {
			t.Fatalf("did not find error in recover")
		}
	}()

	var p *int
	p = (*int)(unsafe.Pointer(^uintptr(0))) // 意図的に不正なアドレスを生成
	println(*p) // ここでメモリアクセス違反が発生し、パニックが期待される
	t.Fatalf("still here - should have faulted")
}

コアとなるコードの解説

  • SetPanicOnFault関数の宣言と実装: runtime/debugパッケージに公開APIとしてSetPanicOnFault(enabled bool) boolが追加されました。この関数は、現在のゴルーチンに対して、予期せぬメモリアクセス違反が発生した際にOSによるクラッシュではなくGoのパニックをトリガーするかどうかを設定します。引数enabledtrueの場合に有効化され、以前の設定値が返されます。内部的には、rdebug.gocで現在のゴルーチン(g)のpaniconfaultフィールドを直接設定しています。
  • G構造体へのpaniconfaultフィールド追加: 各ゴルーチンがこの設定を持つことができるように、runtime.hで定義されているG構造体にbool paniconfault;が追加されました。これにより、SetPanicOnFaultの設定がゴルーチンごとに独立して管理されるようになります。
  • OS固有のシグナルハンドラの変更: os_*.cファイル群(Darwin, Dragonfly, FreeBSD, Linux, NetBSD, OpenBSD, Solaris, Windows)にあるruntime·sigpanic関数が修正されました。この関数はOSから送られてくるシグナルを処理するGoランタイムの主要な部分です。変更点として、SIGBUSSIGSEGV(WindowsではEXCEPTION_ACCESS_VIOLATION)のケースにおいて、従来の低アドレス(0x1000未満)でのフォールトチェックに加えて、g->paniconfaulttrueであるかどうかが条件に追加されました。これにより、SetPanicOnFaultが有効なゴルーチンでは、高アドレスでの予期せぬメモリアクセス違反もパニックとして処理されるようになります。
  • ゴルーチン終了時のpaniconfaultリセット: proc.cgoexit0関数は、ゴルーチンが終了する際のクリーンアップ処理を行います。この関数にgp->paniconfault = 0;が追加されたことで、ゴルーチンが終了する際にpaniconfaultフラグが自動的に無効化されます。これは、ゴルーチンがプールされて再利用される場合に、以前のゴルーチンのSetPanicOnFault設定が意図せず引き継がれることを防ぐための重要な処理です。
  • テストケースの追加: runtime_test.goTestSetPanicOnFaultが追加され、この新機能の動作が検証されています。このテストでは、SetPanicOnFault(true)を設定した後、unsafe.Pointer(^uintptr(0))という非常に高いアドレス(通常はアクセスできないアドレス)をデリファレンスすることで意図的にメモリアクセス違反を発生させています。そして、deferrecoverを使ってこのパニックが捕捉されることを確認し、プログラムがクラッシュせずに回復できることを示しています。

これらの変更により、Goプログラムは、特定の高度なシナリオにおいて、OSレベルのメモリアクセス違反をGoのパニックとして扱い、プログラム内で回復処理を行う柔軟性を獲得しました。

関連リンク

参考にした情報源リンク