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

[インデックス 18056] ファイルの概要

このコミットは、Go言語のリンカであるliblinkにおける、インポートファイルの検索ロジックの変更に関するものです。具体的には、絶対パスを持つインポートファイルに対して、ライブラリディレクトリ(-Lオプションで指定されるパスやGOROOT)を検索しないようにする修正です。これにより、以前の挙動が復元され、go toolを使用せずに6g(Goコンパイラ)や6l(Goリンカ)を直接使用する際に、ドットインポート(import . "package")が正しく機能するようになります。

コミット

commit 0830f64bf0a95c310b6396ea53279b17e35f82ad
Author: Ian Lance Taylor <iant@golang.org>
Date:   Wed Dec 18 10:33:47 2013 -0800

    liblink: don't search for an import file with an absolute path
    
    This restores the old behaviour, and makes it possible to
    continue to use 6g and 6l directly, rather than the go tool,
    with dot imports.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/43710043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0830f64bf0a95c310b6396ea53279b17e35f82ad

元コミット内容

liblink: don't search for an import file with an absolute path (liblink: 絶対パスを持つインポートファイルを検索しない)

This restores the old behaviour, and makes it possible to continue to use 6g and 6l directly, rather than the go tool, with dot imports. (これにより、以前の挙動が復元され、go toolを使用せずに6g6lを直接使用する際に、ドットインポートが引き続き可能になります。)

変更の背景

このコミットの背景には、Go言語のビルドシステムと、特にgo toolが登場する以前のコンパイラ・リンカの直接利用に関する問題があります。

Go言語の初期のビルドプロセスでは、6g(Goコンパイラ、6はターゲットアーキテクチャのビット数を指すことが多い、例: amd64)や6l(Goリンカ)といったコマンドラインツールを直接使用してソースコードをコンパイルし、リンクすることが一般的でした。しかし、Go 1.0以降、go toolコマンドが導入され、ビルド、テスト、依存関係管理など、Goプロジェクトのライフサイクル全体を管理するための主要なインターフェースとなりました。go toolは内部的に6g6lのような低レベルツールを呼び出しますが、その呼び出し方やパスの解決方法には独自のロジックが含まれています。

このコミット以前の特定の変更により、liblink(リンカのライブラリ部分)がインポートファイルを検索する際に、絶対パスが指定されている場合でも、ライブラリパス(-Lオプションで指定されるパスやGOROOT配下のパス)を探索するようになっていました。この挙動は、go toolを使用している場合には問題になりにくいものの、6g6lを直接呼び出すユーザーにとっては、特に「ドットインポート」(import . "some/package")を使用している場合に問題を引き起こしました。

ドットインポートは、インポートしたパッケージのエクスポートされた識別子を、パッケージ名をプレフィックスとして付けずに直接参照できるようにする機能です。例えば、import . "fmt"とすると、fmt.PrintlnではなくPrintlnと書けるようになります。この機能は、特定の状況下でコードを簡潔にするために使用されますが、名前の衝突のリスクがあるため、一般的には推奨されません。

