Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19706] ファイルの概要

このコミットは、GoランタイムがWindows上で動作する際に、Goランタイム自身が管理していない「外部スレッド(foreign threads)」から発生した例外を無視するように変更を加えるものです。これにより、外部スレッドが引き起こす予期せぬクラッシュを防ぎ、Goプログラムの安定性を向上させます。

コミット

commit a1778ec1462c2f3f8865e02e5fd7e72ee25c2b64
Author: Shenghou Ma <minux@golang.org>
Date:   Wed Jul 9 23:55:35 2014 -0400

    runtime: ignore exceptions from foreign threads.
    Fixes #8224.
    
    LGTM=alex.brainman, rsc
    R=alex.brainman, rsc, dave
    CC=golang-codereviews
    https://golang.org/cl/104200046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a1778ec1462c2f3f8865e02e5fd7e7e72ee25c2b64

元コミット内容

runtime: ignore exceptions from foreign threads.
Fixes #8224.

LGTM=alex.brainman, rsc
R=alex.brainman, rsc, dave
CC=golang-codereviews
https://golang.org/cl/104200046

変更の背景

Goランタイムは、自身のゴルーチン(goroutine)をOSスレッドに多重化して実行します。Windows環境では、Goランタイムが管理するスレッドとは別に、外部のライブラリやC/C++コードなどによって作成された「外部スレッド(foreign threads)」が存在する場合があります。これらの外部スレッドが例外(例えば、アクセス違反やゼロ除算など)を発生させた場合、Goランタイムの例外ハンドリングメカニズムがその例外を捕捉しようとします。

しかし、Goランタイムの例外ハンドラは、Goが管理するスレッドのコンテキスト(特にGoのスケジューラが認識するg構造体、つまり現在のゴルーチン情報)に依存しています。外部スレッドはGoランタイムの管理下にないため、Goランタイムが期待するg構造体を持っていません。この状態で外部スレッドからの例外がGoランタイムの例外ハンドラに渡されると、g構造体へのアクセスが不正となり、結果としてGoプログラム全体がクラッシュする可能性がありました。

このコミットは、この問題を解決するために導入されました。コミットメッセージにある Fixes #8224 は、この問題に関連するバグトラッカーのイシューを参照していますが、現在の公開されているGoのイシュートラッカーでは直接的な詳細を見つけることはできませんでした。しかし、この変更の目的は、Goランタイムが外部スレッドからの例外を適切に識別し、それらを無視することで、Goプログラムの堅牢性を高めることにあります。

前提知識の解説

Goランタイムとゴルーチン、OSスレッド

  • ゴルーチン (Goroutine): Go言語の軽量な並行処理単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。GoランタイムのスケジューラがゴルーチンをOSスレッドにマッピングし、実行を管理します。
  • OSスレッド (Operating System Thread): オペレーティングシステムが管理する実行単位です。Goランタイムは、複数のゴルーチンを少数のOSスレッドに多重化して実行します。Goプログラムがシステムコールを実行したり、runtime.LockOSThread()を呼び出したりすると、特定のゴルーチンが特定のOSスレッドに「ピン留め」されることがあります。
  • 外部スレッド (Foreign Threads): Goランタイム自身が作成・管理していないOSスレッドを指します。例えば、C言語で書かれたライブラリをGoから呼び出し、そのライブラリが内部で独自のスレッドを作成した場合、そのスレッドはGoランタイムにとっての「外部スレッド」となります。

Windows Structured Exception Handling (SEH)

Windowsオペレーティングシステムは、プログラム実行中に発生する例外(ハードウェア例外、ソフトウェア例外など)を処理するための独自のメカニズムとして「構造化例外処理(Structured Exception Handling, SEH)」を提供しています。

  • 例外の種類: アクセス違反(メモリの不正アクセス)、ゼロ除算、スタックオーバーフローなど、様々な種類の例外があります。
  • 例外ハンドラ: プログラムは、__try, __except, __finally などのキーワード(C/C++の場合)を使用して、例外発生時に実行されるコードブロック(例外ハンドラ)を登録できます。
  • VEH (Vectored Exception Handling): SEHとは別に、Windows XP以降で導入された例外処理メカニズムです。VEHハンドラは、SEHハンドラよりも先に呼び出されるため、より広範な例外処理を可能にします。Goランタイムのsigtramp関数は、Windows上では主にVEHとして機能します。

