[インデックス 18366] ファイルの概要
このコミットは、Goコンパイラ(cmd/go
)がgccgo
ツールチェーンを使用してバイナリをリンクする際の挙動を修正するものです。具体的には、リンカに渡されるアーカイブファイル(.a
ファイル)の順序が原因で発生する、テストバイナリのリンク失敗問題を解決します。
コミット
commit 9f8f0a1bfa49b6b617c623f51b9d10ba9a5e4641
Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
Date: Tue Jan 28 16:47:09 2014 +1100
cmd/go: When linking with gccgo pass .a files in the order they are discovered
Under some circumstances linking a test binary with gccgo can fail, because
the installed version of the library ends up before the version built for the
test on the linker command line.
This admittedly slightly hackish fix fixes this by putting the library archives
on the linker command line in the order that a pre-order depth first traversal
of the dependencies gives them, which has the side effect of always putting the
version of the library built for the test first.
Fixes #6768
LGTM=rsc
R=golang-codereviews, minux.ma, gobot, rsc, dave
CC=golang-codereviews
https://golang.org/cl/28050043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9f8f0a1bfa49b6b617c623f51b9d10ba9a5e4641
元コミット内容
cmd/go: When linking with gccgo pass .a files in the order they are discovered
(cmd/go
: gccgo
でリンクする際、発見された順に.a
ファイルを渡す)
変更の背景
このコミットは、gccgo
ツールチェーンを使用してGoのテストバイナリをリンクする際に発生する特定の問題を解決するために導入されました。問題の根源は、リンカがライブラリを解決する順序にありました。
通常、Goプロジェクトでは、テストを実行する際に、テスト対象のパッケージとその依存関係がコンパイルされ、最終的にテストバイナリとしてリンクされます。この際、gccgo
のような外部リンカを使用する場合、リンカに渡されるライブラリの順序が重要になります。
問題が発生したのは、システムに既にインストールされているライブラリのバージョンが、テストのために特別にビルドされたライブラリのバージョンよりも先にリンカのコマンドラインに現れてしまう状況でした。リンカは通常、コマンドラインで最初に見つかったシンボル定義を使用するため、この順序の不一致が原因で、テスト用にビルドされた新しいバージョンではなく、古い(または異なる)インストール済みバージョンのライブラリが誤ってリンクされてしまい、結果としてリンクエラーや予期せぬテストの失敗を引き起こしていました。
コミットメッセージでは、この修正が「ややハック的 (slightly hackish)」であると述べられていますが、これはリンカの挙動を直接制御するのではなく、依存関係の走査順序を調整することで間接的に問題を解決しているためと考えられます。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
- Goツールチェーン (
cmd/go
): Go言語のビルド、テスト、実行などを管理するコマンドラインツールです。Goソースコードをコンパイルし、実行可能なバイナリを生成するプロセスをオーケストレーションします。 gccgo
: Go言語のフロントエンドを持つGCCコンパイラです。標準のGoコンパイラ(gc
)とは異なり、gccgo
はGCCのバックエンドを利用してGoコードをコンパイルし、C/C++のライブラリとの連携が容易であるという特徴があります。特に、Cgo(GoとCの相互運用機能)を使用するプロジェクトや、特定のシステムライブラリに依存するケースで利用されることがあります。- リンカ (Linker): コンパイラによって生成されたオブジェクトファイル(
.o
ファイル)やライブラリファイル(.a
や.so
ファイルなど)を結合し、最終的な実行可能バイナリや共有ライブラリを生成するプログラムです。リンカは、未解決のシンボル(関数や変数の参照)を、提供されたオブジェクトファイルやライブラリの中から探し出し、それらを解決します。 - アーカイブファイル (
.a
ファイル): 静的ライブラリとも呼ばれ、複数のオブジェクトファイルを一つにまとめたものです。リンカは、実行可能ファイルをビルドする際に、このアーカイブファイルから必要なオブジェクトコードを抽出し、最終的なバイナリに含めます。 - リンカのライブラリ解決順序: 多くのリンカでは、コマンドラインで指定されたライブラリの順序が、シンボル解決に影響を与えます。通常、リンカは左から右にライブラリを走査し、未解決のシンボルが見つかった場合、そのシンボルを定義している最初のライブラリを使用します。このため、同じシンボルを定義する複数のライブラリが存在する場合、コマンドラインでの順序が重要になります。
- 依存関係の深さ優先探索 (Depth-First Traversal of Dependencies): グラフ構造(この場合はパッケージの依存関係)を探索するアルゴリズムの一つです。あるノードから開始し、可能な限り深く探索を進め、行き止まりに達したらバックトラックして、まだ訪問していない隣接ノードを探索します。このコミットでは、この探索順序を利用してライブラリの順序を制御しています。
技術的詳細
このコミットの技術的な核心は、gccgo
ツールチェーンがリンカに渡すアーカイブファイル(.a
ファイル)のリストを構築する方法を変更した点にあります。
変更前は、ld
関数内でafiles
というmap[*Package]string
型のマップを使用して、パッケージとその対応するアーカイブファイルのパスを管理していました。このマップは、パッケージが処理されるたびに更新され、最終的にマップの値をイテレートしてリンカのコマンドラインに追加していました。マップのイテレーション順序は保証されないため、リンカに渡されるアーカイブファイルの順序が不安定になる可能性がありました。特に、テスト対象のパッケージのアーカイブファイルが、システムにインストールされている同じ名前のライブラリのアーカイブファイルよりも後に処理されると、リンカが誤ったバージョンを選択する問題が発生していました。
変更後、afiles
は[]string
型のスライスに変更され、afilesSeen
というmap[*Package]bool
型のマップが導入されました。
afilesSeen
: 各パッケージが既に処理され、そのアーカイブファイルがafiles
スライスに追加されたかどうかを追跡するために使用されます。afiles
: リンカに渡すアーカイブファイルのパスを、発見された順序で格納するスライスです。
allactions
(ビルドプロセス中のすべてのアクションを表す構造体のスライス)をイテレートする際に、各パッケージa.p
について以下のロジックが適用されます。
!a.p.Standard
(標準ライブラリではない場合)かつ!afilesSeen[a.p]
(まだ処理されていない場合)またはa.objpkg != a.target
(オブジェクトパッケージがターゲットと異なる場合、これは再ビルドが必要なケースなどを示唆する可能性があります)という条件が満たされた場合、そのパッケージのアーカイブファイルa.target
がafiles
スライスにappend
されます。- 同時に、
afilesSeen[a.p]
がtrue
に設定され、そのパッケージが既に処理されたことを記録します。
この変更により、パッケージの依存関係を深さ優先で走査する過程で、各パッケージのアーカイブファイルが「発見された順序」でafiles
スライスに追加されるようになります。コミットメッセージにあるように、この「発見された順序」は、テストのためにビルドされたライブラリのバージョンが、システムにインストールされているバージョンよりも常に先にリンカのコマンドラインに現れるという副作用をもたらします。これにより、リンカはテスト用の正しいライブラリバージョンを優先的に選択するようになり、リンクエラーが解消されます。
同様の変更がsfiles
(Swigによって生成された共有ライブラリのファイルパスを格納する)にも適用され、map[*Package][]string
から[]string
に型が変更され、発見された順序でファイルが追加されるようになりました。
最終的に、ldflags
スライスにafiles
とsfiles
の内容が直接append
されるようになり、以前のようにマップをイテレートする際の順序の不確実性が排除されました。
コアとなるコードの変更箇所
src/cmd/go/build.go
ファイルのgccgoToolchain
構造体のld
メソッドが変更されています。
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1817,8 +1817,9 @@ func (gccgoToolchain) pack(b *builder, p *Package, objDir, afile string, ofiles
func (tools gccgoToolchain) ld(b *builder, p *Package, out string, allactions []*action, mainpkg string, ofiles []string) error {
// gccgo needs explicit linking with all package dependencies,
// and all LDFLAGS from cgo dependencies.
- afiles := make(map[*Package]string)
- sfiles := make(map[*Package][]string)
+ afilesSeen := make(map[*Package]bool)
+ afiles := []string{}
+ sfiles := []string{}
ldflags := b.gccArchArgs()
cgoldflags := []string{}
usesCgo := false
@@ -1826,8 +1827,9 @@ func (tools gccgoToolchain) ld(b *builder, p *Package, out string, allactions []
for _, a := range allactions {
if a.p != nil {
if !a.p.Standard {
- if afiles[a.p] == "" || a.objpkg != a.target {
- afiles[a.p] = a.target
+ if !afilesSeen[a.p] || a.objpkg != a.target {
+ afilesSeen[a.p] = true
+ afiles = append(afiles, a.target)
}
}
cgoldflags = append(cgoldflags, a.p.CgoLDFLAGS...)
@@ -1841,7 +1843,7 @@ func (tools gccgoToolchain) ld(b *builder, p *Package, out string, allactions []
}
for _, f := range stringList(a.p.SwigFiles, a.p.SwigCXXFiles) {
soname := a.p.swigSoname(f)
- sfiles[a.p] = append(sfiles[a.p], filepath.Join(sd, soname))
+ sfiles = append(sfiles, filepath.Join(sd, soname))
}
usesCgo = true
}
@@ -1850,12 +1852,8 @@ func (tools gccgoToolchain) ld(b *builder, p *Package, out string, allactions []
}
}
}
- for _, afile := range afiles {
- ldflags = append(ldflags, afile)
- }
- for _, sfiles := range sfiles {
- ldflags = append(ldflags, sfiles...)
- }
+ ldflags = append(ldflags, afiles...)
+ ldflags = append(ldflags, sfiles...)
ldflags = append(ldflags, cgoldflags...)
if usesCgo && goos == "linux" {
ldflags = append(ldflags, "-Wl,-E")
コアとなるコードの解説
-
変数の型変更と初期化:
afiles
がmap[*Package]string
から[]string
に、sfiles
がmap[*Package][]string
から[]string
に変更されました。- 新たに
afilesSeen map[*Package]bool
が導入され、afiles
とsfiles
は空のスライスとして初期化されます。
-
アーカイブファイルの収集ロジックの変更:
for _, a := range allactions
ループ内で、各アクションa
(Goのビルドプロセスにおける個々のステップやパッケージを表す)が処理されます。- 以前は
afiles[a.p] = a.target
のようにマップに直接代入していましたが、変更後はif !afilesSeen[a.p] || a.objpkg != a.target
という条件が追加されました。!afilesSeen[a.p]
: このパッケージのアーカイブファイルがまだafiles
スライスに追加されていないことを確認します。これにより、同じパッケージのアーカイブファイルが複数回追加されるのを防ぎます。a.objpkg != a.target
: これは、特定のビルドシナリオ(例えば、パッケージが再ビルドされた場合など)で、たとえafilesSeen
がtrue
であっても、新しいアーカイブファイルを追加する必要があることを示唆している可能性があります。
- 条件が満たされた場合、
afilesSeen[a.p] = true
を設定し、afiles = append(afiles, a.target)
によってアーカイブファイルのパスがafiles
スライスに追加されます。これにより、アーカイブファイルが発見された順序でスライスに格納されることが保証されます。 sfiles
についても同様に、sfiles = append(sfiles, filepath.Join(sd, soname))
と変更され、マップではなくスライスに直接追加されるようになりました。
-
リンカフラグの構築:
- 変更前は、
for ... range afiles
とfor ... range sfiles
の2つのループを使って、マップから値を取り出し、ldflags
スライスに追加していました。マップのイテレーション順序は保証されないため、ここでリンカに渡されるアーカイブファイルの順序が不安定になる原因となっていました。 - 変更後、
ldflags = append(ldflags, afiles...)
とldflags = append(ldflags, sfiles...)
という簡潔な行に置き換えられました。これにより、afiles
とsfiles
スライスに格納された順序(つまり、発見された順序)で、アーカイブファイルがリンカのコマンドラインに追加されることが保証されます。
- 変更前は、
この一連の変更により、gccgo
がリンカに渡すアーカイブファイルの順序が決定論的になり、特にテストバイナリのリンク時に、テスト用にビルドされたライブラリがシステムにインストールされているライブラリよりも優先されるようになります。
関連リンク
- Goの公式Issueトラッカー(このコミットが修正したIssue #6768に関連する情報がある可能性がありますが、一般的なWeb検索では見つかりませんでした。Goの内部的なIssueトラッカーで管理されている可能性があります。)
- Go言語の
cmd/go
コマンドに関するドキュメント - GCCGoに関するドキュメント