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

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

このコミットは、GoランタイムにおけるWindowsコールバックの処理方法を根本的に変更するものです。具体的には、NewCallback関数が実行時に動的にアセンブリコードを生成するのではなく、事前に生成された固定のアセンブリスタブを使用するように変更されました。これにより、セキュリティ、パフォーマンス、およびメンテナンス性が向上します。

コミット

commit 05a5de30f099ee60987646ff88238d561b12ddeb
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Jun 24 17:17:45 2013 +1000

    runtime: do not generate code during runtime in windows NewCallback
    
    Update #5494
    
    R=golang-dev, minux.ma, rsc, iant
    CC=golang-dev
    https://golang.org/cl/10368043

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

https://github.com/golang/go/commit/05a5de30f099ee60987646ff88238d561b12ddeb

元コミット内容

このコミットの元のメッセージは以下の通りです。

runtime: do not generate code during runtime in windows NewCallback

Update #5494

これは、WindowsにおけるNewCallback関数が実行時にコードを生成するのを停止するという、簡潔ながらも重要な変更を示しています。

変更の背景

この変更の背景には、主に以下の理由が考えられます。

  1. セキュリティとDEP (Data Execution Prevention) / ASLR (Address Space Layout Randomization) との互換性: 実行時に動的にコードを生成し、それを実行可能メモリとしてマークすることは、セキュリティ上のリスクを伴います。特に、DEPが有効なシステムでは、データ領域からのコード実行がブロックされるため、動的に生成されたコードの実行が困難になります。また、ASLRはメモリレイアウトをランダム化することで、攻撃者が特定のメモリ位置を予測してコードを注入するのを防ぎますが、動的なコード生成はこれらの保護メカニズムと相性が悪い場合があります。
  2. 複雑性の軽減: 実行時にアセンブリコードを動的に構築するロジックは、複雑でエラーが発生しやすくなります。異なるアーキテクチャ(386とAMD64)ごとに異なるコード生成ロジックを持つ必要があり、メンテナンスの負担も大きくなります。
  3. パフォーマンスの向上: 動的なコード生成にはオーバーヘッドが伴います。事前にコンパイルされたスタブを使用することで、コールバックの登録時のパフォーマンスが向上する可能性があります。
  4. Issue #5494の解決: コミットメッセージに「Update #5494」とあることから、この変更はGoのIssueトラッカーに登録されていた特定の問題(おそらく動的コード生成に関連する問題)を解決するために行われたことが示唆されます。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  • Goのsyscall.NewCallback: GoプログラムからC/C++などの外部ライブラリ(特にWindows API)にコールバック関数を渡す際に使用されるメカニズムです。Goの関数ポインタを、外部コードが呼び出せる形式(通常はCの関数ポインタ)に変換します。
  • Windowsコールバック: Windows APIでは、特定のイベントが発生した際にアプリケーションが提供する関数を呼び出す「コールバック」の仕組みが多用されます。例えば、ウィンドウプロシージャ(WNDPROC)やタイマーコールバックなどがあります。
  • アセンブリ言語: コンピュータのCPUが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。OSやランタイムの低レベルな処理(システムコール、メモリ管理、コンテキストスイッチなど)でよく使用されます。
  • スタックフレームと呼び出し規約: 関数が呼び出される際に、引数、ローカル変数、戻りアドレスなどがどのようにスタックに配置されるかを定義するルールです。Windowsの32ビット(x86)および64ビット(x64)アーキテクチャには、それぞれ異なる標準的な呼び出し規約(例: __stdcall, __fastcall, __cdecl for x86; __fastcall for x64)が存在します。
  • src/cmd/dist: Goのビルドシステムの一部であり、Goのソースコードからプラットフォーム固有のファイル(アセンブリファイル、ヘッダーファイルなど)を生成する役割を担っています。
  • runtimeパッケージ: Goプログラムの実行を管理する低レベルな機能(スケジューラ、ガベージコレクタ、メモリ管理、システムコールなど)を提供するGoのコアパッケージです。
  • TEXTディレクティブ (Goアセンブリ): Goのアセンブリ言語で使用され、関数の開始を宣言します。TEXT runtime·callbackasm(SB),7,$0 のように記述され、runtime·callbackasmというシンボルが定義されます。SBは「static base」を意味し、グローバルシンボルであることを示します。7はフラグ、$0はスタックフレームサイズを示します。
  • GLOBLディレクティブ (Goアセンブリ): グローバルシンボルを宣言します。GLOBL runtime·cbctxts(SB), $4 のように記述され、runtime·cbctxtsというグローバル変数が定義されます。

