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

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

このコミットは、Go言語のリンカ (cmd/ld) におけるCgoプログラムのバイナリの一貫性に関する問題を解決します。具体的には、セクションシンボル名に一時的なビルドパス ($WORK) を含むファイル名ではなく、パッケージパスを使用するように変更することで、Cgoプログラムのビルド結果が環境に依存せず常に同じになるように改善しています。

コミット

commit 4d7c81bc674479a6d21cbeec14bb1d6dac5e4a8c
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Sep 20 00:18:41 2012 +0800

    cmd/ld: consistent binary for cgo programs
    We use pkg path instead of file name (which contains $WORK) in section symbols names.
    
    R=golang-dev, fullung, rsc, iant
    CC=golang-dev
    https://golang.org/cl/6445085

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

https://github.com/golang/go/commit/4d7c81bc674479a6d21cbeec14bb1d6dac5e4a8c

元コミット内容

cmd/ld: consistent binary for cgo programs
We use pkg path instead of file name (which contains $WORK) in section symbols names.

変更の背景

Go言語のビルドシステムは、コンパイルやリンクの過程で一時的な作業ディレクトリ ($WORK) を使用します。Cgo(GoとC言語の相互運用機能)を使用するプログラムをビルドする際、リンカは実行ファイルの内部にセクションシンボルを生成します。このセクションシンボル名が、ビルド時の一時的なファイルパス($WORKを含む)に基づいて生成されていました。

この挙動には以下の問題がありました。

  1. バイナリの非一貫性: 同じソースコードからビルドしても、ビルドを実行する環境やタイミングによって $WORK ディレクトリのパスが異なるため、生成されるバイナリのセクションシンボル名が異なってしまい、結果としてバイナリのハッシュ値も異なっていました。これは、再現可能なビルド(Reproducible Builds)の原則に反し、CI/CDパイプラインでのキャッシュの効率性や、ビルドの信頼性を損なう可能性がありました。
  2. デバッグの複雑化: デバッグツールやプロファイリングツールがセクションシンボル名に依存する場合、パスが変動することでデバッグが困難になることがありました。

このコミットは、セクションシンボル名に一時的なファイル名ではなく、Goのパッケージパスを使用することで、この問題を根本的に解決し、Cgoプログラムのバイナリの一貫性を確保することを目的としています。

前提知識の解説

Go言語のリンカ (cmd/ld)

cmd/ld はGo言語のツールチェインの一部であり、Goコンパイラによって生成されたオブジェクトファイル(.o ファイル)や、Cgoによって生成されたC/C++のオブジェクトファイル、そしてGoの標準ライブラリなどを結合して、最終的な実行可能バイナリを生成する役割を担っています。リンカは、シンボル解決、セクションの配置、再配置処理などを行います。

cgo

cgo は、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のC/C++ライブラリをGoから利用したり、パフォーマンスが重要な部分をC/C++で記述したりすることが可能になります。cgo を使用すると、GoのビルドプロセスにC/C++コンパイラとリンカが統合されます。

セクションシンボル (Section Symbols)

実行可能ファイル(バイナリ)は、通常、複数の「セクション」に分割されています。例えば、.text セクションには実行可能なコードが、.data セクションには初期化されたデータが、.bss セクションには初期化されていないデータが格納されます。セクションシンボルは、これらのセクションやその内部の特定のメモリ領域を識別するためのシンボルです。リンカは、これらのセクションシンボルを生成し、バイナリ内のアドレス解決やデバッグ情報の生成に利用します。

$WORK

Goのビルドプロセスでは、コンパイルやリンクの中間生成物を格納するための一時的な作業ディレクトリが作成されます。このディレクトリのパスは、環境変数 $WORK で参照されることがあります。このパスは通常、システムの一時ディレクトリ内にランダムな名前で作成されるため、ビルドごとに異なる値になります。

ELF, Mach-O, PE

これらは、異なるオペレーティングシステムで使用される主要な実行可能ファイルフォーマットです。

  • ELF (Executable and Linkable Format): LinuxやUnix系システムで広く使用されています。
  • Mach-O (Mach Object): macOS (旧OS X) で使用されています。
  • PE (Portable Executable): Windowsで主に使用されています。

Goのリンカ (cmd/ld) は、これらの異なるフォーマットに対応しており、ターゲットOSに応じて適切なフォーマットのバイナリを生成します。このコミットで変更されている ldelf.cldmacho.cldpe.c は、それぞれELF、Mach-O、PEフォーマットのバイナリ生成に関連するコードです。

技術的詳細

このコミットの核心は、リンカがセクションシンボル名を生成する際に使用する情報源を変更することです。

変更前は、セクションシンボル名の一部として、Cgoによって生成されたオブジェクトファイルの名前 (pn 変数で表されることが多い) が使用されていました。この pn には、Goのビルドプロセスが一時的に作成する $WORK ディレクトリのパスが含まれていました。例えば、/tmp/go-build123456789/b001/cgo.o のようなパスがシンボル名に組み込まれていました。

変更後は、pn の代わりに pkg 変数(Goのパッケージパスを表す)が使用されるようになりました。パッケージパスは、ソースコードの構造に基づいて決定されるため、ビルド環境やタイミングに依存せず常に一貫しています。例えば、github.com/user/repo/mypackage のような形式です。

具体的な変更は、smprint 関数(文字列フォーマット関数)の呼び出しにおいて、第一引数として渡される変数を pn から pkg に変更することです。これにより、生成されるセクションシンボル名が $WORK パスを含まなくなり、バイナリの一貫性が保たれます。

