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

[インデックス 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つのシナリオで発生していました。

  1. loadpkgdata 関数における文字列の重複割り当てと未解放: src/cmd/ld/go.cloadpkgdata 関数は、パッケージデータをロードする際にシンボル情報を処理します。 元のコードでは、ilookup(name) でシンボル x を検索し、x->prefix == nil の場合に x->def = def; と直接ポインタを代入していました。ここで def は、loadpkgdata の呼び出し元から渡される一時的な文字列ポインタである可能性があります。もし def が動的に割り当てられたメモリを指している場合、そのメモリの所有権が x->def に移るか、または x->def が指すメモリが別の場所で解放される必要があります。しかし、このコミットの修正を見る限り、defloadpkgdata のスコープ内で 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 引数として渡された動的に割り当てられた文字列も解放されるようになりました。

  2. objfile および ldobj 関数におけるパス文字列の未解放: src/cmd/ld/lib.cobjfile 関数は、オブジェクトファイルをロードするエントリポイントです。この関数は pkgfile (または pn) という文字列ポインタを受け取ります。これらの文字列は、多くの場合、動的に割り当てられたパス文字列です。 元のコードでは、objfileldobj を呼び出し、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 が後で解放されても問題が発生しなくなります。これは、defloadpkgdata の呼び出し元から渡された文字列であり、loadpkgdata のスコープ内で free されることを意図しているため、x->def には独立したコピーが必要だったことを示唆しています。
    • free(name);free(def); の追加: loadpkgdata 関数内で動的に割り当てられた namedef のメモリを、処理の最後に明示的に解放するようにしました。これにより、これらの文字列がリークするのを防ぎます。
    • 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言語のGitHubリポジトリ: https://github.com/golang/go
  • Go言語の公式ドキュメント: https://go.dev/doc/
  • C言語の標準ライブラリ関数に関する一般的な情報源 (例: manページ、C言語のリファレンスサイト)