[インデックス 13206] ファイルの概要
このコミットは、GoランタイムにおけるWindows環境での例外処理メカニズムの改善に焦点を当てています。特に、Cgo(C言語との相互運用)を使用するプログラムにおいて、Windowsの構造化例外処理(SEH: Structured Exception Handling)が適切に機能するように修正が加えられています。これにより、GoプログラムがWindows上で予期せぬ例外(例えば、ヌルポインタ参照など)に遭遇した場合でも、ランタイムがそれを捕捉し、panic
/recover
メカニズムを通じて安全に処理できるようになります。
コミット
commit afe0e97aa65be9dd0c147c4c824c12b1442ef2df
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed May 30 15:10:54 2012 +1000
runtime: handle windows exceptions, even in cgo programs
Fixes #3543.
R=golang-dev, kardianos, rsc
CC=golang-dev, hectorchu, vcc.163
https://golang.org/cl/6245063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/afe0e97aa65be9dd0c147c4c824c12b1442ef2df
元コミット内容
このコミットの元のメッセージは以下の通りです。
runtime: handle windows exceptions, even in cgo programs
Fixes #3543.
R=golang-dev, kardianos, rsc
CC=golang-dev, hectorchu, vcc.163
https://golang.org/cl/6245063
これは、GoランタイムがWindows例外を、Cgoプログラム内であっても適切に処理できるようにするという目的を明確に示しています。Fixes #3543
は、このコミットが特定のバグまたは問題(Go issue 3543)を解決することを示唆しています。
変更の背景
Goプログラムは、通常、Goランタイムが提供する独自の例外処理メカニズム(panic
とrecover
)を使用します。しかし、WindowsのようなOSでは、OSレベルで構造化例外処理(SEH)という独自の例外処理メカニズムが存在します。GoプログラムがCgoを介してC/C++コードと連携する場合、C/C++コード内で発生したOSレベルの例外がGoランタイムに適切に伝播されず、Goのpanic
/recover
メカニズムで捕捉できない問題が発生することがありました。
特に、Go issue 3543では、Windows上でCgoを使用するGoプログラムが、Cコード内で発生したアクセス違反などのOS例外によってクラッシュする問題が報告されていました。Goランタイムは、これらのOS例外を捕捉し、Goのpanic
に変換してGoらしい方法で処理できるようにする必要があります。このコミットは、この問題を解決し、GoプログラムがWindows環境でより堅牢に動作するようにするために導入されました。具体的には、SEHフレームのセットアップと管理を改善し、GoランタイムがOS例外を捕捉できるようにすることが目的です。
前提知識の解説
1. Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ、メモリ管理、そしてpanic
/recover
のような例外処理メカニズムが含まれます。Goプログラムは、OS上で直接実行されるのではなく、このランタイム上で動作します。
2. Cgo
Cgoは、GoプログラムからC言語のコードを呼び出すためのGoの機能です。これにより、既存のCライブラリを利用したり、パフォーマンスが重要な部分をCで記述したりすることが可能になります。Cgoを使用すると、GoとCのコードが同じプロセス空間で実行されるため、両者の間でデータや制御フローがやり取りされます。
3. Windows Structured Exception Handling (SEH)
Windowsの構造化例外処理(SEH)は、ハードウェア例外(例: ゼロ除算、アクセス違反)やソフトウェア例外(例: RaiseException
関数によって意図的に発生させられる例外)を処理するためのOSレベルのメカニズムです。SEHは、例外が発生した際に、登録された例外ハンドラを呼び出すことで、プログラムがクラッシュするのを防ぎ、回復処理を行うことを可能にします。
SEHは、スレッドのスタック上に「例外フレーム」を連鎖させることで機能します。各例外フレームには、そのフレームに対応する例外ハンドラへのポインタが含まれています。例外が発生すると、OSはスタックを巻き戻しながら例外フレームを順にたどり、適切なハンドラを見つけて実行します。
4. panic
とrecover
Go言語におけるエラー処理のメカニズムの一つです。
panic
: プログラムの異常終了を示すために使用されます。panic
が発生すると、現在の関数の実行が中断され、遅延関数(defer
)が実行されながら、呼び出しスタックを遡ります。recover
:panic
が発生した際に、そのpanic
を捕捉し、プログラムの異常終了を防ぐために使用されます。recover
はdefer
関数内でしか効果がありません。
このコミットの目的は、WindowsのSEH例外をGoのpanic
に変換し、Goプログラムがrecover
でそれを捕捉できるようにすることです。
技術的詳細
このコミットの主要な技術的変更点は、WindowsのSEHフレームの管理方法を改善し、GoランタイムがOSレベルの例外を捕捉できるようにすることです。
- SEHフレームの動的な登録: 以前は、Goプログラムの起動時や新しいスレッドの開始時に、アセンブリコード内で静的にSEHフレームが設定されていました。このコミットでは、
runtime·install_exception_handler
という新しい関数が導入され、SEHフレームのセットアップがより動的に、かつGoランタイムの初期化プロセスの一部として行われるようになりました。 M
構造体へのseh
フィールドの追加: GoランタイムのM
構造体(マシン、つまりOSスレッドを表す)にSEH* seh;
フィールドが追加されました。これにより、各OSスレッドが自身のSEHフレームへのポインタを保持できるようになり、ランタイムがスレッドごとの例外ハンドラを管理しやすくなります。runtime·mstart
でのSEHフレームの初期化:runtime·mstart
関数は、GoのスケジューラがOSスレッド上でGoルーチンを実行する際に呼び出される重要な関数です。この関数内でSEH seh;
が宣言され、m->seh = &seh;
によって現在のM
構造体にSEHフレームが関連付けられます。これにより、Goルーチンが実行されるOSスレッド上でSEHが有効になります。- アセンブリコードの変更:
src/pkg/runtime/rt0_windows_386.s
およびsrc/pkg/runtime/sys_windows_386.s
から、起動時およびスレッド開始時のSEHフレームの静的な設定ロジックが削除されました。src/pkg/runtime/sys_windows_386.s
にruntime·install_exception_handler
の実装が追加されました。この関数は、M
構造体に格納されたseh
フィールドを利用して、現在のスレッドのSEHフレームをセットアップします。具体的には、sigtramp
(シグナル/例外トラップハンドラ)を例外ハンドラとして登録し、既存の例外フレームチェーンの先頭に自身のフレームを挿入します。src/pkg/runtime/sys_windows_amd64.s
にもruntime·install_exception_handler
が追加されていますが、amd64ではまだ完全な実装ではなく、将来的な拡張のプレースホルダーとして機能しています。
- テストの追加:
crash_cgo_test.go
とcrash_test.go
という新しいテストファイルが追加されました。これらのテストは、Goプログラムが意図的にヌルポインタ参照などのクラッシュを引き起こし、Goランタイムがそれを捕捉してrecover
できることを検証します。特にcrash_cgo_test.go
は、Cgoプログラムにおける例外処理のテストに特化しています。
これらの変更により、GoランタイムはWindowsのOSレベル例外をより確実に捕捉し、Goのpanic
メカニズムに変換できるようになります。これにより、Cgoプログラムを含むGoアプリケーションの堅牢性が向上します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/pkg/runtime/runtime.h
:SEH
構造体の定義が追加されました。この構造体は、WindowsのSEHフレームの基本的な要素(前の例外フレームへのポインタとハンドラへのポインタ)を模倣しています。typedef struct SEH SEH; struct SEH { void* prev; void* handler; };
M
構造体(GoランタイムのOSスレッドを表す構造体)にSEH* seh;
フィールドが追加されました。これにより、各OSスレッドが自身のSEHフレームへのポインタを保持できるようになります。struct M { // ... #ifdef GOOS_windows void* thread; // thread handle #endif SEH* seh; uintptr end[]; };
-
src/pkg/runtime/os_windows.h
:runtime·install_exception_handler
関数のプロトタイプ宣言が追加されました。void runtime·install_exception_handler(void);
-
src/pkg/runtime/proc.c
:runtime·mstart
関数内で、SEH seh;
が宣言され、m->seh = &seh;
によって現在のM
構造体にSEHフレームが関連付けられるようになりました。これにより、Goルーチンが実行されるOSスレッド上でSEHが有効になります。void runtime·mstart(void) { // It is used by windows-386 only. Unfortunately, seh needs // to be located on os stack, and mstart runs on os stack // for both m0 and m. SEH seh; if(g != m->g0) runtime·throw("bad runtime·mstart"); // ... m->g0->sched.pc = (void*)-1; // make sure it is never used m->seh = &seh; // ここでSEHフレームをMに紐付け runtime·asminit(); runtime·minit(); // ... }
-
src/pkg/runtime/thread_windows.c
:runtime·minit
関数(Goランタイムの初期化の一部として各OSスレッドで実行される)内で、runtime·install_exception_handler()
が呼び出されるようになりました。これにより、スレッドがGoルーチンを実行する準備ができた時点でSEHハンドラがインストールされます。void runtime·minit(void) { runtime·install_exception_handler(); }
-
src/pkg/runtime/sys_windows_386.s
:_rt0_386_windows
およびruntime·tstart
から、SEHフレームをスタックに直接プッシュする古いアセンブリコードが削除されました。runtime·install_exception_handler
のアセンブリ実装が追加されました。この関数は、M
構造体のseh
フィールドを利用して、sigtramp
を例外ハンドラとして登録し、現在のスレッドのSEHフレームチェーンに自身を挿入します。// void install_exception_handler() TEXT runtime·install_exception_handler(SB),7,$0 get_tls(CX) MOVL m(CX), CX // m // Set up SEH frame MOVL m_seh(CX), DX MOVL $runtime·sigtramp(SB), AX MOVL AX, seh_handler(DX) MOVL 0(FS), AX MOVL AX, seh_prev(DX) // Install it MOVL DX, 0(FS) RET
-
src/pkg/runtime/crash_cgo_test.go
およびsrc/pkg/runtime/crash_test.go
:- Windows例外処理の動作を検証するための新しいテストが追加されました。これらのテストは、意図的にヌルポインタ参照などの例外を発生させ、Goの
recover
メカニズムがそれを捕捉できることを確認します。特にcrash_cgo_test.go
はCgo環境でのテストを含みます。
- Windows例外処理の動作を検証するための新しいテストが追加されました。これらのテストは、意図的にヌルポインタ参照などの例外を発生させ、Goの
コアとなるコードの解説
このコミットの核心は、WindowsのSEHをGoランタイムのpanic
/recover
メカニズムと統合することです。
-
SEH
構造体とM.seh
:SEH
構造体は、WindowsのSEHフレームの構造をGoランタイム内で表現するためのものです。prev
は前の例外フレームへのポインタ、handler
はこのフレームの例外ハンドラ関数へのポインタです。M
構造体にseh
フィールドが追加されたことで、各OSスレッド(M
)が自身のSEHフレームの情報を保持できるようになりました。これにより、GoランタイムはスレッドごとにSEHフレームを管理し、必要に応じてアクセスできるようになります。 -
runtime·mstart
でのSEH
初期化:runtime·mstart
は、Goルーチンが実行されるOSスレッドの初期化を行う関数です。ここでSEH seh;
をスタック上に確保し、そのアドレスをm->seh
に設定することで、現在のOSスレッドのスタック上にSEHフレームの領域を確保し、Goランタイムがそのフレームを操作できるようにします。これは、SEHフレームがスレッドのスタック上に存在する必要があるというWindowsの要件を満たすための重要なステップです。 -
runtime·install_exception_handler
: この関数は、実際にSEHハンドラをインストールする役割を担います。get_tls(CX)
: スレッドローカルストレージ(TLS)から現在のM
構造体へのポインタを取得します。MOVL m_seh(CX), DX
:M
構造体から、先ほどruntime·mstart
で設定されたSEH
構造体のアドレスをDX
レジスタにロードします。MOVL $runtime·sigtramp(SB), AX
/MOVL AX, seh_handler(DX)
:runtime·sigtramp
のアドレスをAX
レジスタにロードし、それをSEH
構造体のhandler
フィールドに設定します。runtime·sigtramp
は、GoランタイムがOSからのシグナルや例外を捕捉した際に呼び出される共通のトラップハンドラです。MOVL 0(FS), AX
/MOVL AX, seh_prev(DX)
:0(FS)
は、WindowsのFSセグメントレジスタが指すスレッド情報ブロック(TEB: Thread Environment Block)の先頭に格納されている、現在のSEHフレームチェーンの先頭へのポインタです。これをAX
にロードし、SEH
構造体のprev
フィールドに設定します。これにより、新しいSEHフレームが既存のチェーンの先頭にリンクされます。MOVL DX, 0(FS)
: 新しく設定したSEH
構造体のアドレス(DX
)を0(FS)
に書き込みます。これにより、この新しいSEHフレームが現在のスレッドのSEHチェーンの先頭として登録され、以降に発生する例外はこのハンドラによって最初に処理されるようになります。
この一連の処理により、GoランタイムはOSレベルで発生した例外をruntime·sigtramp
で捕捉し、それをGoのpanic
に変換して、Goプログラムがrecover
で処理できるようにします。これにより、Cgoコード内で発生した例外であっても、Goの例外処理メカニズムを通じて安全に扱うことが可能になります。
関連リンク
- Go issue 3543: https://github.com/golang/go/issues/3543
- Go CL 6245063: https://golang.org/cl/6245063
参考にした情報源リンク
- Windows Structured Exception Handling (SEH):
- Go
panic
andrecover
: - Go Cgo:
- Go Runtime Source Code (general understanding):
- Go Assembly (x86):
- https://go.dev/doc/asm
- https://go.dev/src/runtime/sys_windows_386.s (for context on assembly usage in Go runtime)
- Thread Environment Block (TEB) and FS register on Windows:
- https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-teb
- https://www.geoffchappell.com/studies/windows/win32/ntdll/structs/teb/index.htm
- https://www.aldeid.com/wiki/X86-assembly/FS-GS-registers
- https://www.unknowncheats.me/forum/general-programming-and-reversing/100000-fs-gs-registers.html
- Go runtime
m
andg
structs:- https://go.dev/src/runtime/runtime2.go (for Go 1.x, but concepts are similar)
- https://go.dev/src/runtime/proc.go (for scheduler and
mstart
context)
- Go
sigtramp
:- https://go.dev/src/runtime/signal_windows.go (for context on signal handling in Go runtime)
- https://go.dev/src/runtime/sys_windows_386.s (where
sigtramp
is defined)
- Go
buildruntime.c
:- https://go.dev/src/cmd/dist/buildruntime.c (for context on how runtime is built)
- Go
crash_test.go
:- https://go.dev/src/runtime/crash_test.go (for understanding the test setup)
- Go
thread_windows.c
:- https://go.dev/src/runtime/thread_windows.c (for context on thread initialization)