编译基础知识:编译器、链接器、构建系统概念、目标文件与可执行文件

做跨平台SDK编译,如果你连编译器、链接器、目标文件这些概念都搞不清楚,那后面遇到平台差异、链接错误、符号冲突时,会非常痛苦。我自己早期做嵌入式SDK时,就因为不理解目标文件和可执行文件的区别,在debug上浪费了整整两天。

这一节咱们就一次性把它理清。放心,我不跟你讲太多理论,咱们从实际编译过程入手,把每个环节拆开看。

编译器:从源码到目标文件

编译器干的事,说白了就是把人类看得懂的C/C++代码,转成机器能懂的二进制指令。但这并不是一步到位的。以GCC为例,它内部其实分四步:

  1. 预处理:处理#include、#define、条件编译等。生成.i文件。
  2. 编译:把.i转成汇编代码.s。
  3. 汇编:把.s转成目标文件.o。
  4. 链接:把多个.o和库文件组合成可执行文件或动态库。

我习惯用gcc -v看详细过程,能看到每一步调用了什么子程序。你看下面这个命令:

gcc -v -o hello hello.c

输出的日志里会显示cc1(编译器)、as(汇编器)、collect2(链接器)的调用。我建议你亲手跑一次,能把整个流水线刻在脑子里。

一个小技巧:你可以在编译时加-E只看预处理结果,加-S看汇编代码,加-c只生成目标文件不链接。这样能分步观察中间产物。

链接器:组合目标文件

编译器生成的是一个个独立的.o文件,每个.o内部有自己的变量、函数地址,但互相不知道对方在哪。链接器就是干这个的——它把各个.o的符号(函数、全局变量)重新定位,合并成完整的可执行文件。

链接器主要做两件事:

  • 符号解析:检查每个引用是否都有定义。如果函数声明了但没实现,这里会报undefined reference。
  • 重定位:把每个.o中的相对地址,换算成最终可执行文件里的地址或相对偏移。

我曾经遇到过一个坑:在两个不同的静态库里,定义了同名的全局变量。链接的时候没有报错,但运行时值总是不对。后来我查了链接器文档才发现,默认情况下弱符号规则会把两个定义合并成一个。打那以后,我写库时都会给全局变量加独特的命名前缀。

注意:动态链接(DLL、.so)比静态链接复杂得多。动态库的符号解析会延迟到运行时,如果目标系统上没有对应版本的动态库,程序会直接崩溃。跨平台SDK中我一般优先推荐静态链接,除非你很清楚每个平台的动态库策略。

目标文件与可执行文件

很多人以为.o和.exe是两种完全不同的格式。其实它们都遵循同样的容器规范——在Linux上是ELF,在Windows上是PE(COFF),在macOS上是Mach-O。

我整理了一个简单的对比表格:

特性目标文件 (.o / .obj)可执行文件 (.exe / 无后缀)
是否有入口点无(main或其他入口)有(_start或main)
是否可被加载执行不能可以
包含重定位信息包含完整的重定位表通常不包含(strip后)
包含符号表包含所有内部符号可以strip去掉
典型扩展名.o / .obj无 / .exe / .out

你可以用readelf -h(Linux)或dumpbin /HEADERS(Windows)查看目标文件和可执行文件的头部信息。本质上它们都是同一种文件格式,只是内容上有些差异。

核心理解:目标文件是“半成品”,可执行文件是“成品”。链接器就是把多个半成品组装成成品的那个工序。

构建系统:自动化编译

手动敲gcc -c a.c b.c在只有两个文件时还行,一旦项目超过几十个文件,你就需要构建系统了。构建系统盯着源文件的时间戳——哪个文件改了,它就只编译那个文件,以及所有依赖于它的文件。这能节省大量时间。

跨平台SDK里常用的构建系统:

  • Make:最经典,但语法容易写崩。我一开始写Makefile总被Tab和空格搞疯。
  • CMake:我现在的主力。它能生成各种平台的构建配置(Visual Studio工程、Xcode工程、Unix Makefile等)。
  • Ninja:专门为速度设计,配合CMake使用效果很好。
  • Bazel / Meson:适合大型项目,但学习曲线陡。

我个人建议:如果你的SDK要同时支持Windows、Linux、macOS、Android、iOS,直接用CMake。它虽然语法有点啰嗦,但生态最全。我踩过最深的坑是手动写Makefile时没处理好跨平台路径分隔符——Windows用\,Linux用/,CMake会自动适配。

下面这张图把整个编译流程串起来了。你可以先过一遍,后面各节我们会逐一深入。

编译流程总览 源文件 .c / .cpp 预处理 #include / #define 编译 语法分析/优化 汇编 .s → 机器码 目标文件 .o / .obj 链接器 (ld / link.exe) 可执行文件 .exe / ELF 💡 实际你只需执行 gcc -o hello hello.c 编译器会自动调用链接器,所有步骤对用户透明。 但理解每一步,能帮你快速定位问题出在哪个环节。

看完这张图你应该能感受到:编译不是一锤子买卖,而是一条流水线。中间任何一环出问题,都会体现为不同的错误信息。比如预处理报错通常是头文件找不到,编译报错是语法问题,链接报错是符号未定义。下次遇到错误时,先看信息里提到了哪个阶段(cc1、as还是ld),你就知道该往哪个方向查。