このページの目次

読み込みと起動: _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 __libc_start_main(
    int (*main)(int, char **, char **),  // main 関数
    int argc,
    char **argv,
    void (*init)(void),                   // __libc_csu_init
    void (*fini)(void),                   // __libc_csu_fini
    void (*rtld_fini)(void),              // ld.so クリーンアップ関数
    void *stack_end)
{
    // 1. TLS (スレッドローカルストレージ) の設定
    __pthread_initialize_minimal();

    // 2. スタック保護 (canary) の設定
    __stack_chk_guard = _dl_setup_stack_chk_guard();

    // 3. auxv の読み取り → グローバル変数の初期化:
    //    __environ = envp;
    //    __libc_stack_end = stack_end;
    //    _dl_phdr = AT_PHDR;
    //    _dl_phnum = AT_PHNUM;
    //    __page_size = AT_PAGESZ;

    // 4. init を呼び出し → __libc_csu_init()
    //    → .init_array をトラバース → __attribute__((constructor)) 関数を実行
    //    → これには _init() (存在する場合) も含まれる
    (*init)(argc, argv, envp);

    // 5. main を呼び出し
    result = (*main)(argc, argv, __environ);

    // 6. main の戻り値 → exit(result)
    exit(result);
}

auxv (補助ベクトル)

// カーネルは execve 時にこれを構築し、envp の後にスタックに配置する
// 形式: { type, value } のペア、AT_NULL で終了

// 重要な AT_* タイプ:
AT_PHDR:       プログラムヘッダーのアドレス (dl_iterate_phdr 用)
AT_PHENT:      プログラムヘッダーエントリのサイズ
AT_PHNUM:      プログラムヘッダーの数
AT_PAGESZ:     ページサイズ (4KB / 16KB / 64KB)
AT_BASE:       ld.so のロードベースアドレス
AT_ENTRY:      _start のアドレス
AT_UID / AT_EUID / AT_GID / AT_EGID: プロセスの資格情報
AT_SECURE:     セーフモードが必要かどうか (setuid → 1)
AT_SYSINFO_EHDR: vDSO のアドレス
AT_RANDOM:     ランダムデータ 16 バイト (ASLR、スタックカナリー用)
AT_PLATFORM:   CPU プラットフォーム文字列 (: "x86_64")

auxv の読み取り

#include <sys/auxv.h>
unsigned long hwcap = getauxval(AT_HWCAP);  // CPU 機能フラグ

// 手動トラバース:
#include <elf.h>
extern char **environ;  // envp の直後に auxv が来る
# LD_SHOW_AUXV: ld.so に auxv を出力させる
LD_SHOW_AUXV=1 /bin/true

静的リンクとの比較

静的リンク:
  - 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 倍短い

デバッグ

# 起動プロセスの追跡
strace -e trace=mmap,open,read /bin/true

# ld.so のデバッグ出力
LD_DEBUG=all /bin/true 2>&1 | head -100

# auxv の確認
LD_SHOW_AUXV=1 /bin/true
cat /proc/self/auxv | xxd

# init_array の確認
readelf -x .init_array /bin/ls

# startup パスの静的解析
gdb /bin/ls
(gdb) break _start
(gdb) run

参考

  • 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