問題は、絶対パスのインポートがライブラリパスで検索されることで、リンカが予期しない場所からファイルをロードしようとしたり、存在しないパスを検索し続けたりして、ビルドエラーやパフォーマンスの低下を引き起こす可能性があったことです。このコミットは、この「誤った」検索挙動を元に戻し、6g/6lの直接利用とドットインポートの互換性を回復することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびシステムプログラミングに関する前提知識が必要です。

  1. Go言語のビルドプロセス:

    • コンパイル: Goソースコード(.goファイル)は、6g(またはgo tool compile)によってコンパイルされ、オブジェクトファイル(.oファイル)が生成されます。これらのオブジェクトファイルには、コンパイルされたコードと、パッケージがエクスポートするシンボルに関する情報が含まれています。
    • リンク: 生成されたオブジェクトファイルと、依存するライブラリのオブジェクトファイルは、6l(またはgo tool link)によってリンクされ、実行可能ファイルまたは共有ライブラリが生成されます。リンカは、未解決のシンボル(関数呼び出しや変数参照など)を解決し、それらを適切な定義に結びつけます。
    • パッケージとインポート: Goのコードはパッケージに分割されます。import "path/to/package"構文を使用して、他のパッケージの機能を利用できます。リンカは、インポートされたパッケージのオブジェクトファイルを見つける必要があります。
  2. 6g6l:

    • Go言語の初期から存在する低レベルのコンパイラ(6g)とリンカ(6l)のコマンドです。これらは特定のアーキテクチャ(例: 6はamd64)に特化していました。
    • go toolコマンドが導入されてからは、これらのツールを直接呼び出すことは少なくなりましたが、内部的には依然として使用されています。
  3. go tool:

    • Go 1.0で導入された、Goプロジェクトのビルド、テスト、実行、依存関係管理などを統合的に行うためのコマンドラインツールです。
    • ユーザーは通常、go buildgo rungo testなどを利用し、go toolが適切なコンパイラやリンカのフラグを自動的に設定してくれます。
  4. ドットインポート (import . "package"):

    • Goのインポート構文の一つで、インポートしたパッケージのエクスポートされた識別子(関数、変数、型など)を、パッケージ名をプレフィックスとして付けずに直接参照できるようにします。
    • 例: import . "fmt" とすると、fmt.Println() の代わりに Println() と書ける。
    • コードの簡潔化に役立つ一方で、名前の衝突のリスクを高めるため、使用は慎重に行うべきとされています。
  5. パスの解決とライブラリ検索パス:

    • リンカは、インポートされたパッケージのオブジェクトファイル(通常は.aアーカイブファイル)を見つける必要があります。
    • リンカは、特定のディレクトリパスのリストを検索してこれらのファイルを見つけます。これには、現在の作業ディレクトリ、-Lオプションで指定された追加のライブラリディレクトリ、そしてGOROOT(Goのインストールディレクトリ)配下の標準ライブラリパスなどが含まれます。
    • 絶対パス: ファイルシステム上のルートディレクトリからの完全なパス(例: /usr/local/go/pkg/linux_amd64/fmt.a)。
    • 相対パス: 現在の作業ディレクトリからの相対的なパス(例: ./my_package.a)。
  6. access()システムコールとAEXIST:

    • access()は、指定されたファイルまたはディレクトリへのユーザーのアクセス権限をチェックするPOSIXシステムコールです。
    • AEXISTは、access()に渡されるフラグの一つで、ファイルまたはディレクトリが存在するかどうかをチェックするために使用されます。このコミットの文脈では、ファイルが存在するかどうかを確認するために使われています。
  7. snprintf()cleanname():

    • snprintf()は、C言語の標準ライブラリ関数で、書式付きの文字列を指定されたバッファに安全に書き込むために使用されます。バッファオーバーフローを防ぐために、書き込む最大バイト数を指定できます。
    • cleanname()は、Goの内部リンカコードで使用される可能性のあるユーティリティ関数で、パス文字列を正規化(例: ...の解決、スラッシュの重複除去など)するために使用されます。

技術的詳細

このコミットは、src/liblink/ld.cファイル内のaddlib関数に対する変更です。addlib関数は、リンカが依存するライブラリ(インポートされたパッケージのオブジェクトファイル)を検索し、追加する役割を担っています。

変更前のコードでは、name(インポートされるパッケージのファイル名、例: fmt.a)が与えられた際に、まず現在のディレクトリ(ドット)を試み、次にctxt->libdir-Lオプションで指定されたライブラリパスやGOROOT配下のパス)を順番に検索していました。

変更の核心は、nameが絶対パスであるかどうかをチェックする条件分岐が追加されたことです。

if((!ctxt->windows && name[0] == '/') || (ctxt->windows && name[1] == ':'))

この条件は、nameが絶対パスであるかを判定しています。

  • !ctxt->windows && name[0] == '/': Windows以外のシステム(Linux, macOSなど)で、パスが/で始まる場合(Unix系絶対パス)。
  • ctxt->windows && name[1] == ':': Windowsシステムで、パスがドライブレター(例: C:)で始まる場合(Windows絶対パス)。

もしnameが絶対パスであると判断された場合、リンカはctxt->libdirの検索ループをスキップし、直接その絶対パスをpnameにコピーします。

		snprint(pname, sizeof pname, "%s", name);

これにより、絶対パスが指定されているにもかかわらず、リンカが余計なライブラリディレクトリを検索しようとする挙動が抑制されます。絶対パスはそれ自体が完全な場所を示しているため、追加の検索は不要であり、むしろ誤ったファイルを見つけたり、無駄なI/O操作を行ったりする原因となります。

一方、nameが相対パスである場合(上記のif条件が偽の場合)、従来の検索ロジックが実行されます。

	else {
		// try dot, -L "libdir", and then goroot.
		for(i=0; i<ctxt->nlibdir; i++) {
			snprint(pname, sizeof pname, "%s/%s", ctxt->libdir[i], name);
			if(access(pname, AEXIST) >= 0)
				break;
		}
	}

