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

[インデックス 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リンカ(通常は gccclang)に委ねます。これは、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 関数におけるリンキングモードの決定ロジックの変更にあります。

変更前の問題点

変更前のリンカの挙動は以下の通りでした。

  1. linkmode == LinkExternal && !iscgo の条件: loadlib 関数内で、もしユーザーが -linkmode=external を明示的に指定し、かつGoコード内にCgoの利用を示す iscgo フラグが立っていない場合、リンカは linkmodeLinkInternal に上書きしていました。これは、「Cgoを使っていないなら、外部リンキングは不要だろう」というリンカの推測に基づくものでしたが、ユーザーが明示的に外部リンキングを要求している意図を無視していました。

  2. linkmode == LinkAuto の条件: linkmode == LinkAuto の場合、リンカは iscgo フラグに基づいて LinkExternalLinkInternal を決定していました。しかし、externalobj という新しいフラグが導入される前は、ホストコンパイラによってコンパイルされたオブジェクトファイル(Go以外の言語で書かれたもの)が存在する場合でも、iscgofalse であれば LinkInternal にフォールバックする可能性がありました。

  3. ldhostobj 関数における externalobj の欠如: ldhostobj 関数は、ホストコンパイラ(Goコンパイラではない、例えばGCCなど)によってコンパイルされたオブジェクトファイルをロードする際に呼び出されます。変更前は、この関数内で isinternal (Goの内部パッケージからのオブジェクトかどうか) が false であり、かつ linkmode == LinkAuto の場合に linkmodeLinkExternal に設定していました。しかし、これは LinkAuto モードに限定されており、ユーザーが明示的に -linkmode=external を指定した場合の挙動を保証するものではありませんでした。

これらの問題により、ユーザーが -linkmode=external を指定しても、リンカが内部リンキングに切り替えてしまい、外部リンキングに依存するビルドが失敗したり、予期せぬ挙動を示したりする可能性がありました。

変更による解決策

このコミットは、以下の変更によって問題を解決します。

  1. externalobj グローバル変数の導入: static int externalobj = 0; という新しいグローバル変数が導入されました。この変数は、ホストコンパイラによってコンパイルされたオブジェクトファイル(Goの内部リンキングモードをサポートしないパッケージからのものではない)が検出された場合に 1 に設定されます。

  2. 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初期化を行うように促します。

  3. loadlib 関数におけるリンキングモード決定ロジックの修正: 変更前の if(linkmode == LinkExternal && !iscgo) linkmode = LinkInternal; という行が削除されました。これにより、ユーザーが -linkmode=external を指定した場合に、リンカが勝手に LinkInternal に切り替えることがなくなりました。 if(linkmode == LinkAuto) のブロックも修正されました。 if(iscgo && externalobj) の条件が追加され、Cgoが使用されており、かつホストコンパイラによってコンパイルされたオブジェクトファイルが存在する場合にのみ LinkExternal を選択するように変更されました。それ以外の場合は LinkInternal になります。これにより、LinkAuto モードでの判断がより正確になります。

  4. 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 の変更点

  1. externalobj 変数の追加:

    static int	externalobj = 0;
    

    externalobj は、Goの内部リンキングモードをサポートしない、ホストコンパイラによってコンパイルされたオブジェクトファイルが存在するかどうかを示すフラグです。これが 1 に設定されると、外部リンキングが必要である可能性が高まります。

  2. 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を直接使用しない場合でも、外部リンキングの要件を満たすための適切なランタイム初期化が行われます。

  3. 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 モードでの判断がより正確かつ堅牢になります。

  4. ldhostobj 関数内の externalobj 設定の修正:

    -	if(!isinternal && linkmode == LinkAuto)
    -		linkmode = LinkExternal;
    +	if(!isinternal)
    +		externalobj = 1;
    

    ldhostobj 関数は、Goの内部パッケージではない(!isinternal)、つまりホストコンパイラによってコンパイルされたオブジェクトファイルを処理する際に呼び出されます。変更前は、LinkAuto モードの場合にのみ linkmodeLinkExternal に設定していましたが、変更後は、リンキングモードに関わらず、ホストコンパイラによってコンパイルされたオブジェクトファイルが検出されたら、無条件に externalobj フラグを 1 に設定するようになりました。これにより、loadlib 関数が最終的なリンキングモードを決定する際に、ホストオブジェクトの存在を正確に考慮できるようになります。

これらの変更により、Goリンカは -linkmode=external の指定を常に尊重し、外部リンキングが必要な状況で適切に動作するようになります。

関連リンク

参考にした情報源リンク

  • 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)