TL

ELFバイナリのロードとリンカ・ローダの内部

実行ファイルをダブルクリックしてから最初の命令が走るまでの空白が埋まります。プログラムヘッダのmmap配置、ld.soの再配置とシンボル解決、PLT/GOTの遅延束縛を原理から解剖します。

応用ELF動的リンクローダPLT/GOTASLRリンカ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.カーネルはELFのプログラムヘッダ(PT_LOAD)を読み、各セグメントを保護属性付きでmmapし、動的実行ファイルでは指定されたインタプリタ(ld.so)に制御を渡す。
  • 2.ld.soは共有ライブラリを依存順にロードして再配置(relocation)を適用し、シンボルをグローバルスコープの探索順で解決する。関数呼び出しはPLT/GOT経由で初回のみ解決する遅延束縛を使える。
  • 3.PIE実行ファイルは位置独立コードなので任意の基準アドレスに置け、これがASLRによる本体のランダム化を可能にする。再配置とGOTがこの自由配置を支える。

実行ファイルが走り出すまでの空白

./a.out を実行してから最初のユーザー命令が走るまでには、いくつもの段階が挟まります。カーネルが実行ファイルを読んでメモリに配置し、必要なら動的リンカ(ローダ)に橋渡しし、リンカが共有ライブラリを集めてアドレスを埋め込み、ようやく main へ到達します。この空白を担うのが ELF(Executable and Linkable Format)というファイル形式と、それを解釈するリンカ・ローダです。

ELF ファイルは二つの顔を持ちます。リンク時にリンカ(ld)が見るのはセクションヘッダ.text, .data, .rela.dyn など細かい単位)で、実行時にカーネルとローダが見るのはプログラムヘッダ(実行に必要な粗い単位=セグメント)です。同じファイルを別の地図で読むわけです。実行に効くのは後者なので、本稿はプログラムヘッダ側を中心に追います。

プログラムヘッダとセグメントのmmap配置

プログラムヘッダテーブルは、ファイル内の領域(セグメント)をどうメモリへ写すかを記述したエントリの並びです。実行ファイルのロードに最も重要なのが PT_LOAD エントリで、これ一つひとつが「ファイルのこのオフセットから、この仮想アドレスへ、この保護属性で写せ」という指示になります。

readelf -l で見える典型的な PT_LOAD は次のような情報を持ちます。

Type   Offset    VirtAddr   FileSiz   MemSiz    Flags  Align
PT_LOAD 0x000000 0x...000   0x001a40  0x001a40  R E    0x1000   ← コード(.text)
PT_LOAD 0x002db0 0x...2db0  0x000280  0x000610  RW     0x1000   ← データ+BSS

ここで効くのが FileSizMemSiz の差です。MemSizFileSiz より大きいセグメントは、その差分がゼロ初期化領域(BSS)を表します。ファイルにはゼロを記録せず、ローダが余りをゼロ埋めするため、実ファイルを節約できます(プロセスアドレス空間のレイアウトとASLR の BSS と同じ原理です)。

カーネルは各 PT_LOADmmap でファイルにマップします。FlagsR/W/E がそのままページの保護属性(r--/rw-/r-x)になり、コードは書き込み不可、データは実行不可として置かれます。実体ページはこの時点では割り当てられず、最初にアクセスした瞬間にフォルト経由で読み込まれます(デマンドページングとページフォルト処理 のファイルバックドページに相当)。同じ読み取り専用コードページは複数プロセスで物理ページを共有できるため、同じ実行ファイルを多数起動してもコードのメモリは増えません。

静的リンクと動的リンクで経路が分かれる

プログラムヘッダに PT_INTERP エントリがある(=動的実行ファイル)と、カーネルはそこに書かれたインタプリタ(例 /lib64/ld-linux-x86-64.so.2)も同様に mmap し、エントリポイントを実行ファイル本体ではなくこの ld.so に設定します。静的リンク実行ファイルには PT_INTERP がなく、カーネルは本体のエントリポイントへ直接ジャンプします。動的リンクの面倒な後処理は、すべてこの ld.so がユーザー空間で担います。

動的リンカ(ld.so)の仕事

