このページの目次

動的リンクと ld.so

カバー範囲: PLT/GOT → 遅延バインディング → LD_PRELOAD → LD_LIBRARY_PATH → rpath/runpath → シンボルバージョン → dlopen/dlsym → IFUNC 対象: glibc ld.so, musl ldso

概要

動的リンクにより、複数のプログラムが同じ .so のコードページ(物理メモリ内では1つのコピー)を共有でき、かつライブラリを独立して更新可能になります(プログラムを再リンクする必要がありません)。その代償として、外部関数の初回呼び出しごとに微小な遅延(遅延バインディング)が発生し、ABI 互換性の維持という負担が生じます。PLT/GOT のメカニズムを理解することは、「undefined symbol」や「LD_PRELOAD が効かない」、「libfoo.so.1 vs libfoo.so.2」などの問題をトラブルシューティングする前提条件です。

PLT/GOT: 遅延バインディングのエンジン

静的リンクの問題

// main.c:
extern int foo(int);
int main() { return foo(42); }

// 静的リンク: リンカは foo のアドレスを call 命令にハードコードする
//   call <address_of_foo>
//   問題: libfoo.so が更新されるたびに → foo のアドレスが変化 → 呼び出し元すべてを再リンクする必要がある

動的リンク: PLT + GOT

flowchart TD
    MAIN["プログラム (main)<br/>call foo@PLT<br/>foo を直接呼び出さず、PLT スタブ (stub) を呼び出す"]

    MAIN --> PLT{"PLT[foo]<br/>jmp *GOT[foo]"}

    PLT -->|"初回呼び出し<br/>GOT → PLT の次命令"| LAZY["push index<br/>foo の .rela.plt でのインデックスをプッシュ"]

    PLT -->|"2回目以降の呼び出し<br/>GOT → foo の実際のアドレス"| DIRECT["jmp foo<br/>直接ジャンプ ✅<br/><br/>呼び出しごとに間接ジャンプが1回追加されるだけ"]

    LAZY --> RESOLVER["jmp PLT[0]<br/>→ _dl_runtime_resolve()"]

    RESOLVER --> FIND["🔍 libfoo.so 内で<br/>foo 関数のアドレスを検索"]

    FIND --> UPDATE["📝 GOT[foo] を更新<br/>= foo の実際のアドレス"]

    UPDATE --> EXEC["▶️ jmp foo<br/>実際の foo を実行"]

    classDef entry fill:#e3f2fd,stroke:#1565c0
    classDef decision fill:#fff3e0,stroke:#ef6c00
    classDef resolver fill:#f3e5f5,stroke:#7b1fa2
    classDef done fill:#e8f5e9,stroke:#2e7d32
    class MAIN entry
    class PLT decision
    class LAZY,RESOLVER,FIND,UPDATE resolver
    class DIRECT,EXEC done

3つのメモリレイアウト

.got.plt (Global Offset Table — PLT 専用):
   GOT[0]:  .dynamic セクションのアドレス
   GOT[1]:  struct link_map * (現在の共有ライブラリのリンクマップ)
   GOT[2]:  dl_runtime_resolve のアドレス (ld.so によって埋め込まれる)
   GOT[3+]: 各外部関数のアドレス (初期状態では PLT[func] の2命令目を指す)

.plt (Procedure Linkage Table):
   PLT[0]: 共通エントリ — push link_map + jmp dl_runtime_resolve
   PLT[1+]: 各関数のスタブ — jmp *GOT[n]; push index; jmp PLT[0]

.got (PLT 以外のグローバルデータ):
   グローバル変数のアドレスを格納 (例: extern int errno;)

実測

# コンパイルして PLT/GOT を確認:
gcc -o test main.c -lfoo
objdump -d test | grep -A3 'foo@plt'
objdump -R test | grep foo      # foo の .rela.plt エントリを確認

# 実行時に遅延バインディングを確認:
LD_DEBUG=bindings ./test 2>&1 | grep foo
# 出力例: binding file ./test [0] to ./test [0]: normal symbol `foo'

ld.so: 動的リンカ

ローディングと検索

