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

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

このコミットは、Go言語のリンカ (cmd/ld) におけるホストリンキング(外部リンカとの連携)のサポートを、特に linux/amd64 環境向けに導入するものです。Cgo(GoとC言語の相互運用機能)を使用する際に、Goのツールチェインが生成したオブジェクトファイルを、システムの標準的なCリンカ(例: GCC)と連携させて最終的な実行ファイルを生成する機能の基盤を構築しています。これにより、Cgoがより複雑なCライブラリやシステムライブラリと連携できるようになります。

コミット

commit 60f783d92bc07fa7ca78e8efccbbc841d9f9cbcb
Author: Russ Cox <rsc@golang.org>
Date:   Thu Mar 7 09:19:02 2013 -0500

    cmd/ld: host linking support for linux/amd64
    
    Still to do: non-linux and non-amd64.
    It may work on other ELF-based amd64 systems too, but untested.
    
    "go test -ldflags -hostobj $GOROOT/misc/cgo/test" passes.
    
    Much may yet change, but this seems a reasonable checkpoint.
    
    R=iant
    CC=golang-dev
    https://golang.org/cl/7369057

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

https://github.com/golang/go/commit/60f783d92bc07fa7ca78e8efccbbc841d9f9cbcb

元コミット内容

このコミットは、Goリンカ (cmd/ld) に linux/amd64 環境でのホストリンキング(外部リンカとの連携)のサポートを追加します。これは、Goのツールチェインが生成したオブジェクトファイルを、システムのCリンカ(例: GCC)と連携させて最終的な実行ファイルを生成するためのものです。まだ non-linuxnon-amd64 環境への対応は残っていますが、ELFベースの amd64 システムでは動作する可能性があります。go test -ldflags -hostobj $GOROOT/misc/cgo/test がパスすることを確認しており、現時点での合理的なチェックポイントとされています。

変更の背景

Go言語は、当初から独自のツールチェイン(コンパイラ、アセンブラ、リンカなど)を持つことを目指していました。これにより、クロスコンパイルが容易になり、依存関係の管理がシンプルになるという利点がありました。しかし、Cgo(GoとC言語の相互運用)を使用する場合、GoのコードがCのライブラリやシステムコールと連携する必要があります。特に、動的リンクライブラリ(DLLや共有オブジェクト)や、C言語のランタイムライブラリ(libcなど)に依存するCコードをGoプログラムに組み込む場合、Goのリンカだけでは対応が難しいケースが生じます。

このコミット以前は、Goのリンカは主に「内部リンキング」と呼ばれる方式で動作していました。これは、Goのツールチェインがすべてのリンク処理を完結させる方式です。しかし、Cgoがより高度な機能(例: Cの動的ライブラリのエクスポート/インポート、特定のシステムライブラリへのリンク)を必要とするにつれて、Goのリンカが外部のCリンカ(通常はGCC)と連携する「外部リンキング」または「ホストリンキング」の必要性が高まりました。

この変更の主な目的は、Cgoプログラムが外部のCライブラリとよりシームレスに連携できるようにすること、特に動的リンクを必要とするシナリオに対応することです。これにより、Goプログラムが既存のC/C++エコシステムとより深く統合できるようになります。

前提知識の解説

1. Go言語のリンカ (cmd/ld, 5l, 6l, 8l)

Go言語のビルドシステムは、go build コマンドを通じて、ソースコードをコンパイルし、最終的な実行ファイルを生成します。このプロセスにおいて、リンカは非常に重要な役割を担います。

  • cmd/ld: Goのメインリンカです。これは、アーキテクチャ固有のリンカ(5l for ARM, 6l for amd64, 8l for 386など)を呼び出す共通のフロントエンドとして機能します。
  • 5l, 6l, 8l: それぞれARM、amd64、386アーキテクチャ向けのGoリンカです。これらはGoのオブジェクトファイル(.a.o)を結合し、実行可能ファイルやライブラリを生成します。Goのリンカは、C言語のリンカとは異なり、Go独自のシンボル解決やメモリレイアウトの管理を行います。

