[インデックス 19652] ファイルの概要
このコミットは、GoランタイムのSolaris/AMD64アーキテクチャにおけるシグナルハンドリングに関するバグ修正です。具体的には、runtime·sigtramp
関数がCgoによって作成されたスレッドで呼び出された際に、レジスタの復元が適切に行われない問題を解決します。
コミット
commit d7b678b2ca26928390078c29e3f71868e867a182
Author: Aram Hăvărneanu <aram@mgk.ro>
Date: Wed Jul 2 09:34:06 2014 +1000
runtime: properly restore registers in Solaris runtime·sigtramp
We restored registers correctly in the usual case where the thread
is a Go-managed thread and called runtime·sighandler, but we
failed to do so when runtime·sigtramp was called on a cgo-created
thread. In that case, runtime·sigtramp called runtime·badsignal,
a Go function, and did not restore registers after it returned
LGTM=rsc, dave
R=rsc, dave
CC=golang-codereviews, minux.ma
https://golang.org/cl/105280050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d7b678b2ca26928390078c29e3f71868e867a182
元コミット内容
GoランタイムのSolaris版において、runtime·sigtramp
がCgoによって作成されたスレッドで呼び出された際に、レジスタの復元が正しく行われない問題を修正します。通常、Goが管理するスレッドではruntime·sighandler
が呼び出され、レジスタは正しく復元されていました。しかし、Cgoスレッドの場合、runtime·sigtramp
はruntime·badsignal
というGo関数を呼び出した後、レジスタを復元せずにリターンしていました。このコミットは、runtime·badsignal
からの戻り後にレジスタ復元処理へジャンプするように変更することで、この問題を解決します。
変更の背景
Goプログラムがシグナルを受信した際、オペレーティングシステムはシグナルハンドラを呼び出す前に、現在の実行コンテキスト(レジスタの状態など)を保存します。シグナルハンドラの処理が完了した後、保存されたコンテキストを復元し、元の実行フローに戻ります。
このコミットが修正している問題は、Solaris環境におけるGoランタイムのシグナルハンドリングメカニズム、特にruntime·sigtramp
の動作に関連しています。
通常のGoが管理するゴルーチン(スレッド)の場合、シグナルハンドラはGoランタイムによって適切に処理され、シグナル処理後にレジスタの状態が正しく復元されていました。しかし、Cgo(GoとC言語の相互運用機能)によって作成されたスレッドの場合、シグナルが配送された際にruntime·sigtramp
が呼び出されます。このruntime·sigtramp
は、シグナルが予期せぬ状況で発生した場合などにruntime·badsignal
というGo関数を呼び出すことがありました。
問題は、runtime·badsignal
が呼び出された後、runtime·sigtramp
がレジスタの復元処理をスキップして直接リターンしてしまっていた点にあります。これにより、Cgoスレッドがシグナルを受信し、runtime·badsignal
が実行された場合、シグナル処理後のレジスタの状態が不正になり、プログラムのクラッシュや予期せぬ動作を引き起こす可能性がありました。
このコミットは、このレジスタ復元漏れという特定のバグを修正し、GoプログラムがCgoと連携するSolaris環境での安定性を向上させることを目的としています。
前提知識の解説
- Goランタイム (Go Runtime): Go言語で書かれたプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、メモリ管理、シグナルハンドリングなど、低レベルな機能を提供します。
- シグナル (Signal): オペレーティングシステムがプロセスに送信する非同期の通知です。例えば、Ctrl+Cによる割り込み(SIGINT)、不正なメモリアクセス(SIGSEGV)、子プロセスの終了(SIGCHLD)などがあります。
- シグナルハンドリング (Signal Handling): プロセスがシグナルを受信した際に、どのように応答するかを定義するメカニズムです。通常、特定のシグナルに対してカスタムのハンドラ関数を登録します。
sigtramp
(Signal Trampoline): オペレーティングシステムがシグナルハンドラを呼び出す際に使用する、低レベルのアセンブリコードのルーチンです。これは、シグナルハンドラが呼び出される前にレジスタの状態を保存し、ハンドラから戻った後にレジスタを復元する役割を担います。Goランタイムでは、OSのsigtramp
からGo独自のシグナル処理ルーチンへ制御を移すための橋渡しをします。- Cgo: Go言語とC言語のコードを相互に呼び出すためのGoの機能です。Cgoを使用すると、Goプログラムから既存のCライブラリを利用したり、CコードからGo関数を呼び出したりできます。Cgoによって作成されたスレッドは、Goランタイムが直接管理するゴルーチンとは異なるライフサイクルやコンテキストを持つ場合があります。
- レジスタ (Registers): CPU内部にある高速な記憶領域で、現在実行中の命令やデータ、メモリアドレスなどを一時的に保持します。関数呼び出しやシグナル処理の際には、これらのレジスタの状態を保存・復元することが不可欠です。
- アセンブリ言語 (Assembly Language): 特定のCPUアーキテクチャに特化した低レベルのプログラミング言語です。Goランタイムの低レベルな部分(特にOSとのインタフェースやパフォーマンスが重要な部分)は、アセンブリ言語で記述されることがあります。このコミットで変更されている
sys_solaris_amd64.s
ファイルは、Solaris/AMD64向けのアセンブリコードです。 RET
(Return): アセンブリ言語の命令で、現在のサブルーチンから呼び出し元に戻るために使用されます。通常、スタックに保存されたリターンアドレスにジャンプします。JMP
(Jump): アセンブリ言語の命令で、指定されたアドレスに無条件にジャンプします。サブルーチンからの戻りではなく、コード内の別の場所へ制御を移す際に使用されます。
技術的詳細
このコミットは、src/pkg/runtime/sys_solaris_amd64.s
ファイル内のruntime·sigtramp
アセンブリ関数を変更しています。
runtime·sigtramp
は、SolarisシステムがシグナルをGoプロセスに配送した際に、OSのシグナルディスパッチャから最初に制御を受け取るGoランタイム内のエントリポイントです。この関数は、シグナルコンテキストの初期処理を行い、Goランタイムのより高レベルなシグナルハンドラ(例: runtime·sighandler
)に制御を渡すか、あるいはシグナルが予期せぬものである場合にruntime·badsignal
を呼び出します。
問題の箇所は、runtime·badsignal
が呼び出された後の処理でした。元のコードでは、runtime·badsignal
の呼び出し後にRET
命令が直接配置されていました。
MOVQ DI, 0(SP)
MOVQ $runtime·badsignal(SB), AX
CALL AX
RET // ここが問題の箇所
RET
命令は、関数呼び出しのスタックフレームを破棄し、呼び出し元に戻ります。しかし、runtime·sigtramp
の目的は、シグナル処理後にレジスタの状態を適切に復元し、元の実行コンテキストに戻ることです。runtime·badsignal
が呼び出されたパスでは、このレジスタ復元ロジック(exit
ラベル以降のコード)がスキップされてしまっていました。
特にCgoによって作成されたスレッドの場合、Goランタイムが直接管理するゴルーチンとは異なるコンテキストで動作するため、このレジスタ復元漏れが顕在化しやすかったと考えられます。レジスタが正しく復元されないと、その後の命令実行が不正なレジスタ値に基づいて行われ、クラッシュやデータ破損につながります。
このコミットでは、RET
命令をJMP exit
命令に置き換えることで、この問題を解決しています。
MOVQ DI, 0(SP)
MOVQ $runtime·badsignal(SB), AX
CALL AX
JMP exit // 変更点: RETからJMP exitへ
JMP exit
命令は、無条件にexit
ラベルにジャンプします。exit
ラベルの直後には、シグナル処理によって変更されたレジスタを元の状態に復元するためのアセンブリコードが記述されています。これにより、runtime·badsignal
が呼び出されたパスであっても、必ずレジスタ復元処理が実行されるようになり、Cgoスレッドにおけるシグナル処理後のレジスタ状態の整合性が保証されます。
この修正は、Goランタイムの低レベルな部分、特にOSとのインタフェース層における正確なレジスタ管理の重要性を示しています。アセンブリレベルでのわずかな命令の変更が、システム全体の安定性に大きな影響を与えることがあります。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/sys_solaris_amd64.s
ファイルの一箇所のみです。
--- a/src/pkg/runtime/sys_solaris_amd64.s
+++ b/src/pkg/runtime/sys_solaris_amd64.s
@@ -164,7 +164,7 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0
MOVQ DI, 0(SP)
MOVQ $runtime·badsignal(SB), AX
CALL AX
- RET
+ JMP exit
allgood:
// save g
@@ -259,6 +259,7 @@ allgood:
MOVQ 80(SP), R10
MOVQ R10, g(BX)
+exit:
// restore registers
MOVQ 32(SP), BX
MOVQ 40(SP), BP
コアとなるコードの解説
変更されたのは、TEXT runtime·sigtramp(SB),NOSPLIT,$0
セクション内の以下の部分です。
元のコード:
MOVQ DI, 0(SP)
MOVQ $runtime·badsignal(SB), AX
CALL AX
RET
このシーケンスは、まずレジスタDI
の値をスタックに保存し、次にruntime·badsignal
関数のアドレスをAX
レジスタにロードし、その関数をCALL
(呼び出し)します。CALL
命令は、runtime·badsignal
が実行された後、制御がこの行に戻ることを意味します。問題は、その直後にRET
命令があったことです。RET
は現在のサブルーチン(この場合はruntime·sigtramp
)から戻る命令であり、runtime·sigtramp
の後半にあるレジスタ復元処理(exit:
ラベル以降)をスキップしてしまいます。
修正後のコード:
MOVQ DI, 0(SP)
MOVQ $runtime·badsignal(SB), AX
CALL AX
JMP exit
RET
がJMP exit
に置き換えられました。JMP exit
は、runtime·badsignal
が戻った後、無条件にexit
ラベルにジャンプします。exit
ラベルは、runtime·sigtramp
の後半に位置し、シグナル処理によって変更されたレジスタを元の状態に復元するための共通のコードパスです。この変更により、runtime·badsignal
が呼び出された場合でも、必ずレジスタ復元処理が実行されるようになり、シグナル処理後のレジスタ状態の整合性が保証されます。
また、exit:
ラベルが追加されていますが、これは既存のレジスタ復元コードブロックの開始点にラベルを明示的に付与したものであり、機能的な変更というよりは、JMP exit
のターゲットを明確にするためのものです。
関連リンク
- Go言語のCgoに関する公式ドキュメント: https://go.dev/blog/cgo
- Go言語のシグナルハンドリングに関する議論(一般的な情報): https://go.dev/blog/go-and-signals
参考にした情報源リンク
- Goのソースコード(特に
src/pkg/runtime/sys_solaris_amd64.s
) - Goのコミットメッセージとコードレビューコメント
- アセンブリ言語(x86-64)の基本的な知識
- オペレーティングシステム(Solaris)のシグナル処理に関する一般的な知識I have generated the detailed explanation in Markdown format. I will now print it to standard output.