ld.so に制御が渡ると、リンカはまずカーネルが渡した補助ベクタ(auxiliary vector, AT_PHDR などプログラムヘッダの位置情報)を読み、自分自身と本体の配置を把握します。続いて三つの仕事を順に行います。

  1. 依存ライブラリのロード: 本体の .dynamic セクションにある DT_NEEDED を辿り、必要な共有ライブラリ(libc.so.6 など)を再帰的に探索・mmap する。探索パスは LD_LIBRARY_PATHDT_RUNPATHld.so.cache、既定ディレクトリの順で決まる(旧式の DT_RPATH だけは LD_LIBRARY_PATH より前に効くが非推奨)。
  2. 再配置(relocation)の適用: 各オブジェクトに記録された再配置エントリを処理し、まだ確定していなかったアドレスを実際の番地で埋める。
  3. シンボル解決(symbol resolution): 関数や変数の参照を、定義側の実アドレスに結びつける。

このうち再配置とシンボル解決が動的リンクの核心です。

再配置:埋めるべき空欄を埋める

共有ライブラリは、どの番地にロードされるか事前に決められません(ASLR でロードごとに動きます)。そこでコンパイラは、絶対番地を直書きせずロード時に埋める空欄として残し、その空欄の場所と埋め方を再配置エントリとして記録します。ld.so はロード後の実アドレスを使って空欄を埋めていきます。

x86-64 の主要な再配置型を押さえます。

再配置型埋める対象計算(概念)
R_X86_64_RELATIVE自オブジェクト内の絶対アドレス(PIE等)ロード基準アドレス + 加数
R_X86_64_GLOB_DATGOT 内のデータ/関数シンボル解決したシンボルの実アドレス
R_X86_64_JUMP_SLOTPLT 用 GOT エントリ(関数)解決した関数の実アドレス(遅延可)
R_X86_64_6464bit 絶対参照シンボルアドレス + 加数

R_X86_64_RELATIVE は最も多い型で、シンボル探索を伴わず「ロード基準アドレスを足すだけ」で済むため高速です。PIE 実行ファイルが起動時に処理する再配置の大半はこれで、起動コストの主因にもなります。

PLT/GOTと遅延束縛

外部関数の呼び出しを毎回フルにシンボル解決していては、起動時に全関数のアドレスを引く必要があり重くなります。そこで使われるのが PLT(Procedure Linkage Table)GOT(Global Offset Table) による間接呼び出しと、初回呼び出し時にだけ解決する**遅延束縛(lazy binding)**です。

仕組みは二段構えです。コードは外部関数を直接呼ばず、PLT 内のスタブを呼びます。PLT スタブは GOT エントリを間接ジャンプ先として参照します。

call printf@plt          ; コードは PLT スタブを呼ぶ
─────────────────────────
printf@plt:
    jmp  *GOT[printf]    ; GOT に入っている番地へ間接ジャンプ
    ; ↑初回は「解決ルーチン」を指す。解決後は printf 本体を指す
    push <reloc index>   ; 初回のみここに落ちる
    jmp  PLT[0]          ; ld.so の解決ルーチンへ(_dl_runtime_resolve)

初回呼び出しでは GOT エントリがまだ本体を指しておらず、解決ルーチン(_dl_runtime_resolve)に落ちます。ld.so がそこで printf の実アドレスを解決し、GOT エントリをその実アドレスで上書きしてから printf へ飛びます。二回目以降は GOT が本体を指しているので、jmp *GOT[printf] 一発で直接ジャンプし、解決のオーバーヘッドは消えます。これが遅延束縛で、一度も呼ばれない関数を解決しない分、起動を速くできます。

GOTはデータ、PLTはコード

GOT は書き込み可能なデータ領域に置かれ、ld.so が解決結果を書き込みます。PLT は読み取り・実行のみのコードで、内容は固定です。「コードは不変・データだけ書き換える」という分担により、位置独立なコードページを共有したまま、プロセスごとに異なる解決結果を GOT に持てます。

遅延束縛とセキュリティはトレードオフ

書き込み可能な GOT は攻撃の標的になり得ます(GOT 上書きで制御を奪う手口)。これを防ぐのが Full RELRO で、起動時にすべてのシンボルを即時解決(遅延束縛を無効化)したうえで GOT を読み取り専用に再マップします。起動はわずかに遅くなりますが、解決後の GOT 改ざんを封じられます。Full RELRO は本質的に即時束縛(LD_BIND_NOW 相当)を前提とするため、遅延束縛とは両立しません。

PIEとASLRの関係

ASLR で実行ファイル本体のコード/データまでランダム化するには、本体を任意の基準アドレスに置けなければなりません。これを可能にするのが PIE(Position Independent Executable) です。