2. Cgo

Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。Cgoを使用すると、Goの強力な並行処理とC言語の既存のライブラリやシステムAPIへのアクセスを組み合わせることができます。Cgoは、GoのビルドプロセスにCコンパイラ(通常はGCC)を統合し、CコードをコンパイルしてGoのオブジェクトファイルとリンクします。

3. 内部リンキングと外部リンキング

  • 内部リンキング (Internal Linking): Goのツールチェイン(コンパイラ、リンカ)が、すべてのGoコードとCgoでコンパイルされたCコードを結合し、単一の実行ファイルを生成する方式です。この場合、外部のCリンカは使用されません。GoのリンカがCのオブジェクトファイルも直接処理します。
  • 外部リンキング (External Linking): GoのツールチェインがGoのオブジェクトファイルを生成した後、最終的なリンクステップをシステムのCリンカ(例: GCC)に委ねる方式です。Cgoが外部のCライブラリに依存する場合や、特定のリンカフラグが必要な場合にこの方式が選択されます。このコミットは、この外部リンキングのサポートを強化するものです。

4. 実行ファイルフォーマット (ELF, Mach-O, PE)

オペレーティングシステムは、実行可能ファイルや共有ライブラリを特定のフォーマットで管理します。

  • ELF (Executable and Linkable Format): LinuxやUnix系システムで広く使用されている実行可能ファイル、共有ライブラリ、オブジェクトファイルの標準フォーマットです。
  • Mach-O: macOS (旧OS X) やiOSで使用されている実行可能ファイル、共有ライブラリ、オブジェクトファイルのフォーマットです。
  • PE (Portable Executable): Windowsで使用されている実行可能ファイル、DLL、オブジェクトファイルのフォーマットです。

このコミットでは、特にELFフォーマット(linux/amd64)に焦点を当てていますが、他のフォーマットへの対応も将来的に見据えています。

5. リロケーション (Relocations)

リロケーションとは、コンパイル時にアドレスが確定できないシンボル(変数や関数)への参照を、リンク時に正しいメモリアドレスに修正するプロセスです。オブジェクトファイルには、リロケーションエントリが含まれており、リンカはこれらを参照して最終的な実行ファイルのアドレスを解決します。

  • R_ARM_PLT32, R_X86_64_PC32, R_386_PLT32 など: これらは、特定のアーキテクチャとリロケーションタイプを示す定数です。例えば、PLT32 は Procedure Linkage Table (PLT) を介した関数呼び出しのリロケーションを示し、PC32 はプログラムカウンタ相対アドレスのリロケーションを示します。

6. GOT (Global Offset Table) と PLT (Procedure Linkage Table)

動的リンクされたプログラムでは、共有ライブラリ内の関数やデータへのアクセスは、GOTとPLTという特別なテーブルを介して行われます。

  • GOT (Global Offset Table): グローバル変数や関数へのオフセットを格納するテーブルです。プログラムが共有ライブラリ内のデータにアクセスする際に使用されます。
  • PLT (Procedure Linkage Table): 共有ライブラリ内の関数を呼び出す際に使用されるテーブルです。PLTエントリは、GOTエントリを介して実際の関数アドレスにジャンプします。これにより、共有ライブラリのロードアドレスが実行時に決定されても、プログラムは正しく関数を呼び出すことができます。

