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

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

このコミットは、Go言語のリンカ(ld)におけるライブラリ検索パスの指定方法に関する改善です。具体的には、-Lオプションで指定できるライブラリディレクトリの数に固定の制限があった問題を解決し、動的にメモリを割り当てることで、より多くのライブラリパスを指定できるように変更しています。これにより、大規模なプロジェクトや複雑な依存関係を持つGoプログラムのビルドにおける柔軟性が向上しました。

コミット

commit 5aea33742a946b177590d44e6942ff781a18f111
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Sat Mar 3 04:14:31 2012 +0800

    ld: allow more -L options
            Dynamically allocate the libdir array, so we won't need to bother it again.
            Enhances CL 5727043.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5731043

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

https://github.com/golang/go/commit/5aea33742a946b177590d44e6942ff781a18f111

元コミット内容

このコミットの元の内容は以下の通りです。

「ld: allow more -L options libdir配列を動的に割り当てることで、今後この問題に悩まされることがなくなる。 CL 5727043を改善する。」

変更の背景

Go言語のリンカ(ld)は、コンパイルされたGoプログラムが依存するライブラリを見つけるために、特定のディレクトリを検索します。これらの検索パスは、リンカの-Lオプションを使用して指定できます。しかし、このコミット以前は、ライブラリディレクトリのパスを格納するlibdir配列が固定サイズ(16個)で定義されていました。

この固定サイズという制約は、特に以下のようなシナリオで問題を引き起こす可能性がありました。

  1. 多数の外部ライブラリやモジュール: プロジェクトが多数の外部ライブラリやモジュールに依存している場合、それぞれが異なるパスに配置されていると、-Lオプションで指定する必要があるパスの数が16を超えることがありました。
  2. 複雑なビルド環境: ビルドシステムやCI/CDパイプラインが、一時的なディレクトリやキャッシュディレクトリなど、動的に生成される多数のパスをライブラリ検索パスに追加する必要がある場合。
  3. モノレポ構造: 大規模なモノレポ(単一のリポジトリで複数のプロジェクトを管理する構造)では、異なるサブプロジェクトのビルド成果物が多数の異なるパスに分散していることがあり、これらすべてをリンカに認識させる必要がありました。

このような状況で-Lオプションの数が16を超えると、リンカは「too many -L's」というエラーを出力し、ビルドが失敗していました。このコミットは、この固定サイズの制限を取り除き、リンカがより多くのライブラリ検索パスを柔軟に扱えるようにするために導入されました。

コミットメッセージにある「Enhances CL 5727043」は、この変更が以前の関連する変更(Change List)をさらに改善するものであることを示唆しています。CL 5727043は、おそらくlibdir配列のサイズを増やしたり、関連する問題を部分的に解決しようとしたものと推測されますが、このコミットは根本的な解決策として動的割り当てを導入しています。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。

  1. リンカ (Linker):

    • リンカは、コンパイラによって生成されたオブジェクトファイル(機械語コードとデータを含むファイル)を結合し、実行可能なプログラムやライブラリを作成するソフトウェアツールです。
    • Go言語の場合、go buildコマンドの内部でGoコンパイラとリンカが連携して動作します。
    • リンカの主な役割は、プログラム内の未解決のシンボル(例えば、別のファイルで定義された関数や変数)を解決し、それらが実際にメモリ上のどこに配置されるかを決定することです。
    • -Lオプションは、リンカがライブラリを検索するディレクトリを指定するために使用されます。例えば、-L/path/to/mylibと指定すると、リンカは/path/to/mylibディレクトリ内もライブラリを探しに行きます。
  2. 動的メモリ割り当て (Dynamic Memory Allocation):

    • プログラムの実行中に必要なメモリ量を決定し、その場でメモリを確保する手法です。C言語ではmalloccallocreallocfreeなどの関数がこれにあたります。
    • 対照的に、静的メモリ割り当ては、プログラムのコンパイル時にメモリサイズが固定される方法です(例: char array[16];)。
    • 動的メモリ割り当ての利点は、必要なメモリ量を柔軟に調整できるため、メモリの無駄を減らし、固定サイズの制限による問題を回避できる点です。
  3. C言語のポインタと配列:

    • Go言語のリンカは、C言語で実装されています。C言語では、配列はメモリ上の連続した領域であり、ポインタはメモリアドレスを指す変数です。
    • char* libdir[16]; は、16個のchar*(文字列へのポインタ)を格納できる固定サイズの配列を宣言しています。
    • char** libdir; は、char*へのポインタ、つまり文字列へのポインタの配列を指すポインタを宣言しています。これにより、libdir自体が指すメモリ領域を動的に変更(reallocなど)できるようになります。
  4. realloc関数:

    • C言語の標準ライブラリ関数で、既に割り当てられているメモリブロックのサイズを変更するために使用されます。
    • 新しいサイズが元のサイズより大きい場合、reallocは既存のデータを保持したまま、より大きなメモリブロックを割り当て、古いブロックを解放します。
    • メモリの再割り当てに失敗した場合(メモリ不足など)、NULLを返します。
  5. nil (GoにおけるNULL):

    • Go言語では、ポインタ、スライス、マップ、チャネル、インターフェースなどのゼロ値はnilです。C言語のNULLポインタに相当します。メモリ割り当てが失敗した場合などに返されます。

