[インデックス 18946] ファイルの概要
このコミットは、GoランタイムがWindows/AMD64アーキテクチャ上で例外処理を行う方法を、従来のStructured Exception Handling (SEH)からVectored Exception Handling (VEH)へと移行するものです。これにより、Windows/AMD64ビルドにおける例外処理の信頼性と互換性が向上し、特定のビルド問題が修正されます。
コミット
commit a837347dd95d045e05bf0e6df9bf1c9b157c7c53
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed Mar 26 11:13:50 2014 +1100
runtime: use VEH for windows/amd64 exception handling
Fixes windows/amd64 build.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/79470046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a837347dd95d045e05bf0e6df9bf1c9b157c7c53
元コミット内容
GoランタイムがWindows/AMD64環境での例外処理にVectored Exception Handling (VEH)を使用するように変更します。これにより、Windows/AMD64ビルドの問題が修正されます。
変更の背景
Go言語のランタイムは、プログラムの実行中に発生する様々な例外(例えば、ゼロ除算、無効なメモリアクセス、Goルーチンのパニックなど)を適切に処理する必要があります。Windows環境では、例外処理のためのメカニズムとして主にStructured Exception Handling (SEH)とVectored Exception Handling (VEH)の二つが存在します。
このコミット以前のGoランタイムのWindows/AMD64ビルドでは、例外処理に何らかの問題を抱えていたことが示唆されています。コミットメッセージの「Fixes windows/amd64 build.」という記述から、既存の例外処理メカニズムがWindows/AMD64環境で正しく機能していなかったか、あるいは特定の条件下でビルドが失敗する原因となっていたと考えられます。
SEHはコンパイラによって生成されるコードと密接に連携し、スタックアンワインド情報などを利用して例外を処理します。しかし、Goランタイムのように独自のスタック管理やスケジューリングを行うシステムでは、SEHの統合が複雑になる場合があります。特に、GoのgoroutineスタックとOSのスタックが異なる場合、SEHが期待通りに動作しない可能性があります。
VEHは、SEHよりも低レベルで、OSが例外をディスパッチする前にアプリケーションが独自のハンドラを登録できるメカニズムです。これにより、GoランタイムはOSの例外ディスパッチプロセスに早期に介入し、Go独自のパニック/リカバリメカニズムとOSの例外をより柔軟に連携させることが可能になります。この変更は、Windows/AMD64環境での例外処理の堅牢性を高め、既存のビルド問題を解決することを目的としています。
前提知識の解説
1. Windowsの例外処理メカニズム
-
Structured Exception Handling (SEH): Windowsのネイティブな例外処理メカニズムです。コンパイラによって生成される例外テーブル(PEファイルの
.pdata
や.xdata
セクションに格納されるアンワインド情報など)と連携し、例外発生時にスタックを巻き戻しながら適切な例外ハンドラを探します。C++のtry-catch
ブロックやWindows APIの__try
/__except
などがSEHを利用しています。SEHはスレッドごとに例外ハンドラチェーンを持ちます。 -
Vectored Exception Handling (VEH): Windows XPで導入された、SEHよりも高レベルで、かつSEHよりも先に実行される例外処理メカニズムです。
AddVectoredExceptionHandler
関数を使って登録されたハンドラは、プロセス内のどのスレッドで例外が発生しても、SEHハンドラが呼び出される前に実行されます。VEHハンドラは、例外を処理するか、他のハンドラに処理を委ねるかを決定できます。Goランタイムのように、独自のスタック管理や実行モデルを持つシステムにとって、OSの例外ディスパッチに早期に介入できるVEHは非常に有用です。
2. Portable Executable (PE) ファイルフォーマット
Windowsの実行可能ファイル(.exe)やダイナミックリンクライブラリ(.dll)のフォーマットです。PEファイルは、コード、データ、リソース、そして例外処理情報など、プログラムの実行に必要な様々なセクションを含んでいます。
.pdata
セクション: 関数ポインタとアンワインド情報のオフセットを格納するテーブルです。AMD64アーキテクチャでは、例外発生時のスタックアンワインドに不可欠な情報が含まれます。.xdata
セクション: アンワインド情報自体を格納します。
3. Goランタイムと例外処理
Go言語には、panic
とrecover
という独自のエラー処理メカニズムがあります。これは、Goルーチン内で発生した予期せぬエラー(ランタイムエラーや明示的なpanic
呼び出し)を捕捉し、プログラムのクラッシュを防ぐためのものです。
OSレベルで発生する例外(例えば、セグメンテーション違反やゼロ除算)は、Goランタイムによって捕捉され、Goのpanic
に変換される必要があります。この変換を行うのが、sigtramp
やsighandler
といったランタイム内のシグナル/例外ハンドラです。
4. sigtramp
とsighandler
sigtramp
: OSから直接呼び出される、アセンブリ言語で書かれた低レベルのシグナル/例外トラップハンドラです。OSのコンテキスト(レジスタの状態など)を保存し、GoランタイムのCコードで書かれたsighandler
関数を呼び出す準備をします。sighandler
:sigtramp
から呼び出されるGoランタイムのCコード関数で、具体的な例外の種類を判断し、Goのpanic
に変換したり、デバッガに情報を渡したりする役割を担います。
技術的詳細
このコミットの主要な変更点は、GoランタイムがWindows/AMD64環境で例外を処理するために、従来のSEH関連のメカニズムからVEHへと完全に移行したことです。
-
PEファイルからの例外情報削除 (
src/cmd/ld/pe.c
):addexcept
関数が削除されました。この関数は、リンカがPEファイルに.pdata
(関数テーブル)と.xdata
(アンワインドデータ)セクションを追加し、SEHのための例外処理情報を埋め込む役割を担っていました。- この削除は、GoランタイムがもはやSEHのメカニズムに依存せず、VEHを通じて独自の例外処理を行うことを意味します。リンカがPEファイルにSEH関連の情報を埋め込む必要がなくなったため、このコードが不要になりました。
-
汎用Windowsランタイムからの例外ハンドラ登録/削除の削除 (
src/pkg/runtime/os_windows.c
):runtime·install_exception_handler()
とruntime·remove_exception_handler()
の呼び出しが、runtime·minit()
とruntime·unminit()
から削除されました。- これは、Goランタイムが起動時や終了時に明示的に例外ハンドラを登録・解除する従来の方式を廃止したことを示しています。VEHは
AddVectoredExceptionHandler
によって登録され、プロセス全体で有効になるため、GoルーチンやM(マシン)の初期化/終了とは直接関連付けられなくなります。
-
AMD64固有の例外ハンドラロジックの変更 (
src/pkg/runtime/os_windows_amd64.c
):runtime·sighandler
関数が変更されました。この関数は、VEHハンドラであるsigtramp
から呼び出されます。DBG_PRINTEXCEPTION_C
という新しい例外コードが追加されました。これはデバッガが捕捉することを意図した例外で、Goランタイムはこれを無視するように変更されました。EXCEPTION_BREAKPOINT
の処理がコメントアウトされ、Goランタイムがアセンブリソース内のブレークポイント命令を直接処理しない方針が示されました。runtime·sighandler
の戻り値が変更されました。以前は0
を返すことで他のハンドラに処理を委ねる可能性がありましたが、多くのケースで-1
(例外を処理済み)を返すようになりました。これにより、Goランタイムが例外を完全に捕捉し、Windowsの他のSEHハンドラに処理が渡されないように制御します。- デバッグ出力に
r->Rip
(命令ポインタ)が追加され、例外発生時のPC値がより詳細に表示されるようになりました。
-
AMD64アセンブリレベルでのVEHハンドラの実装 (
src/pkg/runtime/sys_windows_amd64.s
):runtime·sigtramp
関数が大幅に書き換えられました。この関数は、WindowsによってVEHハンドラとして登録され、例外発生時に直接呼び出されます。- 新しい
sigtramp
は、Windowsのコールバック規約に従って、DI
,SI
,BP
,BX
,R12
,R13
,R14
,R15
レジスタとDF
フラグを保存・復元するロジックが追加されました。これは、VEHハンドラが呼び出された後に、Goランタイムがこれらのレジスタの状態を維持する必要があるためです。 PEXCEPTION_POINTERS ExceptionInfo
という引数を受け取るように変更され、例外レコードとコンテキストポインタを適切に取得するようになりました。runtime·sighandler
を呼び出す際に、ExceptionRecord*
,Context*
,G*
(現在のGoルーチン)の3つの引数を渡すようにスタックが設定されます。sigtramp
の戻り値(AX
レジスタ)が、runtime·sighandler
の戻り値に基づいて設定されます。これにより、Windowsに対して例外が処理されたか(-1
)、あるいは他のハンドラに委ねるべきか(0
)を通知します。runtime·install_exception_handler()
とruntime·remove_exception_handler()
のアセンブリ実装が削除されました。これは、これらの関数がもはや使用されないためです。
この変更により、GoランタイムはWindowsの例外処理メカニズムに深く統合され、より堅牢な例外処理が可能になりました。特に、Goの独自のスタック管理とWindowsの例外処理の間のギャップをVEHが埋めることで、GoプログラムがWindows上でより安定して動作するようになります。
コアとなるコードの変更箇所
src/cmd/ld/pe.c
--- a/src/cmd/ld/pe.c
+++ b/src/cmd/ld/pe.c
@@ -529,49 +529,6 @@ addpersrc(void)
dd[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = h->VirtualSize;
}
-static void
-addexcept(IMAGE_SECTION_HEADER *text)
-{
- IMAGE_SECTION_HEADER *pdata, *xdata;
- vlong startoff;
- uvlong n;
- LSym *sym;
-
- USED(text);
- if(thechar != '6')
- return;
-
- // write unwind info
- sym = linklookup(ctxt, "runtime.sigtramp", 0);
- startoff = cpos();
- lputl(9); // version=1, flags=UNW_FLAG_EHANDLER, rest 0
- lputl(sym->value - PEBASE);
- lputl(0);
-
- n = cpos() - startoff;
- xdata = addpesection(".xdata", n, n);
- xdata->Characteristics = IMAGE_SCN_MEM_READ|
- IMAGE_SCN_CNT_INITIALIZED_DATA;
- chksectoff(xdata, startoff);
- strnput("", xdata->SizeOfRawData - n);
-
- // write a function table entry for the whole text segment
- startoff = cpos();
- lputl(text->VirtualAddress);
- lputl(text->VirtualAddress + text->VirtualSize);
- lputl(xdata->VirtualAddress);
-
- n = cpos() - startoff;
- pdata = addpesection(".pdata", n, n);
- pdata->Characteristics = IMAGE_SCN_MEM_READ|
- IMAGE_SCN_CNT_INITIALIZED_DATA;
- chksectoff(pdata, startoff);
- strnput("", pdata->SizeOfRawData - n);
-
- dd[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress = pdata->VirtualAddress;
- dd[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size = pdata->VirtualSize;
-}
-
void
asmbpe(void)
{
@@ -609,7 +566,6 @@ asmbpe(void)
addexports();
addsymtable();
addpersrc();
- addexcept(t);
fh.NumberOfSections = nsect;
fh.TimeDateStamp = time(0);
src/pkg/runtime/os_windows.c
--- a/src/pkg/runtime/os_windows.c
+++ b/src/pkg/runtime/os_windows.c
@@ -248,15 +248,12 @@ runtime·minit(void)
(uintptr)-1, (uintptr)-2, (uintptr)-1, &thandle,
(uintptr)0, (uintptr)0, (uintptr)DUPLICATE_SAME_ACCESS);
runtime·atomicstorep(&m->thread, thandle);
-
- runtime·install_exception_handler();
}
// Called from dropm to undo the effect of an minit.
void
runtime·unminit(void)
{
- runtime·remove_exception_handler();
}
#pragma textflag NOSPLIT
src/pkg/runtime/os_windows_amd64.c
--- a/src/pkg/runtime/os_windows_amd64.c
+++ b/src/pkg/runtime/os_windows_amd64.c
@@ -32,6 +32,11 @@ runtime·dumpregs(Context *r)
runtime·printf("gs %X\n", (uint64)r->SegGs);
}
+#define DBG_PRINTEXCEPTION_C 0x40010006
+
+// Called by sigtramp from Windows VEH handler.
+// Return value signals whether the exception has been handled (-1)
+// or should be made available to other handlers in the chain (0).
uint32
runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
{
@@ -39,8 +44,25 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
uintptr *sp;
switch(info->ExceptionCode) {
+ case DBG_PRINTEXCEPTION_C:
+ // This exception is intended to be caught by debuggers.
+ // There is a not-very-informational message like
+ // "Invalid parameter passed to C runtime function"
+ // sitting at info->ExceptionInformation[0] (a wchar_t*),
+ // with length info->ExceptionInformation[1].
+ // The default behavior is to ignore this exception,
+ // but somehow returning 0 here (meaning keep going)
+ // makes the program crash instead. Maybe Windows has no
+ // other handler registered? In any event, ignore it.
+ return -1;
+
case EXCEPTION_BREAKPOINT:
- return 1;
+ // It is unclear whether this is needed, unclear whether it
+ // would work, and unclear how to test it. Leave out for now.
+ // This only handles breakpoint instructions written in the
+ // assembly sources, not breakpoints set by a debugger, and
+ // there are very few of the former.
+ break;
}
if(gp != nil && runtime·issigpanic(info->ExceptionCode)) {
@@ -65,15 +87,16 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
r->Rsp = (uintptr)sp;
}
r->Rip = (uintptr)runtime·sigpanic;
- return 0;
+ return -1;
}
if(runtime·panicking) // traceback already printed
runtime·exit(2);
runtime·panicking = 1;
- runtime·printf("Exception %x %p %p\n", info->ExceptionCode,
- info->ExceptionInformation[0], info->ExceptionInformation[1]);
+ runtime·printf("Exception %x %p %p %p\n", info->ExceptionCode,
+ info->ExceptionInformation[0], info->ExceptionInformation[1], r->Rip);
+
runtime·printf("PC=%X\n", r->Rip);
if(m->lockedg != nil && m->ncgo > 0 && gp == m->g0) {
@@ -92,7 +115,7 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
runtime·crash();
runtime·exit(2);
- return 0;
+ return -1; // not reached
}
void
src/pkg/runtime/sys_windows_386.s
--- a/src/pkg/runtime/sys_windows_386.s
+++ b/src/pkg/runtime/sys_windows_386.s
@@ -313,14 +313,6 @@ TEXT runtime·setldt(SB),NOSPLIT,$0
MOVL CX, 0x14(FS)
RET
-// void install_exception_handler()
-TEXT runtime·install_exception_handler(SB),NOSPLIT,$0
- RET
-
-// void remove_exception_handler()
-TEXT runtime·remove_exception_handler(SB),NOSPLIT,$0
- RET
-
// Sleep duration is in 100ns units.
TEXT runtime·usleep1(SB),NOSPLIT,$0
MOVL duration+0(FP), BX
src/pkg/runtime/sys_windows_amd64.s
--- a/src/pkg/runtime/sys_windows_amd64.s
+++ b/src/pkg/runtime/sys_windows_amd64.s
@@ -95,49 +95,55 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0
MOVL AX, 0x68(CX)
RET
-TEXT runtime·sigtramp(SB),NOSPLIT,$0
- // CX: exception record
- // R8: context
+// Called by Windows as a Vectored Exception Handler (VEH).
+// First argument is pointer to struct containing
+// exception record and context pointers.
+// Return 0 for 'not handled', -1 for handled.
+TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
+ // CX: PEXCEPTION_POINTERS ExceptionInfo
- // unwinding?
- TESTL $6, 4(CX) // exception flags
- MOVL $1, AX
- JNZ sigdone
-
- // copy arguments for call to sighandler.
-
- // Stack adjustment is here to hide from 6l,
- // which doesn't understand that sigtramp
- // runs on essentially unlimited stack.
- SUBQ $56, SP
- MOVQ CX, 0(SP)
- MOVQ R8, 8(SP)
-
- get_tls(CX)
-
- // check that m exists
- MOVQ m(CX), AX
+ // DI SI BP BX R12 R13 R14 R15 registers and DF flag are preserved
+ // as required by windows callback convention.
+ PUSHFQ
+ SUBQ $88, SP
+ MOVQ DI, 80(SP)
+ MOVQ SI, 72(SP)
+ MOVQ BP, 64(SP)
+ MOVQ BX, 56(SP)
+ MOVQ R12, 48(SP)
+ MOVQ R13, 40(SP)
+ MOVQ R14, 32(SP)
+ MOVQ R15, 24(SP)
+
+ MOVQ 0(CX), BX // ExceptionRecord*
+ MOVQ 8(CX), CX // Context*
+
+ // fetch g
+ get_tls(DX)
+ MOVQ m(DX), AX
CMPQ AX, $0
JNE 2(PC)
CALL runtime·badsignal2(SB)
-
- MOVQ g(CX), CX
- MOVQ CX, 16(SP)
-
- MOVQ BX, 24(SP)
- MOVQ BP, 32(SP)
- MOVQ SI, 40(SP)
- MOVQ DI, 48(SP)
-
+ MOVQ g(DX), DX
+ // call sighandler(ExceptionRecord*, Context*, G*)
+ MOVQ BX, 0(SP)
+ MOVQ CX, 8(SP)
+ MOVQ DX, 16(SP)
CALL runtime·sighandler(SB)
+ // AX is set to report result back to Windows
- MOVQ 24(SP), BX
- MOVQ 32(SP), BP
- MOVQ 40(SP), SI
- MOVQ 48(SP), DI
- ADDQ $56, SP
+ // restore registers as required for windows callback
+ MOVQ 24(SP), R15
+ MOVQ 32(SP), R14
+ MOVQ 40(SP), R13
+ MOVQ 48(SP), R12
+ MOVQ 56(SP), BX
+ MOVQ 64(SP), BP
+ MOVQ 72(SP), SI
+ MOVQ 80(SP), DI
+ ADDQ $88, SP
+ POPFQ
-sigdone:
RET
TEXT runtime·ctrlhandler(SB),NOSPLIT,$8
@@ -277,13 +283,6 @@ TEXT runtime·callbackasm1(SB),NOSPLIT,$0
POPQ -8(CX)(DX*1) // restore bytes just after the args
RET
-TEXT runtime·setstacklimits(SB),NOSPLIT,$0
- MOVQ 0x30(GS), CX
- MOVQ $0, 0x10(CX)
- MOVQ $0xffffffffffff, AX
- MOVQ AX, 0x08(CX)
- RET
-
// uint32 tstart_stdcall(M *newm);\n TEXT runtime·tstart_stdcall(SB),NOSPLIT,$0
// CX contains first arg newm
// R8: context
@@ -315,14 +314,6 @@ TEXT runtime·settls(SB),NOSPLIT,$0
MOVQ DI, 0x28(GS)
RET
-// void install_exception_handler()
-TEXT runtime·install_exception_handler(SB),NOSPLIT,$0
- CALL runtime·setstacklimits(SB)
- RET
-
-TEXT runtime·remove_exception_handler(SB),NOSPLIT,$0
- RET
-
// Sleep duration is in 100ns units.
TEXT runtime·usleep1(SB),NOSPLIT,$0
MOVL duration+0(FP), BX
コアとなるコードの解説
src/cmd/ld/pe.c
の変更
addexcept
関数の削除: この関数は、WindowsのPEファイルフォーマットにおいて、SEH(Structured Exception Handling)に必要な例外処理情報(アンワインド情報)を.pdata
および.xdata
セクションとして追加する役割を担っていました。GoランタイムがVEH(Vectored Exception Handling)に移行することで、リンカがSEH関連の情報をPEファイルに埋め込む必要がなくなりました。これにより、Goの実行ファイルはSEHのフレームワークに依存せず、VEHを通じて独自の例外処理フローを確立できるようになります。
src/pkg/runtime/os_windows.c
の変更
runtime·install_exception_handler()
とruntime·remove_exception_handler()
の呼び出し削除: これらの関数は、Goランタイムが初期化される際(runtime·minit
)と終了する際(runtime·unminit
)に、カスタムの例外ハンドラを登録・解除するために使用されていました。VEHはAddVectoredExceptionHandler
によってプロセス全体に登録されるため、GoのM(マシン)の初期化/終了とは直接関連付ける必要がなくなりました。この変更は、例外ハンドラの管理がよりグローバルなVEHメカニズムに委ねられたことを示しています。
src/pkg/runtime/os_windows_amd64.c
の変更
DBG_PRINTEXCEPTION_C
の追加と処理:0x40010006
という例外コードがDBG_PRINTEXCEPTION_C
として定義され、runtime·sighandler
内でこの例外が捕捉された場合に-1
を返すように変更されました。この例外はデバッガが捕捉することを意図しており、Goランタイムはこれを無視することで、デバッグ時の不要なクラッシュを防ぎます。EXCEPTION_BREAKPOINT
の処理変更: ブレークポイント例外の処理がコメントアウトされました。これは、Goランタイムがアセンブリソースコードに埋め込まれたブレークポイント命令を直接処理するのではなく、デバッガにその処理を委ねる方針を示唆しています。runtime·sighandler
の戻り値の変更:runtime·sighandler
が、多くのケースで-1
(例外を処理済み)を返すように変更されました。これにより、Goランタイムが例外を完全に捕捉し、Windowsの他のSEHハンドラに処理が渡されないように制御します。以前は0
を返すことで他のハンドラに処理を委ねる可能性がありましたが、VEHを主軸とすることで、Goランタイムが例外処理の主導権を握るようになりました。- デバッグ出力の強化:
runtime·printf
による例外情報の出力にr->Rip
(命令ポインタ)が追加されました。これにより、例外発生時のプログラムカウンタの値がより詳細に表示され、デバッグが容易になります。
src/pkg/runtime/sys_windows_amd64.s
の変更
runtime·sigtramp
の大幅な書き換え: このアセンブリ関数は、WindowsによってVEHハンドラとして登録され、例外発生時にOSから直接呼び出されるエントリポイントとなります。- レジスタの保存と復元: Windowsのコールバック規約に従い、
DI
,SI
,BP
,BX
,R12
,R13
,R14
,R15
レジスタとDF
フラグをスタックに保存し、runtime·sighandler
の呼び出し後に復元するロジックが追加されました。これにより、Goランタイムの実行コンテキストがVEHハンドラの呼び出しによって破壊されないように保護されます。 - 引数の処理:
PEXCEPTION_POINTERS ExceptionInfo
という引数(CX
レジスタで渡される)から、ExceptionRecord*
とContext*
のポインタを適切に抽出し、runtime·sighandler
に渡すための準備を行います。 runtime·sighandler
の呼び出し: 抽出したExceptionRecord*
、Context*
、そして現在のGoルーチン(G*
)のポインタを引数としてruntime·sighandler
を呼び出します。- 戻り値の設定:
runtime·sighandler
の戻り値(AX
レジスタ)を、sigtramp
の戻り値として設定します。これにより、Windowsに対して例外がGoランタイムによって処理されたか(-1
)、あるいは他のハンドラに処理を委ねるべきか(0
)を通知します。
- レジスタの保存と復元: Windowsのコールバック規約に従い、
runtime·install_exception_handler()
とruntime·remove_exception_handler()
のアセンブリ実装削除: これらの関数はもはや使用されないため、アセンブリコードからも削除されました。
これらの変更は、GoランタイムがWindowsの例外処理をより直接的かつ効率的に制御できるようにするための重要なステップです。VEHの導入により、Goのパニック/リカバリメカニズムとOSレベルの例外との連携が強化され、Windows/AMD64環境でのGoプログラムの安定性が向上します。
関連リンク
- Go言語の公式ドキュメント
- Go言語のランタイムソースコード
- Windows Structured Exception Handling (SEH)
- Windows Vectored Exception Handling (VEH)
- Portable Executable (PE) Format