从源码到进程:02.目标文件的生成
前言
在之前的文章中,我们初步讲解了从源码到进程这个主题的预处理部分,在这篇文章中,我们将讲解目标文件(.o)的生成过程。
通过了解 .o文件,可以帮助我们:
- 定位
undefined reference/multiple definition等链接错误的根源 - 理解重定位失败与对齐问题导致的运行时崩溃
- 掌控编译器、汇编器与链接器之间的契约(ABI / ELF / linker script)
- 为什么很多编码规范中,不允许在头文件中定义或者声明全局变量
核心理论
什么是目标文件?
目标文件(.o)是编译器将源码转化为机器码的中间产物,遵循 ELF(Executable and Linkable Format)格式。它包含代码、数据和元信息(如符号表、重定位表),但还不是可执行程序,需要链接器进一步处理。
生成目标文件是编译过程的第三步,位于以下流程中:
- 预处理:展开宏和头文件,生成 .i 文件。
- 编译:将源码转为汇编代码(.s 文件),并优化。
- 汇编:将汇编代码转为机器码,打包为 .o 文件。
- 链接:合并多个 .o 文件和库,生成可执行文件。
ELF文件与 .o 文件的核心结构
在 Linux 上,.o 文件通常遵循 ELF规范。一个典型的 ELF 可重定位文件包含:
- ELF Header:文件起始,包含魔数、位宽(ELF32/ELF64)、文件类型(REL 表示 relocatable)等。
- Section Header Table:节区(section)的目录,每个节区都有名字、类型、大小、对齐等属性。
- Sections(节区):实际的数据或元数据,常见节区有:
.text:机器指令(函数体).rodata:只读常量(字符串字面量、常量数组).data:已初始化全局/静态变量.bss:未初始化全局/静态变量(NOBITS).symtab/.strtab:符号表与字符串表.rela.*/.rel.*:重定位表(记录需要在链接或加载时修正的地址).debug_*:DWARF 调试信息(可选)
这种分节存储将代码、可读数据、可写数据和元信息解耦,使链接器可以按节合并、重定位,并允许调试器与性能分析工具独立读取所需信息。
符号表(Symbol Table)与名字修饰
符号表记录每个符号(函数、变量、文件名)在目标文件中的属性:名称、值(在可重定位文件中通常为节内偏移或 0)、大小、类型(FUNC/OBJECT)、绑定(LOCAL/GLOBAL/WEAK)与所属节(Ndx)。
- LOCAL 符号仅在当前翻译单元可见。
- GLOBAL 符号可被链接器解析并跨文件引用。
- WEAK 符号允许被其它强符号覆盖(常用于库或可选实现)。
C++ 采用名字修饰(name mangling)将类型信息编码到符号名中,链接器基于这些 mangled names 做匹配(比如 _Z3foov)。
排障技巧:使用 nm, readelf -s, objdump -t 查看符号属性;注意 UND(未定义)、ABS(绝对符号)、T(text 段)等标志。
重定位(Relocation)机制
.o 文件不是可执行镜像,许多地址(函数调用、全局变量地址、常量地址等)在编译后仍然是相对或占位的。重定位表记录哪些位置需要在链接(静态链接)或加载(动态加载/内核模块)时修正。
重定位包含三要素:
- 偏移(offset)
- 符号(symbol)
- 类型(relocation type)。
不同架构有不同的重定位类型(如 x86_64 的 R_X86_64_PC32 / R_X86_64_64 等),决定链接器如何将符号地址写入目标位置(绝对/相对/高/低半字等)。
重定位错误会导致页错误、未知指令或控制流错误;对于位置无关代码(PIC / PIE)与内核模块,正确的重定位处理尤其关键。
就像之前一直说的,生成源码的四个步骤中。汇编器负责把汇编代码的标签变为节内偏移并产生重定位条目(如果引用外部符号则不填地址)。这正是 .o 成为“部分实现 + 重定位数据”的原因。
实操演示
// object.cpp
int g_var_init = 10;
int g_var_uninit;
static int l_var_init = 15;
static int l_var_uninit;
extern void extern_func();
int main()
{
extern_func();
return 0;
}
通过如下命令生成目标文件
g++ -c object.cpp -o object.o
通过如下命令可以看到elf的头部
readelf -h object.o

观察输出信息可以发现,存在一些魔数,标记文件的真正类型;文件的类型是可重定位文件,因为目标文件不是真正的可执行程序,需要链接器将多个目标文件链接在一起之后,为每个符号找到真正的地址才是可以运行
通过如下命令查看节区表
readelf -S object.o

其中 .data .bss 段分别存储着初始化和未初始化的 全局变量与静态变量,大小分别都是8个字节,因为都是两个int变量。
可以看到 .bss 与 .comment的偏移量,也就是在文件中的实际读取位置都是0000005c。 是因为.bss 段在文件中是不占大小的,只是运行时在内存中占有大小,并且初始化为0;
此外各个段的地址全是 0,因为是可重定位文件,此时分配没有意义。
通过如下命令查看符号表
readelf -s object.o

可以看见,_ZL10l_var_init,_ZL12l_var_uninit,g_var_init,g_var_uninit,_Z11extern_funcv 这样的几个符号。
其中_ZL10l_var_init,g_var_init的Ndx都是3,因为他们都是在 .data 段, Value值分别是0和4,是因为他们在.data段的位置偏移分别是0和4。
同理_ZL12l_var_uninit,g_var_uninit的Ndx都是4,Value值分别是0和4,因为他们在 .bss 段,偏移0个字节和4个字节之后,分别读取4个字节的大小就是对应点的变量的值。
_Z11extern_funcv 这个符号的 UND是因为,这是个外部符号,需要再链接确定。
同时,看到 _ZL10l_var_init,g_var_init的Bind类型分别是 local与global,是因为static这个标识符限定了这个符号本身是局部的,只在当前目标文件可用,链接器是访问不了的。
通过如下命令查看重定位表
readelf -r object.o

_Z11extern_funcv 这个符号,链接器会在连接时候修正,实现call extern_func这个函数的操作
常用命令
readelf -h file.o # 查看 ELF 头
readelf -S file.o # 列出节区
readelf -s file.o # 符号表
readelf -r file.o # 重定位表
nm file.o # 简易符号表(T/U/D 等)
objdump -d file.o # 反汇编
objcopy --strip-debug in.o out.o # 去除调试段
strip file.o # 去符号表与调试信息
addr2line -e vmlinux <addr> # 将地址转为 file:line(对 vmlinux)
总结
-
.o文件是 可重定位文件,遵循 ELF 格式。 -
它包含 节区(代码/数据/元信息)、符号表 和 重定位表。
-
通过符号表和重定位机制,编译器和链接器能够协同工作,把多个
.o文件合并为一个可执行程序。 -
理解
.o文件能帮助我们解决:- 链接错误(undefined reference / multiple definition);
- 符号可见性问题(static / global / weak);
- 调试信息与 strip 的作用;
- 为什么不应该在头文件中定义全局变量。
一句话:
目标文件 (.o) 是源码到可执行程序的关键中间形态,它承载了机器码、符号与重定位信息,是编译器与链接器之间的桥梁。

浙公网安备 33010602011771号