启动
C++ 进程的启动(以现代 Linux x86_64 + glibc + g++ 为例)本质上和 C 程序几乎完全一样,因为 C++ 的运行时模型是建立在 C 之上的。主要区别只在于全局对象/静态对象的构造(constructor)和析构(destructor)环节。
一个普通 C++ 可执行文件从 execve 到进入 main 的详细流程(主流 glibc 2.3x/2.4x 行为)。
1. 用户态发起 → 内核收到 execve 系统调用
// 比如你在 shell 里敲
./myprogram arg1 arg2
// shell 最终调用
execve("./myprogram", {"./myprogram", "arg1", "arg2", NULL}, environ);
内核进入 do_execve() → do_execveat_common() → 各种格式检查 → 最终走到 ELF 加载器 load_elf_binary()。
2. 内核加载 ELF 文件的主要动作(简化)
| 顺序 | 内核动作 | 关键影响 |
|---|---|---|
| 1 | 丢弃旧地址空间 | exec_mmap() → 原进程映像彻底消失 |
| 2 | 读取 ELF header | 检查魔数、架构、入口点 e_entry |
| 3 | 读取 program headers (PT_LOAD 等) | 决定要映射哪些段 |
| 4 | 映射 PT_LOAD 段 | 代码 rx、数据 rw、bss(零初始化) |
| 5 | 如果有 PT_INTERP | 读取动态链接器路径(通常 /lib64/ld-linux-x86-64.so.2) |
| 6 | 映射动态链接器本身 | ld.so 也被当作一个 ELF 加载进来 |
| 7 | 准备新栈 | 放 argc、argv、envp、auxv(非常重要) |
| 8 | 设置入口点 | 如果有解释器 → ld.so 的入口点;否则程序自己的 e_entry |
| 9 | 返回用户态 | 此时 RIP = ld.so 的 _start(动态链接最常见情况) |
静态链接的 C++ 程序会直接跳到自己程序的 _start。
动态链接(99% 的情况)会先跳到 ld.so 的 _start。
3. 动态链接器(ld-linux-x86-64.so.2)接管
ld.so 做的第一批事(非常早期,用户代码还没跑):
- 读取栈上的 auxv(辅助向量),里面有非常关键的信息:
- AT_ENTRY(真正程序的入口点地址)
- AT_PHDR(程序的 program header 表位置)
- AT_CLKTCK、AT_PAGESZ、AT_RANDOM 等
- 进行自身重定位(ld.so 自己也要重定位)
- 加载所有依赖的共享库(.so 文件)
- 对主程序和所有已加载的库执行重定位(REL/RELA)
- 调用所有已加载模块的 .init_array(包括 ld.so 自己的初始化函数)
- 最后跳到主程序的 _start(从 auxv 的 AT_ENTRY 得到)
4. 主程序的 _start(通常由 crt1.o / crti.o 提供)
现代 g++ 链接的 _start 大致长这样(汇编简化版):
1
_start: xor ebp, ebp ; 有的版本清 frame pointer pop rsi ; argc mov rdx, rsp ; argv(栈上紧跟 argc 后面) and rsp, -16 ; 栈 16 字节对齐(x86_64 System V ABI 要求) push rax ; 占位,保持对齐 push rsp ; 栈地址(__libc_start_main 的第 7 个参数) lea r8, [rel __libc_csu_fini] lea rcx, [rel __libc_csu_init] lea rdi, [rel main] ; main 地址 call __libc_start_main ; 永远不返回 hlt ; 万一返回了就死机
5. __libc_start_main(glibc 核心启动函数)
这是 glibc 里最关键的一环,大致顺序:
- 保存 main、init、fini、rtld_fini 等函数指针
- 注册 atexit 析构(主要是 rtld 的析构)
- 初始化 glibc 自身(__libc_init_first 等)
- 调用 __libc_csu_init(C++ 关键!)
- 调用你写的 main(argc, argv, envp)
- main 返回后 → exit(main 的返回值)
6. __libc_csu_init(C++ 全局/静态对象构造的关键)
// 简化伪码(来自 glibc csu/elf-init.c)
void __libc_csu_init(...) {
_init(); // .init 段(老式方式,很少用)
// 调用 .init_array 中的所有函数(最重要!)
for (每个函数指针 in __init_array_start 到 __init_array_end) {
(*函数指针)(argc, argv, envp); // 通常只传一个参数或无参
}
}
这里就是 C++ 全局对象 / 静态对象构造函数被调用的地方 (编译器把所有全局/命名空间/函数内 static 对象的构造都放进 .init_array)
7. 进入 main() → 用户代码开始执行
到这里才真正开始执行你写的 int main()。
8. 程序退出时的对称流程
- main 返回 → __libc_start_main 接手
- 调用 __libc_csu_fini → 执行 .fini_array 中的析构函数(全局/静态对象的析构)
- 调用 atexit 注册的函数
- 调用 _fini(老式)
- 最终调用 exit_group() /_exit 系统调用
总结流程
1
2
3
4
5
6
7
8
9
10
11
12
13
shell → execve()
↓
内核 load_elf_binary()
↓ 动态链接
ld.so _start → 重定位所有 .so → 调用所有 .init_array(ld.so 自己的)
↓
主程序 _start(crtbegin / crt1.o)
↓
__libc_start_main
↓
__libc_csu_init → 调用所有 .init_array(C++ 全局构造!)
↓
你的 main()
快速对照表
| 问题 | 答案 |
|---|---|
| C++ 进程从哪开始执行? | 动态链接:ld.so → 程序 _start → __libc_start_main → main |
| 全局对象什么时候构造? | __libc_csu_init 里 .init_array(在 main 之前) |
| 静态链接的 C++ 程序跳过 ld.so 吗? | 是,直接从程序自己的 _start 开始 |
| -static 链接会影响启动顺序吗? | 不会改变顺序,但 ld.so 阶段被跳过 |
| 用 new/delete 的全局对象呢? | 构造在 .init_array,析构在 .fini_array 或 atexit 机制 |
| 能绕过 glibc 启动代码自己写 _start 吗? | 可以,但非常不推荐(会失去栈对齐、构造/析构、std::cin 等初始化) |
停止
一个 C++ 进程(Linux x86_64 + glibc + g++/clang,2025–2026 年主流环境)在正常退出时(return main / std::exit / std::quick_exit / 线程全部结束等),会执行一系列清理动作。这些动作由 C 运行时(主要是 glibc)和 C++ 运行时(libstdc++)共同负责。
下面按实际执行顺序(从用户代码退出点开始)详细说明进程退出时通常会干什么(正常终止路径)。
1. main() 返回 或 调用 std::exit(status)
- 返回 main() 等价于 std::exit(main 的返回值)(C++ 标准保证)
- 显式调用 std::exit(status) 也一样
进入 __libc_start_main 的退出分支(或直接调用 exit)
2. glibc exit() 的主要清理流程(简化顺序)
glibc 的 exit() 大致做这些事(参考 glibc stdlib/exit.c 和 libc-start.c):
| 顺序 | 动作 | 谁负责 | 关键细节 / 注意事项 |
|---|---|---|---|
| 1 | 标记进程正在退出(设置标志) | glibc 内部 | 防止重入 exit |
| 2 | 调用所有注册的 atexit / on_exit / __cxa_atexit 处理函数(逆序,LIFO) | glibc __run_exit_handlers | 包括 C++ 全局/静态析构、用户 atexit |
| 3 | 冲洗所有标准流(stdin/stdout/stderr) | glibc | fflush(NULL) 语义 |
| 4 | 关闭所有打开的文件流(fcloseall) | glibc | 包括用户 fopen 的流 |
| 5 | 调用 _dl_fini(动态链接器 fini) | glibc → ld.so | 共享库的 .fini_array / 析构 |
| 6 | 最终调用 _exit(status) 系统调用 | glibc | 内核接管,进程彻底终止 |
最关键的部分:第 2 步的 exit handlers 处理。
3. exit handlers 的详细执行顺序
C++ 把全局/静态/线程局部对象析构也注册到这个机制里(通过 __cxa_atexit)。
实际顺序(主流 glibc + libstdc++ 行为):
- 用户通过 std::atexit / std::at_quick_exit 注册的函数(逆注册顺序)
- 先注册的后调用(LIFO)
- C++ 全局/命名空间/函数内 static 对象的析构(~Xxx())
- 析构顺序 ≈ 构造的逆序(大致,标准只部分保证)
- 通过 __cxa_atexit 注册,带__dso_handle(支持 dlclose 时单独析构)
- 线程局部存储(thread_local)变量的析构(当前线程的)
- 每个线程独立,通过 __cxa_thread_atexit
- 共享库的 .fini_array 函数(动态链接器调用)
- 每个已加载的 .so 的 fini 函数(包括 libc 自己的)
- 其他 glibc 内部清理(如 malloc 统计、locale 析构等)
一句话记忆顺序(最常见情况):
1
2
3
4
5
6
7
8
9
10
11
12
13
main 返回 / std::exit()
↓
atexit 注册的函数(用户自定义,LIFO)
↓
C++ 全局/静态对象析构(~Global(),大致逆构造序)
↓
thread_local 变量析构(当前线程)
↓
共享库 fini / _dl_fini
↓
fflush(NULL) + fcloseall
↓
_exit() 系统调用 → 进程结束
4. 特殊情况对比表
| 退出方式 | 调用全局/静态析构? | 调用 atexit 回调? | 冲洗/关闭流? | 线程局部析构? | 备注 |
|---|---|---|---|---|---|
| return main() / std::exit() | 是 | 是 | 是 | 是(当前线程) | 正常 C++ 退出 |
| std::quick_exit() | 否 | 否(只 at_quick_exit) | 否 | 否 | 快速退出,不析构 |
| _exit(status) / exit_group() | 否 | 否 | 否 | 否 | 内核直接杀进程 |
| abort() | 否 | 否 | 否 | 否 | 核心转储,SIGABRT |
| 线程全部结束(main 线程退出) | 是(如果 main 退出) | 是 | 是 | 是 | pthread_exit 不等同进程退出 |
| dlclose() 一个共享库 | 是(该库的静态对象) | 是(该库注册的) | 否 | 否 | 通过 __cxa_finalize |
5. 常见问题
- 析构没执行 → 用 _exit() / quick_exit() / 异常没捕获 + std::terminate / kill -9 等暴力方式
- 析构顺序不可靠 → 不同翻译单元间的全局对象构造/析构顺序未定义(经典“static initialization order fiasco”),退出时同理
- 想完全禁用退出时析构 → 编译时加 -fno-use-cxa-atexit(不标准)或链接时避免注册,或用 quick_exit
- 性能敏感场景 → 很多人故意避免全局对象(或用 trivial destructor),或用 -Wl,–no-as-needed 控制,或直接 _exit(0) 绕过
- 调试技巧 → 在 gdb 里设断点到 __run_exit_handlers /__cxa_finalize /_dl_fini,就能看到退出时真正调了什么
其他
链接选项加上,避免plt延迟绑定,第一次调用时产生延迟(代价启动时间变长)
1
-z now