[インデックス 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
個)で定義されていました。
この固定サイズという制約は、特に以下のようなシナリオで問題を引き起こす可能性がありました。
- 多数の外部ライブラリやモジュール: プロジェクトが多数の外部ライブラリやモジュールに依存している場合、それぞれが異なるパスに配置されていると、
-L
オプションで指定する必要があるパスの数が16を超えることがありました。 - 複雑なビルド環境: ビルドシステムやCI/CDパイプラインが、一時的なディレクトリやキャッシュディレクトリなど、動的に生成される多数のパスをライブラリ検索パスに追加する必要がある場合。
- モノレポ構造: 大規模なモノレポ(単一のリポジトリで複数のプロジェクトを管理する構造)では、異なるサブプロジェクトのビルド成果物が多数の異なるパスに分散していることがあり、これらすべてをリンカに認識させる必要がありました。
このような状況で-L
オプションの数が16を超えると、リンカは「too many -L's」というエラーを出力し、ビルドが失敗していました。このコミットは、この固定サイズの制限を取り除き、リンカがより多くのライブラリ検索パスを柔軟に扱えるようにするために導入されました。
コミットメッセージにある「Enhances CL 5727043」は、この変更が以前の関連する変更(Change List)をさらに改善するものであることを示唆しています。CL 5727043は、おそらくlibdir
配列のサイズを増やしたり、関連する問題を部分的に解決しようとしたものと推測されますが、このコミットは根本的な解決策として動的割り当てを導入しています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。
-
リンカ (Linker):
- リンカは、コンパイラによって生成されたオブジェクトファイル(機械語コードとデータを含むファイル)を結合し、実行可能なプログラムやライブラリを作成するソフトウェアツールです。
- Go言語の場合、
go build
コマンドの内部でGoコンパイラとリンカが連携して動作します。 - リンカの主な役割は、プログラム内の未解決のシンボル(例えば、別のファイルで定義された関数や変数)を解決し、それらが実際にメモリ上のどこに配置されるかを決定することです。
-L
オプションは、リンカがライブラリを検索するディレクトリを指定するために使用されます。例えば、-L/path/to/mylib
と指定すると、リンカは/path/to/mylib
ディレクトリ内もライブラリを探しに行きます。
-
動的メモリ割り当て (Dynamic Memory Allocation):
- プログラムの実行中に必要なメモリ量を決定し、その場でメモリを確保する手法です。C言語では
malloc
、calloc
、realloc
、free
などの関数がこれにあたります。 - 対照的に、静的メモリ割り当ては、プログラムのコンパイル時にメモリサイズが固定される方法です(例:
char array[16];
)。 - 動的メモリ割り当ての利点は、必要なメモリ量を柔軟に調整できるため、メモリの無駄を減らし、固定サイズの制限による問題を回避できる点です。
- プログラムの実行中に必要なメモリ量を決定し、その場でメモリを確保する手法です。C言語では
-
C言語のポインタと配列:
- Go言語のリンカは、C言語で実装されています。C言語では、配列はメモリ上の連続した領域であり、ポインタはメモリアドレスを指す変数です。
char* libdir[16];
は、16個のchar*
(文字列へのポインタ)を格納できる固定サイズの配列を宣言しています。char** libdir;
は、char*
へのポインタ、つまり文字列へのポインタの配列を指すポインタを宣言しています。これにより、libdir
自体が指すメモリ領域を動的に変更(realloc
など)できるようになります。
-
realloc
関数:- C言語の標準ライブラリ関数で、既に割り当てられているメモリブロックのサイズを変更するために使用されます。
- 新しいサイズが元のサイズより大きい場合、
realloc
は既存のデータを保持したまま、より大きなメモリブロックを割り当て、古いブロックを解放します。 - メモリの再割り当てに失敗した場合(メモリ不足など)、
NULL
を返します。
-
nil
(GoにおけるNULL
):- Go言語では、ポインタ、スライス、マップ、チャネル、インターフェースなどのゼロ値は
nil
です。C言語のNULL
ポインタに相当します。メモリ割り当てが失敗した場合などに返されます。
- Go言語では、ポインタ、スライス、マップ、チャネル、インターフェースなどのゼロ値は
技術的詳細
このコミットの技術的な核心は、Goリンカのlibdir
配列の管理方法を、静的割り当てから動的割り当てへと変更した点にあります。
変更前は、src/cmd/ld/lib.c
内でchar* libdir[16];
と宣言されており、libdir
は最大16個のライブラリディレクトリパスを格納できる固定サイズの配列でした。Lflag
関数(-L
オプションの処理を担当)は、nlibdir
(現在登録されているディレクトリの数)がnelem(libdir)-1
(配列の最大インデックス)に達すると、「too many -L's」エラーを出力して終了していました。
変更後、以下の点が変更されました。
-
libdir
の型変更:src/cmd/ld/lib.c
でchar* libdir[16];
からchar** libdir;
に変更されました。これにより、libdir
は文字列ポインタの配列を指すポインタとなり、その配列自体を動的にリサイズできるようになります。src/cmd/ld/lib.h
でも、extern char *libdir[];
からextern char **libdir;
に変更され、外部宣言も新しい型に合わせられました。
-
maxlibdir
変数の導入:static int maxlibdir = 0;
が追加されました。これは、現在libdir
に割り当てられているメモリブロックが保持できるディレクトリパスの最大数を追跡するための変数です。
-
Lflag
関数のロジック変更:Lflag
関数内で、nlibdir
がmaxlibdir
以上になった場合に、libdir
配列のサイズを動的に拡張するロジックが追加されました。- 初期割り当て:
maxlibdir
が0
の場合(初回呼び出し時)、maxlibdir
は8
に初期化されます。これは、最初の割り当てサイズを小さく保ちつつ、ある程度の余裕を持たせるためのヒューリスティックです。 - 倍々での拡張:
maxlibdir
が0
でない場合、maxlibdir
は現在の値の2倍に拡張されます。これは、一般的に動的配列の実装で用いられる効率的な拡張戦略です。これにより、頻繁なrealloc
呼び出しを避けつつ、必要に応じてメモリを確保できます。 realloc
の使用:realloc(libdir, maxlibdir)
を呼び出して、libdir
が指すメモリブロックのサイズをmaxlibdir
が示す新しいサイズに拡張します。realloc
は、新しいメモリブロックへのポインタを返します。- エラーハンドリング:
realloc
がnil
(メモリ割り当て失敗)を返した場合、「too many -L's」エラーを出力して終了します。これは、システムメモリが枯渇した場合の安全策です。 - ポインタの更新:
realloc
が成功した場合、返された新しいポインタp
をlibdir
に代入し、libdir
が新しいメモリブロックを指すように更新します。
-
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;
に変更されました。これにより、libdir
はchar*
(文字列ポインタ)の配列を指すポインタとなり、この配列が指すメモリ領域を動的に変更できるようになります。
- 以前は
-
static int maxlibdir = 0;
:- 新しく導入された変数で、現在
libdir
に割り当てられているメモリブロックが保持できるライブラリディレクトリパスの最大数を追跡します。初期値は0
です。
- 新しく導入された変数で、現在
-
Lflag(char *arg)
関数内の変更:if(nlibdir >= maxlibdir)
: 現在登録されているディレクトリ数nlibdir
が、割り当て済みの最大数maxlibdir
以上になった場合に、メモリを拡張する必要があると判断します。if (maxlibdir == 0) maxlibdir = 8; else maxlibdir *= 2;
:maxlibdir
が0
の場合(リンカ起動後、最初の-L
オプション処理時)、maxlibdir
を8
に設定します。これは、最初のメモリ割り当てを小さく保ちつつ、ある程度の余裕を持たせるための初期サイズです。maxlibdir
が0
でない場合、maxlibdir
を2倍にします。これは、動的配列の一般的な拡張戦略で、メモリ再割り当ての頻度を減らし、効率を向上させます。
p = realloc(libdir, maxlibdir);
:realloc
関数を呼び出し、libdir
が現在指しているメモリブロックのサイズをmaxlibdir
が示す新しいサイズに拡張します。realloc
は、新しいメモリブロックの先頭アドレスを返します。
if (p == nil) { ... usage(); }
:realloc
がnil
を返した場合、メモリ割り当てに失敗したことを意味します。この場合、「too many -L's」というエラーメッセージを出力し、プログラムの使用法を表示して終了します。これは、システムメモリが枯渇した場合の堅牢性を提供します。
libdir = p;
:realloc
が成功した場合、libdir
ポインタをp
(新しいメモリブロックの先頭アドレス)に更新します。これにより、libdir
は拡張されたメモリ領域を指すようになります。
libdir[nlibdir++] = arg;
:- 新しいライブラリパス
arg
をlibdir
配列の次の空きスロットに格納し、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
関数に関するドキュメント
参考にした情報源リンク
- Go CL 5731043 (このコミットのChange Listページ)
- Go CL 5727043 (このコミットが改善したとされるChange Listページ)
- C言語
realloc
のドキュメント (例: https://www.cplusplus.com/reference/cstdlib/realloc/) - Go言語のリンカ
cmd/ld
のソースコード (GoのGitHubリポジトリ内) - Go言語のビルドプロセスに関する一般的な情報源