技術的詳細

以前のGoランタイムでは、runtime·compilecallback関数(src/pkg/runtime/callback_windows_386.cおよびsrc/pkg/runtime/callback_windows_amd64.cに実装)が、Goの関数をWindowsが呼び出せる形式に変換するために、実行時に小さなアセンブリコードの断片を動的に生成していました。この動的に生成されたコードは、Goの関数へのポインタと引数のサイズをレジスタにロードし、最終的にruntime·callbackasmという共通のアセンブリルーチンにジャンプまたはコールする役割を担っていました。

このコミットでは、この動的なコード生成の仕組みが廃止されました。代わりに、以下のような新しいアプローチが導入されました。

  1. 単一の共通アセンブリスタブ: runtime·callbackasmという単一のアセンブリルーチンが、Goのビルドプロセス中に事前にコンパイルされます。このルーチンは、複数のコールバックエントリポイントを持つように設計されています。具体的には、runtime·callbackasmは一連のCALL runtime·callbackasm1(SB)命令で構成されており、各CALL命令は5バイト(32ビット版)または5バイト(64ビット版)の固定長です。
  2. コールバックコンテキストの管理: 新しい構造体WinCallbackContextsrc/pkg/runtime/runtime.hで定義されました。この構造体は、対応するGo関数へのポインタ(gobody)、引数のサイズ(argsize)、およびスタックを復元するための情報(restorestack、386のみ)を保持します。
  3. runtime·cbctxts配列: WinCallbackContext構造体のポインタの配列であるruntime·cbctxtsが導入されました。これは、登録された各コールバックのコンテキスト情報を格納します。
  4. runtime·compilecallbackの変更:
    • 動的なアセンブリコード生成のロジックが削除されました。
    • 代わりに、登録されるGo関数に対応するWinCallbackContext構造体を作成し、runtime·cbctxts配列に格納します。
    • 返されるコールバックアドレスは、runtime·callbackasmの開始アドレスに、そのコールバックに対応するCALL命令のオフセットを加算した値になります。これにより、外部コードがこのアドレスを呼び出すと、runtime·callbackasm内の特定のCALL命令にジャンプし、そこからruntime·callbackasm1が実行されます。
  5. runtime·callbackasm1の導入と役割:
    • runtime·callbackasmから呼び出される新しいアセンブリルーチンruntime·callbackasm1が導入されました。
    • このルーチンは、スタック上の戻りアドレス(runtime·callbackasm内のどのCALL命令から来たかを示す)を利用して、runtime·cbctxts配列内の対応するWinCallbackContextエントリのインデックスを計算します。
    • 計算されたインデックスを使用してWinCallbackContextを取得し、そこからGo関数へのポインタと引数サイズを読み取ります。
    • これらの情報を使って、最終的にGoのランタイム関数runtime·cgocallback_gofuncを呼び出し、実際のGo関数を実行します。
    • Windowsの呼び出し規約に従ってレジスタを保存・復元し、スタックを適切にクリーンアップします。
  6. ビルドプロセスの変更: src/cmd/dist/build.csrc/cmd/dist/buildruntime.cが更新され、mkzsysという新しい関数が追加されました。この関数は、zsys_$GOOS_$GOARCH.sというファイルを生成します。このファイルには、runtime·callbackasmのアセンブリコード(一連のCALL runtime·callbackasm1(SB)命令)が含まれます。これにより、runtime·callbackasmはビルド時に静的に生成されるようになります。

この変更により、Goランタイムは実行時に実行可能メモリを割り当ててコードを書き込む必要がなくなり、DEPなどのセキュリティ機能との互換性が向上しました。また、アセンブリコードの生成ロジックがビルド時に集約され、ランタイムの複雑性が軽減されました。

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

