このページの目次
読み込みと起動: _start → main
カバー範囲: ELF 読み込みプロセス → _start → _libc_start_main → main → exit → auxv → AT* ベクトル → init/fini 配列 → 静的リンク vs 動的リンク 対象: glibc (x86-64 / ARM64), musl 比較
概要
C プログラムのエントリポイントは main ではなく、glibc の _start です。main が呼び出される前に、カーネルは多くの作業(ELF パース、スタック構成、auxv の渡す)を完了し、glibc も独自の初期化(TLS、vDSO のパース、stdio バッファリング、__attribute__((constructor)))を完了しています。このパスを理解することは、「main に到達する前にクラッシュする」といった問題のデバッグにおいて極めて重要です。
カーネル側: ELF 読み込み
flowchart TD
START["execve() → load_elf_binary()"]
START --> VALIDATE["🔍 ELF ヘッダーの検証<br/>magic + class + machine + ABI"]
VALIDATE --> PHDR["プログラムヘッダーテーブルの読み込み"]
PHDR --> LOAD["各 PT_LOAD をトラバース"]
LOAD --> MAP["elf_map() → mmap(セグメント)<br/>コードとデータをメモリにマップ"]
MAP --> INTERP{"PT_INTERP<br/>存在するか?"}
INTERP -->|"はい (動的リンク)"| LD["ld.so を読み込み<br/>ld.so のエントリアドレスを設定"]
LD --> RET_LD["カーネルが ld.so のエントリへ戻る<br/>(プログラムの _start ではない)"]
INTERP -->|"いいえ (静的リンク)"| ENTRY["e_entry を設定"]
ENTRY --> RET_START["カーネルがプログラムの _start へ戻る"]
classDef start fill:#e3f2fd,stroke:#1565c0
classDef step fill:#f3e5f5,stroke:#7b1fa2
classDef decision fill:#fff3e0,stroke:#ef6c00
classDef done fill:#e8f5e9,stroke:#2e7d32
class START start
class VALIDATE,PHDR,LOAD,MAP step
class INTERP decision
class LD,ENTRY,RET_LD,RET_START done
動的リンク: ld.so の実行
flowchart TD
START["カーネルが ld.so エントリへ戻る"] --> BOOT["① ld.so のブートストラップ<br/>ld.so 自身も動的リンクされているため<br/>まず制限付きで自身をリロケート"]
BOOT --> DYNAMIC["② メインプログラムの .dynamic を読み込み"]
DYNAMIC --> NEEDED["DT_NEEDED → libc.so.6, libfoo.so, ..."]
DYNAMIC --> DEBUG["DT_DEBUG → デバッグ構造体を設定<br/>(gdb が読み取る)"]
NEEDED --> DEPS["③ _dl_map_object_deps()<br/>トポロジソート → .so を順に mmap"]
DEPS --> RELOC["④ _dl_relocate_object()<br/>シンボル解決 + リロケーション<br/>GOT を埋め、IFUNC を解決"]
RELOC --> INIT["⑤ ライブラリの init 関数を呼び出し<br/>DT_INIT / DT_INIT_ARRAY<br/>→ __libc_init_first()"]
INIT --> START_["⑥ メインプログラムのエントリを呼び出し<br/>e_entry → _start"]
classDef start fill:#e3f2fd,stroke:#1565c0
classDef step fill:#f3e5f5,stroke:#7b1fa2
classDef done fill:#e8f5e9,stroke:#2e7d32
class START start
class BOOT,DYNAMIC,NEEDED,DEBUG,DEPS,RELOC,INIT step
class START_ done
ユーザー空間: _start
# glibc sysdeps/x86-64/start.S
_start:
xorl %ebp, %ebp # 最外層フレームをマーク (ebp=0 → バックトレースの終点)
movq %rdx, %r9 # rdx=デストラクタ関数 (rtld_fini)
popq %rsi # argc
movq %rsp, %rdx # argv
andq $-16, %rsp # 16 バイトスタックアライメント (ABI 要件)
pushq %rax # アライメント用フィラー
pushq %rsp # stack_end (__libc_stack_end 用)
xorl %r8d, %r8d # 8 番目の引数 (glibc 内部フラグ)
call __libc_start_main # → libc 初期化 → main
hlt # 到達しない (main は exit を通じて戻る)
__libc_start_main
// csu/libc-start.c (簡略化)
int
auxv (補助ベクトル)
// カーネルは execve 時にこれを構築し、envp の後にスタックに配置する
// 形式: { type, value } のペア、AT_NULL で終了
// 重要な AT_* タイプ:
AT_PHDR:
AT_PHENT: プログラムヘッダーエントリのサイズ
AT_PHNUM: プログラムヘッダーの数
AT_PAGESZ:
AT_BASE: ld.so のロードベースアドレス
AT_ENTRY: _start のアドレス
AT_UID / AT_EUID / AT_GID / AT_EGID: プロセスの資格情報
AT_SECURE:
AT_SYSINFO_EHDR: vDSO のアドレス
AT_RANDOM: ランダムデータ 16
AT_PLATFORM: CPU
auxv の読み取り
unsigned long hwcap = ; // CPU 機能フラグ
// 手動トラバース:
extern char **environ; // envp の直後に auxv が来る
# LD_SHOW_AUXV: ld.so に auxv を出力させる
LD_SHOW_AUXV=1
静的リンクとの比較
静的リンク:
- PT_INTERP がない → カーネルが直接 _start にジャンプ
- _start は依然として __libc_start_main を呼び出す (libc.a にリンク)
- 動的リンクではないため → PLT/GOT がない → 遅延バインディングなし
- 全体の libc がリンクされる → バイナリが巨大になる (hello world で ~800KB)
- ただし起動が非常に速い (シンボル解析なし、リロケーションなし)
musl の静的リンクは非常に人気がある (コンテナ世界):
- musl は _start → __libc_start_main → main までわずか ~100 行のコードで実装
- glibc の起動パスよりも約 10 倍短い
デバッグ
# 起動プロセスの追跡
# ld.so のデバッグ出力
LD_DEBUG=all |
# auxv の確認
LD_SHOW_AUXV=1
|
# init_array の確認
# startup パスの静的解析
()
()
参考
- glibc ソースコード:
sysdeps/x86-64/start.S,csu/libc-start.c,elf/dl-start.c,elf/rtld.c - musl ソースコード:
crt/crt1.c,ldso/dlstart.c— glibc よりも 10 倍シンプルで、学習におすすめ - カーネルソースコード:
fs/binfmt_elf.c— load_elf_binary - LWN: "How programs get run", "The init and fini arrays"
キーワード: _start, __libc_start_main, auxv, AT_PHDR, vDSO, .init_array, constructor, TLS, ld.so bootstrap, PT_INTERP