[インデックス 18728] ファイルの概要
このコミットは、GoランタイムにおけるWindows環境でのトレースバック(スタックトレース)の不具合を修正するものです。具体的には、Windowsの例外ハンドラが通常のgoroutineスタック上で動作する際に、スタックコピー処理がその例外ハンドラを横断してトレースバックを試みる際に発生する問題を解決します。これにより、Windows/amd64ビルドが修正され、例外ハンドラをまたいだスタックトレースが正しく行われるようになります。
コミット
- コミットハッシュ:
0a3bd045f50d7a9cfeb3ad1418588c3ab5de329f
- 作者: Russ Cox rsc@golang.org
- コミット日時: Mon Mar 3 23:33:27 2014 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a3bd045f50d7a9cfeb3ad1418588c3ab5de329f
元コミット内容
runtime: fix traceback on Windows
The exception handler runs on the ordinary g stack,
and the stack copier is now trying to do a traceback
across it. That's never been needed before, so it was
unimplemented. Implement it, in all its ugliness.
Fixes windows/amd64 build.
TBR=khr
CC=golang-codereviews
https://golang.org/cl/71030043
変更の背景
Goランタイムは、プログラムの実行中にエラーが発生した際や、デバッグのために、現在の実行スタック(コールスタック)を遡って関数呼び出しの履歴を表示する「トレースバック」機能を提供します。これは、Goのgoroutineスタック上で動作します。
しかし、Windows環境における例外ハンドラの動作が、他のOS(LinuxやmacOSなど)と異なることが問題の根源でした。一般的なUnix系OSでは、シグナルハンドラ(例外ハンドラの一種)は通常、メインスタックとは別の専用のスタック(シグナルスタック)で実行されます。これにより、シグナルハンドラが実行されている間にメインスタックが破損しても、ハンドラ自身は安全に動作し、スタックトレースも比較的容易に行えます。
一方、Windowsでは、例外ハンドラは「通常のgoroutineスタック」上で実行されます。Goのランタイムは、スタックの動的な拡張(スタックコピー)を頻繁に行いますが、このスタックコピー処理中に例外が発生し、その例外ハンドラが通常のgoroutineスタック上で動作すると、スタックコピー処理が例外ハンドラのフレームを横断してトレースバックを試みる必要が生じました。
これまでのGoランタイムでは、このようなシナリオ(例外ハンドラをまたいだトレースバック)は想定されておらず、未実装でした。その結果、Windows/amd64環境でのビルドや実行時に、トレースバックが正しく機能しない、あるいはクラッシュするなどの問題が発生していました。このコミットは、この未実装の動作を「その醜さの全てにおいて」実装することで、問題を解決することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念を把握しておく必要があります。
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、スケジューラ、スタック管理、プリエンプション、システムコールインターフェースなど、Go言語の並行性モデルとメモリ管理を支える重要なコンポーネントを含みます。
- Goroutineスタック (Goroutine Stack): Goのgoroutineは、OSのスレッドよりもはるかに軽量な実行単位です。各goroutineは独自のスタックを持ち、このスタックは必要に応じて動的にサイズが変更されます(スタックの拡大・縮小)。スタックが不足すると、ランタイムはより大きな新しいスタックを割り当て、古いスタックの内容を新しいスタックにコピーします。
- トレースバック (Traceback / Stack Trace): プログラムの実行中にエラーやパニックが発生した際に、その時点での関数呼び出しの履歴(コールスタック)を表示する機能です。これにより、どの関数がどの関数を呼び出し、最終的にエラーが発生した場所に至ったのかを追跡できます。デバッグにおいて非常に重要な情報です。
- 例外ハンドリング (Exception Handling): プログラム実行中に発生する予期せぬイベント(例外やシグナル)を捕捉し、適切に処理するメカニズムです。Windowsでは、構造化例外処理 (Structured Exception Handling, SEH) と呼ばれるメカニズムが用いられます。
sigtramp
(Signal Trampoline): Unix系OSにおけるシグナルハンドラの呼び出しに使われる、アセンブリ言語で書かれた小さなコード片です。カーネルがシグナルを配送する際に、ユーザー空間のシグナルハンドラを呼び出す前に、コンテキストの保存などを行う役割を担います。Goランタイムでは、OSからのシグナル(Windowsでは例外)を受け取り、Goのパニック機構に変換する役割を果たす関数としてruntime·sigtramp
が存在します。Context
構造体 (Windows CONTEXT structure): Windows APIの一部で、スレッドのレジスタの状態(プログラムカウンタ、スタックポインタなど)を含むプロセッサ固有のコンテキスト情報を保持する構造体です。例外発生時に、例外ハンドラに渡される情報の一部として、例外発生時のCPUの状態がこの構造体に格納されます。このコミットでは、このContext
構造体からスタックポインタやプログラムカウンタを復元することで、例外ハンドラをまたいだトレースバックを実現しています。
技術的詳細
このコミットの核心は、Windowsにおける例外ハンドラの特殊な動作と、それに対応するためのGoランタイムのスタックトレース機構の変更です。
Goランタイムのスタックトレースは、基本的に現在のスタックフレームを遡り、各関数の呼び出し元(リターンアドレス)とスタックポインタを特定することで行われます。しかし、Windowsの例外ハンドラが通常のgoroutineスタック上で動作するという特性が、このプロセスを複雑にしました。
問題は、スタックコピー(スタックが不足した際に新しい大きなスタックに内容をコピーする処理)中に例外が発生し、その例外ハンドラが起動した場合に顕在化します。このとき、スタックコピー処理は、例外ハンドラのスタックフレームを「透過的に」スキップして、その下の本来のgoroutineのスタックフレームに到達し、トレースバックを継続する必要があります。しかし、例外ハンドラのフレームは通常の関数呼び出しとは異なるコンテキストで動作するため、通常のスタックトレースロジックではこれを正しく処理できませんでした。
このコミットでは、src/pkg/runtime/traceback_x86.c
内の runtime·gentraceback
関数に、Windows固有の処理を追加することでこの問題を解決しています。具体的には、トレースバック中に runtime·sigtramp
関数(GoランタイムがWindowsの例外を捕捉するために使用する内部関数)のフレームに遭遇した場合、それが例外ハンドラの呼び出しであることを認識します。
そして、runtime·sigtramp
が例外ハンドラに渡す Context
構造体を利用して、例外発生時のCPUの状態(プログラムカウンタ Rip
/Eip
、スタックポインタ Rsp
/Esp
)を復元します。これにより、例外ハンドラのフレームを「飛び越えて」、例外が発生する直前の本来のgoroutineの実行コンテキストにスタックトレースを戻すことが可能になります。
コミットメッセージにある「その醜さの全てにおいて (in all its ugliness)」という表現は、この解決策が、Windowsの例外処理モデルの複雑さに起因する、Goランタイムのスタックトレース機構への侵襲的な変更であることを示唆しています。通常のスタックフレームの連なりを追うのではなく、特定の関数(sigtramp
)の内部構造と、そこに渡されるOS固有のコンテキスト情報に依存してスタックを巻き戻す必要があるため、コードが複雑になり、移植性も低下する可能性があることを開発者が認識していることを示しています。
コアとなるコードの変更箇所
変更は src/pkg/runtime/traceback_x86.c
ファイルに集中しています。
--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -8,9 +8,16 @@
#include "arch_GOARCH.h"
#include "malloc.h"
#include "funcdata.h"
+#ifdef GOOS_windows
+#include "defs_GOOS_GOARCH.h"
+#endif
void runtime·sigpanic(void);
+#ifdef GOOS_windows
+void runtime·sigtramp(void);
+#endif
+
// This code is also used for the 386 tracebacks.
// Use uintptr for an appropriate word-sized integer.
@@ -95,8 +102,50 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\
frame.fn = f;
continue;
}
+\t\t
\t\tf = frame.fn;\
+#ifdef GOOS_windows
+\t\t// Windows exception handlers run on the actual g stack (there is room
+\t\t// dedicated to this below the usual "bottom of stack"), not on a separate
+\t\t// stack. As a result, we have to be able to unwind past the exception
+\t\t// handler when called to unwind during stack growth inside the handler.
+\t\t// Recognize the frame at the call to sighandler in sigtramp and unwind
+\t\t// using the context argument passed to the call. This is awful.
+\t\tif(f != nil && f->entry == (uintptr)runtime·sigtramp && frame.pc > f->entry) {
+\t\t\tContext *r;\
+\t\t\t
+\t\t\t// Invoke callback so that stack copier sees an uncopyable frame.
+\t\t\tif(callback != nil) {
+\t\t\t\tframe.argp = nil;\
+\t\t\t\tframe.arglen = 0;\
+\t\t\t\tif(!callback(&frame, v))\
+\t\t\t\t\treturn n;\
+\t\t\t}\
+\t\t\tr = (Context*)((uintptr*)frame.sp)[1];
+#ifdef GOARCH_amd64
+\t\t\tframe.pc = r->Rip;\
+\t\t\tframe.sp = r->Rsp;\
+#else
+\t\t\tframe.pc = r->Eip;\
+\t\t\tframe.sp = r->Esp;\
+#endif
+\t\t\tframe.lr = 0;\
+\t\t\tframe.fp = 0;\
+\t\t\tframe.fn = nil;\
+\t\t\tif(printing && runtime·showframe(nil, gp))\
+\t\t\t\truntime·printf(\"----- exception handler -----\\n\");
+\t\t\tf = runtime·findfunc(frame.pc);\
+\t\t\tif(f == nil) {
+\t\t\t\truntime·printf(\"runtime: unknown pc %p after exception handler\\n\", frame.pc);\
+\t\t\t\tif(callback != nil)\
+\t\t\t\t\truntime·throw(\"unknown pc\");
+\t\t\t}\
+\t\t\tframe.fn = f;\
+\t\t\tcontinue;\
+\t\t}\
+#endif
+\n \t\t// Found an actual function.\
\t\t// Derive frame pointer and link register.\
\t\tif(frame.fp == 0) {
コアとなるコードの解説
追加されたコードは、runtime·gentraceback
関数(スタックトレースを生成する主要な関数)の内部に、Windows固有の条件付きコンパイルブロック (#ifdef GOOS_windows
) として実装されています。
-
runtime·sigtramp
の前方宣言:#ifdef GOOS_windows void runtime·sigtramp(void); #endif
runtime·sigtramp
関数がWindows環境でのみ使用されることを示し、その存在をコンパイラに知らせます。 -
Windows例外ハンドラフレームの検出と処理:
runtime·gentraceback
のループ内で、現在のスタックフレームがruntime·sigtramp
のものであるかをチェックします。if(f != nil && f->entry == (uintptr)runtime·sigtramp && frame.pc > f->entry) { // ... }
f != nil
: 関数情報が存在することを確認。f->entry == (uintptr)runtime·sigtramp
: 現在のフレームの開始アドレスがruntime·sigtramp
関数のエントリポイントと一致するかを確認。frame.pc > f->entry
: プログラムカウンタがsigtramp
関数の内部にあることを確認(エントリポイントより大きい)。
この条件が真の場合、現在のフレームがWindowsの例外ハンドラを呼び出す
sigtramp
のフレームであると判断します。 -
Context
構造体からのレジスタ復元:Context *r; // ... r = (Context*)((uintptr*)frame.sp)[1]; #ifdef GOARCH_amd64 frame.pc = r->Rip; frame.sp = r->Rsp; #else frame.pc = r->Eip; frame.sp = r->Esp; #endif
r = (Context*)((uintptr*)frame.sp)[1];
: ここが最も重要な部分です。sigtramp
は、例外発生時のCPUコンテキストを保持するContext
構造体へのポインタを、スタック上の特定の位置(frame.sp
の1つ上のワード)に引数として渡します。このコードは、そのポインタを取得し、Context
型にキャストしています。#ifdef GOARCH_amd64
/#else
: アーキテクチャ(amd64またはx86)に応じて、Context
構造体からプログラムカウンタ (Rip
/Eip
) とスタックポインタ (Rsp
/Esp
) を抽出し、frame.pc
とframe.sp
を更新します。これにより、トレースバックの視点がsigtramp
のフレームから、例外が発生する直前の本来の実行コンテキストへと「巻き戻され」ます。
-
フレーム情報のクリアと再検索:
frame.lr = 0; frame.fp = 0; frame.fn = nil; // ... f = runtime·findfunc(frame.pc); // ... frame.fn = f; continue;
sigtramp
フレームをスキップした後、リターンアドレス (lr
) やフレームポインタ (fp
) をクリアし、新しいframe.pc
に基づいて再度関数情報を検索 (runtime·findfunc
) します。continue
によって、次のトレースバックループのイテレーションに進み、例外ハンドラをまたいだトレースバックが継続されます。
この変更により、GoランタイムはWindows環境においても、例外ハンドラが通常のgoroutineスタック上で動作するシナリオで、正確なスタックトレースを生成できるようになりました。
関連リンク
- Go issue: https://golang.org/cl/71030043 (このコミットのChange-ID)
- Go issue: https://github.com/golang/go/issues/7103 (関連する可能性のあるGoのIssueトラッカー)
参考にした情報源リンク
- Goのソースコード (src/pkg/runtime/traceback_x86.c) (現在のGoのバージョンではファイル名が変更されていますが、当時のコードの論理は類似しています)
- Windows Structured Exception Handling (SEH) (Microsoft Learn)
- Go runtime: stack management (Goのスタック管理に関するソースコード)
- Go runtime: sigtramp (Windows/amd64における
sigtramp
のアセンブリコード) - CONTEXT structure (WinNT.h) (Microsoft Learn)