主要な変更は以下のファイルに集中しています。

  • src/cmd/dist/a.h: mkzsys関数のプロトタイプが追加されました。
  • src/cmd/dist/build.c: ビルドシステムにzsys_$GOOS_$GOARCH.sファイルの生成とmkzsys関数の使用が追加されました。
  • src/cmd/dist/buildruntime.c:
    • MAXWINCBマクロ(Windowsコールバックの最大数)が定義されました。
    • mkzsys関数が追加されました。この関数は、runtime·callbackasmのアセンブリコード(CALL runtime·callbackasm1(SB)の繰り返し)を生成し、zsys_$GOOS_$GOARCH.sファイルに書き込みます。
    • mkzasm関数にWinCallbackContext構造体のオフセット定義が追加されました。
  • src/pkg/runtime/{callback_windows_386.c => callback_windows.c}:
    • ファイル名がcallback_windows_386.cからcallback_windows.cに変更され、32ビットと64ビットで共通のCコードを使用するようになりました。
    • Callback構造体が削除され、WinCallbackContext構造体を使用するように変更されました。
    • runtime·compilecallback関数から動的なアセンブリコード生成ロジックが完全に削除されました。
    • runtime·cbctxtsというグローバルポインタ配列が導入され、WinCallbackContextのインスタンスを格納するようになりました。
    • 返されるアドレスが、runtime·callbackasmの開始アドレスに、対応するCALL命令のオフセットを加算した値に変更されました。
  • src/pkg/runtime/callback_windows_amd64.c: このファイルは完全に削除されました。32ビットと64ビットのコールバック処理がcallback_windows.cに統合されたためです。
  • src/pkg/runtime/runtime.h: WinCallbackContext構造体が定義されました。
  • src/pkg/runtime/sys_windows_386.s:
    • runtime·callbackasmruntime·callbackasm1にリネームされ、そのロジックが大幅に変更されました。
    • runtime·cbctxtsグローバル変数が宣言されました。
    • スタック上の戻りアドレスからWinCallbackContextのインデックスを計算し、対応するGo関数と引数サイズを取得するロジックが追加されました。
    • runtime·cgocallback_gofuncを呼び出す前に、WinCallbackContextから取得した情報を使用して引数を設定するようになりました。
  • src/pkg/runtime/sys_windows_amd64.s:
    • runtime·callbackasmruntime·callbackasm1にリネームされ、そのロジックが大幅に変更されました。
    • runtime·cbctxtsグローバル変数が宣言されました。
    • 32ビット版と同様に、スタック上の戻りアドレスからWinCallbackContextのインデックスを計算し、対応するGo関数と引数サイズを取得するロジックが追加されました。
    • runtime·cgocallback_gofuncを呼び出す前に、WinCallbackContextから取得した情報を使用して引数を設定するようになりました。

コアとなるコードの解説

このコミットの核心は、runtime·compilecallbackが実行時にアセンブリコードを生成するのをやめ、代わりにビルド時に生成された静的なアセンブリスタブと、コールバックコンテキストを格納する配列を使用する点にあります。

src/cmd/dist/buildruntime.cにおけるmkzsys関数: この関数は、zsys_$GOOS_$GOARCH.sというアセンブリファイルを生成します。Windowsの場合、このファイルにはruntime·callbackasmというシンボルが定義され、その中にはMAXWINCB(2000)回繰り返されるCALL runtime·callbackasm1(SB)命令が含まれます。

// mkzsys writes zsys_$GOOS_$GOARCH.h,
// which contains arch or os specific asm code.
void
mkzsys(char *dir, char *file)
{
    // ... (省略)
    if(streq(goos, "windows")) {
        // ... (省略)
        for(i=0; i<MAXWINCB; i++) {
            bwritef(&out, "\tCALL\truntime·callbackasm1(SB)\\n");
        }
        bwritef(&out, "\tRET\\n");
    }
    // ... (省略)
}

これにより、runtime·callbackasmは、Goの関数を呼び出すための「トラポリン」の集合体として機能します。各CALL命令は、異なるGoコールバック関数に対応するエントリポイントとなります。

src/pkg/runtime/callback_windows.cにおけるruntime·compilecallback関数: この関数は、Goの関数をWindowsコールバックとして登録する際に呼び出されます。以前はここで動的にアセンブリコードを生成していましたが、変更後はそのロジックが削除されました。