sigtramp関数

Goランタイムにおけるsigtramp(シグナルトランポリン)は、OSからのシグナルや例外を捕捉し、Goランタイムの例外処理メカニズムに橋渡しをする低レベルの関数です。Windows環境では、POSIXシグナルが存在しないため、sigtrampは主にWindowsのSEHやVEHと連携して動作します。

  • 役割: ハードウェア例外が発生した際に、OSによって呼び出されます。Goランタイムのスタックに切り替えたり、Windowsの例外をGoのパニック(panic)に変換したりする役割を担います。
  • 実装: 非常に低レベルな処理であるため、通常はアセンブリ言語で実装されています。

技術的詳細

このコミットの核心は、Goランタイムの例外ハンドラであるruntime·sigtrampが、例外を処理する前に、その例外を発生させたスレッドがGoランタイムによって管理されているスレッドであるかどうかをチェックする点にあります。

Goランタイムが管理するスレッドは、スレッドローカルストレージ(TLS: Thread Local Storage)に現在のゴルーチン(g構造体)へのポインタを格納しています。このgポインタは、Goランタイムがスレッドのコンテキストを識別するために不可欠です。

変更前のsigtrampは、例外が発生すると無条件にTLSからgポインタを取得しようとしました。しかし、外部スレッドはGoランタイムによって初期化されていないため、TLSに有効なgポインタを持っていません。この状態でgポインタを読み取ろうとすると、不正なメモリアクセスが発生し、プログラムがクラッシュする原因となっていました。

このコミットでは、sigtrampの冒頭でTLSから取得した値(gポインタが格納されるべき場所)がゼロであるかどうかをチェックするコードが追加されました。

  • もしTLSの値がゼロであれば、それはGoランタイムが管理していない外部スレッドからの例外であると判断されます。
  • この場合、Goランタイムはその例外を無視し、Windowsに処理を継続するように指示します(AXレジスタに0を設定してリターン)。これにより、外部スレッドが引き起こした例外がGoランタイムの内部処理を妨害することを防ぎます。
  • TLSの値がゼロでなければ、それはGoランタイムが管理するスレッドからの例外であると判断され、通常の例外処理フロー(runtime·sighandlerの呼び出しなど)に進みます。

この変更により、Goランタイムは自身の管理外のスレッドからの例外に対して堅牢になり、Goプログラム全体の安定性が向上しました。

コアとなるコードの変更箇所

src/pkg/runtime/sys_windows_386.s および src/pkg/runtime/sys_windows_amd64.sruntime·sigtramp 関数に以下の変更が加えられました。

--- a/src/pkg/runtime/sys_windows_386.s
+++ b/src/pkg/runtime/sys_windows_386.s
@@ -88,6 +88,10 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
 
  	// fetch g
  	get_tls(DX)
+	CMPL	DX, $0
+	JNE	3(PC)
+	MOVL	$0, AX // continue
+	JMP	done
  	MOVL	g(DX), DX
  	CMPL	DX, $0
  	JNE	2(PC)
@@ -99,6 +103,7 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
  	CALL	runtime·sighandler(SB)
  	// AX is set to report result back to Windows
 
+done:
  	// restore callee-saved registers
  	MOVL	24(SP), DI
  	MOVL	20(SP), SI
--- a/src/pkg/runtime/sys_windows_amd64.s
+++ b/src/pkg/runtime/sys_windows_amd64.s
@@ -120,6 +120,10 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
 
  	// fetch g
  	get_tls(DX)
+	CMPQ	DX, $0
+	JNE	3(PC)
+	MOVQ	$0, AX // continue
+	JMP	done
  	MOVQ	g(DX), DX
  	CMPQ	DX, $0
  	JNE	2(PC)