PIE はすべての内部参照を、絶対番地ではなく**現在の命令位置からの相対(RIP 相対)**か、GOT を経由した間接参照として生成します。だからどこにロードしても正しく動き、ld.so は R_X86_64_RELATIVE 再配置でロード基準を足し込むだけで配置を確定できます。逆に PIE でない(位置依存の)実行ファイルは、テキスト/データが固定番地を前提にコンパイルされているため、本体だけは決まった番地にしか置けず、ここだけ ASLR が効きません(プロセスアドレス空間のレイアウトとASLR を参照)。

観点PIE 実行ファイル非PIE(位置依存)実行ファイル
本体のロード位置任意(ASLRでランダム化)リンク時に決めた固定番地
内部参照の方式RIP相対 / GOT経由絶対番地を直書き
起動時の再配置RELATIVE再配置が多く発生本体の再配置はほぼ不要
本体のASLR効く効かない(共有ライブラリは別途有効)
ビルド-fPIE -pie-no-pie

注意したいのは、共有ライブラリ(.so)はもともと位置独立としてビルドされるため、PIE でなくても mmap 配置はランダム化される点です。PIE が追加で動かすのは実行ファイル本体だけです。再配置と GOT という仕組みがあるからこそ、コードを固定番地に縛らずに済み、それが本体の ASLR を支えています。

つまずきポイント

  • セクションとセグメントは別の地図.text.bss はセクション(リンク時の単位)、PT_LOAD はセグメント(実行時の単位)で、一つのセグメントは複数セクションをまとめます。実行に効くのはセグメント側です。
  • 遅延束縛は関数だけ。データシンボル(外部変数)の参照は遅延できず、起動時に必ず解決されます。遅延できるのは PLT を経由する関数呼び出しのみです。
  • R_X86_64_RELATIVE はシンボル解決ではない。番地に基準を足すだけの再配置で、シンボルテーブルの探索を伴いません。PIE の起動コストの主因はこの大量の RELATIVE 処理で、シンボル解決の重さとは別物です。
  • PIE と ASLR は別レイヤー。ASLR はカーネルの緩和策、PIE はコンパイラ/リンカが作る性質です。PIE は ASLR が本体に効くための前提条件であって、PIE 自体がランダム化するわけではありません。

まとめ

  • カーネルは ELF のプログラムヘッダ(PT_LOAD)を保護属性付きで mmap し、MemSizFileSiz の差を BSS としてゼロ埋めする。PT_INTERP があれば ld.so に制御を渡す。
  • ld.so は依存ライブラリを再帰ロードし、再配置で空欄を実アドレスで埋め、シンボルをスコープ探索順で解決する。R_X86_64_RELATIVE は基準加算だけの軽い再配置で、PIE 起動コストの主因になる。
  • PLT/GOT による間接呼び出しと遅延束縛で、外部関数は初回呼び出し時にだけ解決し、GOT を上書きして以後は直接ジャンプする。Full RELRO は遅延束縛を捨てて GOT を読み取り専用化する。
  • PIE は内部参照を相対・間接にすることで任意配置を可能にし、それが本体の ASLR を成立させる。再配置と GOT がこの自由配置を支えている。

ロード後のアドレス変換のハードウェア側は 仮想記憶のアドレス変換とMMU/TLBの内部、カーネルとユーザー空間の境界をまたぐ呼び出し機構は システムコールのABIと呼び出し機構の内部 と合わせて読むと、配置・呼び出し・変換が一本につながります。

OS Article

ELFバイナリのロードとリンカ・ローダの内部を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

ELF

比較で見る軸

難易度: advanced / カテゴリ: OS / タグ数: 6

導入後に効く点

ld.soは共有ライブラリを依存順にロードして再配置(relocation)を適用し、シンボルをグローバルスコープの探索順で解決する。関数呼び出しはPLT/GOT経由で初回のみ解決する遅延束縛を使える。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
OS
タグ数
6

判断チェックリスト

  • 自社の用途が「ELF / 動的リンク」に近いか確認する。
  • 強みである「カーネルはELFのプログラムヘッダ(PT_LOAD)を読み、各セグメントを保護属性付きでmmapし、動的実行ファイルでは指定されたインタプリタ(ld.so)に制御を渡す。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

ELF動的リンクローダPLT/GOTASLRELF動的リンクローダ
参考: 公式情報