byte *
runtime·compilecallback(Eface fn, bool cleanstack)
{
    // ... (引数チェック、argsize計算など)

    runtime·lock(&cbs);
    // 既存のコールバックを検索するロジック (変更なし)
    if(runtime·cbctxts == nil)
        runtime·cbctxts = &(cbs.ctxt[0]);
    n = cbs.n;
    for(i=0; i<n; i++) {
        if(cbs.ctxt[i]->gobody == fn.data) {
            runtime·unlock(&cbs);
            // 既存のコールバックが見つかった場合、対応するオフセットを計算して返す
            return (byte*)runtime·callbackasm + i * 5; // 5はCALL命令のバイト数
        }
    }
    if(n >= cb_max)
        runtime·throw("too many callback functions");

    // 新しいWinCallbackContextを作成し、情報を格納
    c = runtime·mal(sizeof *c);
    c->gobody = fn.data;
    c->argsize = argsize;
    if(cleanstack && argsize!=0)
        c->restorestack = argsize;
    else
        c->restorestack = 0;
    cbs.ctxt[n] = c; // グローバル配列にコンテキストを保存
    cbs.n++;
    runtime·unlock(&cbs);

    // runtime·callbackasmの開始アドレスにオフセットを加えて返す
    return (byte*)runtime·callbackasm + n * 5;
}

この変更により、runtime·compilecallbackは、Go関数に対応するWinCallbackContext構造体を初期化し、それをcbs.ctxt配列に格納します。そして、runtime·callbackasmの開始アドレスに、このコールバックが配列内で何番目かを示すインデックスにCALL命令のサイズ(5バイト)を掛けたオフセットを加算したアドレスを返します。このアドレスが、外部コードに渡されるコールバック関数ポインタとなります。

src/pkg/runtime/sys_windows_386.sおよびsrc/pkg/runtime/sys_windows_amd64.sにおけるruntime·callbackasm1: 外部コードがruntime·compilecallbackから返されたアドレスを呼び出すと、実行はruntime·callbackasm内の特定のCALL runtime·callbackasm1(SB)命令に飛びます。そこからruntime·callbackasm1が実行されます。

TEXT runtime·callbackasm1+0(SB),7,$0
  MOVL    0(SP), AX    // will use to find our callback context
  // remove return address from stack, we are not returning there
  ADDL    $4, SP

  // determine index into runtime·cbctxts table
  SUBL    $runtime·callbackasm(SB), AX // runtime·callbackasmからのオフセットを計算
  MOVL    $0, DX
  MOVL    $5, BX    // divide by 5 because each call instruction in runtime·callbacks is 5 bytes long
  DIVL    BX, // AX / 5 でインデックスを計算

  // find correspondent runtime·cbctxts table entry
  MOVL    runtime·cbctxts(SB), BX
  MOVL    -4(BX)(AX*4), BX // 計算したインデックスを使ってWinCallbackContext*を取得

  // extract callback context
  MOVL    cbctxt_gobody(BX), AX    // Go関数ポインタ
  MOVL    cbctxt_argsize(BX), DX   // 引数サイズ

  // ... (レジスタの保存、スタックの調整など)

  // call target Go function
  PUSHL    DX            // argsize (including return value)
  PUSHL    CX            // callback parameters
  PUSHL    AX            // address of target Go function
  CLD
  CALL    runtime·cgocallback_gofunc(SB) // 実際のGo関数を呼び出すランタイム関数
  // ... (レジスタの復元、スタックのクリーンアップなど)

runtime·callbackasm1は、スタック上の戻りアドレス(runtime·callbackasm内のどのCALL命令から呼び出されたかを示す)を利用して、runtime·callbackasmの開始アドレスからのオフセットを計算します。このオフセットをCALL命令のサイズ(5バイト)で割ることで、runtime·cbctxts配列内の対応するWinCallbackContextのインデックスを特定します。 このインデックスを使ってWinCallbackContextを取得し、そこからGo関数へのポインタ(gobody)と引数サイズ(argsize)を読み取ります。これらの情報と、Windowsの呼び出し規約に従って渡された引数を使って、最終的にruntime·cgocallback_gofuncを呼び出し、Go関数を実行します。

この一連の変更により、Goランタイムは実行時の動的なコード生成を避け、事前にコンパイルされた静的なアセンブリスタブと、コールバックコンテキストを管理するデータ構造を使用することで、より安全で効率的なWindowsコールバックメカニズムを実現しています。

関連リンク

参考にした情報源リンク

  • Goのソースコード (特にsrc/pkg/runtimeおよびsrc/cmd/distディレクトリ)
  • Windows APIのコールバックに関するドキュメント
  • x86およびx64アーキテクチャの呼び出し規約に関する情報
  • DEP (Data Execution Prevention) および ASLR (Address Space Layout Randomization) に関するセキュリティ情報