[インデックス 16860] ファイルの概要
このコミットは、Goランタイムにおけるcgocallback_gofunc
のスタックフレーム管理に関する改善とバグ修正を目的としています。特に、amd64
アーキテクチャでのスタック使用量の最適化と、windows/386
アーキテクチャでのStructured Exception Handling (SEH)フレームの適切な配置に関する問題に対処しています。
コミット
commit f01128257858e98be7354aa887a5142cc756d7a8
Author: Russ Cox <rsc@golang.org>
Date: Wed Jul 24 09:01:57 2013 -0400
runtime: more cgocallback_gofunc work
Debugging the Windows breakage I noticed that SEH
only exists on 386, so we can balance the two stacks
a little more on amd64 and reclaim another word.
Now we're down to just one word consumed by
cgocallback_gofunc, having reclaimed 25% of the
overall budget (4 words out of 16).
Separately, fix windows/386 - the SEH must be on the
m0 stack, as must the saved SP, so we are forced to have
a three-word frame for 386. It matters much less for
386, because there 128 bytes gives 32 words to use.
R=dvyukov, alex.brainman
CC=golang-dev
https://golang.org/cl/11551044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f01128257858e98be7354aa887a5142cc756d7a8
元コミット内容
このコミットは、Goランタイムのcgocallback_gofunc
関数に関するさらなる作業です。
Windowsでの問題のデバッグ中に、SEH (Structured Exception Handling) が386アーキテクチャにのみ存在することに気づきました。これにより、amd64
では2つのスタックのバランスをもう少し調整し、さらに1ワードを再利用できるようになりました。
その結果、cgocallback_gofunc
によって消費されるワードはわずか1ワードとなり、全体の予算(16ワード中4ワード)の25%を再利用できました。
これとは別に、windows/386
の修正も行われました。SEHと保存されたSPはm0
スタック上に存在する必要があるため、386では3ワードのフレームを持つことが強制されます。386では128バイトで32ワードを使用できるため、この変更の重要性ははるかに低いです。
変更の背景
このコミットの背景には、主に以下の2つの課題がありました。
-
amd64
アーキテクチャにおけるスタックフレームの最適化の余地:cgocallback_gofunc
は、CコードからGo関数が呼び出される際に使用される重要なランタイム関数です。この関数のスタックフレームが、必要以上に多くのメモリを消費していることが判明しました。特にamd64
環境では、WindowsのSEH機構が関与しないため、スタックフレームのレイアウトをより効率的に設計できる可能性がありました。開発者は、この関数が消費するスタック領域を削減し、リソースの効率化を図りたいと考えていました。 -
windows/386
アーキテクチャにおけるSEHの正しい取り扱い: Windowsオペレーティングシステムでは、Structured Exception Handling (SEH) という独自の例外処理メカニズムが存在します。特に32ビット (386
) 環境では、SEHフレームがスタック上に特定の形式で配置される必要があります。GoランタイムがCコードと相互運用する際(Cgoコールバック)、このSEHフレームの配置が不適切であると、例外発生時にシステムがクラッシュするなどの問題("Windows breakage")が発生する可能性がありました。コミットメッセージにあるように、SEHが386にのみ存在するという発見が、この問題の根本原因を特定するきっかけとなりました。m0
スタック(Goランタイムのスケジューラが使用する特別なスタック)と、保存されたスタックポインタ(SP)がSEHフレームと正しく連携するように修正する必要がありました。
これらの課題に対処することで、Goランタイムの安定性と効率性を向上させることが、このコミットの主要な動機となっています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとOSの概念に関する知識が不可欠です。
-
GoのM/G/Pモデル:
- M (Machine/OS Thread): オペレーティングシステムのスレッドを表します。Goランタイムは、OSスレッド上でGoのコードを実行します。
- G (Goroutine): Goの軽量な並行処理単位です。Goの関数呼び出しはすべてゴルーチン上で行われます。ゴルーチンはOSスレッドよりもはるかに軽量で、数百万個作成することも可能です。
- P (Processor): 論理プロセッサを表します。Goスケジューラは、MとGの間にPを配置し、GをM上で実行するためのコンテキストを提供します。Pは、実行可能なGのキューを保持し、MがGを実行する際に必要なリソース(例えば、ローカルなキャッシュ)を提供します。
m->g0
: 各Mには、特別なゴルーチンであるg0
が関連付けられています。g0
は、Goランタイムの内部処理(スケジューリング、スタック切り替え、Cgo呼び出しなど)を実行するためのスタック(m0
スタック)を持ちます。通常のGoコードはg0
スタックではなく、ユーザーゴルーチンのスタックで実行されます。Cgoコールバックのように、CコードからGoコードに制御が戻る際には、g0
スタックが重要な役割を果たします。
-
Cgoとコールバック:
- Cgo: GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。
- Cgoコールバック: CコードがGo関数を呼び出すメカニズムです。これは、GoランタイムがCの呼び出し規約に従ってGo関数を公開し、C側からその関数ポインタを呼び出すことで実現されます。このプロセスには、GoとCのスタックの切り替え、レジスタの保存・復元など、複雑なランタイム処理が伴います。
cgocallback_gofunc
はこのコールバック処理の中心的な部分を担います。
-
スタックフレームとスタックポインタ (SP):
- スタックフレーム: 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレス、保存されたレジスタなどがスタック上に確保される領域です。
- スタックポインタ (SP): 現在のスタックの最上位(または最下位、アーキテクチャによる)を指すレジスタです。関数呼び出しや戻り、ローカル変数の確保などによってSPの値が変化します。
- ベースポインタ (BP/FP): 一部のアーキテクチャでは、スタックフレームの特定の固定位置を指すベースポインタ(フレームポインタ)が使用されます。これにより、ローカル変数や引数へのアクセスが容易になります。
-
Structured Exception Handling (SEH) (Windows固有):
- Windowsオペレーティングシステムが提供する例外処理メカニズムです。プログラム実行中に発生した例外(例えば、ゼロ除算、無効なメモリアクセスなど)を捕捉し、処理するための構造です。
- SEHは、例外ハンドラのアドレスや関連情報を含む特別なレコードをスタック上に連鎖的に配置することで機能します。例外が発生すると、OSはスタックを巻き戻しながらこれらのSEHレコードを検索し、適切なハンドラを見つけます。
- 特に32ビット (
386
) Windowsでは、SEHフレームはスタック上の特定のオフセットに配置されることが期待されます。GoランタイムがCgoコールバックを処理する際に、このSEHフレームの整合性を維持することが重要になります。amd64
では、SEHの動作が異なり、スタックフレームに直接SEHレコードを埋め込む必要がない場合があります。
これらの概念を理解することで、コミットがなぜ特定のレジスタやスタックオフセットを操作しているのか、そしてなぜアーキテクチャやOSによって異なる処理が必要なのかが明確になります。
技術的詳細
このコミットの技術的詳細は、主にGoランタイムのCgoコールバック処理におけるスタックフレームのレイアウトと、レジスタの使用方法の最適化、およびWindows固有のSEH(Structured Exception Handling)の取り扱いに焦点を当てています。
cgocallback_gofunc
の役割
cgocallback_gofunc
は、CコードからGo関数が呼び出された際に、Goランタイムが制御を受け取る最初のアセンブリ関数です。この関数は、CスタックからGoスタック(具体的にはm->g0
スタック)への切り替え、Goスケジューラへのゴルーチン実行の委譲、そしてGo関数の実行後にCスタックへ制御を戻す役割を担います。このスタック切り替えの過程で、レジスタの保存・復元や、スタックフレームの適切な構築が非常に重要になります。
amd64
アーキテクチャでの最適化
コミットメッセージにあるように、amd64
ではSEHの制約がないため、スタックフレームの最適化が可能でした。
- 変更前:
cgocallback_gofunc
のスタックフレームは16バイト(2ワード)を消費していました。 - 変更後: スタックフレームを8バイト(1ワード)に削減しました。
- これは、以前はスタックに保存されていた
oldm
(cgocallback_gofunc
が呼び出された時点での現在のMポインタ)の値を、一時的に汎用レジスタ(R8
)に保持するように変更することで実現されました。 asm_amd64.s
の変更を見ると、MOVQ BP, 8(SP)
(BPレジスタの値をスタックの8バイトオフセットに保存)がMOVQ BP, R8
(BPの値をR8レジスタに移動)に変わり、その後MOVQ R8, 0(SP)
(R8の値をスタックの0バイトオフセットに保存)が行われています。これにより、oldm
の保存場所がスタックフレームのより効率的な位置に移動し、最終的にスタックフレーム全体のサイズを削減できました。- スタックポインタの調整(
LEAQ -(8+16)(DI), SP
からLEAQ -(8+8)(DI), SP
)も、このフレームサイズ削減を反映しています。 - これにより、
cgocallback_gofunc
が消費するスタック領域が25%削減され、リソース効率が向上しました。
- これは、以前はスタックに保存されていた
windows/386
アーキテクチャでの修正
windows/386
では、SEHの制約が厳しく、スタックフレームのサイズを増やす必要がありました。
- 変更前:
cgocallback_gofunc
のスタックフレームは8バイト(2ワード)でした。 - 変更後: スタックフレームを12バイト(3ワード)に増やしました。
- これは、WindowsのSEHフレーム(通常2ワード)と、Goランタイムが内部的に使用するスタックポインタの保存場所を適切に確保するためです。
asm_386.s
の変更を見ると、TEXT runtime·cgocallback_gofunc(SB),7,$8-12
がTEXT runtime·cgocallback_gofunc(SB),7,$12-12
に変更されており、スタックフレームサイズが4バイト増加していることがわかります。amd64
と同様に、oldm
の値はDX
レジスタに一時的に保持され、スタックの0(SP)
に保存されます。proc.c
のruntime·needm
関数では、windows/386
の場合にのみ、SEHフレームを現在のスタック(m->curg->sched.sp
の下の未使用ワード)に配置するロジックが追加されました。具体的には、m->seh = (SEH*)((uintptr*)&x + 1);
という行が追加され、SEH構造体へのポインタがm
構造体内に保存されます。これは、GoランタイムがSEHフレームの存在を認識し、例外発生時にWindowsが正しくスタックを巻き戻せるようにするために不可欠です。runtime·mstart
とruntime·dropm
でも、windows/386
に特化したm->seh
の初期化とクリアが追加されています。
CBARGS
マクロの調整
src/pkg/runtime/cgocall.c
では、CallbackArgs
構造体へのポインタを計算するCBARGS
マクロが、各アーキテクチャの新しいスタックフレームレイアウトに合わせて調整されました。これは、Go関数に渡される引数がスタック上のどこに配置されているかを正確に特定するために必要です。
GOARCH_amd64
では、スタックフレームが1ワードになったため、m->g0->sched.sp
からのオフセットが2*sizeof(void*)
(1ワードのフレーム + 1ワードの呼び出し元PC)に変更されました。GOARCH_386
では、スタックフレームが3ワードになったため、オフセットが4*sizeof(void*)
(3ワードのフレーム + 1ワードの呼び出し元PC)に変更されました。
これらの変更は、GoランタイムがCgoコールバックを処理する際のスタックの整合性と効率性を確保するために、非常に低レベルかつ精密な調整が行われたことを示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下の4つのファイルに分散しています。
-
src/pkg/runtime/asm_386.s
: 32ビットIntelアーキテクチャ(386)向けのアセンブリコード。runtime·cgocallback_gofunc
関数のスタックフレームサイズ定義の変更。- スタックポインタ(SP)とベースポインタ(BP)の操作、およびレジスタ(DX)の使用方法の変更。
- スタック上のデータオフセットの調整。
-
src/pkg/runtime/asm_amd64.s
: 64ビットIntelアーキテクチャ(amd64)向けのアセンブリコード。runtime·cgocallback_gofunc
関数のスタックフレームサイズ定義の変更。- スタックポインタ(SP)とベースポインタ(BP)の操作、およびレジスタ(R8)の使用方法の変更。
- スタック上のデータオフセットの調整。
-
src/pkg/runtime/cgocall.c
: Cgoコールバック処理に関するC言語コード。CBARGS
マクロの定義を、アーキテクチャ(GOARCH_arm
,GOARCH_amd64
,GOARCH_386
)ごとに条件付きで変更。これにより、コールバック引数のスタック上の位置を正確に計算。
-
src/pkg/runtime/proc.c
: Goランタイムのプロセッサ(M/G/PモデルのM)管理に関するC言語コード。GOOS_windows
かつGOARCH_386
の場合にのみ、m
構造体内のseh
フィールド(Structured Exception Handlingフレームへのポインタ)の初期化、設定、クリアを行うロジックを追加。runtime·mstart
,runtime·needm
,runtime·dropm
関数内で、SEHフレームの配置と管理に関するコードを追加。
これらのファイルは、Goランタイムの低レベルな部分、特にOSとのインタラクションやスタック管理、Cgoの相互運用性に関わる重要なコンポーネントです。
コアとなるコードの解説
src/pkg/runtime/asm_386.s
および src/pkg/runtime/asm_amd64.s
これらのアセンブリファイルでは、runtime·cgocallback_gofunc
関数のスタックフレームの定義と、その内部でのレジスタおよびスタック操作が変更されています。
amd64
の変更点(最適化):
TEXT runtime·cgocallback_gofunc(SB),7,$16-24
からTEXT runtime·cgocallback_gofunc(SB),7,$8-24
へ変更。- これは、
cgocallback_gofunc
のスタックフレームサイズが16バイトから8バイトに削減されたことを示します。$16-24
の16
はフレームサイズ、24
は引数のサイズです。
- これは、
MOVQ BP, 8(SP)
がMOVQ BP, R8
に、そしてMOVQ R8, 0(SP)
に変更。- 以前は
BP
レジスタ(m
ポインタを保持)の値を直接スタックの8(SP)
に保存していましたが、これをR8
レジスタに一時的に退避させ、その後R8
の値をスタックの0(SP)
に保存するように変更しました。これにより、スタック上のoldm
の保存位置が変わり、フレームサイズ削減に寄与しています。
- 以前は
- スタックポインタの調整 (
LEAQ -(8+16)(DI), SP
からLEAQ -(8+8)(DI), SP
)。- これは、新しいゴルーチンに切り替える際にスタックポインタを調整する命令です。フレームサイズが8バイト削減されたことに合わせて、オフセットも変更されています。
MOVQ 16(SP), BP
からMOVQ 8(SP), BP
へ変更。- Goルーチンから戻る際に、保存された
BP
レジスタの値をスタックから復元するオフセットが変更されました。
- Goルーチンから戻る際に、保存された
386
の変更点(SEH対応):
TEXT runtime·cgocallback_gofunc(SB),7,$8-12
からTEXT runtime·cgocallback_gofunc(SB),7,$12-12
へ変更。- これは、
cgocallback_gofunc
のスタックフレームサイズが8バイトから12バイトに増加したことを示します。これはWindowsのSEHフレームを収容するためです。
- これは、
MOVL BP, 4(SP)
がMOVL BP, DX
に、そしてMOVL DX, 0(SP)
に変更。amd64
と同様に、BP
レジスタ(m
ポインタを保持)の値をDX
レジスタに一時的に退避させ、その後DX
の値をスタックの0(SP)
に保存するように変更しました。
- スタックポインタの調整 (
LEAL -(4+8)(DI), SP
からLEAL -(4+12)(DI), SP
)。- 新しいゴルーチンに切り替える際にスタックポインタを調整する命令です。フレームサイズが4バイト増加したことに合わせて、オフセットも変更されています。
MOVL 8(SP), BP
からMOVL 12(SP), BP
へ変更。- Goルーチンから戻る際に、保存された
BP
レジスタの値をスタックから復元するオフセットが変更されました。
- Goルーチンから戻る際に、保存された
これらのアセンブリコードの変更は、スタックフレームのレイアウトを直接操作し、レジスタの使用方法を最適化することで、Cgoコールバックの効率とWindows環境での安定性を向上させています。
src/pkg/runtime/cgocall.c
このファイルでは、CBARGS
マクロの定義がアーキテクチャごとに条件付きで変更されています。このマクロは、m->g0->sched.sp
(g0
スタックの現在のスタックポインタ)を基準として、Cgoコールバックの引数がスタック上のどこに配置されているかを計算するために使用されます。
// On amd64, stack frame is one word, plus caller PC.
#ifdef GOARCH_amd64
#define CBARGS (CallbackArgs*)((byte*)m->g0->sched.sp+2*sizeof(void*))
#endif
// On 386, stack frame is three words, plus caller PC.
#ifdef GOARCH_386
#define CBARGS (CallbackArgs*)((byte*)m->g0->sched.sp+4*sizeof(void*))
#endif
GOARCH_amd64
の場合、cgocallback_gofunc
のスタックフレームが1ワード(8バイト)に削減されたため、引数へのオフセットは「1ワードのフレーム + 1ワードの呼び出し元PC」で合計2ワードとなります。GOARCH_386
の場合、スタックフレームが3ワード(12バイト)に増加したため、引数へのオフセットは「3ワードのフレーム + 1ワードの呼び出し元PC」で合計4ワードとなります。
この変更は、アセンブリコードで変更されたスタックフレームのレイアウトと整合性を保ち、GoランタイムがCgoコールバックの引数を正しく読み取れるようにするために不可欠です。
src/pkg/runtime/proc.c
このファイルでは、windows/386
に特化したSEHフレームの管理ロジックが追加されています。
#ifdef GOOS_windows
#ifdef GOARCH_386
m->seh = &seh; // runtime·mstart
#endif
#endif
runtime·mstart
関数内で、windows/386
の場合にのみ、m
構造体のseh
フィールドにローカル変数seh
のアドレスを割り当てています。これは、OSスレッドが開始される際にSEHフレームを初期化するものです。
#ifdef GOOS_windows
#ifdef GOARCH_386
// On windows/386, we need to put an SEH frame (two words)
// somewhere on the current stack. We are called from cgocallback_gofunc
// and we know that it will leave two unused words below m->curg->sched.sp.
// Use those.
m->seh = (SEH*)((uintptr*)&x + 1); // runtime·needm
#endif
#endif
runtime·needm
関数内で、windows/386
の場合にのみ、SEHフレームを現在のスタックに配置するロジックが追加されています。コメントにあるように、cgocallback_gofunc
がm->curg->sched.sp
の下に2つの未使用ワードを残すことを利用し、そこにSEHフレームを配置しています。これは、Cgoコールバック中にGoランタイムが一時的にOSスレッドを「借りる」際に、そのスレッドのSEHチェーンを正しく設定するために重要です。
#ifdef GOOS_windows
#ifdef GOARCH_386
m->seh = nil; // reset dangling typed pointer // runtime·dropm
#endif
#endif
runtime·dropm
関数内で、windows/386
の場合にのみ、m->seh
をnil
にリセットしています。これは、OSスレッドを解放する際に、SEHフレームへのポインタがぶら下がったままにならないようにするためのクリーンアップ処理です。
これらの変更は、Windowsの32ビット環境における例外処理の要件を満たし、Cgoコールバックが安定して動作するようにするために不可欠な修正です。
関連リンク
- Go言語のCgoドキュメント: https://pkg.go.dev/cmd/cgo
- GoランタイムのM/G/Pモデルに関する解説(一般的な情報源): Goのスケジューラに関するブログ記事や公式ドキュメントを参照してください。例えば、"Go's work-stealing scheduler"などで検索すると良いでしょう。
- Structured Exception Handling (SEH) の詳細(Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
参考にした情報源リンク
- Goのソースコード(特に
src/pkg/runtime/
ディレクトリ内のアセンブリファイルとCファイル) - Goの公式ドキュメント
- Goのコミット履歴とコードレビュー(
https://golang.org/cl/11551044
) - アセンブリ言語(x86, x86-64)およびスタックフレームに関する一般的な知識
- WindowsのSEHに関する技術文書