@@ -131,6 +135,7 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
  	CALL	runtime·sighandler(SB)
  	// AX is set to report result back to Windows
 
+done:
  	// restore registers as required for windows callback
  	MOVQ	24(SP), R15
  	MOVQ	32(SP), R14

コアとなるコードの解説

追加されたアセンブリコードは、32-bit (sys_windows_386.s) と 64-bit (sys_windows_amd64.s) の両方で同様のロジックを実装しています。

sys_windows_386.s (32-bit) の変更点

 	// fetch g
 	get_tls(DX)     // スレッドローカルストレージ (TLS) から現在のgポインタのアドレスをDXレジスタに取得
+	CMPL	DX, $0  // DXレジスタの値が0と比較
+	JNE	3(PC)   // DXが0でなければ、現在のPCから3バイト先にジャンプ (次のMOVL g(DX), DX命令をスキップ)
+	MOVL	$0, AX  // AXレジスタに0を移動 (Windowsに例外処理を継続させることを伝える)
+	JMP	done    // doneラベルにジャンプし、関数を終了
 	MOVL	g(DX), DX // DXが0でない場合、TLSからgポインタ自体をDXレジスタにロード
 	CMPL	DX, $0
 	JNE	2(PC)
  • get_tls(DX): このマクロは、現在のスレッドのTLSから、Goランタイムが管理するg構造体(現在のゴルーチン)へのポインタが格納されているメモリ位置のアドレスをDXレジスタにロードします。Goランタイムが管理するスレッドの場合、このアドレスは有効なメモリ領域を指します。外部スレッドの場合、このアドレスは通常ゼロ、または不正な値になります。
  • CMPL DX, $0: DXレジスタの値がゼロ($0)と比較されます。
  • JNE 3(PC): もしDXがゼロでなければ(JNEはJump if Not Equal)、プログラムカウンタ(PC)から3バイト先にジャンプします。これは、次の命令であるMOVL g(DX), DXをスキップすることを意味します。つまり、Goランタイムが管理するスレッドであれば、このチェックを通過して通常のgポインタのロードに進みます。
  • MOVL $0, AX: もしDXがゼロであれば(つまり、外部スレッドからの例外である場合)、AXレジスタにゼロをロードします。Windowsの例外処理メカニズムでは、例外ハンドラが0を返すと、OSは例外処理を継続(つまり、次のハンドラを探すか、デフォルトの処理を行う)します。
  • JMP done: doneラベルに無条件にジャンプします。これにより、runtime·sigtramp関数の残りの部分(runtime·sighandlerの呼び出しなど)をスキップし、関数を終了します。
  • done:: 新しく追加されたラベルで、関数の終了処理(レジスタの復元など)が行われる場所です。

sys_windows_amd64.s (64-bit) の変更点

64-bit版も同様のロジックですが、レジスタ名と命令が64-bit用に変更されています。

 	// fetch g
 	get_tls(DX)     // TLSから現在のgポインタのアドレスをDXレジスタに取得
+	CMPQ	DX, $0  // DXレジスタの値が0と比較 (64-bit)
+	JNE	3(PC)   // DXが0でなければ、現在のPCから3バイト先にジャンプ
+	MOVQ	$0, AX  // AXレジスタに0を移動 (64-bit)
+	JMP	done    // doneラベルにジャンプ
 	MOVQ	g(DX), DX // DXが0でない場合、TLSからgポインタ自体をDXレジスタにロード (64-bit)
 	CMPQ	DX, $0
 	JNE	2(PC)
  • CMPQ DX, $0: 64-bitレジスタの比較命令。
  • MOVQ $0, AX: 64-bitレジスタへの移動命令。

これらの変更により、Goランタイムは、Goが管理していないスレッドから発生した例外を安全に無視し、Goプログラムのクラッシュを防ぐことができるようになりました。

関連リンク

  • Go CL (Code Review) リンク: https://golang.org/cl/104200046
  • 関連する可能性のあるGoイシュー(ただし、直接的な詳細は見つからず): Fixes #8224

参考にした情報源リンク