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

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

このコミットは、GoランタイムがGoによって作成されていないスレッドでシグナルを受信した場合に、エラーメッセージを出力するように変更を加えるものです。これは、Go 1リリース前の暫定的な対応として、問題の診断を容易にすることを目的としています。

コミット

commit b23691148f4860721a659347a3d6e693f93538da
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 12 15:55:18 2012 -0400

    runtime: print error on receipt of signal on non-Go thread
    
    It's the best we can do before Go 1.
    
    For issue 3250; not a fix but at least less mysterious.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5797068

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

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

元コミット内容

このコミットは、GoランタイムがGo言語によって管理されていない(非Go)スレッドでOSシグナルを受け取った際に、エラーメッセージを標準エラー出力に表示する機能を追加します。これは、Go 1の正式リリース前に、このような状況が発生した際のデバッグを支援するための暫定的な措置であり、根本的な解決策ではありません。issue 3250 に関連するもので、問題がより分かりやすくなるようにするための変更です。

変更の背景

Goランタイムは、自身のゴルーチン(goroutine)とOSスレッド(thread)のスケジューリングを厳密に管理しています。Goプログラムが実行される際、ランタイムはOSスレッドを生成し、その上でゴルーチンを実行します。しかし、Cgo(GoとC言語の相互運用機能)などを使用する場合、Goランタイムが直接管理していない外部のCライブラリなどがOSスレッドを生成し、そのスレッド上でシグナルを受信する可能性があります。

このような非Goスレッドでシグナルが受信された場合、Goランタイムは予期せぬ状態に陥る可能性がありました。特に、シグナルハンドラがGoランタイムの内部状態(例えば、現在のmgのコンテキスト)に依存している場合、それらの情報が正しく設定されていない非Goスレッド上では、クラッシュや未定義の動作を引き起こす原因となります。

issue 3250 は、まさにこの問題、つまり非Goスレッドでのシグナル受信時の挙動の不明瞭さや、それに伴うデバッグの困難さを指摘していたと考えられます。このコミットは、その問題に対する「根本的な修正ではないが、少なくとも謎を少なくする」というアプローチで、問題発生時にユーザーに明確なエラーメッセージを提供することで、デバッグの手がかりを与えることを目的としています。Go 1リリース前という時期的な制約もあり、完全な解決策ではなく、診断を助けるための暫定的な対応が選択されました。

前提知識の解説

  • Goランタイム: Goプログラムの実行を管理するシステム。ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、システムコールなど、Go言語の並行処理モデルを支える基盤です。
  • ゴルーチン (Goroutine): Go言語における軽量な実行単位。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。GoランタイムがゴルーチンをOSスレッドにマッピングして実行します。
  • OSスレッド (OS Thread): オペレーティングシステムが管理する実行単位。CPUによって直接実行される最小単位です。Goランタイムは複数のOSスレッドを生成し、その上でゴルーチンを多重化して実行します。
  • シグナルハンドリング: オペレーティングシステムがプロセスに対してイベント(シグナル)を通知するメカニズム。例えば、Ctrl+Cによる割り込み(SIGINT)、不正なメモリアクセス(SIGSEGV)などがあります。プログラムはこれらのシグナルを捕捉し、特定の処理を実行するシグナルハンドラを登録できます。
  • m (Machine) と g (Goroutine) 構造体: Goランタイムの内部で、mはOSスレッドを表し、gはゴルーチンを表す構造体です。mは現在のOSスレッドが実行しているgへのポインタを持ち、gは自身のスタック情報や状態を持ちます。シグナルハンドラが実行される際には、通常、現在のmgのコンテキストが重要になります。
  • TLS (Thread Local Storage): スレッドごとに独立したデータを保存するためのメカニズム。Goランタイムでは、現在のmgへのポインタをTLSに保存し、高速にアクセスできるようにしています。
  • アセンブリ言語: CPUが直接理解できる機械語に近い低レベル言語。Goランタイムのコア部分は、パフォーマンスやOSとの直接的な連携のためにアセンブリ言語で記述されていることがあります。このコミットで変更されている.sファイルはアセンブリ言語のソースファイルです。
  • TEXT ディレクティブ: Goのアセンブリ言語(Plan 9アセンブラ)で使用されるディレクティブで、関数の定義を開始します。
  • MOVL/MOVQ: アセンブリ言語の命令で、データをレジスタやメモリ間で移動させます。MOVLは32ビット、MOVQは64ビットのデータを扱います。
  • CMPL/CMPQ: 比較命令。2つのオペランドを比較し、フラグレジスタを設定します。
  • JNE: 条件分岐命令。比較結果が等しくない場合にジャンプします。
  • CALL: 関数呼び出し命令。

