リンカーの基礎:オブジェクトファイルから実行ファイルへ
はじめに
コンパイラがソースコードをオブジェクトファイルに変換した後、リンカーが複数のオブジェクトファイルを結合して実行可能なプログラムを作成します。この記事では、リンカーの動作原理と現代的なリンク技術について解説します。
リンカーの役割
コンパイルパイプラインにおけるリンカー
ソースコード (.c, .cpp)
↓
プリプロセッサ
↓
コンパイラ
↓
オブジェクトファイル (.o, .obj)
↓
リンカー ← 今回の主役
↓
実行ファイル (.exe, ELF, Mach-O)
リンカーが行うこと
- シンボル解決: 未定義のシンボルを他のオブジェクトファイルやライブラリから探す
- 再配置: アドレスを決定し、コード内の参照を修正する
- セクション結合: 複数のオブジェクトファイルのセクションをマージする
- ライブラリリンク: 静的・動的ライブラリをリンクする
オブジェクトファイルの構造
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
__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のみがエクスポートされる
まとめ
リンカーは、コンパイルパイプラインの最終段階で重要な役割を果たします。
重要なポイント
- シンボル解決: 未定義シンボルを見つけて結合
- 再配置: アドレスの決定とコードの修正
- 静的vs動的: 用途に応じた選択
- 最適化: LTOやガベージコレクションの活用
- デバッグ: 適切なツールでの問題解決
学習リソース
- Linkers and Loaders (John R. Levine)
- System V ABI - ELF形式の標準仕様
- ld(1) - GNU リンカーのマニュアル
- Linker and Libraries Guide (Oracle)
リンカーの理解は、システムプログラミングやデバッグにおいて非常に重要です。特に共有ライブラリの問題やリンクエラーの解決には、リンカーの知識が不可欠です。