[インデックス 15368] ファイルの概要
このコミットは、Go言語のランタイムにおけるWindows環境でのコールバック処理に関連する変更です。具体的には、Goの関数値の内部表現が変更されたことに伴い、Windows APIへのコールバックを行うためのコードがその新しい表現に適合するように修正されています。
変更されたファイルは以下の通りです。
src/pkg/runtime/callback_windows_386.c: Windows 32-bit (x86) アーキテクチャ向けのコールバック処理コード。src/pkg/runtime/callback_windows_amd64.c: Windows 64-bit (x64) アーキテクチャ向けのコールバック処理コード。
コミット
commit 6ec551887a8d4bad243cf462c3cfc6aa4fa727a5
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Feb 22 12:21:42 2013 +1100
runtime: windows callback code to match new func value representation
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7393048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6ec551887a8d4bad243cf462c3cfc6aa4fa727a5
元コミット内容
runtime: windows callback code to match new func value representation
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7393048
変更の背景
このコミットの背景には、Go言語のランタイム内部における「関数値 (func value)」の表現方法の変更があります。Go言語では、関数も第一級オブジェクトとして扱われ、変数に代入したり、引数として渡したり、戻り値として返したりすることができます。これらの関数は、内部的には「関数値」として表現されます。
Windows環境では、GoプログラムがC言語で書かれたWindows APIを呼び出す際や、逆にWindows APIからGoの関数をコールバックとして呼び出す際に、Goの関数をWindowsが理解できる形式(通常は関数ポインタ)に変換する必要があります。この変換処理はGoランタイムのruntime·compilecallback関数で行われます。
以前のGoランタイムでは、関数値の内部表現が直接関数ポインタを含んでいた可能性があります。しかし、Go言語の進化に伴い、ガベージコレクションや最適化、あるいはより複雑な型システムをサポートするために、関数値の内部表現が変更されたと考えられます。この変更により、Eface構造体(Goのinterface{}型がC言語レベルで表現されたもの)のdataフィールドが、直接関数ポインタを指すのではなく、関数ポインタを格納している別のメモリ領域へのポインタを指すようになったと推測されます。
このコミットは、その内部表現の変更に対応し、runtime·compilecallback関数が正しく関数ポインタを抽出し、Windowsが利用できる形式でコールバックを設定できるようにするための修正です。
前提知識の解説
Go言語のランタイム (Runtime)
Go言語のプログラムは、CやC++のように直接OSのシステムコールを呼び出すのではなく、Goランタイムと呼ばれる層を介して実行されます。ランタイムは、ガベージコレクション、スケジューリング、goroutineの管理、チャネル通信、そしてOSとのインタラクション(ファイルI/O、ネットワーク、メモリ管理など)といった低レベルな処理を担当します。src/pkg/runtimeディレクトリには、このランタイムのC言語およびアセンブリ言語で書かれたコードが含まれています。
関数値 (Function Values)
Go言語では、関数は値として扱われます。例えば、func() {}のような無名関数や、func myFunc() {}のような名前付き関数は、プログラム内で「関数値」として表現されます。この関数値は、実行可能なコードへのポインタと、その関数がクロージャである場合に必要となる環境(キャプチャされた変数など)へのポインタを内部に持っています。
interface{}型とEface構造体
Go言語のinterface{}型は、任意の型の値を保持できる特殊な型です。ランタイム内部では、interface{}型の値はEfaceというC言語の構造体で表現されます。Eface構造体は通常、以下の2つのフィールドを持ちます。
type: 保持している値の型情報へのポインタ。data: 保持している値そのものへのポインタ。
このコミットでは、fnというEface型の変数がGoの関数値を保持していると想定されます。
コールバック (Callback)
コールバックとは、ある関数(またはライブラリ、OSなど)が、特定のイベントが発生したときや処理が完了したときに呼び出すために、あらかじめ登録しておく関数を指します。Windowsプログラミングでは、GUIイベント処理や非同期I/Oなど、様々な場面でコールバック関数が利用されます。GoプログラムがWindows APIと連携する場合、Goの関数をWindowsが呼び出せる形式(関数ポインタ)で提供する必要があります。
アセンブリ言語とポインタ操作
このコミットで変更されているコードはC言語で書かれていますが、その中ではアセンブリ言語の命令(MOVL, MOVQ)を生成しています。これは、Goランタイムが動的に機械語コードを生成して、Goの関数をWindowsが呼び出せる形式のスタブ(小さなアダプタコード)を作成しているためです。
MOVL(Move Long): 32-bitの値をレジスタやメモリ間で移動するx86アセンブリ命令。MOVQ(Move Quad): 64-bitの値をレジスタやメモリ間で移動するx64アセンブリ命令。
C言語におけるポインタ操作、特に多重ポインタ(byte**)の理解が重要です。*(uint32*)p = (uint32)fn.data; は、fn.dataが指すアドレスの値を32-bit整数としてpが指すメモリ位置に書き込むことを意味します。一方、*(uint32*)p = (uint32)(*(byte**)fn.data); は、fn.dataが指すアドレスにある値をbyte**(バイトへのポインタへのポインタ)として解釈し、そのポインタが指す先の値(つまり、実際の関数ポインタ)を32-bit整数としてpが指すメモリ位置に書き込むことを意味します。
技術的詳細
このコミットの核心は、Goの関数値がEface構造体のdataフィールドにどのように格納されているか、その表現方法の変更に対応することです。
変更前:
*(uint32*)p = (uint32)fn.data; (32-bit)
*(uint64*)p = (uint64)fn.data; (64-bit)
これは、fn.dataが直接、Goの関数コードのエントリポイント(つまり関数ポインタ)を指していることを示唆しています。fnはEface型であり、そのdataフィールドはvoid*のような汎用ポインタです。このポインタをuint32またはuint64にキャストして、pが指すメモリ位置(動的に生成されるアセンブリコードの命令の一部)に直接書き込んでいました。これは、MOVLやMOVQ命令のオペランドとして、関数ポインタの値を直接埋め込むことを意味します。
変更後:
*(uint32*)p = (uint32)(*(byte**)fn.data); (32-bit)
*(uint64*)p = (uint64)(*(byte**)fn.data); (64-bit)
この変更は、fn.dataが直接関数ポインタを保持するのではなく、関数ポインタを格納している別のメモリ領域へのポインタを保持するようになったことを示しています。
具体的には、以下のステップで値が取得されます。
fn.data:Eface構造体のdataフィールドが指すアドレス。このアドレスには、Goの関数値の内部表現の一部が格納されています。(byte**)fn.data:fn.dataが指すアドレスを、byteへのポインタへのポインタ(byte**)として解釈します。これは、そのアドレスに別のポインタが格納されていることを示唆しています。*(byte**)fn.data:(byte**)fn.dataが指す先の値、つまり、実際にGoの関数コードのエントリポイントを指すポインタ(関数ポインタ)を取得します。(uint32)(...)または(uint64)(...): 取得した関数ポインタを、適切なサイズの整数型にキャストします。*(uint32*)p = ...または*(uint64*)p = ...: キャストされた関数ポインタの値を、動的に生成されるアセンブリコードの命令の一部としてメモリに書き込みます。
この変更は、Goランタイムが関数値を表現する方法をより柔軟にするためのものであり、例えば、関数ポインタの前にメタデータ(型情報など)を追加したり、ガベージコレクタが関数値をより効率的に追跡できるようにしたりするための一環である可能性があります。これにより、runtime·compilecallback関数は、新しい関数値の表現から正しい関数ポインタを抽出し、Windowsが呼び出せる形式のスタブコードを生成できるようになります。
コアとなるコードの変更箇所
src/pkg/runtime/callback_windows_386.c と src/pkg/runtime/callback_windows_amd64.c の両方で、runtime·compilecallback関数内の以下の行が変更されています。
変更前:
// MOVL fn, AX
*p++ = 0xb8;
*(uint32*)p = (uint32)fn.data; // 32-bit
p += 4;
または
// MOVQ fn, AX
*p++ = 0x48;
*p++ = 0xb8;
*(uint64*)p = (uint64)fn.data; // 64-bit
p += 8;
変更後:
// MOVL fn, AX
*p++ = 0xb8;
*(uint32*)p = (uint32)(*(byte**)fn.data); // 32-bit
p += 4;
または
// MOVQ fn, AX
*p++ = 0x48;
*p++ = 0xb8;
*(uint64*)p = (uint64)(*(byte**)fn.data); // 64-bit
p += 8;
コアとなるコードの解説
この変更は、Goの関数値がEface構造体のdataフィールドに格納される方法の変更に直接対応しています。
runtime·compilecallback関数は、Goの関数(fn)を受け取り、それをWindowsが呼び出せる形式のコールバック関数として設定するためのアセンブリコードを動的に生成します。この生成されるアセンブリコードは、最終的にGoの関数を呼び出すためのジャンプ命令を含みます。そのジャンプ先のアドレス、つまりGoの関数のエントリポイントのアドレスを、アセンブリ命令のオペランドとして埋め込む必要があります。
-
*p++ = 0xb8;(32-bit) または*p++ = 0x48; *p++ = 0xb8;(64-bit): これらは、それぞれMOVLまたはMOVQ命令のオペコード(機械語の命令コード)をpが指すメモリ位置に書き込んでいます。これらの命令は、続くオペランド(ここでは関数ポインタの値)をAXレジスタ(またはEAX/RAX)にロードするために使用されます。 -
*(uint32*)p = (uint32)fn.data;(変更前): この行は、fn.dataが直接Goの関数のエントリポイントのアドレスを保持していると仮定し、そのアドレスを32-bitまたは64-bitの整数として抽出し、MOVL/MOVQ命令のオペランドとして直接埋め込んでいました。 -
*(uint32*)p = (uint32)(*(byte**)fn.data);(変更後): この行は、fn.dataが、実際の関数ポインタが格納されているメモリ位置へのポインタを保持していると解釈します。したがって、*(byte**)fn.dataという間接参照(ポインタのデリファレンス)を一度追加することで、fn.dataが指す先にあるポインタ(つまり、実際の関数ポインタ)を取得しています。この取得した関数ポインタが、MOVL/MOVQ命令のオペランドとして埋め込まれます。
この変更により、Goランタイムは、関数値の新しい内部表現から正確な関数ポインタを抽出し、Windowsのコールバックメカニズムが期待する形式でアセンブリコードを生成できるようになります。これは、Go言語の内部的な進化と、それが低レベルなOS連携に与える影響を示す典型的な例です。
関連リンク
- Go CL 7393048: https://golang.org/cl/7393048
参考にした情報源リンク
- Go言語のインターフェースの内部構造に関する一般的な情報:
- x86/x64 アセンブリ命令 (MOVL, MOVQ) に関する情報:
- C言語のポインタと多重ポインタに関する一般的な情報。
- Go言語のランタイムとコールバックに関する一般的な情報。