low-layer

リンカーの基礎:オブジェクトファイルから実行ファイルへ

リンカーの動作原理、シンボル解決、再配置、そして現代のリンカーの最適化技術について解説します。

6 min read

リンカーの基礎:オブジェクトファイルから実行ファイルへ

はじめに

コンパイラがソースコードをオブジェクトファイルに変換した後、リンカーが複数のオブジェクトファイルを結合して実行可能なプログラムを作成します。この記事では、リンカーの動作原理と現代的なリンク技術について解説します。

リンカーの役割

コンパイルパイプラインにおけるリンカー

ソースコード (.c, .cpp) ↓ プリプロセッサ ↓ コンパイラ ↓ オブジェクトファイル (.o, .obj) ↓ リンカー ← 今回の主役 ↓ 実行ファイル (.exe, ELF, Mach-O)

リンカーが行うこと

  1. シンボル解決: 未定義のシンボルを他のオブジェクトファイルやライブラリから探す
  2. 再配置: アドレスを決定し、コード内の参照を修正する
  3. セクション結合: 複数のオブジェクトファイルのセクションをマージする
  4. ライブラリリンク: 静的・動的ライブラリをリンクする

オブジェクトファイルの構造

ELF形式(Linux)

+------------------+ | ELF Header | +------------------+ | Program Headers | (実行ファイルのみ) +------------------+ | .text section | コード +------------------+ | .data section | 初期化済みデータ +------------------+ | .bss section | 未初期化データ +------------------+ | .rodata section | 読み取り専用データ +------------------+ | .symtab section | シンボルテーブル +------------------+ | .strtab section | 文字列テーブル +------------------+ | .rel.text | 再配置情報 +------------------+ | Section Headers | +------------------+

重要なセクション

セクション説明
.text実行コード
.data初期化済みグローバル変数
.bss未初期化グローバル変数
.rodata定数データ(読み取り専用)
.symtabシンボルテーブル
.strtabシンボル名の文字列
.rel/.rela再配置エントリ
.debug_*デバッグ情報

シンボルテーブル

HLJSBASH
# シンボルテーブルの確認 nm -C main.o # 出力例 0000000000000000 T main U printf 0000000000000000 D global_var 0000000000000004 d static_var.0

シンボルタイプ:

  • T (Text): コードセクションのグローバルシンボル
  • U (Undefined): 未定義シンボル(外部参照)
  • D (Data): データセクションのグローバルシンボル
  • d: ローカルデータシンボル
  • t: ローカルコードシンボル

リンクのプロセス

1. シンボル解決

HLJSC
// main.c extern int add(int a, int b); // 未定義シンボル int main() { return add(1, 2); }
HLJSC
// math.c int add(int a, int b) { // 定義されたシンボル return a + b; }
HLJSBASH
# オブジェクトファイルの生成 gcc -c main.c -o main.o gcc -c math.c -o math.o # リンク gcc main.o math.o -o program # シンボルの確認 nm main.o # U add # 0000000000000000 T main nm math.o # 0000000000000000 T add

2. 再配置(Relocation)

オブジェクトファイルは相対アドレスを使用し、リンク時に絶対アドレスが決定されます。

HLJSBASH
# 再配置エントリの確認 objdump -r main.o # 出力例 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000007 R_X86_64_PC32 add-0x0000000000000004

再配置タイプ:

  • R_X86_64_64: 絶対64ビットアドレス
  • R_X86_64_PC32: PC相対32ビット
  • R_X86_64_PLT32: PLT経由の呼び出し
  • R_X86_64_GOTPCRELX: GOT経由のアクセス

3. セクションのマージ

main.o: math.o: .text (64 bytes) .text (32 bytes) .data (16 bytes) .data (8 bytes) ↓ リンク後 program: .text (96 bytes) ← 連結 .data (24 bytes) ← 連結

静的リンクと動的リンク

静的リンク

HLJSBASH
# 静的ライブラリの作成 gcc -c lib.c -o lib.o ar rcs libmylib.a lib.o # 静的リンク gcc main.c -L. -lmylib -o program # または gcc main.c libmylib.a -o program

特徴:

  • ライブラリコードが実行ファイルに組み込まれる
  • ファイルサイズが大きくなる
  • 別途ライブラリファイルが不要
  • 更新には再コンパイルが必要

動的リンク

HLJSBASH
# 共有ライブラリの作成 gcc -shared -fPIC lib.c -o libmylib.so # 動的リンク gcc main.c -L. -lmylib -o program # 実行時のライブラリパス設定 LD_LIBRARY_PATH=. ./program

特徴:

  • 実行時にライブラリを読み込む
  • ファイルサイズが小さい
  • 複数プログラムでライブラリを共有可能
  • ライブラリの更新が容易

動的リンクの仕組み

実行ファイル ↓ ローダー ↓ 動的リンカー (ld-linux.so / dyld) ↓ 必要な共有ライブラリを検索・ロード ↓ シンボル解決と再配置 ↓ プログラム開始

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

HLJSASM
# PLTエントリの例 printf@plt: jmp *GOT_PRINTF(%rip) # GOT経由でprintfにジャンプ push $index # 初回呼び出しの場合 jmp resolver # 動的リンカーで解決 # GOT GOT_PRINTF: .quad printf # 初回はresolver、以降はprintfのアドレス
HLJSBASH
# GOTとPLTの確認 objdump -d -j .plt program objdump -s -j .got program

実践:リンクの詳細観察

オブジェクトファイルの解析

HLJSBASH
# セクションヘッダの確認 objdump -h main.o # 詳細なELF情報 readelf -a main.o # アセンブリ表示 objdump -d main.o # 全情報の表示 objdump -x main.o

