[インデックス 16168] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) において、-linkmode=external
オプションが指定された場合に、常に外部リンキングが実行されるように修正するものです。これにより、特定の条件下で内部リンキングにフォールバックしてしまう問題を解決し、ユーザーの意図通りのリンキングモードを保証します。
コミット
commit 696901204f2e7a5180ed4beb4046fbfea05841b2
Author: Ian Lance Taylor <iant@golang.org>
Date: Fri Apr 12 13:21:17 2013 -0700
cmd/ld: always do external link for -linkmode=external
There are tests in run.bash for -linkmode=external.
Fixes #5238.
R=golang-dev, bradfitz, remyoudompheng, r
CC=golang-dev
https://golang.org/cl/8716044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/696901204f2e7a5180ed4beb4046fbfea05841b2
元コミット内容
Goリンカ (cmd/ld
) が、-linkmode=external
オプションが指定された際に、常に外部リンキングを実行するように変更します。この変更は、-linkmode=external
のテストが run.bash
に存在することに関連しており、Issue #5238 を修正します。
変更の背景
このコミットは、Go言語のリンカにおける -linkmode=external
オプションの挙動に関するバグ、具体的には Issue #5238 を修正するために導入されました。
Go言語のビルドシステムでは、コンパイルされたGoコードと、C/C++などの他の言語で書かれたコード(Cgo経由で利用される)をリンクする際に、いくつかのリンキングモードが選択できます。
- 内部リンキング (internal linking): Goリンカがすべてのオブジェクトファイルを処理し、単一の実行可能ファイルを生成します。これはGoのデフォルトのリンキングモードであり、クロスコンパイルが容易であるという利点があります。
- 外部リンキング (external linking): Goリンカは、Goのオブジェクトファイルを生成した後、最終的なリンクステップをシステムのCリンカ(通常は
gcc
やclang
)に委ねます。これは、Cgoを使用している場合や、特定のシステムライブラリにリンクする必要がある場合に必要となります。
Issue #5238 の問題は、ユーザーが明示的に -linkmode=external
を指定したにもかかわらず、リンカが特定の条件下(例えば、Cgoコードが含まれていない場合や、Goの内部リンカが処理できると判断した場合)で内部リンキングにフォールバックしてしまうというものでした。これは、ユーザーの意図に反する挙動であり、特にCgoを使用しないが外部リンキングを必要とする特定のビルド環境や、デバッグの際に問題を引き起こす可能性がありました。
このコミットの目的は、ユーザーが -linkmode=external
を指定した場合は、その指定を尊重し、常に外部リンキングを実行するようにリンカの挙動を修正することです。
前提知識の解説
Goリンカ (cmd/ld
)
cmd/ld
はGo言語の標準リンカです。Goのソースコードがコンパイルされて生成されたオブジェクトファイル(.o
ファイル)や、Goの標準ライブラリ、そして必要に応じてCgo経由で利用されるC/C++のオブジェクトファイルを結合し、最終的な実行可能ファイルや共有ライブラリを生成する役割を担います。
リンキングモード (-linkmode
)
Goのビルドコマンド(go build
など)では、-linkmode
オプションを使用してリンキングの挙動を制御できます。主なモードは以下の通りです。
internal
: Goリンカがすべてのリンキング処理を行います。Cgoを使用しないGoプログラムのデフォルトです。external
: 最終的なリンキング処理をシステムのCリンカに委ねます。Cgoを使用するGoプログラムのデフォルトです。auto
: リンカが自動的に最適なモードを判断します。Cgoコードが存在する場合はexternal
を選択し、存在しない場合はinternal
を選択します。
runtime/cgo
パッケージ
runtime/cgo
パッケージは、GoプログラムがCgo機能(GoとC/C++コードの相互運用)を使用する際に、GoランタイムとCランタイム間の橋渡しをするための内部パッケージです。このパッケージがインポートされると、GoプログラムはCgoを使用しているとリンカに認識され、通常は外部リンキングが必要になります。
TLS (Thread Local Storage) の初期化
TLS (Thread Local Storage) は、各スレッドが独自のデータを持つことを可能にするメカニズムです。C/C++プログラムでは、スレッドごとに初期化が必要なデータがある場合、TLSが利用されます。GoプログラムがCgoを介してC/C++コードと連携する場合、CランタイムのTLS初期化が必要になることがあります。リンカは、runtime/cgo
のインポートを検出することで、このTLS初期化が必要かどうかを判断する手がかりとします。
技術的詳細
このコミットの技術的な核心は、Goリンカ (cmd/ld
) の loadlib
関数と ldhostobj
関数におけるリンキングモードの決定ロジックの変更にあります。
変更前の問題点
変更前のリンカの挙動は以下の通りでした。
-
linkmode == LinkExternal && !iscgo
の条件:loadlib
関数内で、もしユーザーが-linkmode=external
を明示的に指定し、かつGoコード内にCgoの利用を示すiscgo
フラグが立っていない場合、リンカはlinkmode
をLinkInternal
に上書きしていました。これは、「Cgoを使っていないなら、外部リンキングは不要だろう」というリンカの推測に基づくものでしたが、ユーザーが明示的に外部リンキングを要求している意図を無視していました。 -
linkmode == LinkAuto
の条件:linkmode == LinkAuto
の場合、リンカはiscgo
フラグに基づいてLinkExternal
かLinkInternal
を決定していました。しかし、externalobj
という新しいフラグが導入される前は、ホストコンパイラによってコンパイルされたオブジェクトファイル(Go以外の言語で書かれたもの)が存在する場合でも、iscgo
がfalse
であればLinkInternal
にフォールバックする可能性がありました。 -
ldhostobj
関数におけるexternalobj
の欠如:ldhostobj
関数は、ホストコンパイラ(Goコンパイラではない、例えばGCCなど)によってコンパイルされたオブジェクトファイルをロードする際に呼び出されます。変更前は、この関数内でisinternal
(Goの内部パッケージからのオブジェクトかどうか) がfalse
であり、かつlinkmode == LinkAuto
の場合にlinkmode
をLinkExternal
に設定していました。しかし、これはLinkAuto
モードに限定されており、ユーザーが明示的に-linkmode=external
を指定した場合の挙動を保証するものではありませんでした。
これらの問題により、ユーザーが -linkmode=external
を指定しても、リンカが内部リンキングに切り替えてしまい、外部リンキングに依存するビルドが失敗したり、予期せぬ挙動を示したりする可能性がありました。
変更による解決策
このコミットは、以下の変更によって問題を解決します。
-
externalobj
グローバル変数の導入:static int externalobj = 0;
という新しいグローバル変数が導入されました。この変数は、ホストコンパイラによってコンパイルされたオブジェクトファイル(Goの内部リンキングモードをサポートしないパッケージからのものではない)が検出された場合に1
に設定されます。 -
loadlib
関数におけるruntime/cgo
の強制インポート:if(linkmode == LinkExternal)
のブロックがloadlib
関数に追加されました。 ユーザーが-linkmode=external
を明示的に指定した場合、リンカはruntime/cgo
パッケージを強制的にロードします (loadinternal("runtime/cgo")
)。 さらに、go.importpath.runtime/cgo.
というシンボルをSDATA
タイプとしてreachable
に設定します。これは、runtime/cgo
が実際にインポートされたかのようにリンカに認識させるためのトリックです。 なぜこれが必要か?: Goの起動コードは、runtime/cgo
のインポートの有無に基づいてTLSの初期化を行うかどうかを決定します。ユーザーが外部リンキングを要求している場合、たとえGoコード内で直接Cgoを使用していなくても、外部リンカがCライブラリにリンクする可能性があり、その際にTLS初期化が必要になることがあります。runtime/cgo
を強制的にインポートされたかのように見せかけることで、Goランタイムが適切にTLS初期化を行うように促します。 -
loadlib
関数におけるリンキングモード決定ロジックの修正: 変更前のif(linkmode == LinkExternal && !iscgo) linkmode = LinkInternal;
という行が削除されました。これにより、ユーザーが-linkmode=external
を指定した場合に、リンカが勝手にLinkInternal
に切り替えることがなくなりました。if(linkmode == LinkAuto)
のブロックも修正されました。if(iscgo && externalobj)
の条件が追加され、Cgoが使用されており、かつホストコンパイラによってコンパイルされたオブジェクトファイルが存在する場合にのみLinkExternal
を選択するように変更されました。それ以外の場合はLinkInternal
になります。これにより、LinkAuto
モードでの判断がより正確になります。 -
ldhostobj
関数におけるexternalobj
の設定:if(!isinternal && linkmode == LinkAuto) linkmode = LinkExternal;
という行がif(!isinternal) externalobj = 1;
に変更されました。 これは非常に重要な変更です。ホストコンパイラによってコンパイルされたオブジェクトファイルがロードされた場合(!isinternal
)、リンキングモードが何であれ、無条件にexternalobj
フラグを1
に設定します。これにより、loadlib
関数が最終的なリンキングモードを決定する際に、ホストオブジェクトの存在を正確に考慮できるようになります。
これらの変更により、ユーザーが -linkmode=external
を指定した場合は、リンカがその意図を尊重し、常に外部リンキングを実行するようになります。また、LinkAuto
モードでの判断もより堅牢になりました。
コアとなるコードの変更箇所
--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -45,6 +45,10 @@ int nlibdir = 0;
static int maxlibdir = 0;
static int cout = -1;
+// Set if we see an object compiled by the host compiler that is not
+// from a package that is known to support internal linking mode.
+static int externalobj = 0;
+
static void hostlinksetup(void);
char* goroot;
@@ -295,6 +299,19 @@ loadlib(void)
loadinternal("math");
if(flag_race)
loadinternal("runtime/race");
+ if(linkmode == LinkExternal) {
+ // This indicates a user requested -linkmode=external.
+ // The startup code uses an import of runtime/cgo to decide
+ // whether to initialize the TLS. So give it one. This could
+ // be handled differently but it's an unusual case.
+ loadinternal("runtime/cgo");
+ // Pretend that we really imported the package.
+ // This will do no harm if we did in fact import it.
+ s = lookup("go.importpath.runtime/cgo.", 0);
+ s->type = SDATA;
+ s->dupok = 1;
+ s->reachable = 1;
+ }
for(i=0; i<libraryp; i++) {
if(debug['v'])
@@ -303,14 +320,11 @@ loadlib(void)
objfile(library[i].file, library[i].pkg);
}
- if(linkmode == LinkExternal && !iscgo)
- linkmode = LinkInternal;
-
- // If we got this far in automatic mode, there were no
- // cgo uses that suggest we need external mode.
- // Switch to internal.
if(linkmode == LinkAuto) {
- linkmode = LinkInternal;
+ if(iscgo && externalobj)
+ linkmode = LinkExternal;
+ else
+ linkmode = LinkInternal;
}
if(linkmode == LinkInternal) {
@@ -532,8 +546,8 @@ ldhostobj(void (*ld)(Biobuf*, char*, int64, char*), Biobuf *f, char *pkg, int64
}
}
- if(!isinternal && linkmode == LinkAuto)
- linkmode = LinkExternal;
+ if(!isinternal)
+ externalobj = 1;
if(nhostobj >= mhostobj) {
if(mhostobj == 0)
コアとなるコードの解説
lib.c
の変更点
-
externalobj
変数の追加:static int externalobj = 0;
externalobj
は、Goの内部リンキングモードをサポートしない、ホストコンパイラによってコンパイルされたオブジェクトファイルが存在するかどうかを示すフラグです。これが1
に設定されると、外部リンキングが必要である可能性が高まります。 -
loadlib
関数内のLinkExternal
モード処理の追加:if(linkmode == LinkExternal) { // This indicates a user requested -linkmode=external. // The startup code uses an import of runtime/cgo to decide // whether to initialize the TLS. So give it one. This could // be handled differently but it's an unusual case. loadinternal("runtime/cgo"); // Pretend that we really imported the package. // This will do no harm if we did in fact import it. s = lookup("go.importpath.runtime/cgo.", 0); s->type = SDATA; s->dupok = 1; s->reachable = 1; }
ユーザーが
-linkmode=external
を明示的に指定した場合、runtime/cgo
パッケージを内部的にロードし、そのインポートパスシンボル (go.importpath.runtime/cgo.
) を到達可能 (reachable = 1
) に設定します。これは、GoランタイムがTLS初期化の必要性を判断する際に、runtime/cgo
の存在を検出できるようにするためのものです。これにより、Cgoを直接使用しない場合でも、外部リンキングの要件を満たすための適切なランタイム初期化が行われます。 -
loadlib
関数内のリンキングモード決定ロジックの修正:- if(linkmode == LinkExternal && !iscgo) - linkmode = LinkInternal;
この行が削除されました。これにより、ユーザーが
-linkmode=external
を指定した際に、Cgoが使用されていないという理由でリンカが勝手にLinkInternal
に切り替える挙動がなくなりました。ユーザーの明示的な指定が尊重されるようになります。if(linkmode == LinkAuto) { if(iscgo && externalobj) linkmode = LinkExternal; else linkmode = LinkInternal; }
LinkAuto
モードでのリンキングモード決定ロジックが変更されました。以前はiscgo
の有無のみで判断していましたが、externalobj
の状態も考慮するようになりました。つまり、Cgoが使用されており (iscgo
)、かつホストコンパイラによってコンパイルされたオブジェクトファイルが存在する場合 (externalobj
) にのみLinkExternal
を選択します。それ以外の場合はLinkInternal
になります。これにより、LinkAuto
モードでの判断がより正確かつ堅牢になります。 -
ldhostobj
関数内のexternalobj
設定の修正:- if(!isinternal && linkmode == LinkAuto) - linkmode = LinkExternal; + if(!isinternal) + externalobj = 1;
ldhostobj
関数は、Goの内部パッケージではない(!isinternal
)、つまりホストコンパイラによってコンパイルされたオブジェクトファイルを処理する際に呼び出されます。変更前は、LinkAuto
モードの場合にのみlinkmode
をLinkExternal
に設定していましたが、変更後は、リンキングモードに関わらず、ホストコンパイラによってコンパイルされたオブジェクトファイルが検出されたら、無条件にexternalobj
フラグを1
に設定するようになりました。これにより、loadlib
関数が最終的なリンキングモードを決定する際に、ホストオブジェクトの存在を正確に考慮できるようになります。
これらの変更により、Goリンカは -linkmode=external
の指定を常に尊重し、外部リンキングが必要な状況で適切に動作するようになります。
関連リンク
- Go Issue #5238: cmd/ld: -linkmode=external should always do external link https://github.com/golang/go/issues/5238
- Go CL 8716044: cmd/ld: always do external link for -linkmode=external https://golang.org/cl/8716044
参考にした情報源リンク
- Go Issue #5238 の内容
- Go CL 8716044 の内容
- Go言語のリンカ (
cmd/ld
) およびビルドオプション (-linkmode
) に関する一般的な知識 - Go言語におけるCgoおよび
runtime/cgo
パッケージに関する一般的な知識 - TLS (Thread Local Storage) の概念に関する一般的な知識
- Goのソースコード (
src/cmd/ld/lib.c
) の分析 - Goのドキュメント (Go linker, cgo)