この修正により、リンカのパス解決ロジックがより正確になり、特にgo toolを介さずに6g/6lを直接使用するシナリオにおいて、絶対パスのインポートが意図通りに扱われるようになります。これにより、ドットインポートを含む特定のビルド構成が正しく機能するようになります。

コアとなるコードの変更箇所

変更はsrc/liblink/ld.cファイルのaddlib関数内で行われています。

--- a/src/liblink/ld.c
+++ b/src/liblink/ld.c
@@ -61,11 +61,15 @@ addlib(Link *ctxt, char *src, char *obj, char *pathname)
 	if(p != nil)
 		*p = '.';
 
-	// try dot, -L "libdir", and then goroot.
-	for(i=0; i<ctxt->nlibdir; i++) {
-		snprint(pname, sizeof pname, "%s/%s", ctxt->libdir[i], name);
-		if(access(pname, AEXIST) >= 0)
-			break;
+	if((!ctxt->windows && name[0] == '/') || (ctxt->windows && name[1] == ':'))
+		snprint(pname, sizeof pname, "%s", name);
+	else {
+		// try dot, -L "libdir", and then goroot.
+		for(i=0; i<ctxt->nlibdir; i++) {
+			snprint(pname, sizeof pname, "%s/%s", ctxt->libdir[i], name);
+			if(access(pname, AEXIST) >= 0)
+				break;
+		}
 	}
 	cleanname(pname);
 

コアとなるコードの解説

変更の中心は、addlib関数内のインポートファイル検索ロジックです。

  1. パスの正規化(変更前からの既存コード):

    if(p != nil)
    	*p = '.';
    

    この部分は、おそらくインポートされるパッケージ名からファイル名(例: fmt.a)を生成する過程で、パスの区切り文字を.に置き換えるなどの処理を行っていると推測されます。

  2. 絶対パスの判定と処理(追加されたコード):

    if((!ctxt->windows && name[0] == '/') || (ctxt->windows && name[1] == ':'))
    	snprint(pname, sizeof pname, "%s", name);
    
    • ctxt->windows: リンカがWindows環境で動作しているかを示すコンテキスト変数。
    • name[0] == '/': Unix系OS(Linux, macOSなど)における絶対パスの開始文字。
    • name[1] == ':': Windowsにおけるドライブレター(例: C:)の後のコロン。name[0]はドライブレター自体(例: C)。
    • この条件式は、nameがUnix系の絶対パス(/path/to/file)またはWindows系の絶対パス(C:/path/to/file)であるかを判定します。
    • もし絶対パスであれば、snprint(pname, sizeof pname, "%s", name);によって、nameの内容がそのままpname(最終的なファイルパスを格納するバッファ)にコピーされます。これにより、絶対パスが指定されている場合は、それ以上の検索は行われません。
  3. 相対パスの検索ロジック(既存コードをelseブロックに移動):

    else {
    	// try dot, -L "libdir", and then goroot.
    	for(i=0; i<ctxt->nlibdir; i++) {
    		snprint(pname, sizeof pname, "%s/%s", ctxt->libdir[i], name);
    		if(access(pname, AEXIST) >= 0)
    			break;
    	}
    }
    
    • elseブロックは、nameが相対パスである場合に実行されます。
    • forループは、リンカが設定されたライブラリディレクトリ(ctxt->libdir)を順番に探索します。これには、現在のディレクトリ(ドット)、-Lオプションで指定されたパス、およびGOROOT配下の標準ライブラリパスが含まれます。
    • snprint(pname, sizeof pname, "%s/%s", ctxt->libdir[i], name);は、各ライブラリディレクトリとnameを結合して完全なパスを構築します。
    • access(pname, AEXIST) >= 0は、構築されたパスにファイルが存在するかどうかをチェックします。ファイルが見つかれば、ループはbreakで終了します。
  4. パスのクリーンアップ(変更前からの既存コード):

    cleanname(pname);
    

    pnameに格納されたパス文字列を正規化します。例えば、/a/./b/a/bに、/a/../b/bに変換するなどの処理を行います。

この修正により、リンカは絶対パスのインポートに対しては余計な検索を行わず、相対パスのインポートに対してのみ設定されたライブラリパスを検索するという、より論理的で効率的な挙動をするようになりました。これは、特にgo toolの抽象化レイヤーを介さずに低レベルのコンパイラ/リンカを直接使用する開発者にとって重要でした。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/cmd/linkディレクトリやsrc/liblinkディレクトリ)
  • Go言語のIssueトラッカーやメーリングリスト(golang-devなど)での議論
  • C言語のaccess()snprintf()などの標準ライブラリ関数のドキュメント
  • Go言語のビルドシステムに関する技術ブログや解説記事