1. カーネルが ELF をロード → PT_INTERP セグメントを確認 → /lib64/ld-linux-x86-64.so.2
2. カーネルが ld.so をメモリにロード → ld.so の _start にジャンプ
3. ld.so が自己初期化 (ld.so 自身も動的リンクされているため、自己再配置が必要!)
4. ld.so がメインプログラムの .dynamic を読み取り → DT_NEEDED を確認 → 依存する .so をロード
5. 各 .so に対して: .dynamic を読み取り → 依存する依存関係をロード
6. シンボル解決: 読み込まれたすべての .so を走査 → 未定義のシンボルをすべて解決
7. 再配置: GOT を埋め、R_X86_64_GLOB_DAT / R_X86_64_JUMP_SLOT を処理
8. メインプログラムの _start を呼び出す

検索パスの順序

1. DT_RPATH (ELF .dynamic 内の RPATH。廃止済み。DT_RUNPATH に置き換え)
2. LD_LIBRARY_PATH (ユーザーによる上書き)
3. DT_RUNPATH (ELF .dynamic 内の RUNPATH)
4. /etc/ld.so.cache (ldconfig によって生成)
5. /lib64, /usr/lib64 (デフォルトパス)

確認コマンド:
  readelf -d /bin/ls | grep -E 'RPATH|RUNPATH|NEEDED'
  ldconfig -p | grep libfoo
  LD_DEBUG=libs ./test 2>&1  # 検索をすべて追跡

LD_PRELOAD

# シンボル解決の最前面に共有ライブラリを強制的に挿入 → 任意のシンボルを上書き
LD_PRELOAD=./override.so ./program

# 典型的な用途:
#   1. malloc を置換 → メモリ割り当てを追跡
#   2. connect を置換 → ネットワークのリダイレクト
#   3. open を置換 → ファイルシステムサンドボックス

# 条件: 静的リンクされた関数には使用できない
#       (静的リンクには PLT/GOT がないため、call は直接関数アドレスへ飛ぶ)

dlopen / dlsym

#include <dlfcn.h>
void *handle = dlopen("libfoo.so", RTLD_LAZY | RTLD_LOCAL);
void (*func)(int) = dlsym(handle, "foo");
func(42);
dlclose(handle);

// RTLD_LAZY:   遅延バインディング (デフォルト)
// RTLD_NOW:    シンボルを即時解決 (dlopen の失敗が早期に露呈)
// RTLD_GLOBAL: このライブラリのシンボルが、後続の dlopen で参照可能になる
// RTLD_LOCAL:  シンボルはこの handle のみに限定 (デフォルト)
// RTLD_NODELETE: dlclose でアンロードしない (デングリングポインタを防ぐ)
// RTLD_NOLOAD:  ロードせず、すでにロードされているかのみを確認

シンボルバージョンと ABI

// シンボルバージョン (GNU 拡張):
//   libfoo.so: foo@@VERS_2.0 (デフォルト), foo@VERS_1.0 (旧版)
//   リンク時: デフォルトバージョンを取得
//   実行時: 読み込まれた .so に一致するバージョンがあるかチェック

// GCC の -fno-semantic-interposition (5.x 以降):
//   コンパイラに対し、ライブラリ内の関数呼び出しを「自ライブラリのバージョン」と仮定するよう指示
//   → PLT をスキップ → 直接呼び出し → 高速化
//   ただし、この関数に対する LD_PRELOAD の効果を無効化する

デバッグ

# リンクされている共有ライブラリをすべて表示
ldd /bin/ls

# シンボル解決の詳細表示 (非常に詳細)
LD_DEBUG=all ./test 2>&1 | head -50

# 現在プロセスがロードしている .so を表示
cat /proc/self/maps | grep '\.so'

# 未定義のシンボル (リンク時)
nm -u /bin/ls

参考

  • ソースコード⁠: glibc elf/dl-runtime.c (_dl_runtime_resolve), elf/rtld.c (ld.so main)
  • man ページ⁠: ld.so(8), dlopen(3), dlsym(3), elf(5)
  • LWN: "How to write shared libraries", "The cost of lazy binding"

キーワード: PLT, GOT, 遅延バインディング, dl_runtime_resolve, LD_PRELOAD, dlopen, dlsym, DT_NEEDED, RPATH, RUNPATH, シンボルバージョン