7. 動的エクスポートと静的エクスポート (#pragma cgo_export_static, #pragma cgo_export_dynamic)

Cgoでは、Goの関数をC言語から呼び出せるように「エクスポート」することができます。

  • #pragma cgo_export_static: Goの関数を静的にエクスポートします。これは、Goのリンカが生成する最終的なバイナリに、その関数が直接含まれることを意味します。
  • #pragma cgo_export_dynamic: Goの関数を動的にエクスポートします。これは、その関数が共有ライブラリの一部としてエクスポートされ、実行時に動的にロードされることを意味します。

このコミットでは、Sym 構造体の dynexport フィールドが cgoexport に変更され、静的エクスポートと動的エクスポートの両方をビットマスクで管理できるようになっています。

8. メモリ管理関数 (realloc, strdup, erealloc, estrdup)

C言語のプログラムでは、メモリ管理が重要です。

  • realloc: 既に割り当てられたメモリブロックのサイズを変更します。
  • strdup: 文字列を複製し、新しいメモリを割り当てます。
  • erealloc, estrdup: このコミットで導入された、reallocstrdup のラッパー関数です。これらの関数は、メモリ割り当てが失敗した場合にエラーを診断し、プログラムを終了させることで、より堅牢なエラーハンドリングを提供します。

技術的詳細

このコミットの主要な技術的変更点は、Goリンカが外部のCリンカと連携するためのメカニズムを導入したことです。

  1. Sym 構造体の変更:

    • src/cmd/5l/l.h, src/cmd/6l/l.h, src/cmd/8l/l.h において、シンボル構造体 Symuchar dynexport; フィールドが uchar cgoexport; に変更されました。
    • src/cmd/ld/lib.h に新しい列挙型 CgoExportDynamic = 1<<0CgoExportStatic = 1<<1 が追加され、cgoexport がビットマスクとして機能するようになりました。これにより、シンボルが動的にエクスポートされるか、静的にエクスポートされるか、あるいはその両方であるかを柔軟に表現できるようになりました。
  2. リンキングモードの導入:

    • src/cmd/ld/lib.hLinkAuto, LinkInternal, LinkExternal という新しい列挙型が導入され、リンカの動作モードを制御する linkmode 変数が追加されました。
    • LinkAuto は自動検出モードで、Cgoの使用状況に基づいて LinkInternal または LinkExternal に切り替わります。
    • src/cmd/5l/obj.c, src/cmd/6l/obj.c, src/cmd/8l/obj.cmain 関数で、linkmode の初期値が LinkInternal に設定されています。
    • src/cmd/ld/lib.cloadlib 関数内で、LinkAuto モードの場合にCgoの使用状況に応じて LinkExternal に切り替えるロジックが追加されました。
  3. ホストリンキングのプロセス (hostlink 関数):

    • src/cmd/ld/lib.chostlink 関数が追加されました。この関数は、linkmodeLinkExternal の場合に呼び出されます。
    • hostlink は、Goリンカが生成したオブジェクトファイルと、Cgoが生成したCのオブジェクトファイルを、システムのCリンカ(通常はGCC)に渡して最終的な実行ファイルを生成します。
    • 具体的には、一時ディレクトリを作成し、GoのオブジェクトファイルとCのオブジェクトファイルをその一時ディレクトリにコピーします。
    • その後、gcc コマンドを exec し、-o で出力ファイル名を指定し、-rdynamic フラグ(動的シンボルテーブルにすべてのグローバルシンボルを含める)を付けて、すべての一時オブジェクトファイルをリンクします。
    • -ggdb フラグも追加され、デバッグ情報が生成されるようになっています。
    • リンクが完了すると、一時ディレクトリは atexit ハンドラによってクリーンアップされます。
  4. オブジェクトファイル読み込みの変更 (ldobj, ldhostobj):

    • src/cmd/ld/lib.cldobj 関数のシグネチャが変更され、file 引数が追加されました。これは、オブジェクトファイルがアーカイブ(.a)内に含まれている場合に、元のアーカイブファイル名を追跡するために使用されます。
    • ldobj 関数内で、ELF、Mach-O、PE形式のオブジェクトファイルを検出した場合に、直接 ldelf, ldmacho, ldpe を呼び出すのではなく、新しく導入された ldhostobj 関数を呼び出すようになりました。
    • ldhostobj は、オブジェクトファイルの情報を Hostobj 構造体に格納し、後で hostobjs 関数によってまとめて処理されるようにキューに入れます。これにより、外部リンキングが必要なオブジェクトファイルが効率的に収集されます。
  5. メモリ割り当て関数の堅牢化:

    • src/cmd/ld/lib.cestrduperealloc 関数が追加されました。これらは、それぞれ strduprealloc のラッパーであり、メモリ割り当てが失敗した場合にエラーメッセージを出力してプログラムを終了させることで、より安全なメモリ管理を提供します。既存のコードベース全体で strduprealloc の呼び出しがこれらの新しい関数に置き換えられています。
  6. Cgoコールバックとメイン関数のエクスポート:

    • src/pkg/runtime/cgo/callbacks.c では、crosscall2 関数が #pragma cgo_export_static crosscall2#pragma cgo_export_dynamic crosscall2 の両方でエクスポートされるようになりました。これは、共有ライブラリからのコールバックをサポートするために、リンキングモードに関わらずこのシンボルが必要であることを示しています。
    • src/pkg/runtime/cgocall.c では、外部リンキングを使用するCgoプログラムのために、Goの main 関数が #pragma cgo_export_static main としてエクスポートされるようになりました。これにより、libcがCランタイムの起動を処理し、GoプログラムをCの main 関数として呼び出すことができるようになります。

これらの変更により、Goのリンカは、Cgoが外部のCライブラリと連携する際の複雑なリンキング要件に対応できるようになり、GoとCの相互運用性が大幅に向上しました。

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

このコミットで変更された主要なファイルと、その変更の概要は以下の通りです。

  • src/cmd/{5l,6l,8l}/asm.c:
    • adddynrel 関数内で、シンボルの動的エクスポートを判定する条件が targ->dynexport から (targ->cgoexport & CgoExportDynamic) に変更されました。
    • adddynsym 関数内で、シンボルの動的エクスポートを判定する条件が同様に s->dynexport から (s->cgoexport & CgoExportDynamic) に変更されました。
  • src/cmd/{5l,6l,8l}/l.h:
    • Sym 構造体の uchar dynexport; フィールドが uchar cgoexport; に変更されました。
  • src/cmd/{5l,6l,8l}/obj.c:
    • main 関数内で linkmode = LinkInternal; が追加され、リンキングモードの初期設定が行われています。
    • hostlink() 関数の呼び出しが追加され、外部リンキングの処理が実行されるようになりました。
  • src/cmd/ld/data.c:
    • realloc の呼び出しが erealloc に置き換えられ、メモリ割り当てエラーのハンドリングが強化されました。
    • dynrelocsym 関数内で、動的エクスポートの判定条件が r->sym->dynexport から (r->sym->cgoexport & CgoExportDynamic) に変更されました。
  • src/cmd/ld/dwarf.c:
    • realloc の呼び出しが erealloc に、strdup の呼び出しが estrdup に置き換えられました。
  • src/cmd/ld/go.c:
    • strdup の呼び出しが estrdup に、realloc の呼び出しが erealloc に置き換えられました。
    • loadcgo 関数内で、cgo_export_staticcgo_export_dynamic の処理が追加され、s->dynexport = 1;s->cgoexport |= CgoExportStatic; または s->cgoexport |= CgoExportDynamic; に変更されました。
    • dynexp グローバル変数が Sym** dynexp; として宣言され、エクスポートされるシンボルを管理するようになりました。
  • src/cmd/ld/ldelf.c, src/cmd/ld/ldmacho.c:
    • s->dynexport の判定が !(s->cgoexport & CgoExportDynamic) に変更されました。
  • src/cmd/ld/lib.c:
    • Lflag 関数内で reallocerealloc に変更されました。
    • addlibpath 関数内で reallocerealloc に変更されました。
    • loadlib 関数内で、linkmode の自動切り替えロジックと、dynexp リストのフィルタリング、hostobjs() および hostlinksetup() の呼び出しが追加されました。
    • objfile 関数と ldobj 関数のシグネチャが変更され、file 引数が追加されました。
    • ldobj 関数内で、ELF, Mach-O, PE形式のオブジェクトファイルを ldhostobj を介して処理するように変更されました。
    • dowrite 関数が hostlink の内部に移動されました。
    • Hostobj 構造体、ldhostobj, hostobjs, hostlinksetup, hostlink 関数が新しく追加されました。これらは外部リンキングの主要なロジックを実装しています。
    • estrduperealloc 関数が追加されました。
  • src/cmd/ld/lib.h:
    • EXTERN Sym** dynexp; が追加されました。
    • LinkAuto, LinkInternal, LinkExternal の列挙型と EXTERN int linkmode; が追加されました。
    • CgoExportDynamic, CgoExportStatic の列挙型が追加されました。
    • ldobj 関数のシグネチャが更新されました。
    • hostobjs, hostlink, estrdup, erealloc 関数のプロトタイプが追加されました。
  • src/cmd/ld/macho.c:
    • reallocerealloc に置き換えられました。
  • src/cmd/ld/pe.c:
    • initdynimport および initdynexport 関数内で、動的エクスポートの判定条件が s->dynexport から (s->cgoexport & CgoExportDynamic) に変更されました。
  • src/cmd/ld/symtab.c:
    • putelfsym 関数内で、linkmode == LinkExternal かつ !(x->cgoexport&CgoExportStatic) の場合にシンボルを STB_LOCAL としてマークするロジックが追加されました。これにより、外部リンキング時に不要なシンボルが動的シンボルテーブルにエクスポートされるのを防ぎます。
  • src/pkg/runtime/cgo/callbacks.c:
    • crosscall2 関数に #pragma cgo_export_static crosscall2#pragma cgo_export_dynamic crosscall2 の両方が追加されました。
  • src/pkg/runtime/cgocall.c:
    • main 関数に #pragma cgo_export_static main が追加されました。

コアとなるコードの解説

Sym 構造体と cgoexport ビットマスク

以前は uchar dynexport; という単一のフラグでシンボルの動的エクスポートを管理していましたが、このコミットでは uchar cgoexport; に変更され、CgoExportDynamicCgoExportStatic というビットマスクが導入されました。

// src/cmd/ld/lib.h
enum
{
	CgoExportDynamic = 1<<0,
	CgoExportStatic = 1<<1,
};

これにより、シンボルが動的にエクスポートされるか(共有ライブラリ用)、静的にエクスポートされるか(静的リンクされた実行ファイル用)、あるいはその両方であるかをより細かく制御できるようになりました。例えば、crosscall2 のように、どちらのリンキングモードでもエクスポートが必要なシンボルに対しては、両方のフラグを立てることができます。

リンキングモード (linkmode) の制御

linkmode 変数は、Goリンカがどのように動作するかを決定します。

// src/cmd/ld/lib.h
enum
{
	LinkAuto = 0,
	LinkInternal,
	LinkExternal,
};
EXTERN int	linkmode;
  • LinkAuto: デフォルトのモードです。Cgoが使用されているかどうか、およびCgoが外部リンキングを必要とするかどうかをGoリンカが自動的に判断します。
  • LinkInternal: Goリンカがすべてのリンク処理を内部で完結させます。外部のCリンカは使用されません。
  • LinkExternal: GoリンカはGoのオブジェクトファイルを生成した後、最終的なリンクステップを外部のCリンカ(GCCなど)に委ねます。

src/cmd/ld/lib.cloadlib 関数内で、LinkAuto の場合にCgoの使用状況に基づいて LinkExternal に切り替えるロジックがあります。

// src/cmd/ld/lib.c (抜粋)
	// If we got this far in automatic mode, there were no
	// cgo uses that suggest we need external mode.
	// Switch to internal.
	if(linkmode == LinkAuto)
		linkmode = LinkInternal;

	// Now that we know the link mode, trim the dynexp list.
	x = CgoExportDynamic;
	if(linkmode == LinkExternal)
		x = CgoExportStatic;
	w = 0;
	for(i=0; i<ndynexp; i++)
		if(dynexp[i]->cgoexport & x)
			dynexp[w++] = dynexp[i];
	ndynexp = w;
	
	// In internal link mode, read the host object files.
	if(linkmode == LinkInternal)
		hostobjs();
	else
		hostlinksetup();

このコードは、linkmodeLinkAuto のままであれば LinkInternal に設定し、その後、現在の linkmode に応じて dynexp リスト(エクスポートされるシンボルのリスト)をフィルタリングします。LinkInternal モードでは hostobjs() を呼び出してホストオブジェクトファイルを読み込み、LinkExternal モードでは hostlinksetup() を呼び出して外部リンキングの準備を行います。

hostlink 関数は、LinkExternal モードの際に最終的なリンク処理を実行します。

// src/cmd/ld/lib.c (抜粋)
void
hostlink(void)
{
	char *p, **argv;
	int i, w, n, argc, len;
	Hostobj *h;
	Biobuf *f;
	static char buf[64<<10];

	if(linkmode != LinkExternal)
		return;

	argv = malloc((10+nhostobj+nldflag)*sizeof argv[0]);
	argc = 0;
	argv[argc++] = "gcc"; // GCCを呼び出す
	if(!debug['s'])
		argv[argc++] = "-ggdb"; // デバッグ情報を生成
	argv[argc++] = "-o";
	argv[argc++] = outfile; // 出力ファイル名
	
	argv[argc++] = "-rdynamic"; // 動的シンボルテーブルにすべてのグローバルシンボルを含める

	// 一時ディレクトリにコピーされたホストオブジェクトファイルを追加
	for(i=0; i<nhostobj; i++) {
		h = &hostobj[i];
		// ... ファイルコピーロジック ...
		argv[argc++] = p; // 一時ファイルパスを引数に追加
	}
	
	argv[argc++] = smprint("%s/go.o", tmpdir); // Goリンカが生成したオブジェクトファイル
	for(i=0; i<nldflag; i++)
		argv[argc++] = ldflag[i]; // 追加のリンカフラグ
	argv[argc] = nil;

	// ... ログ出力 ...

	if(runcmd(argv) < 0) { // GCCを実行
		diag("%s: running %s failed: %r", argv0, argv[0]);
		errorexit();
	}
}

この関数は、Goリンカが生成したGoのオブジェクトファイル (go.o) と、Cgoが使用する外部のCオブジェクトファイル(hostobj リストに格納されているもの)をすべて集め、それらを gcc コマンドの引数として渡して実行します。これにより、GoとCのコードが外部のCリンカによって最終的にリンクされ、実行可能ファイルが生成されます。

安全なメモリ割り当て関数 (estrdup, erealloc)

このコミットでは、メモリ割り当ての失敗に対する堅牢性を高めるために、strduprealloc のラッパー関数が導入されました。

// src/cmd/ld/lib.c
char*
estrdup(char *p)
{
	p = strdup(p);
	if(p == nil) {
		cursym = S;
		diag("out of memory");
		errorexit();
	}
	return p;
}

void*
erealloc(void *p, long n)
{
	p = realloc(p, n);
	if(p == nil) {
		cursym = S;
		diag("out of memory");
		errorexit();
	}
	return p;
}

これらの関数は、メモリ割り当てが失敗した場合(nil を返す場合)に、エラーメッセージを出力してプログラムを終了させます。これにより、メモリ不足による未定義動作やクラッシュを防ぎ、デバッグを容易にします。既存のコードベース全体でこれらの新しい関数が使用されるように変更されています。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント (Cgo, リンカに関する情報)
  • ELF (Executable and Linkable Format) 仕様
  • Mach-O (Mach Object) ファイルフォーマットに関するドキュメント
  • PE (Portable Executable) フォーマットに関するドキュメント
  • GCC (GNU Compiler Collection) のリンカオプションに関するドキュメント
  • 動的リンク、GOT、PLTに関する一般的なコンピュータサイエンスの資料