本页目录
加载与启动: _start → main
覆盖: ELF 加载过程 → _start → _libc_start_main → main → exit → auxv → AT* vectors → init/fini arrays → 静态链接 vs 动态链接 适用: glibc (x86-64 / ARM64), musl 对比
概述
一个 C 程序的入口不是 main——而是 glibc 的 _start。在 main 被调用之前,内核已经做了大量工作(ELF 解析、栈布置、auxv 传递),glibc 也完成了自己的初始化(TLS、vDSO 解析、stdio 缓冲、__attribute__((constructor)))。理解这条路径对于调试"程序还没到 main 就 crash"至关重要。
内核侧: ELF 加载
flowchart TD
START["execve() → load_elf_binary()"]
START --> VALIDATE["🔍 验证 ELF header<br/>magic + class + machine + ABI"]
VALIDATE --> PHDR["读 program header table"]
PHDR --> LOAD["遍历每个 PT_LOAD"]
LOAD --> MAP["elf_map() → mmap(segment)<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 entry"] --> BOOT["① ld.so 自举 (bootstrap)<br/>ld.so 自己是动态链接的<br/>先有限制地重定位自己"]
BOOT --> DYNAMIC["② 读主程序 .dynamic"]
DYNAMIC --> NEEDED["DT_NEEDED → libc.so.6, libfoo.so, ..."]
DYNAMIC --> DEBUG["DT_DEBUG → 设置 debug 结构<br/>(gdb 读取)"]
NEEDED --> DEPS["③ _dl_map_object_deps()<br/>拓扑排序 → 逐个 mmap .so"]
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=destructor function (rtld_fini)
popq %rsi # argc
movq %rsp, %rdx # argv
andq $-16, %rsp # 16 字节栈对齐 (ABI 要求)
pushq %rax # 对齐 filler
pushq %rsp # stack_end (用于 __libc_stack_end)
xorl %r8d, %r8d # 第 8 个参数 (glibc internal flag)
call __libc_start_main # → libc 初始化 → main
hlt # 永不到达 (main 通过 exit 返回)
__libc_start_main
// csu/libc-start.c (简化)
int
auxv (Auxiliary Vector)
// 内核在 execve 时构建, 放在栈上 envp 之后
// 格式: { type, value } pairs, 终止于 AT_NULL
// 关键 AT_* types:
AT_PHDR: program header
AT_PHENT: program header entry size
AT_PHNUM: program header 数量
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 bytes of random
AT_PLATFORM: CPU
读取 auxv
unsigned long hwcap = ; // CPU feature flags
// 手动遍历:
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 → 无 lazy binding
- 整个 libc 链接进去 → 二进制巨大 (~800KB for hello world)
- 但启动极快 (无符号解析, 无重定位)
musl 的静态链接非常流行 (容器世界):
- musl 从 _start → __libc_start_main 到 main 只需 ~100 行代码
- 比 glibc 的启动路径短 ~10x
调试
# 追踪启动过程
# 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 简单 10x, 学习推荐 - 内核源码:
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