[インデックス 18983] ファイルの概要
このコミットは、GoランタイムがWindows上で例外を処理する際の挙動を改善するものです。具体的には、Goバイナリのコードセグメント外で発生したWindows例外をGoランタイムが無視するように変更し、不必要なクラッシュやデバッガの誤動作を防ぐことを目的としています。これにより、Goプログラムが外部のDLLやシステムコールと連携する際の安定性が向上します。
コミット
commit 2dc7552f5701c110fd54609fc8eed421cef7f20f
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Mar 28 17:35:00 2014 +1100
runtime: ignore windows exception if not in Go binary
LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/80400043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2dc7552f5701c110fd54609fc8eed421cef7f20f
元コミット内容
runtime: ignore windows exception if not in Go binary
このコミットは、GoランタイムがWindows上で発生する例外を処理する際に、その例外がGoバイナリのコード内で発生したものでない限り無視するように変更します。
変更の背景
Windowsオペレーティングシステムでは、プログラムの実行中に様々な例外(エラーや予期せぬイベント)が発生する可能性があります。Goランタイムは、これらの例外を捕捉し、適切に処理するための独自のメカニズム(シグナルハンドラに相当するもの)を持っています。しかし、GoプログラムがWindows APIを呼び出したり、C/C++で書かれた外部ライブラリ(DLLなど)を使用したりする場合、Goバイナリのコードセグメント外で例外が発生することがあります。
このような場合、Goランタイムがその例外を捕捉して処理しようとすると、以下のような問題が発生する可能性がありました。
- 不必要なクラッシュ: Goランタイムが意図しない例外を捕捉し、それをGoのパニックとして処理しようとすることで、プログラム全体がクラッシュする。
- デバッガとの競合:
DBG_PRINTEXCEPTION_C
のようなデバッガ向けの例外がGoランタイムによって捕捉され、デバッガの挙動を妨げる。 - 誤ったスタックトレース: Goのコードとは無関係な場所で発生した例外に対して、Goのスタックトレースが生成され、デバッグを困難にする。
このコミットは、これらの問題を解決するために、例外が発生した命令ポインタ(EIP/RIP)がGoバイナリのコードセグメント内にあるかどうかをチェックし、そうでない場合はGoランタイムがその例外を無視するように変更することで、Goプログラムの安定性とデバッグのしやすさを向上させています。
前提知識の解説
Windows Structured Exception Handling (SEH)
Windowsでは、プログラムの実行中に発生するハードウェア例外(例: ゼロ除算、無効なメモリアクセス)やソフトウェア例外(例: RaiseException
関数による意図的な例外)を処理するために、構造化例外処理(SEH)というメカニズムが提供されています。SEHは、__try
, __except
, __finally
ブロックを使用して例外を捕捉し、処理することを可能にします。
Goランタイムは、Windows上で動作する際に、このSEHメカニズムを利用して、Goプログラム内で発生するパニックやその他のランタイムエラーを捕捉し、Goのパニックハンドラに変換します。
ExceptionRecord
とContext
構造体
Windowsの例外ハンドラに渡される主要な情報構造体です。
ExceptionRecord
: 発生した例外の種類(ExceptionCode
)、例外が発生したアドレス(ExceptionAddress
)などの例外に関する詳細情報を含みます。Context
: 例外発生時のCPUレジスタの状態(EAX, EBX, ECX, EDX, EIP/RIPなど)を含みます。これにより、例外発生時のプログラムの状態を再現したり、変更したりすることが可能です。
DBG_PRINTEXCEPTION_C
これは、MicrosoftのCランタイムライブラリ(CRT)がデバッガに対して特定のメッセージを出力するために使用する特別な例外コードです。通常、_invalid_parameter
のようなCRT関数が不正な引数を受け取った場合に発生します。この例外はデバッガによって捕捉されることを意図しており、デバッガがアタッチされていない場合は、デフォルトの例外ハンドラによって処理され、アプリケーションがクラッシュする可能性があります。Goランタイムがこれを捕捉すると、デバッガの挙動を妨げたり、Goのパニックとして誤って処理したりする問題がありました。
EXCEPTION_BREAKPOINT
これは、デバッガがブレークポイントを設定した際に発生する例外コードです。Goランタイムがこれを捕捉すると、デバッガの通常の動作を妨げる可能性があります。
Goバイナリのtext
とetext
シンボル
Goコンパイラは、生成されるバイナリのコードセグメントの開始アドレスをtext
シンボル、終了アドレスをetext
シンボルとして定義します。これらのシンボルは、Goプログラムの実行可能コードがメモリ上のどこに配置されているかを示します。
このコミットでは、例外が発生した命令ポインタ(Eip
for x86, Rip
for x64)がtext
とetext
の範囲内にあるかどうかをチェックすることで、例外がGoのコード内で発生したものであるかを判断しています。
runtime·sighandler
とruntime·sigtramp
runtime·sigtramp
: WindowsのSEHメカニズムによって呼び出される、Goランタイムの低レベルな例外ディスパッチャです。これは、Windowsから受け取った例外情報をGoのランタイムが理解できる形式に変換し、runtime·sighandler
を呼び出します。runtime·sighandler
: Goランタイムの主要な例外ハンドラです。sigtramp
から渡された例外情報に基づいて、Goのパニックを生成したり、特定の例外を無視したりするなどの処理を行います。
技術的詳細
このコミットの主要な変更点は、Goランタイムの例外ハンドラが、例外が発生した命令ポインタ(EIP/RIP)がGoバイナリのコードセグメント内にあるかどうかをチェックするロジックを追加したことです。
src/pkg/runtime/os_windows_386.c
および src/pkg/runtime/os_windows_amd64.c
の変更
これらのファイルは、それぞれ32ビット(x86)と64ビット(AMD64)のWindowsアーキテクチャにおけるGoランタイムのシグナルハンドラ(runtime·sighandler
)の実装を含んでいます。
変更前は、DBG_PRINTEXCEPTION_C
例外をswitch
文のcase
として直接処理していました。変更後は、この例外をif
文で先にチェックし、常に-1
(例外を処理済みとしてWindowsに通知し、Goランタイムはこれ以上処理しない)を返すようにしました。これにより、デバッガ向けの例外がGoランタイムの通常の例外処理フローに入り込むのを防ぎます。
さらに重要な変更として、DBG_PRINTEXCEPTION_C
の処理後に、以下のチェックが追加されました。
// Only handle exception if executing instructions in Go binary
// (not Windows library code).
if(r->Eip < (uint32)text || (uint32)etext < r->Eip)
return 0;
(os_windows_amd64.c
ではr->Rip
とuint64
を使用)
このコードは、Context
構造体r
から現在の命令ポインタ(Eip
またはRip
)を取得し、それがGoバイナリのコードセグメント(text
からetext
の範囲)外にあるかどうかをチェックします。もし命令ポインタがGoバイナリの範囲外であれば、0
を返します。これは、Goランタイムがこの例外を処理しないことをWindowsに通知し、Windowsの次の例外ハンドラに処理を委ねることを意味します。
これにより、Goのコードとは無関係な場所(例えば、WindowsのシステムDLLや外部のC/C++ライブラリ)で発生した例外は、Goランタイムによって無視され、Goのパニックに変換されることがなくなります。
src/pkg/runtime/sys_windows_386.s
の変更
このファイルは、32ビットWindowsにおけるGoランタイムの低レベルアセンブリコードを含んでおり、特にruntime·sigtramp
関数の実装が含まれています。
変更前は、runtime·sigtramp
内で命令ポインタがGoバイナリの範囲内にあるかどうかのチェック(CMPL DX, $text(SB)
など)を試みていましたが、コメントで「これは良いアイデアのように聞こえるが、Goが提供するトレースバックはWindowsのクラッシュダイアログよりも優れている」と述べられており、最終的にJMP skipcheckpc
によってこのチェックをスキップしていました。つまり、Goバイナリ外で発生した例外もすべてruntime·sighandler
に渡されていました。
このコミットでは、runtime·sigtramp
からこの命令ポインタのチェックロジックが完全に削除されました。代わりに、このチェックはより高レベルのruntime·sighandler
関数(C言語で記述されている)に移されました。これにより、アセンブリレベルでの複雑な条件分岐を避け、C言語でのより柔軟で読みやすいロジックで例外のフィルタリングを行うことができるようになりました。
コアとなるコードの変更箇所
src/pkg/runtime/os_windows_386.c
(同様の変更が os_windows_amd64.c
にも適用)
--- a/src/pkg/runtime/os_windows_386.c
+++ b/src/pkg/runtime/os_windows_386.c
@@ -34,9 +34,9 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
{\n \tbool crash;\n \tuintptr *sp;\n+\textern byte text[], etext[];\n \n-\tswitch(info->ExceptionCode) {\n-\tcase DBG_PRINTEXCEPTION_C:\n+\tif(info->ExceptionCode == DBG_PRINTEXCEPTION_C) {\n \t\t// This exception is intended to be caught by debuggers.\n \t\t// There is a not-very-informational message like\n \t\t// \"Invalid parameter passed to C runtime function\"\n@@ -47,7 +47,14 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)\n \t\t// makes the program crash instead. Maybe Windows has no\n \t\t// other handler registered? In any event, ignore it.\n \t\treturn -1;\n+\t}\n \n+\t// Only handle exception if executing instructions in Go binary\n+\t// (not Windows library code). \n+\tif(r->Eip < (uint32)text || (uint32)etext < r->Eip)\n+\t\treturn 0;\n+\n+\tswitch(info->ExceptionCode) {\n \tcase EXCEPTION_BREAKPOINT:\n \t\t// It is unclear whether this is needed, unclear whether it\n \t\t// would work, and unclear how to test it. Leave out for now.\n```
### `src/pkg/runtime/sys_windows_386.s`
```diff
--- a/src/pkg/runtime/sys_windows_386.s
+++ b/src/pkg/runtime/sys_windows_386.s
@@ -74,37 +74,18 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0
// exception record and context pointers.\n // Return 0 for 'not handled', -1 for handled.\n TEXT runtime·sigtramp(SB),NOSPLIT,$0-0\n-\tMOVL\tptrs+0(FP), DI\n+\tMOVL\tptrs+0(FP), CX\n \tSUBL\t$28, SP\n-\tMOVL\t0(DI), BX // ExceptionRecord*\n-\tMOVL\t4(DI), CX // Context*\n-\n-\t// Only handle exception if executing instructions in Go binary\n-\t// (not Windows library code). Except don't - keep reading.\n-\t// \n-\t// This sounds like a good idea but the tracebacks that\n-\t// Go provides are better than the Windows crash dialog,\n-\t// especially if it's something that Go needs to do.\n-\t// So take all the exceptions, not just the ones at Go PCs.\n-\t// If you re-enable this check by removing the JMP, you will\n-\t// need to arrange to handle exception 0x40010006 during\n-\t// non-Go code here. Right now that case is handled by sighandler\n-\t// in os_windows_386.c.\n-\tJMP skipcheckpc\n-\tMOVL\t$0, AX\n-\tMOVL\t184(CX), DX // saved PC\n-\tCMPL\tDX, $text(SB)\n-\tJB\tvehret\n-\tCMPL\tDX, $etext(SB)\n-\tJA\tvehret\n-\n-skipcheckpc:\n+\n \t// save callee-saved registers\n \tMOVL\tBX, 12(SP)\n \tMOVL\tBP, 16(SP)\n \tMOVL\tSI, 20(SP)\n \tMOVL\tDI, 24(SP)\n \n+\tMOVL\t0(CX), BX // ExceptionRecord*\n+\tMOVL\t4(CX), CX // Context*\n+\n \t// fetch g\n \tget_tls(DX)\n \tMOVL\tm(DX), AX\n@@ -117,6 +98,7 @@ skipcheckpc:\n \tMOVL\tCX, 4(SP)\n \tMOVL\tDX, 8(SP)\n \tCALL\truntime·sighandler(SB)\n+\t// AX is set to report result back to Windows\n \n \t// restore callee-saved registers\n \tMOVL\t24(SP), DI\n@@ -124,7 +106,6 @@ skipcheckpc:\n \tMOVL\t16(SP), BP\n \tMOVL\t12(SP), BX\n \n-vehret:\n \tADDL\t$28, SP\n \t// RET 4 (return and pop 4 bytes parameters)\n \tBYTE $0xC2; WORD $4\n```
## コアとなるコードの解説
### `os_windows_386.c` および `os_windows_amd64.c` の変更点
1. **`extern byte text[], etext[];` の追加**:
Goバイナリのコードセグメントの開始と終了アドレスを示す`text`と`etext`シンボルを外部参照として宣言しています。これにより、Cコード内でこれらのアドレスを使用して、命令ポインタがGoのコード範囲内にあるかをチェックできるようになります。
2. **`DBG_PRINTEXCEPTION_C` の先行処理**:
`switch`文の前に`if(info->ExceptionCode == DBG_PRINTEXCEPTION_C)`という条件を追加し、この特定の例外コードを先に処理するように変更しました。この例外はデバッガ向けのものであるため、Goランタイムがこれを捕捉してパニックに変換するのではなく、すぐに`-1`を返してWindowsに処理済みであることを通知します。これにより、デバッガの挙動が妨げられるのを防ぎます。
3. **Goバイナリ内での実行チェック**:
`DBG_PRINTEXCEPTION_C`の処理後、かつ他の例外タイプを処理する`switch`文の前に、以下の重要なチェックが追加されました。
```c
if(r->Eip < (uint32)text || (uint32)etext < r->Eip)
return 0;
```
このコードは、例外が発生した時点の命令ポインタ(`r->Eip` for x86, `r->Rip` for x64)が、Goバイナリのコードセグメント(`text`から`etext`の範囲)の外にあるかどうかを判断します。
* `r->Eip < (uint32)text`: 命令ポインタがGoコードの開始アドレスより前にある場合。
* `(uint32)etext < r->Eip`: 命令ポインタがGoコードの終了アドレスより後にある場合。
これらの条件のいずれかが真であれば、例外はGoのコード外で発生したと判断され、`return 0;`が実行されます。`0`を返すことは、Goランタイムがこの例外を処理せず、Windowsの次の例外ハンドラに処理を委ねることを意味します。これにより、Goのコードとは無関係な例外がGoのパニックとして誤って処理されることを防ぎます。
### `sys_windows_386.s` の変更点
1. **命令ポインタチェックロジックの削除**:
変更前は、`runtime·sigtramp`アセンブリ関数内で命令ポインタがGoバイナリの範囲内にあるかをチェックするロジックが存在しましたが、`JMP skipcheckpc`によって常にスキップされていました。このコミットでは、この不要なチェックロジックと関連するラベル(`skipcheckpc`, `vehret`)が完全に削除されました。
2. **レジスタの受け渡し方法の変更**:
`MOVL ptrs+0(FP), DI` が `MOVL ptrs+0(FP), CX` に変更され、`ExceptionRecord*` と `Context*` のポインタを格納するレジスタが変更されています。これは、後続のC関数呼び出し(`runtime·sighandler`)への引数渡しの一貫性を保つための調整です。
3. **`runtime·sighandler`への処理委譲**:
アセンブリレベルでの複雑な条件分岐を削除し、例外のフィルタリングロジックをC言語で記述された`runtime·sighandler`に完全に委譲することで、コードの可読性と保守性が向上しました。`runtime·sigtramp`は、Windowsから受け取った例外情報を`runtime·sighandler`に渡し、その戻り値(`AX`レジスタに格納される)をWindowsに報告する役割に特化しました。
これらの変更により、GoランタイムはWindows例外処理において、より選択的かつ効率的に動作するようになり、Goプログラムの堅牢性が向上しました。
## 関連リンク
* Go CL 80400043: [https://golang.org/cl/80400043](https://golang.org/cl/80400043)
## 参考にした情報源リンク
* Windows Structured Exception Handling (SEH) の概念に関するMicrosoftドキュメント
* Go言語のランタイムソースコード(特に`src/pkg/runtime`ディレクトリ)
* Go言語のIssueトラッカーやメーリングリストでの関連議論(必要に応じて検索)
* `DBG_PRINTEXCEPTION_C`に関する情報源(例: Stack Overflow, Microsoft Docs)# [インデックス 18983] ファイルの概要
このコミットは、GoランタイムがWindows上で例外を処理する際の挙動を改善するものです。具体的には、Goバイナリのコードセグメント外で発生したWindows例外をGoランタイムが無視するように変更し、不必要なクラッシュやデバッガの誤動作を防ぐことを目的としています。これにより、Goプログラムが外部のDLLやシステムコールと連携する際の安定性が向上します。
## コミット
commit 2dc7552f5701c110fd54609fc8eed421cef7f20f Author: Alex Brainman alex.brainman@gmail.com Date: Fri Mar 28 17:35:00 2014 +1100
runtime: ignore windows exception if not in Go binary
LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/80400043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/2dc7552f5701c110fd54609fc8eed421cef7f20f](https://github.com/golang/go/commit/2dc7552f5701c110fd54609fc8eed421cef7f20f)
## 元コミット内容
`runtime: ignore windows exception if not in Go binary`
このコミットは、GoランタイムがWindows上で発生する例外を処理する際に、その例外がGoバイナリのコード内で発生したものでない限り無視するように変更します。
## 変更の背景
Windowsオペレーティングシステムでは、プログラムの実行中に様々な例外(エラーや予期せぬイベント)が発生する可能性があります。Goランタイムは、これらの例外を捕捉し、適切に処理するための独自のメカニズム(シグナルハンドラに相当するもの)を持っています。しかし、GoプログラムがWindows APIを呼び出したり、C/C++で書かれた外部ライブラリ(DLLなど)を使用したりする場合、Goバイナリのコードセグメント外で例外が発生することがあります。
このような場合、Goランタイムがその例外を捕捉して処理しようとすると、以下のような問題が発生する可能性がありました。
1. **不必要なクラッシュ**: Goランタイムが意図しない例外を捕捉し、それをGoのパニックとして処理しようとすることで、プログラム全体がクラッシュする。
2. **デバッガとの競合**: `DBG_PRINTEXCEPTION_C`のようなデバッガ向けの例外がGoランタイムによって捕捉され、デバッガの挙動を妨げる。
3. **誤ったスタックトレース**: Goのコードとは無関係な場所で発生した例外に対して、Goのスタックトレースが生成され、デバッグを困難にする。
このコミットは、これらの問題を解決するために、例外が発生した命令ポインタ(EIP/RIP)がGoバイナリのコードセグメント内にあるかどうかをチェックし、そうでない場合はGoランタイムがその例外を無視するように変更することで、Goプログラムの安定性とデバッグのしやすさを向上させています。
## 前提知識の解説
### Windows Structured Exception Handling (SEH)
Windowsでは、プログラムの実行中に発生するハードウェア例外(例: ゼロ除算、無効なメモリアクセス)やソフトウェア例外(例: `RaiseException`関数による意図的な例外)を処理するために、構造化例外処理(SEH)というメカニズムが提供されています。SEHは、`__try`, `__except`, `__finally`ブロックを使用して例外を捕捉し、処理することを可能にします。
Goランタイムは、Windows上で動作する際に、このSEHメカニズムを利用して、Goプログラム内で発生するパニックやその他のランタイムエラーを捕捉し、Goのパニックハンドラに変換します。
### `ExceptionRecord`と`Context`構造体
Windowsの例外ハンドラに渡される主要な情報構造体です。
* `ExceptionRecord`: 発生した例外の種類(`ExceptionCode`)、例外が発生したアドレス(`ExceptionAddress`)などの例外に関する詳細情報を含みます。
* `Context`: 例外発生時のCPUレジスタの状態(EAX, EBX, ECX, EDX, EIP/RIPなど)を含みます。これにより、例外発生時のプログラムの状態を再現したり、変更したりすることが可能です。
### `DBG_PRINTEXCEPTION_C`
これは、MicrosoftのCランタイムライブラリ(CRT)がデバッガに対して特定のメッセージを出力するために使用する特別な例外コードです。通常、`_invalid_parameter`のようなCRT関数が不正な引数を受け取った場合に発生します。この例外はデバッガによって捕捉されることを意図しており、デバッガがアタッチされていない場合は、デフォルトの例外ハンドラによって処理され、アプリケーションがクラッシュする可能性があります。Goランタイムがこれを捕捉すると、デバッガの挙動を妨げたり、Goのパニックとして誤って処理したりする問題がありました。
### `EXCEPTION_BREAKPOINT`
これは、デバッガがブレークポイントを設定した際に発生する例外コードです。Goランタイムがこれを捕捉すると、デバッガの通常の動作を妨げる可能性があります。
### Goバイナリの`text`と`etext`シンボル
Goコンパイラは、生成されるバイナリのコードセグメントの開始アドレスを`text`シンボル、終了アドレスを`etext`シンボルとして定義します。これらのシンボルは、Goプログラムの実行可能コードがメモリ上のどこに配置されているかを示します。
このコミットでは、例外が発生した命令ポインタ(`Eip` for x86, `Rip` for x64)が`text`と`etext`の範囲内にあるかどうかをチェックすることで、例外がGoのコード内で発生したものであるかを判断しています。
### `runtime·sighandler`と`runtime·sigtramp`
* `runtime·sigtramp`: WindowsのSEHメカニズムによって呼び出される、Goランタイムの低レベルな例外ディスパッチャです。これは、Windowsから受け取った例外情報をGoのランタイムが理解できる形式に変換し、`runtime·sighandler`を呼び出します。
* `runtime·sighandler`: Goランタイムの主要な例外ハンドラです。`sigtramp`から渡された例外情報に基づいて、Goのパニックを生成したり、特定の例外を無視したりするなどの処理を行います。
## 技術的詳細
このコミットの主要な変更点は、Goランタイムの例外ハンドラが、例外が発生した命令ポインタ(EIP/RIP)がGoバイナリのコードセグメント内にあるかどうかをチェックするロジックを追加したことです。
### `src/pkg/runtime/os_windows_386.c` および `src/pkg/runtime/os_windows_amd64.c` の変更
これらのファイルは、それぞれ32ビット(x86)と64ビット(AMD64)のWindowsアーキテクチャにおけるGoランタイムのシグナルハンドラ(`runtime·sighandler`)の実装を含んでいます。
変更前は、`DBG_PRINTEXCEPTION_C`例外を`switch`文の`case`として直接処理していました。変更後は、この例外を`if`文で先にチェックし、常に`-1`(例外を処理済みとしてWindowsに通知し、Goランタイムはこれ以上処理しない)を返すようにしました。これにより、デバッガ向けの例外がGoランタイムの通常の例外処理フローに入り込むのを防ぎます。
さらに重要な変更として、`DBG_PRINTEXCEPTION_C`の処理後に、以下のチェックが追加されました。
```c
// Only handle exception if executing instructions in Go binary
// (not Windows library code).
if(r->Eip < (uint32)text || (uint32)etext < r->Eip)
return 0;
(os_windows_amd64.c
ではr->Rip
とuint64
を使用)
このコードは、Context
構造体r
から現在の命令ポインタ(Eip
またはRip
)を取得し、それがGoバイナリのコードセグメント(text
からetext
の範囲)外にあるかどうかをチェックします。もし命令ポインタがGoバイナリの範囲外であれば、0
を返します。これは、Goランタイムがこの例外を処理しないことをWindowsに通知し、Windowsの次の例外ハンドラに処理を委ねることを意味します。
これにより、Goのコードとは無関係な場所(例えば、WindowsのシステムDLLや外部のC/C++ライブラリ)で発生した例外は、Goランタイムによって無視され、Goのパニックに変換されることがなくなります。
src/pkg/runtime/sys_windows_386.s
の変更
このファイルは、32ビットWindowsにおけるGoランタイムの低レベルアセンブリコードを含んでおり、特にruntime·sigtramp
関数の実装が含まれています。
変更前は、runtime·sigtramp
内で命令ポインタがGoバイナリの範囲内にあるかどうかのチェック(CMPL DX, $text(SB)
など)を試みていましたが、コメントで「これは良いアイデアのように聞こえるが、Goが提供するトレースバックはWindowsのクラッシュダイアログよりも優れている」と述べられており、最終的にJMP skipcheckpc
によってこのチェックをスキップしていました。つまり、Goバイナリ外で発生した例外もすべてruntime·sighandler
に渡されていました。
このコミットでは、runtime·sigtramp
からこの命令ポインタのチェックロジックが完全に削除されました。代わりに、このチェックはより高レベルのruntime·sighandler
関数(C言語で記述されている)に移されました。これにより、アセンブリレベルでの複雑な条件分岐を避け、C言語でのより柔軟で読みやすいロジックで例外のフィルタリングを行うことができるようになりました。
コアとなるコードの変更箇所
src/pkg/runtime/os_windows_386.c
(同様の変更が os_windows_amd64.c
にも適用)
--- a/src/pkg/runtime/os_windows_386.c
+++ b/src/pkg/runtime/os_windows_386.c
@@ -34,9 +34,9 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
{\n \tbool crash;\n \tuintptr *sp;\n+\textern byte text[], etext[];\n \n-\tswitch(info->ExceptionCode) {\n-\tcase DBG_PRINTEXCEPTION_C:\n+\tif(info->ExceptionCode == DBG_PRINTEXCEPTION_C) {\n \t\t// This exception is intended to be caught by debuggers.\n \t\t// There is a not-very-informational message like\n \t\t// \"Invalid parameter passed to C runtime function\"\n@@ -47,7 +47,14 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)\n \t\t// makes the program crash instead. Maybe Windows has no\n \t\t// other handler registered? In any event, ignore it.\n \t\treturn -1;\n+\t}\n \n+\t// Only handle exception if executing instructions in Go binary\n+\t// (not Windows library code). \n+\tif(r->Eip < (uint32)text || (uint32)etext < r->Eip)\n+\t\treturn 0;\n+\n+\tswitch(info->ExceptionCode) {\n \tcase EXCEPTION_BREAKPOINT:\n \t\t// It is unclear whether this is needed, unclear whether it\n \t\t// would work, and unclear how to test it. Leave out for now.\n```
### `src/pkg/runtime/sys_windows_386.s`
```diff
--- a/src/pkg/runtime/sys_windows_386.s
+++ b/src/pkg/runtime/sys_windows_386.s
@@ -74,37 +74,18 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0
// exception record and context pointers.\n // Return 0 for 'not handled', -1 for handled.\n TEXT runtime·sigtramp(SB),NOSPLIT,$0-0\n-\tMOVL\tptrs+0(FP), DI\n+\tMOVL\tptrs+0(FP), CX\n \tSUBL\t$28, SP\n-\tMOVL\t0(DI), BX // ExceptionRecord*\n-\tMOVL\t4(DI), CX // Context*\n-\n-\t// Only handle exception if executing instructions in Go binary\n-\t// (not Windows library code). Except don't - keep reading.\n-\t// \n-\t// This sounds like a good idea but the tracebacks that\n-\t// Go provides are better than the Windows crash dialog,\n-\t// especially if it's something that Go needs to do.\n-\t// So take all the exceptions, not just the ones at Go PCs.\n-\t// If you re-enable this check by removing the JMP, you will\n-\t// need to arrange to handle exception 0x40010006 during\n-\t// non-Go code here. Right now that case is handled by sighandler\n-\t// in os_windows_386.c.\n-\tJMP skipcheckpc\n-\tMOVL\t$0, AX\n-\tMOVL\t184(CX), DX // saved PC\n-\tCMPL\tDX, $text(SB)\n-\tJB\tvehret\n-\tCMPL\tDX, $etext(SB)\n-\tJA\tvehret\n-\n-skipcheckpc:\n+\n \t// save callee-saved registers\n \tMOVL\tBX, 12(SP)\n \tMOVL\tBP, 16(SP)\n \tMOVL\tSI, 20(SP)\n \tMOVL\tDI, 24(SP)\n \n+\tMOVL\t0(CX), BX // ExceptionRecord*\n+\tMOVL\t4(CX), CX // Context*\n+\n \t// fetch g\n \tget_tls(DX)\n \tMOVL\tm(DX), AX\n@@ -117,6 +98,7 @@ skipcheckpc:\n \tMOVL\tCX, 4(SP)\n \tMOVL\tDX, 8(SP)\n \tCALL\truntime·sighandler(SB)\n+\t// AX is set to report result back to Windows\n \n \t// restore callee-saved registers\n \tMOVL\t24(SP), DI\n@@ -124,7 +106,6 @@ skipcheckpc:\n \tMOVL\t16(SP), BP\n \tMOVL\t12(SP), BX\n \n-vehret:\n \tADDL\t$28, SP\n \t// RET 4 (return and pop 4 bytes parameters)\n \tBYTE $0xC2; WORD $4\n```
## コアとなるコードの解説
### `os_windows_386.c` および `os_windows_amd64.c` の変更点
1. **`extern byte text[], etext[];` の追加**:
Goバイナリのコードセグメントの開始と終了アドレスを示す`text`と`etext`シンボルを外部参照として宣言しています。これにより、Cコード内でこれらのアドレスを使用して、命令ポインタがGoのコード範囲内にあるかをチェックできるようになります。
2. **`DBG_PRINTEXCEPTION_C` の先行処理**:
`switch`文の前に`if(info->ExceptionCode == DBG_PRINTEXCEPTION_C)`という条件を追加し、この特定の例外コードを先に処理するように変更しました。この例外はデバッガ向けのものであるため、Goランタイムがこれを捕捉してパニックに変換するのではなく、すぐに`-1`を返してWindowsに処理済みであることを通知します。これにより、デバッガの挙動が妨げられるのを防ぎます。
3. **Goバイナリ内での実行チェック**:
`DBG_PRINTEXCEPTION_C`の処理後、かつ他の例外タイプを処理する`switch`文の前に、以下の重要なチェックが追加されました。
```c
if(r->Eip < (uint32)text || (uint32)etext < r->Eip)
return 0;
```
このコードは、例外が発生した時点の命令ポインタ(`r->Eip` for x86, `r->Rip` for x64)が、Goバイナリのコードセグメント(`text`から`etext`の範囲)の外にあるかどうかを判断します。
* `r->Eip < (uint32)text`: 命令ポインタがGoコードの開始アドレスより前にある場合。
* `(uint32)etext < r->Eip`: 命令ポインタがGoコードの終了アドレスより後にある場合。
これらの条件のいずれかが真であれば、例外はGoのコード外で発生したと判断され、`return 0;`が実行されます。`0`を返すことは、Goランタイムがこの例外を処理せず、Windowsの次の例外ハンドラに処理を委ねることを意味します。これにより、Goのコードとは無関係な例外がGoのパニックとして誤って処理されることを防ぎます。
### `sys_windows_386.s` の変更点
1. **命令ポインタチェックロジックの削除**:
変更前は、`runtime·sigtramp`アセンブリ関数内で命令ポインタがGoバイナリの範囲内にあるかをチェックするロジックが存在しましたが、`JMP skipcheckpc`によって常にスキップされていました。このコミットでは、この不要なチェックロジックと関連するラベル(`skipcheckpc`, `vehret`)が完全に削除されました。
2. **レジスタの受け渡し方法の変更**:
`MOVL ptrs+0(FP), DI` が `MOVL ptrs+0(FP), CX` に変更され、`ExceptionRecord*` と `Context*` のポインタを格納するレジスタが変更されています。これは、後続のC関数呼び出し(`runtime·sighandler`)への引数渡しの一貫性を保つための調整です。
3. **`runtime·sighandler`への処理委譲**:
アセンブリレベルでの複雑な条件分岐を削除し、例外のフィルタリングロジックをC言語で記述された`runtime·sighandler`に完全に委譲することで、コードの可読性と保守性が向上しました。`runtime·sigtramp`は、Windowsから受け取った例外情報を`runtime·sighandler`に渡し、その戻り値(`AX`レジスタに格納される)をWindowsに報告する役割に特化しました。
これらの変更により、GoランタイムはWindows例外処理において、より選択的かつ効率的に動作するようになり、Goプログラムの堅牢性が向上しました。
## 関連リンク
* Go CL 80400043: [https://golang.org/cl/80400043](https://golang.org/cl/80400043)
## 参考にした情報源リンク
* Windows Structured Exception Handling (SEH) の概念に関するMicrosoftドキュメント
* Go言語のランタイムソースコード(特に`src/pkg/runtime`ディレクトリ)
* Go言語のIssueトラッカーやメーリングリストでの関連議論(必要に応じて検索)
* `DBG_PRINTEXCEPTION_C`に関する情報源(例: Stack Overflow, Microsoft Docs)