技術的詳細

このコミットの主要な変更は、Goランタイムのシグナルハンドラのエントリポイントに、現在のOSスレッドがGoランタイムによって管理されているかどうかをチェックするロジックを追加したことです。具体的には、m(Machine、OSスレッドを表すランタイム構造体)が存在するかどうかを確認します。

変更は、Goがサポートする様々なOS(Darwin, FreeBSD, Linux, NetBSD, OpenBSD, Windows)の386およびamd64アーキテクチャ向けのアセンブリ言語ファイル(src/pkg/runtime/sys_*.s)と、C言語で記述されたスレッド関連のファイル(src/pkg/runtime/thread_*.c)にわたっています。

アセンブリ言語ファイル (src/pkg/runtime/sys_*.s) の変更点:

各OS/アーキテクチャのruntime·sigtramp関数(シグナルハンドラの入り口となるアセンブリコード)に以下のロジックが追加されました。

  1. get_tls(CX/BX): スレッドローカルストレージ(TLS)から現在のスレッドのコンテキスト(mへのポインタなど)を取得します。
  2. MOVL/MOVQ m(CX/BX), AX/BP/BX: TLSから取得したmへのポインタをレジスタにロードします。
  3. CMPL/CMPQ AX/BP/BX, $0: ロードしたmへのポインタがNULL(0)であるかをチェックします。NULLであれば、そのスレッドはGoランタイムによって管理されていない可能性が高いことを意味します。
  4. JNE 2(PC): mがNULLでない場合(つまり、Go管理下のスレッドである場合)、通常のシグナル処理フローに進みます。
  5. CALL runtime·badsignal(SB): mがNULLである場合(非Goスレッドである場合)、新しく追加されたruntime·badsignal関数を呼び出します。この関数は、エラーメッセージを標準エラー出力に書き込みます。

Windows環境では、runtime·badsignal関数自体もアセンブリで実装され、GetStdHandleWriteFileというWindows APIを直接呼び出してエラーメッセージを出力しています。

C言語ファイル (src/pkg/runtime/thread_*.c) の変更点:

各OSのスレッド関連のCファイルに、runtime·badsignal関数のC言語での定義が追加されました。この関数は、"runtime: signal received on thread not created by Go.\\n"というメッセージを標準エラー出力(ファイルディスクリプタ2)に書き込むシンプルな処理を行います。

  • #pragma textflag 7: このプラグマは、関数がGoランタイムの通常のスタック分割メカニズム(スタックの自動拡張)なしで実行されるべきであることを示します。これは、シグナルハンドラが「外部のスタック」で、mgのコンテキストが確立されていない状態で呼び出される可能性があるため、非常に重要です。スタック分割が有効な場合、スタックが不足するとランタイムが新しいスタックフレームを割り当てようとしますが、この状況ではそれが不可能であり、クラッシュにつながるためです。

この変更により、非Goスレッドでシグナルが発生した場合でも、Goプログラムが沈黙してクラッシュするのではなく、明確なエラーメッセージを出力するようになり、デバッグが容易になります。

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

ここでは、src/pkg/runtime/sys_darwin_386.ssrc/pkg/runtime/thread_darwin.c の変更を例に挙げます。他のOS/アーキテクチャでも同様のパターンで変更が適用されています。

src/pkg/runtime/sys_darwin_386.s (アセンブリコード)

--- a/src/pkg/runtime/sys_darwin_386.s
+++ b/src/pkg/runtime/sys_darwin_386.s
@@ -126,13 +126,18 @@ TEXT runtime·sigaction(SB),7,$0
 //	20(FP)	context
 TEXT runtime·sigtramp(SB),7,$40
 	get_tls(CX)
+\t
+\t// check that m exists
+\tMOVL\tm(CX), BP
+\tCMPL\tBP, $0
+\tJNE\t2(PC)
+\tCALL\truntime·badsignal(SB)
 \n
 	// save g
 	MOVL\tg(CX), DI
 	MOVL\tDI, 20(SP)
 \n
 	// g = m->gsignal
-\tMOVL\tm(CX), BP
 	MOVL\tm_gsignal(BP), BP
 	MOVL\tBP, g(CX)
 \n

src/pkg/runtime/thread_darwin.c (C言語コード)

--- a/src/pkg/runtime/thread_darwin.c
+++ b/src/pkg/runtime/thread_darwin.c
@@ -487,3 +487,13 @@ runtime·badcallback(void)\n {\n 	runtime·write(2, badcallback, sizeof badcallback - 1);\n }\n+\n+static int8 badsignal[] = "runtime: signal received on thread not created by Go.\\n";\n+\n+// This runs on a foreign stack, without an m or a g.  No stack split.\n+#pragma textflag 7\n+void\n+runtime·badsignal(void)\n+{\n+\truntime·write(2, badsignal, sizeof badsignal - 1);\n+}\n```

