このページの目次
リンクとロード
コンパイラが
.oを出力した後、実行可能になるまでにはリンクとロードが残っている——リロケーションはシンボル参照をアドレスに変換し、PLT/GOT の遅延バインディングにより動的ライブラリは初回呼び出し時のみ解決され、LTO はモジュール間の最適化をリンク時に一元化してプログラム全体での解析を行う。
概要
コンパイラが .o(オブジェクトファイル)を出力した後、「実行可能な実行ファイル」になるまでには、2つのステップが残っている:リンク(複数の .o とライブラリのシンボル参照をアドレスに解決する)とロード(実行ファイルをプロセスのアドレス空間にマップし、初期化後にエントリポイントへジャンプする)。リンカは単なる「連結」ではなく、このステップでリロケーション(再配置)、未使用コードの GC セクションの削除、LTO(コンパイル単位間のグローバル最適化)を実行する。本稿では「コンパイラ出力 → 静的リンク → 動的リンク → 動的ロード」という流れに沿って、リンクとロードの各層のメカニズムを詳しく解説する。
コンパイラの出力:オブジェクトファイル(ELF)
.o ファイル(ELF形式)は以下の要素を含む:
- .text: マシンコード(コードセクション)
- .data: 初期化済みグローバル変数
- .bss: 未初期化(またはゼロ初期化)のグローバル変数 —— ファイルサイズを消費せず、OSはロード時にゼロページをマップする
- .rodata: 読み取り専用データ(文字列リテラル、浮動小数点定数)
- .symtab: シンボルテーブル(本ファイルがエクスポート/インポートするシンボル)
- .rela.text: テキストセクションのリロケーションテーブル(外部シンボルを参照する命令、リンカによる修正が必要)
- .rela.data: データセクションのリロケーションテーブル
- .debug_info, .debug_line, ...: DWARFデバッグ情報
コンパイラの最終ステップ(アセンブラ)は .o を出力する。リンカはすべての .o とライブラリのシンボルテーブルを読み取り、リロケーションを実行して実行ファイルを生成する。
リロケーション:「シンボル参照」から「アドレス」へ
.o ファイルのマシンコードにおいて、外部シンボルを参照する箇所には 0 またはプレースホルダが埋められている。.rela.text はこれらの「穴」の位置を記録している:
典型的なリロケーションエントリ (R_X86_64_PC32):
偏移: 0x42 ← .text 内で修正が必要な位置
シンボル: printf ← 参照するシンボル
種類: R_X86_64_PC32 ← 相対ジャンプ (PC + オフセット)
加算値: -4 ← 補正(命令中のPCは次命令を指すため、4バイト差)
リンカはすべてのセクションのロードアドレスを割り当てた後、すべてのリロケーションエントリを走査し、ターゲットシンボルの最終アドレスを計算して、種類に応じた数式でターゲット位置のバイトを書き換える:
R_X86_64_PC32 修正数式: *(target) = sym_addr - (target_addr + 4) + addend
R_X86_64_64 修正数式: *(target) = sym_addr + addend (絶対アドレス)
静的リンク:.a (archive)
静的ライブラリは .o のパッケージ(ar 形式)であり、リンク時に必要に応じて取り出される:
gcc main.o -L. -lmylib → リンカが行う処理:
1. main.o 中の未解決(unresolved)シンボルを収集
2. libmylib.a 内で各未解決シンボルを検索
3. 見つかった場合 → .a から対応する .o を抽出し、リンク対象に追加
4. 抽出された .o が新たな未解決シンボルを導入する可能性あり → 再帰的(または複数パス)スキャン
5. すべてのシンボルが解決 → リロケーション実行 → 実行ファイルを出力
リンカのシンボル解決は順序に依存する——従来のUnixリンカは .o と .a を左から右へ処理するため、libfoo.a がそれを参照する .o の前に配置されている場合、スキャン対象外になる可能性がある(その時点で未解決シンボルがないため)。最新のリンカ(LLD)には --start-group / --end-group による反復スキャン機能があり、順序依存の問題を解消する。
リンカ GC(デッドセクションの削除)
コンパイラは関数単位で .text セクションの断片を生成する(gold および LLD は --gc-sections をサポート)。リンカはエントリポイント(_start)から始まり、call/jump 参照を追跡し、未使用の関数セクションをデッドとしてマークして破棄する。これにより最終的なバイナリサイズが削減される——DCE(デッドコード除去)と似ているが、.o 単位を跨いで行われる。
動的リンク:.so (shared object)
動的ライブラリは、リンク時に実行ファイル内にコピーされるのではなく、リンク時に依存関係を記録し、実行時にロードして動的に解決する。
GOT と PLT:遅延バインディングの2層
ターゲットコードとABI でPICにはGOTが必要だと述べた。動的リンクの完全な図は GOT + PLT である:
これが 遅延バインディング (lazy binding) である:動的ライブラリのシンボルは、初めて呼び出されたときにのみ解決され、GOT がキャッシュとして機能する。LD_BIND_NOW=1 環境変数は、ロード時にすべてのシンボルを即時解決する(遅延なし)——起動は遅くなるが、実行時にPLTオーバーヘッドが発生しない。
シンボル挿入 (symbol interposition)
動的リンクはシンボル挿入をサポートしている:複数の .so が同じ名前の malloc を定義している場合、最初にロードされた定義が有効になる。実行ファイルで定義されたシンボルは .so のシンボルを上書きする(デフォルト動作)、LD_PRELOAD により明示的に挿入することも可能。これは jemalloc/tcmalloc がシステム malloc を置き換える仕組み——ただしパフォーマンスの罠にもなる:挿入によりコンパイラは malloc をインライン展開できず、呼び出しのたびにPLTを経由する必要がある。
dlopen: ランタイムロード
プログラム実行中に任意のライブラリを動的にロードできる:
void *handle = dlopen("libplugin.so", RTLD_NOW);
void (*fn)(int) = dlsym(handle, "plugin_init");
fn(42);
dlclose(handle);
JITコンパイラは dlopen を使用してコンパイル済みの .so をロードする——コンパイルされたコードは .o → .so へのリンク → dlopen → dlsym で具体的な関数を呼び出す。V8 や Julia もこのアプローチを採用している——JITコンパイルされたマシンコードを .so としてパッケージ化し動的ロードする。これにより、直接実行可能メモリに書き込むよりも安全である(OSは .text セクションの NX/書き込み保護を保証するため、手動で mprotect を呼び出す必要がない)。
LTO (Link-Time Optimization): リンク時一元化最適化
従来の各 .c(コンパイル単位)は個別に .o にコンパイルされ、最適化は単位内でのみ行われる——ユニット間の関数インライン展開、デッドコード除去、定数伝播などは、他のユニットを参照できない。LTO はコンパイル単位間の最適化をリンク時まで遅延させる:
- ThinLTO: プログラム全体最適化のスケーラブルなアプローチ——すべてのIRを単一モジュールにマージする(メモリ使用量が大きすぎる)のではなく、「呼び出し関係」に基づいてモジュール間のサマリーを作成し、各モジュール内で個別に最適化を実行し、ホットパスの跨モジュール呼び出しのみをインライン展開する。ThinLTO は gold/LLD のデフォルトLTOモードである。
LTO はコンパイル時間に大きな影響を与える——プログラム全体の最適化がリンク時に再実行されるため、大規模プロジェクトではリンク時間が倍増する可能性がある。その一方で利点も非常に大きい:モジュール間のインライン展開により呼び出し境界が解消され、通常 5〜15% のパフォーマンス向上が期待できる。
参考
- Levine: "Linkers and Loaders" — リンクとロードの古典的教科書
- Ian Lance Taylor: "Linkers" シリーズ (https://lwn.net/Articles/276782/) — gold リンカの著者がリンク原理を解説
- System V AMD64 ABI: 動的リンク部分 (GOT/PLT の正式仕様)
Keywords: static linking, dynamic linking, archive, .a, shared object, .so, ELF, .text, .data, .bss, .symtab, relocation, R_X86_64_PC32, GOT, PLT, lazy binding, _dl_runtime_resolve, symbol interposition, LD_PRELOAD, dlopen, dlsym, linker script, gc-sections, LTO, ThinLTO, link-time optimization, gold, LLD