1.2 从源代码到可执行程序
“编译程序”这句话经常指的是整个构建流水线,而不只是某个编译器可执行文件(compiler executable)。在 C 风格系统里,源代码可能经过预处理、编译、汇编、链接、加载、动态链接、运行时初始化,最后才进入用户代码。每一步都有不同输入、输出、失败模式和调试工具。高级编译器学习必须理解一个工具的职责在哪里结束,另一个工具的职责从哪里开始。
考虑一个两文件程序:
// main.c
#include "calc.h"
int main(void) {
return twice(21);
}
// calc.c
int twice(int x) {
return x * 2;
}当它变成可执行文件时,编译器并不是把两个文件神奇地当作一个整体读取。每个源文件通常会被编译成单独的翻译单元(translation unit)。编译器可以为每个翻译单元检查语法并生成目标代码,但它不一定知道每个函数最终地址。目标文件会包含符号和重定位记录,让链接器稍后把各部分连接起来。
预处理与翻译单元
在带有预处理器的语言中,预处理会在正常编译前变换源文本。它展开 include,替换宏,并计算条件编译指令。这个能力很强,也很危险,因为语法分析器(parser)看到的并不完全是你写的文件;它看到的是预处理后的翻译单元。
例如:
#define SIZE 4
int data[SIZE];语法分析器实际上接近看到:
int data[4];这意味着预处理会影响诊断、构建性能、依赖追踪和可复现性。头文件包含可能把大量文本复制到多个翻译单元中。条件编译可能制造很少被测试的构建配置。宏展开可能在看起来和原始源码不太对应的位置产生错误。许多现代语言设计会避免文本级预处理,但理解它对于系统语言和构建工具非常关键。
编译器、汇编器、链接器、加载器
简化的本地构建流水线(native build pipeline)如下:
main.c
-> preprocessor
main.i
-> compiler frontend/middle/backend
main.s
-> assembler
main.o
-> linker
app
-> loader
running process狭义编译器负责语言理解和目标代码生成。它可能输出汇编文本、目标代码、字节码或另一种 IR。汇编器把汇编变成目标文件。目标文件包含代码、数据等节(section),以及符号和重定位信息。链接器合并目标文件和库,解析外部引用,布局节,并写出可执行文件或共享库。加载器属于操作系统运行路径;它把可执行文件映射到内存,加载所需动态库,在必要时应用重定位,准备初始栈,然后把控制权交给程序入口。
这些边界对调试很重要。语法错误不是链接错误。未定义符号不是语法分析器失败。程序启动时缺少共享库不是优化器缺陷。如果你能识别失败阶段,就能选择正确工具:预处理输出、编译器诊断、汇编列表、nm、objdump、链接器映射(linker map)、加载器跟踪或运行时日志。
符号与重定位
当一个目标文件引用另一个目标文件中定义的函数时,编译器不一定能填入最终地址。目标文件会表达类似这样的请求:“这里引用了符号 twice,最终布局确定后请修正。”这个请求通过重定位信息表示。
目标文件有符号表。有些符号由当前目标文件定义,有些是未解析导入。链接器收集定义,并用它们满足导入。如果某个导入没有定义,链接器报告未定义引用(undefined reference)。如果多个强定义声称拥有同一符号,链接器可能报告重复符号(duplicate symbol)。静态库会让问题更复杂,因为链接器可能只拉取当前未解析符号所需要的目标文件。
这也是某些工具链里库顺序会影响构建结果的原因。如果库出现在需要它的目标文件之前,链接器可能不会重新搜索它。现代构建系统隐藏了一部分细节,但底层模型仍然塑造错误消息和链接行为。
优化级别与调试构建
构建命令常包含 -O0、-O2、-g、-Wall、-march 或 --target 等参数。这些参数会改变编译器行为。优化级别影响编译器重写 IR 和机器码的激进程度。调试参数保留能把生成代码映射回源码概念的信息。目标参数影响指令选择和 ABI 假设。
同一个源程序在不同参数下可能生成不同机器码,同时仍然保持语言定义的行为。这并不矛盾。源代码描述语义;机器码只是语义的一种实现。优化器可以在证明可观察行为不变时改变实现细节。这也是 C/C++ 等语言中未定义行为(undefined behavior)如此重要的原因:如果语言没有给某个程序定义含义,优化器受到的约束就更少。
跟踪一个符号穿过构建产物
设 main.mini 调用另一个文件定义的 printTotal。编译调用方(caller)时尚不知道最终地址,因此目标文件记录符号引用和重定位:“把这里补成 printTotal 的地址”。定义所在的目标文件导出该符号;链接器匹配两者、布置节、应用重定位,动态链接时还可能生成桩(stub)。加载器映射段(segment)、解析剩余动态符号、建立进程状态并跳到入口点(entry point)。
所以未定义引用多半是链接期的符号解析问题,不是语法分析器;找不到共享库是加载期配置问题;静态初始化器崩溃才是运行时问题。排障时应保留预处理源码、目标文件、符号表、重定位、可执行文件和调试信息。
构建卫生
- 依赖图必须包含生成文件、文法、编译参数和工具版本。
- 追求可复现构建:路径、时间、环境与依赖版本都应稳定。
- 调试信息是一等产物;没有源码映射(source map)的优化代码很难排查。