## コアとなるコードの解説

**アセンブリコードの解説 (`runtime·sigtramp`):**

```assembly
	get_tls(CX)
	
	// check that m exists
	MOVL	m(CX), BP
	CMPL	BP, $0
	JNE	2(PC)
	CALL	runtime·badsignal(SB)
  1. get_tls(CX): 現在のスレッドのTLS(Thread Local Storage)から、Goランタイムが管理するスレッド固有のデータ(この場合はm構造体へのポインタ)を取得し、CXレジスタに格納します。
  2. MOVL m(CX), BP: CXレジスタが指すTLS領域から、m構造体へのポインタをBPレジスタにロードします。m(CX)は、CXが指すアドレスからmフィールドのオフセットにある値を意味します。
  3. CMPL BP, $0: BPレジスタ(mへのポインタ)がゼロ(NULL)であるかどうかを比較します。mがNULLであるということは、このOSスレッドがGoランタイムによって初期化・管理されていないことを示唆します。
  4. JNE 2(PC): もしBPがゼロでなければ(つまりmが存在すれば)、通常のシグナル処理フローに進むために、次の2命令をスキップします。2(PC)は、現在のプログラムカウンタ(PC)から2バイト先にジャンプすることを意味します。これは、次のCALL命令をスキップするための一般的なアセンブリのテクニックです。
  5. CALL runtime·badsignal(SB): もしBPがゼロであれば(mが存在しない場合)、runtime·badsignal関数を呼び出します。この関数は、非Goスレッドでシグナルが受信されたことを示すエラーメッセージを出力します。

このロジックにより、シグナルがGo管理下のスレッドで発生した場合は通常の処理が続行され、非Goスレッドで発生した場合はエラーメッセージが出力されるようになります。

C言語コードの解説 (runtime·badsignal):

static int8 badsignal[] = "runtime: signal received on thread not created by Go.\\n";

// This runs on a foreign stack, without an m or a g.  No stack split.
#pragma textflag 7
void
runtime·badsignal(void)
{
	runtime·write(2, badsignal, sizeof badsignal - 1);
}
  1. static int8 badsignal[] = "runtime: signal received on thread not created by Go.\\n";: 標準エラー出力に表示するエラーメッセージを定義しています。int8はGoのbyte型に相当し、文字列をバイト配列として扱います。
  2. #pragma textflag 7: この重要なプラグマは、コンパイラに対して、この関数がGoランタイムの通常のスタック分割メカニズムを使用しないように指示します。シグナルハンドラは、Goランタイムの制御外のスタックで実行される可能性があり、その場合、mgのコンテキストが利用できません。スタック分割はmgに依存するため、このプラグマは、そのような状況でのクラッシュを防ぐために不可欠です。
  3. void runtime·badsignal(void): runtime·badsignal関数の定義です。GoのCgoメカニズムを通じてアセンブリから呼び出されるため、Goの命名規則(package·Function)に従っています。
  4. runtime·write(2, badsignal, sizeof badsignal - 1);: この行は、定義されたエラーメッセージを標準エラー出力に書き込みます。
    • 2: 標準エラー出力のファイルディスクリプタです。
    • badsignal: 書き込むメッセージのバイト配列です。
    • sizeof badsignal - 1: メッセージの長さを指定します。sizeofはヌル終端文字も含むため、-1して実際の文字列長を取得します。

このC言語の関数は、アセンブリコードから呼び出され、Goランタイムの内部状態に依存せずに、安全にエラーメッセージを出力する役割を担っています。

関連リンク

  • Go issue 3250 (直接的なリンクは見つかりませんでしたが、コミットメッセージで言及されています)
  • Go CL 5797068: https://golang.org/cl/5797068

参考にした情報源リンク

  • go.dev - Vulnerability Report GO-2024-3250 (これは新しい脆弱性レポートであり、直接的な関連はないが、issue 3250という番号がGoプロジェクト内で再利用される可能性を示唆している)
  • h-da.de - Go issue3250.go (Goプロジェクト内の古いテストファイルで、シグナルハンドリングに関連する過去のissue 3250の存在を示唆)
  • go.googlesource.com - go/misc/cgo/test/issue3250.go (上記と同様のソースコードリポジトリへのリンク)
  • go.dev - Go issue 3250 (GoのIssueトラッカーでissue 3250を検索したが、直接的な情報は見つからなかった。これは、古いIssueがクローズされたか、別のシステムで管理されていた可能性を示唆する)