Skip to content

C Cpp 程序启动流程(ELF64)

C/C++ 程序的启动流程是一个复杂且严谨的过程,涉及操作系统内核、动态链接器、C 运行时库(CRT, C Runtime)以及用户代码的协同工作。虽然开发者通常认为程序从 main 函数开始执行,但在此之前,系统已经完成了一系列初始化操作。

以下基于 Linux x86_64 环境下的 ELF (Executable and Linkable Format) 文件格式,详细阐述 C/C++ 程序的通用启动流程。

1. 内核阶段:加载与执行 (execve)

程序的生命周期始于系统调用。当父进程(如 shell)决定执行一个程序时,会调用 execve 系统调用。

  1. 加载二进制文件:内核读取可执行文件的头部信息(ELF Header),检查文件格式有效性(如 Magic Number)。

  2. 创建进程地址空间:内核销毁当前进程的旧内存映射,根据 ELF 文件的程序头表(Program Header Table),将代码段(.text)、数据段(.data)、未初始化数据段(.bss)等映射到新的虚拟内存空间。

  3. 处理动态链接:

    • 如果程序是静态链接的,内核将程序计数器(RIP)直接指向 ELF 头中指定的入口地址(Entry Point)。

    • 如果程序是动态链接的(常见情况),内核会先加载 ELF 中指定的动态链接器(Interpreter,通常是 ld-linux.so),并将控制权交给动态链接器。

2. 动态链接阶段(仅动态链接程序)

在控制权移交给程序本身之前,动态链接器负责完成以下工作:

  1. 加载共享库:递归加载程序依赖的所有共享对象(.so 文件)。

  2. 重定位(Relocation):修正代码和数据中的地址引用。这包括处理全局偏移表(GOT)和过程链接表(PLT),确保函数调用和全局变量访问指向正确的内存地址。

  3. 初始化共享库:按依赖顺序的逆序,调用各个共享库的初始化函数(如 .init 段代码)。

完成这些工作后,动态链接器跳转到主程序的入口点。

3. 用户空间入口:_start

程序的真正入口点并非 main,而是汇编代码实现的 _start 符号。该符号由 C 运行时库(通常位于 crt1.o)提供。

_start 的主要职责是建立 C 语言运行环境,其汇编逻辑通常包含以下步骤:

  1. 清零帧指针:xor ebp, ebp。标记这是调用栈的最底层,防止栈回溯越界。

  2. 提取参数:从栈中获取 argc(参数个数)、argv(参数列表指针)和 envp(环境变量指针)。

  3. 栈对齐:确保栈指针(RSP)按 16 字节对齐(SIMD 指令集的要求)。

  4. 调用初始化函数:将关键参数压栈或存入寄存器,调用 __libc_start_main 函数。

4. C 运行时初始化:__libc_start_main

__libc_start_main 是 glibc 提供的核心初始化函数,它完成了绝大部分环境准备工作。其主要流程如下:

  1. 安全初始化:

    • 设置栈金丝雀(Stack Canary)值,用于检测栈溢出。

    • 初始化指针混淆(Pointer Guard)等安全机制。

  2. 线程环境初始化:初始化线程局部存储(TLS, Thread Local Storage)和 Pthreads 库。

  3. 注册清理函数:

    • 注册 rtld_fini:用于动态链接器的清理。

    • 注册 fini:用于程序退出时的清理。

    • 注册栈解退(Stack Unwind)相关处理函数。

  4. 调用全局初始化入口:调用 __libc_csu_init(或现代 glibc 中的等效逻辑),这是执行用户代码前最重要的步骤。

5. 全局构造与初始化函数

在 main 函数执行前,C++ 全局对象的构造函数以及 C 语言中标记为 __attribute__((constructor)) 的函数必须执行。这一阶段主要由以下两个 ELF 段控制:

  1. .init 段:包含可执行代码,是传统的初始化入口。

  2. .init_array 段:包含函数指针数组。现代编译器(GCC/Clang)主要使用此段。

    • 链接器会将所有标记为构造函数的代码地址收集到 .init_array 中。

    • 运行时库会遍历该数组,依次调用其中的函数。

    • C++ 的全局变量构造函数通常在此阶段执行。

执行顺序:通常先执行 .init 中的代码,再按顺序执行 .init_array 中的函数。

6. 用户主函数:main

经过上述所有准备后,环境已完全就绪,系统正式调用用户的 main(argc, argv, envp) 函数。

此时,程序进入用户编写的业务逻辑控制流。

7. 程序终止流程

当 main 函数执行完毕并通过 return 返回,或者在代码中调用 exit() 时,程序进入终止流程:

  1. 执行注册的退出函数:依次执行用户通过 atexit() 注册的函数(逆序执行)。

  2. 全局析构:执行 C++ 全局对象的析构函数和标记为 __attribute__((destructor)) 的函数。

    • 这主要通过遍历 .fini_array 段(函数指针数组)和执行 .fini 段代码来实现。
  3. 清理标准 I/O:刷新所有打开的文件缓冲区(flush buffers),关闭文件描述符。

  4. 系统调用退出:C 运行时库调用内核的 _exit (或 exit_group) 系统调用。

  5. 资源回收:内核销毁进程结构,回收内存、文件句柄等资源,并将退出码(Exit Code)返回给父进程。

总结流程图(文本版)

  1. Kernel (execve) -> 加载 ELF,映射内存

  2. Dynamic Linker (ld.so) -> 加载依赖库,重定位

  3. User Entry (_start) -> 汇编级栈准备

  4. Libc Init (__libc_start_main) -> 环境、TLS、安全初始化

  5. Constructors (.init / .init_array) -> 全局变量构造 / init 函数

  6. User Code (main) -> 业务逻辑

  7. Destructors (exit -> .fini_array / .fini) -> 全局变量析构 / fini 函数

  8. Kernel (_exit) -> 进程销毁