[インデックス 15403] ファイルの概要
このコミットは、Go言語のブートストラップツールである cmd/dist
内の src/cmd/dist/unix.c
ファイルに対する変更です。src/cmd/dist/unix.c
は、Goのビルドプロセスにおいて、Unix系システム特有の処理、特にCPUの機能検出(cpuid
命令の使用)などを行うためのコードを含んでいます。cmd/dist
は、Goのツールチェインをゼロからビルドする際に使用される初期コンパイラやツール群を指します。
コミット
- コミットハッシュ:
89ec3a610d2a1c9887fe9ee0bd622f3ab14c6750
- 作者: Shenghou Ma minux.ma@gmail.com
- コミット日時: 2013年2月24日 22:45:53 +0800
- コミットメッセージ:
cmd/dist: avoid using %ebx on i386. Or gcc (-fPIC) will complain: cmd/dist/unix.c: In function ‘cansse2’ cmd/dist/unix.c:774: error: can't find a register in class ‘BREG’ while reloading ‘asm’ cmd/dist/unix.c:774: error: ‘asm’ operand has impossible constraints This affects bootstrapping on native Darwin/386 where all code is compiled with -fPIC. R=golang-dev, iant CC=golang-dev https://golang.org/cl/7394047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/89ec3a610d2a1c9887fe9ee0bd622f3ab14c6750
元コミット内容
cmd/dist: avoid using %ebx on i386.
Or gcc (-fPIC) will complain:
cmd/dist/unix.c: In function ‘cansse2’
cmd/dist/unix.c:774: error: can't find a register in class ‘BREG’ while reloading ‘asm’
cmd/dist/unix.c:774: error: ‘asm’ operand has impossible constraints
This affects bootstrapping on native Darwin/386 where all code is
compiled with -fPIC.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7394047
変更の背景
この変更は、Go言語のブートストラッププロセスにおいて、特にDarwin/386
(macOS上の32ビットIntelアーキテクチャ)環境で発生するコンパイルエラーを解決するために導入されました。エラーメッセージ「can't find a register in class ‘BREG’ while reloading ‘asm’
」と「‘asm’ operand has impossible constraints
」が示すように、GCCコンパイラがインラインアセンブリコード内で%ebx
レジスタを扱う際に問題が発生していました。
この問題の根本原因は、Darwin/386
環境ではすべてのコードが**Position-Independent Code (PIC)**としてコンパイルされることにあります。PICでは、%ebx
レジスタがグローバルオフセットテーブル(GOT)へのポインタとして予約されることが多く、通常の汎用レジスタとしての使用が制限されます。cpuid
命令は、その性質上、%ebx
レジスタを特定の目的(CPUIDの出力の一部)に使用するため、PICの制約と衝突し、GCCが適切なレジスタ割り当てを行えずにコンパイルエラーを引き起こしていました。
Goのブートストラップは、既存のGoコンパイラがない環境でGoをビルドするために、C言語で書かれたcmd/dist
ツールを使用します。このツールが特定の環境でコンパイルできないと、Goのビルド自体が不可能になるため、この問題はGoの移植性にとって重要な課題でした。
前提知識の解説
cmd/dist
cmd/dist
は、Go言語のブートストラッププロセスで使用されるツールです。Goのコンパイラやツールチェインをゼロからビルドする際に、初期のコンパイラとして機能します。GoのソースコードはGo自身で書かれているため、GoがまだインストールされていないシステムでGoをビルドするには、まずC言語で書かれたcmd/dist
のようなツールを使って、Goの基本的なツールチェインを構築する必要があります。
%ebx
レジスタ
%ebx
は、x86アーキテクチャにおける汎用レジスタの一つです。しかし、Position-Independent Code (PIC) の文脈では特別な役割を持つことがあります。PICでは、コードがメモリ上のどこにロードされても正しく実行されるように、絶対アドレスではなく相対アドレスを使用します。この際、グローバル変数や関数へのアクセスには、グローバルオフセットテーブル(GOT)やプロシージャリンケージテーブル(PLT)が利用されます。多くのシステム(特にLinuxやmacOSの32ビット環境)では、PICを生成する際に%ebx
レジスタをGOTへのポインタとして予約し、その値を保持する規約があります。これにより、%ebx
を他の目的で自由に使用することが制限される場合があります。
i386
とx86_64
これらはIntel x86プロセッサのアーキテクチャ名です。
i386
(またはx86
): 32ビットのIntelプロセッサアーキテクチャを指します。レジスタは32ビット幅です。x86_64
(またはamd64
): 64ビットのIntel/AMDプロセッサアーキテクチャを指します。レジスタは64ビット幅で、より多くの汎用レジスタが利用可能です。64ビット環境では、PICにおける%ebx
の制約は通常存在しません。
cpuid
命令
cpuid
は、x86プロセッサが提供する命令で、CPUの機能や情報を問い合わせるために使用されます。この命令を実行すると、CPUのベンダーID、モデル情報、サポートする機能(SSE2、AVXなど)といった情報が、特定のレジスタ(eax
, ebx
, ecx
, edx
)に格納されて返されます。cpuid
命令は、入力としてeax
レジスタを使用し、出力としてeax
, ebx
, ecx
, edx
の4つのレジスタを使用します。
PIC (Position-Independent Code)
PICは、実行可能コードがメモリ上の任意のアドレスにロードされても正しく動作するように設計されたコードです。これは主に共有ライブラリ(DLLや.soファイル)で使用されます。PICでは、データや関数への参照が絶対アドレスではなく、現在の命令ポインタからの相対オフセットや、グローバルオフセットテーブル(GOT)を介して行われます。これにより、ライブラリが複数のプロセスで共有される際に、各プロセスのアドレス空間に異なるベースアドレスでロードされても問題なく動作します。
asm volatile
C/C++言語におけるインラインアセンブリの構文の一部です。
asm
: インラインアセンブリコードを記述することを示します。volatile
: コンパイラに対して、このアセンブリコードを最適化によって削除したり、移動したりしないように指示します。特に、アセンブリコードがメモリやハードウェアの状態を変更する場合に重要です。
GCCのエラーメッセージ
can't find a register in class ‘BREG’ while reloading ‘asm’
: GCCがインラインアセンブリの制約を満たすために、BREG
クラス(通常は%ebx
レジスタを指す)のレジスタを見つけられないことを示しています。これは、%ebx
がPICの規約によって予約されているため、コンパイラが自由に使用できない状況で発生します。‘asm’ operand has impossible constraints
: インラインアセンブリのオペランド(入力や出力のレジスタ指定)が、現在のコンパイル設定(例: PIC)と矛盾していることを示します。
Darwin/386
AppleのmacOS(旧OS X)上で動作する32ビットIntelプロセッサアーキテクチャを指します。この環境では、システムライブラリや多くのアプリケーションがPICとしてコンパイルされることが一般的です。
技術的詳細
この問題は、__cpuid
関数内のインラインアセンブリがcpuid
命令の出力レジスタとして%ebx
を使用していることと、Darwin/386
環境でのPICの制約が衝突したために発生しました。
cpuid
命令は、その仕様上、結果の一部を%ebx
レジスタに書き込みます。
cpuid
この命令を実行すると、eax
, ebx
, ecx
, edx
の各レジスタにCPU情報が格納されます。
元のコードでは、__cpuid
関数がcpuid
命令の出力を直接dst[1]
(%ebx
に対応)にマッピングしようとしていました。
// Original (simplified)
asm volatile("cpuid"
: "=a" (dst[0]), "=b" (dst[1]), "=c" (dst[2]), "=d" (dst[3])
: "0" (ax));
ここで、"=b" (dst[1])
という制約は、cpuid
命令の出力である%ebx
レジスタの値をdst[1]
に格納することを意味します。しかし、Darwin/386
で-fPIC
オプションが有効な場合、GCCは%ebx
をGOTポインタとして使用するため、このレジスタをインラインアセンブリの出力として自由に使用することができません。GCCは、%ebx
がすでに別の目的で予約されているため、cpuid
命令の出力を%ebx
からdst[1]
に「リロード」しようとした際に、適切なレジスタを見つけられずにエラーを発生させました。
この問題を解決するために、コミットでは%ebx
レジスタを一時的に別の汎用レジスタである%edi
に退避させ、cpuid
命令の実行後に元の%ebx
に戻すという手法が取られました。
具体的には、以下の手順が実行されます。
mov %%ebx, %%edi
:cpuid
命令を実行する前に、現在の%ebx
レジスタの内容を%edi
レジスタに退避させます。これにより、cpuid
命令が%ebx
を上書きしても、元の値が失われません。cpuid
:cpuid
命令を実行します。この命令は%ebx
を含むレジスタに結果を書き込みます。xchgl %%ebx, %%edi
:cpuid
命令の実行後、%ebx
と%edi
の内容を交換します。これにより、cpuid
命令によって上書きされた%ebx
の値が%edi
に移動し、元の%ebx
の値(cpuid
実行前に%edi
に退避されていたもの)が%ebx
に戻されます。同時に、cpuid
の出力である%ebx
の値は%edi
に格納されることになります。
そして、インラインアセンブリの出力制約も変更され、dst[1]
が%edi
に対応するように"=D"
(%edi
レジスタ)が指定されています。これにより、cpuid
命令の%ebx
出力は、xchgl
命令によって%edi
に移動し、最終的にdst[1]
に格納されることになります。
この変更により、%ebx
レジスタがPICの制約に縛られずにcpuid
命令の出力を処理できるようになり、Darwin/386
環境でのコンパイルエラーが解消されました。
コアとなるコードの変更箇所
--- a/src/cmd/dist/unix.c
+++ b/src/cmd/dist/unix.c
@@ -770,7 +770,15 @@ sigillhand(int signum)
static void
__cpuid(int dst[4], int ax)
{
-#if defined(__i386__) || defined(__x86_64__)\n
+#ifdef __i386__
+\t// we need to avoid ebx on i386 (esp. when -fPIC).\n
+\tasm volatile(\n
+\t\t\"mov %%ebx, %%edi\\n\\t\"\n
+\t\t\"cpuid\\n\\t\"\n
+\t\t\"xchgl %%ebx, %%edi\"\n
+\t\t: \"=a\" (dst[0]), \"=D\" (dst[1]), \"=c\" (dst[2]), \"=d\" (dst[3])\n
+\t\t: \"0\" (ax));
+#elif defined(__x86_64__)\n
\tasm volatile(\"cpuid\"\n
\t\t: \"=a\" (dst[0]), \"=b\" (dst[1]), \"=c\" (dst[2]), \"=d\" (dst[3])\n
\t\t: \"0\" (ax));
コアとなるコードの解説
変更は__cpuid
関数内で行われています。この関数は、cpuid
命令を実行してCPU情報を取得し、その結果をdst
配列に格納するためのものです。
元のコードでは、__i386__
と__x86_64__
の両方で同じインラインアセンブリを使用していました。
#if defined(__i386__) || defined(__x86_64__)
asm volatile("cpuid"
: "=a" (dst[0]), "=b" (dst[1]), "=c" (dst[2]), "=d" (dst[3])
: "0" (ax));
#endif
このコードは、cpuid
命令の出力を直接eax
, ebx
, ecx
, edx
レジスタからdst
配列の対応する要素にマッピングしていました。特に"=b" (dst[1])
は、ebx
レジスタの値をdst[1]
に格納することを示しています。
変更後のコードでは、__i386__
の場合にのみ特別な処理が追加されました。
#ifdef __i386__
// we need to avoid ebx on i386 (esp. when -fPIC).
asm volatile(
"mov %%ebx, %%edi\\n\\t" // ebxの内容をediに退避
"cpuid\\n\\t" // cpuid命令を実行
"xchgl %%ebx, %%edi" // ebxとediの内容を交換
: "=a" (dst[0]), "=D" (dst[1]), "=c" (dst[2]), "=d" (dst[3]) // 出力制約
: "0" (ax)); // 入力制約
#elif defined(__x86_64__)
// x86_64では元のコードを維持
asm volatile("cpuid"
: "=a" (dst[0]), "=b" (dst[1]), "=c" (dst[2]), "=d" (dst[3])
: "0" (ax));
#endif
#ifdef __i386__
: 32ビットIntelアーキテクチャの場合にのみ、以下の特殊なインラインアセンブリが適用されます。// we need to avoid ebx on i386 (esp. when -fPIC).
: コメントで、i386
(特にPICが有効な場合)でebx
を避ける必要があることが明記されています。"mov %%ebx, %%edi\\n\\t"
:cpuid
命令を実行する前に、現在の%ebx
レジスタの内容を%edi
レジスタに移動(退避)します。\\n\\t
はアセンブリ命令の区切り文字です。"cpuid\\n\\t"
:cpuid
命令を実行します。この命令は%ebx
を含むレジスタに結果を書き込みます。"xchgl %%ebx, %%edi"
:cpuid
命令の実行後、%ebx
と%edi
の値を交換します。これにより、cpuid
命令によって上書きされた%ebx
の値が%edi
に移動し、元の%ebx
の値が%ebx
に戻されます。- 出力制約
: "=a" (dst[0]), "=D" (dst[1]), "=c" (dst[2]), "=d" (dst[3])
:"=a" (dst[0])
:eax
の値をdst[0]
に。"=D" (dst[1])
: ここが重要な変更点です。cpuid
命令のebx
出力はxchgl
命令によってedi
に移動するため、dst[1]
にはedi
の値を格納するように指定しています。D
はedi
レジスタに対応する制約文字です。"=c" (dst[2])
:ecx
の値をdst[2]
に。"=d" (dst[3])
:edx
の値をdst[3]
に。
- 入力制約
: "0" (ax)
:eax
レジスタにax
の値を入力として渡します。"0"
は、出力オペランドの0番目(eax
)と同じレジスタを使用することを示します。
この変更により、i386
環境、特にPICが有効な環境で%ebx
レジスタがGOTポインタとして予約されている場合でも、cpuid
命令の出力を正しく取得できるようになりました。x86_64
環境では%ebx
の制約がないため、元のシンプルなインラインアセンブリが維持されています。
関連リンク
- Go CL (Code Review) 7394047: https://golang.org/cl/7394047
参考にした情報源リンク
- 本解説は、提供されたコミット情報、Go言語のブートストラッププロセスに関する一般的な知識、x86アセンブリ、PIC、およびGCCインラインアセンブリの制約に関する一般的な知識に基づいて作成されました。