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

[インデックス 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を他の目的で自由に使用することが制限される場合があります。

i386x86_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に戻すという手法が取られました。

具体的には、以下の手順が実行されます。

  1. mov %%ebx, %%edi: cpuid命令を実行する前に、現在の%ebxレジスタの内容を%ediレジスタに退避させます。これにより、cpuid命令が%ebxを上書きしても、元の値が失われません。
  2. cpuid: cpuid命令を実行します。この命令は%ebxを含むレジスタに結果を書き込みます。
  3. 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の値を格納するように指定しています。Dediレジスタに対応する制約文字です。
    • "=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言語のブートストラッププロセスに関する一般的な知識、x86アセンブリ、PIC、およびGCCインラインアセンブリの制約に関する一般的な知識に基づいて作成されました。