
文章目录先回答一个问题编译器和链接器到底在忙什么1. 翻译环境和运行环境1.1 翻译环境把源代码变成可执行文件1.2 先区分两个环境2. 翻译环境的四步2.1 预处理预编译先把源文件整理好2.2 编译把 C 语言翻译为汇编2.2.1 词法分析字符组成一个个记号2.2.2 语法分析记号是否符合 C 的句子结构2.2.3 语义分析结构合法还要“有意义”2.3 汇编把汇编文本变成目标文件2.4 链接解决“这个名字和地址在哪里”3. 运行环境把可执行文件变成一个进程4. 用命令亲眼观察每个中间产物5. 常见报错应该归到哪个阶段6. 容易混淆的两件事6.1 头文件不是库6.2 .o 文件不是“半个可执行文件”总结先回答一个问题编译器和链接器到底在忙什么C 源代码是给人看的文本处理器只能执行特定指令编码。工具链要完成几次“翻译和拼接”先处理文本指令再把 C 语句翻译成汇编和机器码最后把多个文件及库中的代码地址接起来生成操作系统可以加载的可执行文件。预处理处理文本编译理解 C汇编生成目标文件链接解决跨文件关系运行环境负责加载和执行。不同平台的文件扩展名可能不同Windows 常见 .obj/.exeLinux 常见 .o/无扩展名但这条主线基本一致。本文用 GCC/Clang 风格命令演示Visual Studio 的按钮名称不同背后的阶段类似。1. 翻译环境和运行环境1.1 翻译环境把源代码变成可执行文件翻译环境包括预处理器、编译器、汇编器和链接器。它们可以由一个编译器驱动程序如 gcc按顺序调用也可以手动拆开观察。gcc hello.c-ohello ./hello-o hello 只是指定输出文件名。命令成功只说明翻译阶段没有发现错误不保证程序逻辑正确更不保证运行时不会崩溃。1.2 先区分两个环境翻译环境解决“能不能生成程序”运行环境解决“程序启动后怎样获得资源并执行”。2. 翻译环境的四步2.1 预处理预编译先把源文件整理好预处理器主要处理以 # 开头的指令include把头文件内容展开到当前位置define进行宏替换if、ifdef根据条件决定保留哪些文本删除注释并处理行连接等规则。#includestdio.h#defineMESSAGEhellointmain(){puts(MESSAGE);return0;}用 -E 只做预处理gcc-Ehello.c-ohello.i打开 hello.i会看到大量 stdio.h 声明以及 MESSAGE 已经变成字符串字面量。预处理器并不理解变量类型宏只是文本替换所以要给复杂宏的参数和整体表达式加括号#defineSQUARE(x)((x)*(x))SQUARE(1 2) 如果定义成 x * x会变成 1 2 * 1 2结果不是 9。宏参数还可能被求值多次SQUARE(i) 就是带有副作用危险用法。2.2 编译把 C 语言翻译为汇编编译器读取预处理结果经过词法分析、语法分析和语义分析再进行优化并生成汇编文本。可以用 -S 停在汇编阶段gcc-Shello.c-ohello.s2.2.1 词法分析字符组成一个个记号词法分析器从左到右扫描字符把字符归类为关键字、标识符、常量、运算符和分隔符。例如inttotalprice1;会被看成 int关键字、total标识符、、price、、1 和 ;。无法识别的字符会在这一阶段附近报错intn32;// 不是这里合法的 C 运算符2.2.2 语法分析记号是否符合 C 的句子结构语法分析器根据 C 语言文法建立表达式和语句的结构。括号、分号和大括号缺失往往属于语法错误intmain(void){puts(hi)// 少了分号return0;}编译器可能把错误位置标在下一行因为它直到继续读取后才确定前面的结构无法闭合。看到 expected ‘;’ 时先检查上一行和配对括号。2.2.3 语义分析结构合法还要“有意义”语义分析会检查类型、声明、作用域和表达式规则。下面代码的结构像赋值语句但左边不是可修改对象3value;// 语义错误常量不能被赋值再看一个跨阶段的例子intadd(int,int);// 只有声明intmain(){returnadd(2,3);// 调用形式正确}编译器可以据此生成目标代码但如果工程没有提供 add 的定义链接器才会报告未定义引用。这说明“编译通过”不等于“链接通过”。2.3 汇编把汇编文本变成目标文件汇编器把 .s 中类似 mov、call 的指令翻译成机器码并生成目标文件.o 或 .objgcc-chello.c-ohello.o-c 会完成预处理、编译和汇编但不进行链接。目标文件已经包含机器码却可能仍有无法确定的地址例如调用了另一个源文件里的函数。它还包含符号表、重定位信息和调试信息启用 -g 时。2.4 链接解决“这个名字和地址在哪里”链接器把多个目标文件和库合并完成两件事符号解析把一个文件中对 add 的调用匹配到另一个文件提供的 add 定义重定位目标文件生成时还不知道最终地址链接器根据合并后的布局修补跳转和数据引用。准备两个源文件// math.cintadd(inta,intb){returnab;}// main.c#includestdio.hintadd(int,int);// 函数接口声明intmain(){printf(%d\n,add(2,3));return0;}分别编译再链接gcc-cmath.c-omath.o gcc-cmain.c-omain.o gcc main.o math.o-ocalc运行 calc 输出 5。如果漏掉 math.o通常会出现 undefined reference to addWindows 链接器有类似提示。这不是语法错误而是链接器找不到定义。标准库函数也需要链接头文件只提供声明不会把实现本身复制进程序。3. 运行环境把可执行文件变成一个进程操作系统启动程序时会创建进程、映射代码和数据、准备栈与堆再调用 C 运行库初始化代码最后进入 main。main 返回的整数会交给操作系统通常 0 表示成功非 0 表示失败。#includestdio.hintmain(){puts(program is running);return0;// 返回给操作系统的状态码}翻译环境解决“能不能生成程序”运行环境解决“程序启动后怎样获得资源并执行”。数组越界、空指针和未初始化变量通常不会在链接阶段报错而是在运行时造成未定义行为。4. 用命令亲眼观察每个中间产物以 hello.c 为例可以依次执行gcc-Ehello.c-ohello.i# 预处理文本变大gcc-Shello.i-ohello.s# 编译得到汇编gcc-chello.s-ohello.o# 汇编得到目标文件gcc hello.o-ohello# 链接得到可执行文件也可以让 gcc 自动完成前面步骤只保留目标文件gcc-chello.c-ohello.o.i 和 .s 通常是文本.o 和可执行文件是二进制不能用普通文本编辑器判断它们是否正确。5. 常见报错应该归到哪个阶段现象常见阶段先检查什么expected ;、括号不匹配语法分析分号、大括号、上一行undeclared identifier语义分析声明、头文件、作用域undefined reference/unresolved external链接是否加入定义所在的 .o/库运行后崩溃、输出错误运行环境/逻辑越界、空指针、未初始化变量分阶段编译的好处就是可以判断错误发生在翻译的哪一步。不要看到所有红字都统称为“编译错误”。6. 容易混淆的两件事6.1 头文件不是库头文件提供声明、宏和类型定义库提供已经编译好的函数实现。include 让编译器知道函数怎么调用链接器才负责把实现接入。6.2 .o 文件不是“半个可执行文件”它是一个可以重定位的目标文件代码和数据已经有机器表示但地址和跨文件符号可能未最终确定。多个 .o 能被链接成一个程序也能被打包进静态库。总结在最初的学习过程中可以不必要一定了解编译与链接的底层实现本文只浅谈编译与链接的一些简要大致内容让人起码能够知晓一段C语言代码是如何一步步变成一个可执行程序进而了解到从源代码到程序并不是神秘黑盒而是一条可以拆开的流水线预处理让文本就位编译器确认 C 语言结构和含义汇编器产出机器码目标文件链接器把符号和地址接好运行环境再创建进程并调用 main。。