また、USED(pkg); の削除も行われています。USED マクロは、変数が使用されていることをコンパイラに伝えるためのもので、未使用変数に関する警告を抑制するために使われます。pkg 変数が smprint の引数として明示的に使用されるようになったため、この USED マクロは不要になりました。

この変更は、Goのリンカがサポートする主要な3つの実行ファイルフォーマット(ELF, Mach-O, PE)すべてに適用されており、それぞれのフォーマットに対応するソースファイル (ldelf.c, ldmacho.c, ldpe.c) で同様の修正が行われています。

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

以下の3つのファイルで変更が行われています。

src/cmd/ld/ldelf.c

--- a/src/cmd/ld/ldelf.c
+++ b/src/cmd/ld/ldelf.c
@@ -331,7 +331,6 @@ ldelf(Biobuf *f, char *pkg, int64 len, char *pn)
 
 	symbols = nil;
 
-	USED(pkg);
 	if(debug['v'])
 		Bprint(&bso, "%5.2f ldelf %s\n", cputime(), pn);
 
@@ -519,7 +518,7 @@ ldelf(Biobuf *f, char *pkg, int64 len, char *pn)
 		if(sect->type != ElfSectNobits && map(obj, sect) < 0)
 			goto bad;
 		
-		name = smprint("%s(%s)", pn, sect->name);
+		name = smprint("%s(%s)", pkg, sect->name);
 		s = lookup(name, version);
 		free(name);
 		switch((int)sect->flags&(ElfSectFlagAlloc|ElfSectFlagWrite|ElfSectFlagExec)) {

src/cmd/ld/ldmacho.c

--- a/src/cmd/ld/ldmacho.c
+++ b/src/cmd/ld/ldmacho.c
@@ -440,7 +440,6 @@ ldmacho(Biobuf *f, char *pkg, int64 len, char *pn)
 	Reloc *r, *rp;
 	char *name;
 
-	USED(pkg);
 	version++;
 	base = Boffset(f);
 	if(Bread(f, hdr, sizeof hdr) != sizeof hdr)
@@ -566,7 +565,7 @@ ldmacho(Biobuf *f, char *pkg, int64 len, char *pn)
 			continue;
 		if(strcmp(sect->name, "__eh_frame") == 0)
 			continue;
-		name = smprint("%s(%s/%s)", pn, sect->segname, sect->name);
+		name = smprint("%s(%s/%s)", pkg, sect->segname, sect->name);
 		s = lookup(name, version);
 		if(s->type != 0) {
 			werrstr("duplicate %s/%s", sect->segname, sect->name);

src/cmd/ld/ldpe.c

--- a/src/cmd/ld/ldpe.c
+++ b/src/cmd/ld/ldpe.c
@@ -145,7 +145,6 @@ ldpe(Biobuf *f, char *pkg, int64 len, char *pn)
 	PeSym *sym;
 
 	USED(len);
-	USED(pkg);
 	if(debug['v'])
 		Bprint(&bso, "%5.2f ldpe %s\n", cputime(), pn);
 	
@@ -213,7 +212,7 @@ ldpe(Biobuf *f, char *pkg, int64 len, char *pn)
 		if(map(obj, sect) < 0)
 			goto bad;
 		
-		name = smprint("%s(%s)", pn, sect->name);
+		name = smprint("%s(%s)", pkg, sect->name);
 		s = lookup(name, version);
 		free(name);
 		switch(sect->sh.Characteristics&(IMAGE_SCN_CNT_UNINITIALIZED_DATA|IMAGE_SCN_CNT_INITIALIZED_DATA|\

コアとなるコードの解説

各ファイルにおける変更は、以下の2点に集約されます。

  1. USED(pkg); の削除:

    • これは、pkg 変数が以前はコード内で直接使用されていなかったため、コンパイラが未使用変数として警告を出すのを防ぐためのマクロでした。
    • 今回の変更で pkgsmprint 関数の引数として明示的に使用されるようになったため、このマクロは不要となり削除されました。これはコードのクリーンアップと見なせます。
  2. smprint 関数の第一引数の変更:

    • ldelf.cldpe.c では、name = smprint("%s(%s)", pn, sect->name);name = smprint("%s(%s)", pkg, sect->name); に変更されています。
    • ldmacho.c では、name = smprint("%s(%s/%s)", pn, sect->segname, sect->name);name = smprint("%s(%s/%s)", pkg, sect->segname, sect->name); に変更されています。
    • smprint 関数は、フォーマット文字列と可変個の引数を受け取り、フォーブルされた文字列を生成するユーティリティ関数です。
    • 変更前は、pn (プログラム名またはファイル名、$WORK パスを含む可能性があった) がセクションシンボル名のプレフィックスとして使用されていました。
    • 変更後は、pkg (Goのパッケージパス) がプレフィックスとして使用されるようになりました。
    • この変更により、生成されるセクションシンボル名がビルド環境に依存しない一貫した形式になり、Cgoプログラムのバイナリの再現性が向上します。

これらの変更は、Goのビルドシステムが生成するバイナリの安定性と再現性を高める上で重要な改善です。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/ld ディレクトリ)
  • Go言語の公式ドキュメント
  • 一般的なリンカの概念 (ELF, Mach-O, PEフォーマット) に関する情報
  • Goのビルドプロセスにおける $WORK ディレクトリの役割に関する情報 (Goのブログ記事や関連する技術記事)
  • Goのコミット履歴とコードレビュー (CL 6445085)