[インデックス 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ではSIGSEGV
やSIGBUS
、WindowsではEXCEPTION_ACCESS_VIOLATION
など)をプロセスに送信します。Goランタイムはこれらのシグナルを捕捉し、通常はプログラムを即座にクラッシュさせます。これは、メモリアクセス違反がプログラムの深刻なバグ(例えば、nilポインタのデリファレンスや解放済みメモリへのアクセス)を示しているため、安全策として妥当な挙動です。
しかし、特定の高度なプログラミングシナリオでは、意図的に不正なメモリアドレスへのアクセスを試み、その結果として発生するメモリフォールトを捕捉して処理したい場合があります。例えば、以下のようなケースが挙げられます。
- メモリマップドファイル (Memory-mapped files): 非常に大きなファイルをメモリにマップし、その一部を読み書きする際に、マップされていない領域にアクセスするとメモリフォールトが発生します。これを捕捉して、必要に応じて追加のページをマップしたり、エラーを適切に処理したりしたい場合があります。
- アドレス空間のプロービング (Probing the address space): プログラム自身のアドレス空間の特定の領域がアクセス可能かどうかをテストするために、意図的にアクセスを試みることがあります。これは、カスタムアロケータの実装や、特定のメモリ領域の存在確認などに利用されます。
これらのシナリオでは、メモリフォールトは必ずしもプログラムのバグを示すものではなく、むしろ予期されたイベントとして扱われるべきです。しかし、従来のGoランタイムの挙動では、このようなフォールトが発生するとプログラムがクラッシュしてしまい、回復の機会がありませんでした。
このコミットは、このような特殊なケースに対応するため、メモリフォールトが発生した際にプログラムをクラッシュさせる代わりに、Goのパニック機構をトリガーする機能を提供します。これにより、Goのdefer
とrecover
メカニズムを使用して、メモリアクセス違反から回復し、エラーを適切に処理することが可能になります。
前提知識の解説
このコミットの理解には、以下の概念が重要です。
1. メモリフォールト (Memory Fault) / セグメンテーション違反 (Segmentation Fault) / バスエラー (Bus Error)
- メモリフォールト: プログラムがアクセス権のないメモリ領域にアクセスしようとしたり、存在しないメモリ領域にアクセスしようとしたりする際に発生するエラーです。OSがこれを検知し、プロセスにシグナルを送信します。
- セグメンテーション違反 (
SIGSEGV
): 主に、プログラムがアクセスを許可されていないメモリ領域(例: 読み取り専用のコードセグメントへの書き込み、OSが予約している領域へのアクセス)にアクセスしようとした場合に発生します。また、nilポインタのデリファレンスもこれに該当することが多いです。 - バスエラー (
SIGBUS
): 主に、存在しない物理アドレスにアクセスしようとしたり、アラインメントされていないメモリアクセスを行ったりした場合に発生します。メモリマップドファイルで、マップされていない領域にアクセスしようとした場合もこれに該当することがあります。 - Windowsにおける
EXCEPTION_ACCESS_VIOLATION
: Windows OSにおけるメモリアクセス違反の例外です。Unix系のSIGSEGV
やSIGBUS
に相当します。
これらのエラーは、通常、プログラムの深刻なバグを示しており、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から送られてくるSIGSEGV
やSIGBUS
(Unix系)、EXCEPTION_ACCESS_VIOLATION
(Windows)といったシグナルを捕捉するための独自のシグナルハンドラを持っています。通常、これらのシグナルは、nilポインタのデリファレンスなど、Goのコード内で発生した特定の既知のメモリアクセス違反をGoのパニックに変換します。しかし、それ以外の「予期せぬ」メモリアクセス違反(例えば、0x1000
以上の高アドレスでのフォールト)は、ランタイムによって「回復不能なエラー」と判断され、プログラムが即座にクラッシュする原因となっていました。
SetPanicOnFault
がtrue
に設定されると、このランタイムのシグナルハンドラの挙動が変更されます。具体的には、OSからメモリフォールトシグナルが送られてきた際に、それが通常のnilポインタデリファレンスなどによるものではなく、かつpaniconfault
フラグが設定されている場合、ランタイムはプログラムをクラッシュさせる代わりに、Goのパニックをトリガーします。これにより、アプリケーションコードはdefer
とrecover
を使ってこのパニックを捕捉し、エラー処理を行うことができるようになります。
この機能は、ゴルーチンごとに有効/無効を設定できます。これは、G
(ゴルーチン)構造体にpaniconfault
という新しいブール型フィールドが追加されたことからもわかります。SetPanicOnFault
関数が呼び出されると、現在のゴルーチンのG
構造体にあるpaniconfault
フィールドが設定されます。
OS固有のシグナルハンドラ(os_darwin.c
, os_linux.c
など)では、sigpanic
関数内でシグナルを処理する際に、従来の条件(例: g->sigcode1 < 0x1000
、つまり低アドレスでのフォールト)に加えて、g->paniconfault
がtrue
であるかどうかがチェックされます。この条件が追加されたことで、paniconfault
が有効な場合は、高アドレスでの予期せぬフォールトであってもパニックに変換されるようになります。
また、ゴルーチンが終了する際には、proc.c
内のgoexit0
関数でg->paniconfault
が0
(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のパニックをトリガーするかどうかを設定します。引数enabled
がtrue
の場合に有効化され、以前の設定値が返されます。内部的には、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ランタイムの主要な部分です。変更点として、SIGBUS
やSIGSEGV
(WindowsではEXCEPTION_ACCESS_VIOLATION
)のケースにおいて、従来の低アドレス(0x1000
未満)でのフォールトチェックに加えて、g->paniconfault
がtrue
であるかどうかが条件に追加されました。これにより、SetPanicOnFault
が有効なゴルーチンでは、高アドレスでの予期せぬメモリアクセス違反もパニックとして処理されるようになります。 - ゴルーチン終了時の
paniconfault
リセット:proc.c
のgoexit0
関数は、ゴルーチンが終了する際のクリーンアップ処理を行います。この関数にgp->paniconfault = 0;
が追加されたことで、ゴルーチンが終了する際にpaniconfault
フラグが自動的に無効化されます。これは、ゴルーチンがプールされて再利用される場合に、以前のゴルーチンのSetPanicOnFault
設定が意図せず引き継がれることを防ぐための重要な処理です。 - テストケースの追加:
runtime_test.go
にTestSetPanicOnFault
が追加され、この新機能の動作が検証されています。このテストでは、SetPanicOnFault(true)
を設定した後、unsafe.Pointer(^uintptr(0))
という非常に高いアドレス(通常はアクセスできないアドレス)をデリファレンスすることで意図的にメモリアクセス違反を発生させています。そして、defer
とrecover
を使ってこのパニックが捕捉されることを確認し、プログラムがクラッシュせずに回復できることを示しています。
これらの変更により、Goプログラムは、特定の高度なシナリオにおいて、OSレベルのメモリアクセス違反をGoのパニックとして扱い、プログラム内で回復処理を行う柔軟性を獲得しました。
関連リンク
- Go CL 66590044: https://golang.org/cl/66590044
参考にした情報源リンク
- Goの公式ドキュメント(
runtime/debug
パッケージ): https://pkg.go.dev/runtime/debug#SetPanicOnFault - Goのパニックと回復に関する一般的な情報: https://go.dev/blog/defer-panic-and-recover
- OSシグナルに関する一般的な情報(SIGSEGV, SIGBUSなど)
- メモリマップドファイルに関する一般的な情報
- Goのランタイムソースコード(特に
src/runtime
ディレクトリ)