[インデックス 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
関数が実行時にコードを生成するのを停止するという、簡潔ながらも重要な変更を示しています。
変更の背景
この変更の背景には、主に以下の理由が考えられます。
- セキュリティとDEP (Data Execution Prevention) / ASLR (Address Space Layout Randomization) との互換性: 実行時に動的にコードを生成し、それを実行可能メモリとしてマークすることは、セキュリティ上のリスクを伴います。特に、DEPが有効なシステムでは、データ領域からのコード実行がブロックされるため、動的に生成されたコードの実行が困難になります。また、ASLRはメモリレイアウトをランダム化することで、攻撃者が特定のメモリ位置を予測してコードを注入するのを防ぎますが、動的なコード生成はこれらの保護メカニズムと相性が悪い場合があります。
- 複雑性の軽減: 実行時にアセンブリコードを動的に構築するロジックは、複雑でエラーが発生しやすくなります。異なるアーキテクチャ(386とAMD64)ごとに異なるコード生成ロジックを持つ必要があり、メンテナンスの負担も大きくなります。
- パフォーマンスの向上: 動的なコード生成にはオーバーヘッドが伴います。事前にコンパイルされたスタブを使用することで、コールバックの登録時のパフォーマンスが向上する可能性があります。
- 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
という共通のアセンブリルーチンにジャンプまたはコールする役割を担っていました。
このコミットでは、この動的なコード生成の仕組みが廃止されました。代わりに、以下のような新しいアプローチが導入されました。
- 単一の共通アセンブリスタブ:
runtime·callbackasm
という単一のアセンブリルーチンが、Goのビルドプロセス中に事前にコンパイルされます。このルーチンは、複数のコールバックエントリポイントを持つように設計されています。具体的には、runtime·callbackasm
は一連のCALL runtime·callbackasm1(SB)
命令で構成されており、各CALL
命令は5バイト(32ビット版)または5バイト(64ビット版)の固定長です。 - コールバックコンテキストの管理: 新しい構造体
WinCallbackContext
がsrc/pkg/runtime/runtime.h
で定義されました。この構造体は、対応するGo関数へのポインタ(gobody
)、引数のサイズ(argsize
)、およびスタックを復元するための情報(restorestack
、386のみ)を保持します。 runtime·cbctxts
配列:WinCallbackContext
構造体のポインタの配列であるruntime·cbctxts
が導入されました。これは、登録された各コールバックのコンテキスト情報を格納します。runtime·compilecallback
の変更:- 動的なアセンブリコード生成のロジックが削除されました。
- 代わりに、登録されるGo関数に対応する
WinCallbackContext
構造体を作成し、runtime·cbctxts
配列に格納します。 - 返されるコールバックアドレスは、
runtime·callbackasm
の開始アドレスに、そのコールバックに対応するCALL
命令のオフセットを加算した値になります。これにより、外部コードがこのアドレスを呼び出すと、runtime·callbackasm
内の特定のCALL
命令にジャンプし、そこからruntime·callbackasm1
が実行されます。
runtime·callbackasm1
の導入と役割:runtime·callbackasm
から呼び出される新しいアセンブリルーチンruntime·callbackasm1
が導入されました。- このルーチンは、スタック上の戻りアドレス(
runtime·callbackasm
内のどのCALL
命令から来たかを示す)を利用して、runtime·cbctxts
配列内の対応するWinCallbackContext
エントリのインデックスを計算します。 - 計算されたインデックスを使用して
WinCallbackContext
を取得し、そこからGo関数へのポインタと引数サイズを読み取ります。 - これらの情報を使って、最終的にGoのランタイム関数
runtime·cgocallback_gofunc
を呼び出し、実際のGo関数を実行します。 - Windowsの呼び出し規約に従ってレジスタを保存・復元し、スタックを適切にクリーンアップします。
- ビルドプロセスの変更:
src/cmd/dist/build.c
とsrc/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·callbackasm
がruntime·callbackasm1
にリネームされ、そのロジックが大幅に変更されました。runtime·cbctxts
グローバル変数が宣言されました。- スタック上の戻りアドレスから
WinCallbackContext
のインデックスを計算し、対応するGo関数と引数サイズを取得するロジックが追加されました。 runtime·cgocallback_gofunc
を呼び出す前に、WinCallbackContext
から取得した情報を使用して引数を設定するようになりました。
src/pkg/runtime/sys_windows_amd64.s
:runtime·callbackasm
がruntime·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 Issue #5494: https://code.google.com/p/go/issues/detail?id=5494 (古いGoogle Codeのリンクですが、関連するIssueです)
- Go CL 10368043: https://golang.org/cl/10368043 (このコミットに対応するGoの変更リスト)
参考にした情報源リンク
- Goのソースコード (特に
src/pkg/runtime
およびsrc/cmd/dist
ディレクトリ) - Windows APIのコールバックに関するドキュメント
- x86およびx64アーキテクチャの呼び出し規約に関する情報
- DEP (Data Execution Prevention) および ASLR (Address Space Layout Randomization) に関するセキュリティ情報