技術的詳細

このコミットの技術的な核心は、Goリンカのlibdir配列の管理方法を、静的割り当てから動的割り当てへと変更した点にあります。

変更前は、src/cmd/ld/lib.c内でchar* libdir[16];と宣言されており、libdirは最大16個のライブラリディレクトリパスを格納できる固定サイズの配列でした。Lflag関数(-Lオプションの処理を担当)は、nlibdir(現在登録されているディレクトリの数)がnelem(libdir)-1(配列の最大インデックス)に達すると、「too many -L's」エラーを出力して終了していました。

変更後、以下の点が変更されました。

  1. libdirの型変更:

    • src/cmd/ld/lib.cchar* libdir[16];からchar** libdir;に変更されました。これにより、libdirは文字列ポインタの配列を指すポインタとなり、その配列自体を動的にリサイズできるようになります。
    • src/cmd/ld/lib.hでも、extern char *libdir[];からextern char **libdir;に変更され、外部宣言も新しい型に合わせられました。
  2. maxlibdir変数の導入:

    • static int maxlibdir = 0;が追加されました。これは、現在libdirに割り当てられているメモリブロックが保持できるディレクトリパスの最大数を追跡するための変数です。
  3. Lflag関数のロジック変更:

    • Lflag関数内で、nlibdirmaxlibdir以上になった場合に、libdir配列のサイズを動的に拡張するロジックが追加されました。
    • 初期割り当て: maxlibdir0の場合(初回呼び出し時)、maxlibdir8に初期化されます。これは、最初の割り当てサイズを小さく保ちつつ、ある程度の余裕を持たせるためのヒューリスティックです。
    • 倍々での拡張: maxlibdir0でない場合、maxlibdirは現在の値の2倍に拡張されます。これは、一般的に動的配列の実装で用いられる効率的な拡張戦略です。これにより、頻繁なrealloc呼び出しを避けつつ、必要に応じてメモリを確保できます。
    • reallocの使用: realloc(libdir, maxlibdir)を呼び出して、libdirが指すメモリブロックのサイズをmaxlibdirが示す新しいサイズに拡張します。reallocは、新しいメモリブロックへのポインタを返します。
    • エラーハンドリング: reallocnil(メモリ割り当て失敗)を返した場合、「too many -L's」エラーを出力して終了します。これは、システムメモリが枯渇した場合の安全策です。
    • ポインタの更新: reallocが成功した場合、返された新しいポインタplibdirに代入し、libdirが新しいメモリブロックを指すように更新します。
  4. libinit関数の変更:

    • libinit関数内で、goroot(Goのインストールルートディレクトリ)のpkgディレクトリパスをlibdirに追加する部分が変更されました。
    • 変更前はlibdir[nlibdir++] = smprint(...)と直接配列に代入していましたが、変更後はLflag(smprint(...))Lflag関数を呼び出すように変更されました。これにより、gorootのパスも動的割り当てのロジックを通じて追加されるようになり、一貫性が保たれます。

この変更により、リンカは-Lオプションで指定されるライブラリディレクトリの数に事実上無限の(システムメモリが許す限り)対応できるようになりました。

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

src/cmd/ld/lib.c

--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -39,8 +39,9 @@ int iconv(Fmt*);
 
 char	symname[]	= SYMDEF;
 char	pkgname[]	= "__.PKGDEF";
-char*	libdir[16];
+char**	libdir;
 int	nlibdir = 0;
+static int	maxlibdir = 0;
 static int	cout = -1;
 
 char*	goroot;
