[インデックス 12339] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) におけるメモリ割り当てのバグを修正するものです。具体的には、realloc
関数を使用する際に、確保すべきメモリサイズが誤って計算されていた問題を修正し、FreeBSD/amd64環境でのビルドが正しく行われるようにします。この修正により、リンカがライブラリパスを格納するためのメモリを適切に確保できるようになり、メモリ破損やビルドエラーを防ぎます。
コミット
commit a142ed99d525d93b91f9f3ea6ef9b7e03a1a88ae
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat Mar 3 04:47:42 2012 +0800
fix build for FreeBSD/amd64
R=rsc, golang-dev, iant
CC=golang-dev
https://golang.org/cl/5732043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a142ed99d525d93b91f9f3ea6ef9b7e03a1a88ae
元コミット内容
fix build for FreeBSD/amd64
R=rsc, golang-dev, iant
CC=golang-dev
https://golang.org/cl/5732043
変更の背景
この変更は、Go言語のビルドプロセスがFreeBSD/amd64アーキテクチャで失敗する問題を解決するために行われました。問題の根本原因は、Goリンカ (cmd/ld
) がライブラリ検索パスを管理するために動的にメモリを再割り当てする際に、必要なメモリサイズを誤って計算していたことにあります。
具体的には、realloc
関数が、確保すべき要素数ではなくバイト数で直接指定されていたため、ポインタのサイズが8バイトである64ビットシステム(amd64)では、確保されるメモリが不足していました。このメモリ不足は、リンカがより多くのライブラリパスを処理しようとした際に、メモリの境界外書き込み(out-of-bounds write)やメモリ破損を引き起こし、結果としてビルドエラーにつながっていました。
FreeBSD/amd64環境でこの問題が顕在化したのは、特定のメモリ割り当てパターンや、システムコール、またはリンカの動作の違いが、この潜在的なバグを露呈させたためと考えられます。この修正は、クロスプラットフォーム対応を強化し、Go言語が様々なオペレーティングシステムやアーキテクチャで安定して動作することを保証する上で重要でした。
前提知識の解説
1. realloc
関数
realloc
はC標準ライブラリの関数で、既に割り当てられているメモリブロックのサイズを変更するために使用されます。そのプロトタイプは通常以下のようになります。
void *realloc(void *ptr, size_t size);
ptr
: 以前にmalloc
,calloc
, またはrealloc
によって割り当てられたメモリブロックへのポインタ。NULL
の場合、malloc(size)
と同様に新しいメモリブロックを割り当てます。size
: 新しいメモリブロックのサイズ(バイト単位)。
realloc
は、新しいメモリブロックへのポインタを返します。メモリの再割り当てに失敗した場合(例えば、メモリが不足している場合)は NULL
を返します。重要なのは、size
引数がバイト単位で指定されるという点です。
2. sizeof
演算子
sizeof
はC言語の単項演算子で、変数や型のサイズをバイト単位で返します。
sizeof(type)
: 指定された型のサイズを返します(例:sizeof(int)
は通常4バイト)。sizeof(expression)
: 式の評価結果の型のサイズを返します(例:int *p; sizeof(*p)
はint
型のサイズを返します)。
このコミットでは sizeof(*p)
が使用されています。ここで p
は realloc
の戻り値を受け取るポインタであり、*p
はそのポインタが指す先の型(この場合は char *
または void *
のようなポインタ型)を意味します。したがって、sizeof(*p)
はポインタ自体のサイズ(例えば、32ビットシステムでは4バイト、64ビットシステムでは8バイト)を返します。
3. ポインタのサイズと64ビットシステム
64ビットシステム(例: amd64)では、メモリアドレスを表現するために64ビット(8バイト)のポインタが使用されます。これに対し、32ビットシステムでは32ビット(4バイト)のポインタが使用されます。この違いは、メモリを動的に割り当てる際に、ポインタの配列を確保する場合に特に重要になります。
例えば、100個のポインタを格納する配列を確保したい場合、
- 32ビットシステムでは
100 * 4 = 400
バイトが必要です。 - 64ビットシステムでは
100 * 8 = 800
バイトが必要です。
このコミットのバグは、このポインタサイズの差異を考慮せずにメモリを割り当てていたために発生しました。
4. Go言語のビルドプロセスとリンカ (cmd/ld
)
Go言語のビルドプロセスは、ソースコードをコンパイルし、最終的に実行可能なバイナリを生成します。このプロセスには、コンパイラ (cmd/compile
)、アセンブラ (cmd/asm
)、そしてリンカ (cmd/ld
) など、複数のツールが関与します。
- リンカ (
cmd/ld
): コンパイルされたオブジェクトファイルやライブラリを結合し、最終的な実行可能ファイルを生成する役割を担います。リンカは、プログラムが依存する外部ライブラリ(標準ライブラリやサードパーティライブラリなど)を解決し、それらを実行可能ファイルに含めるか、動的にリンクするように設定します。この過程で、リンカはライブラリ検索パス(-L
フラグで指定されるパスなど)を管理し、適切なライブラリファイルを見つけ出す必要があります。
このコミットで修正されたのは、まさにこのリンカの内部処理、特にライブラリ検索パスを動的に管理する部分でのメモリ割り当ての不備でした。
技術的詳細
このコミットの技術的詳細は、C言語における動的メモリ割り当ての一般的な落とし穴と、それが異なるアーキテクチャ(特に32ビットと64ビット)でどのように顕在化するかを示しています。
問題のコードは src/cmd/ld/lib.c
内の Lflag
関数にありました。この関数は、リンカのコマンドライン引数として渡される -L
フラグ(ライブラリ検索パスを指定する)を処理するものです。リンカは、複数の -L
フラグが指定された場合に備えて、これらのパスを動的に拡張可能な配列に格納する必要があります。
元のコードは以下のようになっていました。
p = realloc(libdir, maxlibdir);
ここで、libdir
はライブラリパスの文字列へのポインタの配列(例えば char **libdir
)であると推測されます。maxlibdir
は、この配列が保持できる要素の最大数(つまり、ライブラリパスの最大数)を意図していたと考えられます。
しかし、realloc
関数の第2引数 size
はバイト単位で指定する必要があります。元のコードでは、maxlibdir
をそのまま size
として渡していました。これは、maxlibdir
が要素数である場合、確保されるメモリが maxlibdir
バイトにしかならないことを意味します。
- 32ビットシステムの場合: ポインタのサイズは4バイトです。もし
maxlibdir
が例えば100であれば、realloc
は100バイトを確保します。これは、25個のポインタ(100 / 4 = 25
)しか格納できないことになります。もしmaxlibdir
が要素数として意図されており、かつmaxlibdir
がポインタのサイズで割れるような値であれば、偶然にも問題が表面化しなかった可能性があります。しかし、それでも意図した要素数分のメモリは確保されていません。 - 64ビットシステム(FreeBSD/amd64など)の場合: ポインタのサイズは8バイトです。もし
maxlibdir
が100であれば、realloc
は100バイトを確保します。これは、わずか12個のポインタ(100 / 8 = 12.5
、切り捨てて12個)しか格納できないことになります。リンカが12個を超えるライブラリパスを処理しようとすると、割り当てられたメモリブロックの境界を越えて書き込みが行われ、メモリ破損が発生します。これが、FreeBSD/amd64環境でビルドが失敗する原因でした。
修正後のコードは以下のようになっています。
p = realloc(libdir, maxlibdir * sizeof(*p));
この修正では、maxlibdir
に sizeof(*p)
を乗算しています。前述の通り、sizeof(*p)
はポインタ自体のサイズ(64ビットシステムでは8バイト)を返します。これにより、realloc
は maxlibdir
個のポインタを格納するために必要な正しいバイト数(maxlibdir * 8
バイト)を確保するようになります。
この変更により、リンカは必要な数のライブラリパスを安全に格納できるようになり、メモリ破損が解消され、FreeBSD/amd64を含むすべてのアーキテクチャでGoのビルドが安定して行われるようになりました。これは、ポータブルなCコードを書く上で sizeof
を適切に使用することの重要性を示す典型的な例です。
コアとなるコードの変更箇所
変更は src/cmd/ld/lib.c
ファイルの1箇所のみです。
--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -59,7 +59,7 @@ Lflag(char *arg)
maxlibdir = 8;
else
maxlibdir *= 2;
- p = realloc(libdir, maxlibdir);
+ p = realloc(libdir, maxlibdir * sizeof(*p));
if (p == nil) {
print("too many -L's: %d\n", nlibdir);
usage();
コアとなるコードの解説
変更された行は src/cmd/ld/lib.c
の Lflag
関数内にあります。
Lflag(char *arg)
: この関数は、リンカのコマンドライン引数-L
を処理します。-L
は、ライブラリを検索するディレクトリパスを指定するために使用されます。maxlibdir
: この変数は、libdir
配列が現在保持できるライブラリパスの最大数を表しています。コードの前の部分で、maxlibdir
は必要に応じて倍々に増やされています(maxlibdir *= 2;
)。これは、動的配列の一般的な拡張戦略です。libdir
: これは、ライブラリ検索パスの文字列へのポインタを格納する配列(char **
型またはそれに類するもの)であると推測されます。p = realloc(libdir, maxlibdir);
(変更前):realloc
はlibdir
が指すメモリブロックのサイズをmaxlibdir
バイトに再割り当てしようとします。- しかし、
maxlibdir
は「要素の数」として意図されており、各要素はポインタ(char *
)です。 - したがって、この行は
maxlibdir
個のポインタを格納するのに必要なバイト数ではなく、単にmaxlibdir
バイトを確保していました。64ビットシステムではポインタが8バイトであるため、これは深刻なメモリ不足を引き起こしました。
p = realloc(libdir, maxlibdir * sizeof(*p));
(変更後):sizeof(*p)
は、ポインタp
が指す先の型(つまり、libdir
配列の各要素の型、すなわちポインタ型)のサイズをバイト単位で返します。64ビットシステムではこれは8になります。maxlibdir * sizeof(*p)
は、maxlibdir
個のポインタを格納するために必要な正確なバイト数を計算します。- これにより、
realloc
は適切なサイズのメモリブロックを確保し、リンカがライブラリパスを安全に格納できるようになります。
if (p == nil)
:realloc
がメモリ割り当てに失敗した場合(NULL
を返す)、エラーメッセージを出力してプログラムを終了します。このエラーハンドリングは変更されていません。
この修正は、C言語で動的配列を扱う際の基本的なベストプラクティスを示しています。要素の数を基にメモリを割り当てる際には、必ず 要素数 * sizeof(各要素の型)
の形式でサイズを計算する必要があります。
関連リンク
- Go CL (Code Review) へのリンク: https://golang.org/cl/5732043
参考にした情報源リンク
- C言語
realloc
関数に関するドキュメント (例: cppreference.com, man pages) - C言語
sizeof
演算子に関するドキュメント - 64ビットシステムにおけるポインタのサイズに関する一般的な情報
- Go言語のビルドプロセスとリンカ (
cmd/ld
) に関する一般的な情報 (Goの公式ドキュメントやソースコード) - GitHubのコミット履歴と差分表示