C 语言编译、链接与 ELF
更新时间:2026-08-25。本文是
languages/c/主题第 ③ 篇。从.c到可执行文件,中间发生了什么?理解编译链接,你才能看懂链接错误、理解动态库加载、明白perf拿到的符号来自哪里。
本文要回答的问题
gcc hello.c -o hello这条命令内部做了哪四件事?- 静态库
.a和动态库.so的区别是什么?为什么程序启动时找不到.so会报错? - ELF 文件里到底存了什么?
perf火焰图里的函数名是哪来的?
一、编译四阶段

| 阶段 | 命令 | 产物 | 输出内容 |
|---|---|---|---|
| ① 预处理 | gcc -E hello.c | hello.i | 展开 #include、#define 宏替换 |
| ② 编译 | gcc -S hello.c | hello.s | C → 汇编指令(可读文本) |
| ③ 汇编 | gcc -c hello.c | hello.o | 汇编 → 机器码,未链接(可重定位目标文件) |
| ④ 链接 | gcc hello.o -o hello | hello | 合并各 .o,解析符号,生成可执行文件 |
调试技巧:
gcc -S看汇编——本站 perf 反汇编 的热点视图就是这种格式。-Wall -Wextra开启警告;-O0 -g是本站默认(保留符号便于剖析)。
二、链接:符号解析与重定位
2.1 符号是什么
每个 .o 文件都导出/引用符号(函数名、全局变量名)。链接器做的事:
- 符号解析:把每个"未定义引用"(
U)对应到某个"已定义符号"(T/D)。 - 重定位:把各段的地址调整成最终加载地址,形成单一可执行文件。
2.2 用 nm 看符号
bash
gcc -c main.c
nm main.o输出中:T(代码段已定义)、D(已初始化数据)、B(BSS 未初始化)、U(未定义,待链接)。未定义符号必须由其他 .o 或库提供,否则报 undefined reference。
三、静态库与动态库
| 维度 | 静态库 .a | 动态库 .so |
|---|---|---|
| 本质 | 多个 .o 打包 | 独立 ELF 文件,运行时加载 |
| 链接时机 | 编译期打入可执行文件 | 加载/运行期动态解析 |
| 生成 | ar rcs libfoo.a foo.o | gcc -shared -fPIC -o libfoo.so foo.c |
| 使用 | gcc main.o -lfoo(-static 全静态) | gcc main.o -lfoo -L. -Wl,-rpath,<路径> |
| 体积 | 可执行文件变大 | 可执行文件小,.so 共享 |
| 升级 | 需重新编译可执行文件 | 替换 .so 即可(需兼容 ABI) |
| 依赖 | 无运行时依赖 | 运行时需能找到 .so |
常见报错:error while loading shared libraries: libfoo.so: cannot open shared object file —— 程序启动时找不到 .so。解决:LD_LIBRARY_PATH、ldconfig、或链接时加 -Wl,-rpath。
3.1 用 ldd 查动态依赖
bash
ldd ./hello
# linux-vdso.so.1 ...
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6ldd 列出运行时需要加载的共享库及解析路径——排查"缺库"的入口命令,详见 动态库加载与链接器。
四、ELF 文件格式
链接生成的可执行文件是 ELF(Executable and Linkable Format)。readelf -h 查看头部,readelf -S 查看节表:
| ELF 部分 | 内容 | 与 C 的对应 |
|---|---|---|
| ELF 头 | 类型、入口点、程序头/节头位置 | — |
| .text | 机器指令(只读) | 函数的编译结果 |
| .data | 已初始化全局/静态变量 | int g = 5; |
| .bss | 未初始化全局/静态变量(零初始化) | int g; |
| .rodata | 只读数据(字符串字面量) | "hello" |
| .symtab | 符号表(函数/变量名) | perf 定位函数名依赖它 |
| .strtab | 符号名字符串 | — |
| .debug_* | DWARF 调试信息(-g 产生) | 行号映射 |
为什么 -g 重要:.debug_* 提供源码行号 ↔ 指令地址的映射,perf annotate/火焰图才能把热点定位到具体行;去掉它(strip)后 perf 只剩函数地址。完整原理见 L5 ELF 与装载。
五、常见编译/链接错误速查
| 错误 | 原因 | 解决 |
|---|---|---|
undefined reference to 'foo' | 链接时找不到 foo 定义 | 补上定义/链接对应库 -lfoo |
multiple definition of 'g' | 两个 .o 都定义了全局符号 | 用 static 限制作用域 |
implicit declaration of function | 缺头文件/未声明 | #include 对应头文件 |
cannot open shared object file | 运行时找不到 .so | LD_LIBRARY_PATH/ldconfig/rpath |
Segmentation fault (core dumped) | 运行时崩溃 | 见 crash 排查线 |
六、与本站主线衔接
- L5 层:ELF 格式、装载、
mmap、符号表的完整原理,见 ELF 与二进制基础。 - perf:火焰图函数名来自
.symtab/.debug_*,反汇编热点见 perf 使用指南。 - 动态库:
LD_PRELOAD拦截、strace看加载过程,见 strace。 - 崩溃线:
strip后的 core 如何分析,见 core dump 排查。
一句话总结
编译链接 = 预处理→编译→汇编→链接四步把 C 变成机器码,.a 静态打入、.so 动态加载,最终产物是带符号表的 ELF 文件;看懂这条链,链接报错、缺库崩溃、perf 符号定位都不再神秘。