@@ -51,9 +52,19 @@ char*	theline;
 void
 Lflag(char *arg)
 {
-	if(nlibdir >= nelem(libdir)-1) {
-		print("too many -L's: %d\n", nlibdir);
-		usage();
+	char **p;
+
+	if(nlibdir >= maxlibdir) {
+		if (maxlibdir == 0)
+			maxlibdir = 8;
+		else
+			maxlibdir *= 2;
+		p = realloc(libdir, maxlibdir);
+		if (p == nil) {
+			print("too many -L's: %d\n", nlibdir);
+			usage();
+		}
+		libdir = p;
 	}
 	libdir[nlibdir++] = arg;
 }
@@ -69,7 +80,7 @@ libinit(void)
 		print("goarch is not known: %s\\n", goarch);
 
 	// add goroot to the end of the libdir list.
-	libdir[nlibdir++] = smprint("%s/pkg/%s_%s", goroot, goos, goarch);
+	Lflag(smprint("%s/pkg/%s_%s", goroot, goos, goarch));
 
 	// Unix doesn't like it when we write to a running (or, sometimes,
 	// recently run) binary, so remove the output file before writing it.

src/cmd/ld/lib.h

--- a/src/cmd/ld/lib.h
+++ b/src/cmd/ld/lib.h
@@ -103,7 +103,7 @@ struct Section
 };
 
 extern	char	symname[];
-extern	char	*libdir[];
+extern	char	**libdir;
 extern	int	nlibdir;
 
 EXTERN	char*	INITENTRY;

コアとなるコードの解説

src/cmd/ld/lib.c

  • char** libdir;:

    • 以前はchar* libdir[16];という固定サイズの配列でしたが、char** libdir;に変更されました。これにより、libdirchar*(文字列ポインタ)の配列を指すポインタとなり、この配列が指すメモリ領域を動的に変更できるようになります。
  • static int maxlibdir = 0;:

    • 新しく導入された変数で、現在libdirに割り当てられているメモリブロックが保持できるライブラリディレクトリパスの最大数を追跡します。初期値は0です。
  • Lflag(char *arg)関数内の変更:

    • if(nlibdir >= maxlibdir): 現在登録されているディレクトリ数nlibdirが、割り当て済みの最大数maxlibdir以上になった場合に、メモリを拡張する必要があると判断します。
    • if (maxlibdir == 0) maxlibdir = 8; else maxlibdir *= 2;:
      • maxlibdir0の場合(リンカ起動後、最初の-Lオプション処理時)、maxlibdir8に設定します。これは、最初のメモリ割り当てを小さく保ちつつ、ある程度の余裕を持たせるための初期サイズです。
      • maxlibdir0でない場合、maxlibdirを2倍にします。これは、動的配列の一般的な拡張戦略で、メモリ再割り当ての頻度を減らし、効率を向上させます。
    • p = realloc(libdir, maxlibdir);:
      • realloc関数を呼び出し、libdirが現在指しているメモリブロックのサイズをmaxlibdirが示す新しいサイズに拡張します。reallocは、新しいメモリブロックの先頭アドレスを返します。
    • if (p == nil) { ... usage(); }:
      • reallocnilを返した場合、メモリ割り当てに失敗したことを意味します。この場合、「too many -L's」というエラーメッセージを出力し、プログラムの使用法を表示して終了します。これは、システムメモリが枯渇した場合の堅牢性を提供します。
    • libdir = p;:
      • reallocが成功した場合、libdirポインタをp(新しいメモリブロックの先頭アドレス)に更新します。これにより、libdirは拡張されたメモリ領域を指すようになります。
    • libdir[nlibdir++] = arg;:
      • 新しいライブラリパスarglibdir配列の次の空きスロットに格納し、nlibdirをインクリメントします。この部分は変更前と同じロジックですが、libdirが動的に拡張されるようになったため、固定サイズの制限を受けなくなりました。
  • libinit()関数内の変更:

    • libdir[nlibdir++] = smprint("%s/pkg/%s_%s", goroot, goos, goarch);からLflag(smprint("%s/pkg/%s_%s", goroot, goos, goarch));に変更されました。
    • これは、Goの標準ライブラリパス($GOROOT/pkg/...)も、-Lオプションで指定されるパスと同様に、Lflag関数を通じて追加されるようにしたものです。これにより、すべてのライブラリパスの追加が一貫した動的割り当てロジックに従うようになります。

src/cmd/ld/lib.h

  • extern char **libdir;:
    • libdir変数の外部宣言も、lib.cでの型変更に合わせてchar* *に変更されました。これにより、他のファイルからlibdirを参照する際に正しい型情報が提供されます。

これらの変更により、Goリンカは-Lオプションで指定できるライブラリディレクトリの数に固定の制限がなくなり、より柔軟なビルド環境に対応できるようになりました。

関連リンク

  • Go言語のリンカに関するドキュメント(公式ドキュメントや関連する設計ドキュメントがあればここに記載)
  • Go言語のChange List (CL) システムに関する情報
  • C言語のrealloc関数に関するドキュメント

参考にした情報源リンク