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

[インデックス 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つの課題がありました。

  1. amd64アーキテクチャにおけるスタックフレームの最適化の余地: cgocallback_gofuncは、CコードからGo関数が呼び出される際に使用される重要なランタイム関数です。この関数のスタックフレームが、必要以上に多くのメモリを消費していることが判明しました。特にamd64環境では、WindowsのSEH機構が関与しないため、スタックフレームのレイアウトをより効率的に設計できる可能性がありました。開発者は、この関数が消費するスタック領域を削減し、リソースの効率化を図りたいと考えていました。

  2. 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の概念に関する知識が不可欠です。

  1. 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スタックが重要な役割を果たします。
  2. Cgoとコールバック:

    • Cgo: GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。
    • Cgoコールバック: CコードがGo関数を呼び出すメカニズムです。これは、GoランタイムがCの呼び出し規約に従ってGo関数を公開し、C側からその関数ポインタを呼び出すことで実現されます。このプロセスには、GoとCのスタックの切り替え、レジスタの保存・復元など、複雑なランタイム処理が伴います。cgocallback_gofuncはこのコールバック処理の中心的な部分を担います。
  3. スタックフレームとスタックポインタ (SP):

    • スタックフレーム: 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレス、保存されたレジスタなどがスタック上に確保される領域です。
    • スタックポインタ (SP): 現在のスタックの最上位(または最下位、アーキテクチャによる)を指すレジスタです。関数呼び出しや戻り、ローカル変数の確保などによってSPの値が変化します。
    • ベースポインタ (BP/FP): 一部のアーキテクチャでは、スタックフレームの特定の固定位置を指すベースポインタ(フレームポインタ)が使用されます。これにより、ローカル変数や引数へのアクセスが容易になります。
  4. 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ワード)に削減しました。
    • これは、以前はスタックに保存されていたoldmcgocallback_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-12TEXT runtime·cgocallback_gofunc(SB),7,$12-12に変更されており、スタックフレームサイズが4バイト増加していることがわかります。
    • amd64と同様に、oldmの値はDXレジスタに一時的に保持され、スタックの0(SP)に保存されます。
    • proc.cruntime·needm関数では、windows/386の場合にのみ、SEHフレームを現在のスタック(m->curg->sched.spの下の未使用ワード)に配置するロジックが追加されました。具体的には、m->seh = (SEH*)((uintptr*)&x + 1);という行が追加され、SEH構造体へのポインタがm構造体内に保存されます。これは、GoランタイムがSEHフレームの存在を認識し、例外発生時にWindowsが正しくスタックを巻き戻せるようにするために不可欠です。
    • runtime·mstartruntime·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つのファイルに分散しています。

  1. src/pkg/runtime/asm_386.s: 32ビットIntelアーキテクチャ(386)向けのアセンブリコード。

    • runtime·cgocallback_gofunc関数のスタックフレームサイズ定義の変更。
    • スタックポインタ(SP)とベースポインタ(BP)の操作、およびレジスタ(DX)の使用方法の変更。
    • スタック上のデータオフセットの調整。
  2. src/pkg/runtime/asm_amd64.s: 64ビットIntelアーキテクチャ(amd64)向けのアセンブリコード。

    • runtime·cgocallback_gofunc関数のスタックフレームサイズ定義の変更。
    • スタックポインタ(SP)とベースポインタ(BP)の操作、およびレジスタ(R8)の使用方法の変更。
    • スタック上のデータオフセットの調整。
  3. src/pkg/runtime/cgocall.c: Cgoコールバック処理に関するC言語コード。

    • CBARGSマクロの定義を、アーキテクチャ(GOARCH_arm, GOARCH_amd64, GOARCH_386)ごとに条件付きで変更。これにより、コールバック引数のスタック上の位置を正確に計算。
  4. 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-2416はフレームサイズ、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レジスタの値をスタックから復元するオフセットが変更されました。

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レジスタの値をスタックから復元するオフセットが変更されました。

これらのアセンブリコードの変更は、スタックフレームのレイアウトを直接操作し、レジスタの使用方法を最適化することで、Cgoコールバックの効率とWindows環境での安定性を向上させています。

src/pkg/runtime/cgocall.c

このファイルでは、CBARGSマクロの定義がアーキテクチャごとに条件付きで変更されています。このマクロは、m->g0->sched.spg0スタックの現在のスタックポインタ)を基準として、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_gofuncm->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->sehnilにリセットしています。これは、OSスレッドを解放する際に、SEHフレームへのポインタがぶら下がったままにならないようにするためのクリーンアップ処理です。

これらの変更は、Windowsの32ビット環境における例外処理の要件を満たし、Cgoコールバックが安定して動作するようにするために不可欠な修正です。

関連リンク

参考にした情報源リンク

  • Goのソースコード(特にsrc/pkg/runtime/ディレクトリ内のアセンブリファイルとCファイル)
  • Goの公式ドキュメント
  • Goのコミット履歴とコードレビュー(https://golang.org/cl/11551044
  • アセンブリ言語(x86, x86-64)およびスタックフレームに関する一般的な知識
  • WindowsのSEHに関する技術文書