[インデックス 10642] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) におけるメモリリークを修正するものです。具体的には、パッケージデータのロード時やオブジェクトファイルの処理時に動的に割り当てられたメモリが適切に解放されていなかった問題を解決しています。
コミット
commit a2ba34d37463cacc6c7fd2a1882d6aadc0102a2c
Author: Scott Lawrence <bytbox@gmail.com>
Date: Wed Dec 7 11:50:39 2011 -0500
ld: fix memory leaks
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5434068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a2ba34d37463cacc6c7fd2a1882d6aadc0102a2c
元コミット内容
このコミットの目的は、Goリンカ(ld
)におけるメモリリークを修正することです。リンカは、コンパイルされたオブジェクトファイルを結合して実行可能ファイルを生成する際に、多くの文字列やデータ構造をメモリにロードします。これらのデータが適切に解放されないと、リンカの実行中にメモリ使用量が増加し続け、特に大規模なプロジェクトのビルド時や、リンカが繰り返し実行されるような環境で問題を引き起こす可能性があります。
変更の背景
Go言語の初期段階では、パフォーマンスと機能の実装が優先され、メモリ管理の細部が後回しにされることがありました。このコミットは、リンカの安定性と効率性を向上させるための継続的な取り組みの一環として、発見されたメモリリークを修正するために行われました。リンカはビルドプロセスの重要な部分であり、そのメモリ効率はビルド時間とシステムリソースの使用に直接影響します。メモリリークは、特にCI/CD環境や開発者のローカルマシンで、ビルドの失敗やパフォーマンスの低下につながる可能性があるため、修正が不可欠でした。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が必要です。
- リンカ (ld): リンカは、コンパイラによって生成された複数のオブジェクトファイル(
.o
ファイルなど)とライブラリを結合し、単一の実行可能ファイルまたはライブラリを生成するプログラムです。Go言語のビルドプロセスでは、go build
コマンドの内部でリンカが呼び出され、Goのランタイム、標準ライブラリ、およびユーザーコードをリンクします。 - メモリリーク: プログラムが動的に割り当てたメモリを、不要になった後も解放せずに保持し続ける状態を指します。これにより、プログラムの実行中に使用可能なメモリが徐々に減少し、最終的にはシステム全体のパフォーマンス低下やプログラムのクラッシュを引き起こす可能性があります。C言語のような手動でメモリ管理を行う言語では、
malloc
で確保したメモリはfree
で明示的に解放する必要があります。 strdup()
: C標準ライブラリ関数の一つで、引数として与えられた文字列の複製を動的に割り当てられたメモリに作成し、そのポインタを返します。返されたメモリは、不要になったらfree()
で解放する必要があります。free()
: C標準ライブラリ関数の一つで、malloc()
、calloc()
、realloc()
、またはstrdup()
によって以前に割り当てられたメモリブロックを解放します。malloc()
: C標準ライブラリ関数の一つで、指定されたサイズのメモリブロックを動的に割り当てます。smprint()
: Goリンカの内部関数で、sprintf
に似た機能を提供し、動的に割り当てられた文字列を返します。この関数が返す文字列も、使用後にfree()
で解放する必要があります。Biobuf
: Goリンカの内部で使われるバッファリングされたI/O構造体です。ファイルからの読み込みを効率化します。Sym
: リンカがシンボルテーブルを管理するために使用する構造体です。シンボルは、関数名、変数名など、プログラム内の識別子を指します。ilookup()
: シンボルをルックアップ(検索)するためのリンカ内部関数です。loadpkgdata()
: パッケージデータをロードするためのリンカ内部関数です。Goのパッケージは、コンパイル時に生成されるメタデータを含んでおり、リンカがこれらを処理します。objfile()
: オブジェクトファイルを処理するためのリンカ内部関数です。ldobj()
: オブジェクトファイルのヘッダを解析し、適切なリンカ関数(ldelf
,ldmacho
,ldpe
など)にディスパッチするリンカ内部関数です。ldelf()
,ldmacho()
,ldpe()
: それぞれELF (Linux/Unix), Mach-O (macOS), PE (Windows) 形式のオブジェクトファイルをロードするためのリンカ内部関数です。diag()
: リンカが診断メッセージ(エラーや警告)を出力するための関数です。goos
,thestring
,getgoversion()
: Goのビルド環境に関する情報(OS、Goのバージョン文字列など)を取得するための変数や関数です。リンカは、オブジェクトファイルが現在のビルド環境と互換性があるかを確認するためにこれらの情報を使用します。
技術的詳細
このコミットで修正されているメモリリークは、主に以下の2つのシナリオで発生していました。
-
loadpkgdata
関数における文字列の重複割り当てと未解放:src/cmd/ld/go.c
のloadpkgdata
関数は、パッケージデータをロードする際にシンボル情報を処理します。 元のコードでは、ilookup(name)
でシンボルx
を検索し、x->prefix == nil
の場合にx->def = def;
と直接ポインタを代入していました。ここでdef
は、loadpkgdata
の呼び出し元から渡される一時的な文字列ポインタである可能性があります。もしdef
が動的に割り当てられたメモリを指している場合、そのメモリの所有権がx->def
に移るか、またはx->def
が指すメモリが別の場所で解放される必要があります。しかし、このコミットの修正を見る限り、def
はloadpkgdata
のスコープ内でfree
されるべき一時的な文字列であり、x->def
にはその内容のコピーを保持する必要がありました。 修正前は、x->def = def;
とすることで、def
が指すメモリがx->def
にコピーされず、def
自体がloadpkgdata
の最後でfree(def)
されると、x->def
が指すメモリが不正な状態になるか、またはdef
が指すメモリが解放されずにリークする可能性がありました。 この修正では、x->def = strdup(def);
とすることで、def
の内容が新しいメモリ領域にコピーされ、x->def
がそのコピーを指すようになります。これにより、loadpkgdata
の最後でfree(name);
とfree(def);
が呼び出されても、x->def
が指すデータは安全に保持されます。 また、loadpkgdata
の最後にfree(file);
が追加され、file
引数として渡された動的に割り当てられた文字列も解放されるようになりました。 -
objfile
およびldobj
関数におけるパス文字列の未解放:src/cmd/ld/lib.c
のobjfile
関数は、オブジェクトファイルをロードするエントリポイントです。この関数はpkg
とfile
(またはpn
) という文字列ポインタを受け取ります。これらの文字列は、多くの場合、動的に割り当てられたパス文字列です。 元のコードでは、objfile
がldobj
を呼び出し、ldobj
がさらに特定のオブジェクトファイル形式(ELF, Mach-O, PEなど)に応じたリンカ関数(ldelf
,ldmacho
,ldpe
)を呼び出していました。これらの関数に渡されたpn
(パス名) 引数は、処理が完了した後に解放されるべきでした。 修正前は、これらの関数がリターンする際にpn
が解放されていませんでした。 修正では、ldelf
,ldmacho
,ldpe
の各関数のリターンパスにfree(pn);
が追加されました。これにより、オブジェクトファイルの処理が完了した時点で、そのパス名文字列に割り当てられたメモリが適切に解放されるようになります。 また、objfile
関数自体も、pkg
引数として渡された文字列を、処理の完了後にfree(pkg);
で解放するようになりました。 さらに、ldobj
関数内でsmprint
によって生成された一時的な文字列t
や、pn
がエラーパスで解放されないケースも修正されています。特に、オブジェクトファイルのヘッダチェックが失敗した場合など、早期リターンするパスでpn
が解放されていなかった問題が解決されています。
これらの修正は、Goリンカが実行されるたびに、特に多数のパッケージやオブジェクトファイルを処理する場合に、メモリ使用量が不必要に増加するのを防ぎ、リンカの安定性と効率性を向上させます。
コアとなるコードの変更箇所
src/cmd/ld/go.c
--- a/src/cmd/ld/go.c
+++ b/src/cmd/ld/go.c
@@ -235,7 +235,7 @@ loadpkgdata(char *file, char *pkg, char *data, int len)
x = ilookup(name);
if(x->prefix == nil) {
x->prefix = prefix;
- x->def = def;
+ x->def = strdup(def);
x->file = file;
} else if(strcmp(x->prefix, prefix) != 0) {
fprintf(2, "%s: conflicting definitions for %s\n", argv0, name);
@@ -248,7 +248,10 @@ loadpkgdata(char *file, char *pkg, char *data, int len)
fprintf(2, "%s:\t%s %s %s\n", file, prefix, name, def);
nerrors++;
}
+ free(name);
+ free(def);
}
+ free(file);
}
// replace all "". with pkg.
@@ -264,7 +267,7 @@ expandpkg(char *t0, char *pkg)
n++;
if(n == 0)
- return t0;
+ return strdup(t0);
// use malloc, not mal, so that caller can free
w0 = malloc(strlen(t0) + strlen(pkg)*n);
src/cmd/ld/lib.c
--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -351,6 +351,7 @@ objfile(char *file, char *pkg)
Bseek(f, 0L, 0);
ldobj(f, pkg, l, file, FileObj);
Bterm(f);
+ free(pkg);
return;
}
@@ -412,6 +413,7 @@ objfile(char *file, char *pkg)
out:
Bterm(f);
+ free(pkg);
}
void
@@ -439,14 +441,17 @@ ldobj(Biobuf *f, char *pkg, int64 len, char *pn, int whence)
if(magic == 0x7f454c46) { // \x7F E L F
ldelf(f, pkg, len, pn);
+ free(pn);
return;
}
if((magic&~1) == 0xfeedface || (magic&~0x01000000) == 0xcefaedfe) {
ldmacho(f, pkg, len, pn);
+ free(pn);
return;
}
if(c1 == 0x4c && c2 == 0x01 || c1 == 0x64 && c2 == 0x86) {
ldpe(f, pkg, len, pn);
+ free(pn);
return;
}
@@ -472,16 +477,18 @@ ldobj(Biobuf *f, char *pkg, int64 len, char *pn, int whence)
return;
}
diag("%s: not an object file", pn);
+ free(pn);
return;
}
// First, check that the basic goos, string, and version match.
t = smprint("%s %s %s ", getgoos(), thestring, getgoversion());
line[n] = ' ';
if(strncmp(line+10, t, strlen(t)) != 0 && !debug['f']) {
line[n] = '\0';
diag("%s: object is [%s] expected [%s]", pn, line+10, t);
free(t);
+ free(pn);
return;
}
@@ -496,6 +503,7 @@ ldobj(Biobuf *f, char *pkg, int64 len, char *pn, int whence)
line[n] = '\0';
diag("%s: object is [%s] expected [%s]", pn, line+10, theline);
free(t);
+ free(pn);
return;
}
}
@@ -521,10 +529,12 @@ ldobj(Biobuf *f, char *pkg, int64 len, char *pn, int whence)
Bseek(f, import1, 0);
ldobj1(f, pkg, eof - Boffset(f), pn);
+ free(pn);
return;
eof:
diag("truncated object file: %s", pn);
+ free(pn);
}
static Sym*
コアとなるコードの解説
src/cmd/ld/go.c
の変更点
loadpkgdata
関数内:x->def = def;
をx->def = strdup(def);
に変更:def
が指す文字列の内容を新しいメモリ領域にコピーし、そのポインタをx->def
に代入するようにしました。これにより、def
が一時的なメモリを指している場合でも、x->def
が指すデータが安全に保持され、def
が後で解放されても問題が発生しなくなります。これは、def
がloadpkgdata
の呼び出し元から渡された文字列であり、loadpkgdata
のスコープ内でfree
されることを意図しているため、x->def
には独立したコピーが必要だったことを示唆しています。free(name);
とfree(def);
の追加:loadpkgdata
関数内で動的に割り当てられたname
とdef
のメモリを、処理の最後に明示的に解放するようにしました。これにより、これらの文字列がリークするのを防ぎます。free(file);
の追加:loadpkgdata
関数に渡されたfile
引数も、動的に割り当てられた文字列である可能性があるため、関数の最後に解放するようにしました。
expandpkg
関数内:return t0;
をreturn strdup(t0);
に変更:n == 0
の場合(置換が行われない場合)、元の文字列t0
をそのまま返すのではなく、その複製を返すようにしました。これにより、呼び出し元が返された文字列をfree
する際に、t0
が静的な文字列リテラルであったり、別の場所で管理されているメモリであったりしても、安全に解放できるようになります。これは、expandpkg
のコメントにある「use malloc, not mal, so that caller can free」という意図と一致します。
src/cmd/ld/lib.c
の変更点
objfile
関数内:free(pkg);
の追加:objfile
関数に渡されたpkg
引数(パッケージ名)が動的に割り当てられた文字列であるため、オブジェクトファイルの処理が完了した後に解放するようにしました。
ldobj
関数内:ldelf
,ldmacho
,ldpe
の各呼び出し後にfree(pn);
の追加: ELF、Mach-O、PE形式のオブジェクトファイルをロードする各関数(ldelf
,ldmacho
,ldpe
)がリターンする際に、pn
(パス名) 引数として渡された動的に割り当てられた文字列を解放するようにしました。これにより、これらのパス名がリークするのを防ぎます。- エラーパスでの
free(pn);
の追加: オブジェクトファイルの形式が認識できない場合や、ヘッダチェックが失敗した場合など、ldobj
関数が早期にリターンするパスでpn
が解放されていなかった問題を修正しました。これにより、どのような終了パスでもpn
が適切に解放されるようになります。 smprint
で生成されたt
の解放に加えてfree(pn);
の追加: オブジェクトファイルのバージョンチェックが失敗した場合、smprint
で生成された一時的な文字列t
は解放されていましたが、pn
は解放されていませんでした。この修正により、pn
も適切に解放されるようになりました。
これらの変更は、Goリンカがメモリをより効率的に管理し、メモリリークによる潜在的な問題を回避するために不可欠です。
関連リンク
- Go言語のリンカ (cmd/ld) のソースコード
- Go言語のメモリ管理 (Go言語自体はガベージコレクションを持つが、リンカのような低レベルツールはC言語で書かれており、手動メモリ管理が必要)
- C言語の
malloc
,free
,strdup
関数
参考にした情報源リンク
- Go言語のGitHubリポジトリ: https://github.com/golang/go
- Go言語の公式ドキュメント: https://go.dev/doc/
- C言語の標準ライブラリ関数に関する一般的な情報源 (例: manページ、C言語のリファレンスサイト)