リンクマップの生成

HLJSBASH
# リンクマップでリンクプロセスを可視化 gcc -Wl,-Map=output.map main.c math.c -o program # マップファイルの内容 cat output.map

未定義シンボルの確認

HLJSBASH
# 未定義シンボルの一覧 nm -u program # 動的シンボルの依存関係 ldd program # 詳細な依存関係 readelf -d program | grep NEEDED

リンカースクリプト

デフォルトのリンカースクリプト確認

HLJSBASH
# デフォルトスクリプトの確認 ld --verbose # 特定のエミュレーション ld -m elf_x86_64 --verbose

カスタムリンカースクリプト

HLJSLD
/* simple.ld */ ENTRY(_start) SECTIONS { . = 0x400000; .text : { *(.text) *(.text.*) } . = ALIGN(4096); .rodata : { *(.rodata) *(.rodata.*) } . = ALIGN(4096); .data : { *(.data) *(.data.*) } .bss : { *(.bss) *(COMMON) } /DISCARD/ : { *(.comment) *(.note.*) } }
HLJSBASH
# カスタムスクリプトの使用 ld -T simple.ld main.o -o program

リンカーの最適化

セクションガベージコレクション

HLJSBASH
# 未使用セクションの削除 gcc -ffunction-sections -fdata-sections main.c -o program gcc -Wl,--gc-sections program # 確認 nm program | wc -l

リンク時最適化(LTO)

HLJSC
// main.c #include <stdio.h> __attribute__((noinline)) int add(int a, int b) { return a + b; } int main() { printf("%d\n", add(1, 2)); return 0; }
HLJSBASH
# LTOなしでコンパイル gcc -O2 main.c -o program_no_lto # LTOありでコンパイル gcc -O2 -flto main.c -o program_lto # バイナリサイズの比較 ls -l program_* # インライン化の確認 objdump -d program_no_lto | grep -A5 "<main>" objdump -d program_lto | grep -A5 "<main>"

Identical Code Folding(ICF)

HLJSBASH
# ICFの有効化 gcc -Wl,--icf=all main.c -o program # 重複コードの確認 nm -C program | sort | uniq -c | sort -rn | head

一般的なリンクエラー

未定義シンボル

undefined reference to `symbol'

解決策:

HLJSBASH
# シンボルがどのライブラリにあるか検索 nm -C /usr/lib/lib*.a 2>/dev/null | grep symbol # 必要なライブラリをリンク gcc main.c -lm -lpthread

重複シンボル

multiple definition of `symbol'

解決策:

  • ヘッダーファイルでの定義を避ける
  • staticまたはinlineを使用
  • extern宣言と定義を分ける
HLJSC
// header.h - 悪い例 int global_var = 0; // 複数の.cでインクルードされると重複 // header.h - 良い例 extern int global_var; // source.c int global_var = 0; // または inline関数 inline int get_value() { return 42; }

ライブラリの順序

HLJSBASH
# 間違った順序(依存関係が逆) gcc -lmylib main.o -o program # エラー # 正しい順序 gcc main.o -lmylib -o program # 依存関係がある場合 gcc main.o -lA -lB -o program # AがBに依存

プラットフォーム別の違い

Linux(ELF)

HLJSBASH
# 動的リンカーの確認 file /bin/ls # /bin/ls: ELF 64-bit LSB executable, x86-64, # dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 # 共有ライブラリパスの確認 ldconfig -p | grep libc

Windows(PE/COFF)

HLJSBASH
# DLLの依存関係確認(MinGW) objdump -p program.exe | grep DLL # インポートライブラリ gcc main.c -lkernel32 -luser32 -o program.exe

macOS(Mach-O)

HLJSBASH
# 動的ライブラリの確認 otool -L program # インストール名の設定 install_name_tool -id @rpath/libmylib.dylib libmylib.dylib

デバッグとトラブルシューティング

詳細なリンク情報

HLJSBASH
# 詳細なリンク過程の表示 gcc -Wl,--verbose main.c -o program # トレースモード gcc -Wl,--trace main.c -o program

共有ライブラリの問題

HLJSBASH
# ライブラリが見つからない場合 ldd program # libmylib.so => not found # 解決策1: LD_LIBRARY_PATH export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH # 解決策2: rpath gcc -Wl,-rpath,/path/to/lib main.c -o program # 解決策3: ldconfig sudo ldconfig /path/to/lib

シンボルの可視性

HLJSC
// visibility.c __attribute__((visibility("default"))) int public_function() { return 1; } __attribute__((visibility("hidden"))) int internal_function() { return 2; } // デフォルトは default int default_function() { return 3; }
HLJSBASH
gcc -fvisibility=hidden -shared visibility.c -o libvis.so nm -D libvis.so | grep function # public_functionのみがエクスポートされる

まとめ

リンカーは、コンパイルパイプラインの最終段階で重要な役割を果たします。

重要なポイント

  1. シンボル解決: 未定義シンボルを見つけて結合
  2. 再配置: アドレスの決定とコードの修正
  3. 静的vs動的: 用途に応じた選択
  4. 最適化: LTOやガベージコレクションの活用
  5. デバッグ: 適切なツールでの問題解決

学習リソース

  • Linkers and Loaders (John R. Levine)
  • System V ABI - ELF形式の標準仕様
  • ld(1) - GNU リンカーのマニュアル
  • Linker and Libraries Guide (Oracle)

リンカーの理解は、システムプログラミングやデバッグにおいて非常に重要です。特に共有ライブラリの問題やリンクエラーの解決には、リンカーの知識が不可欠です。