GCC 编译器这个名字,在 Linux 和嵌入式开发里几乎是绕不开的。很多人刚开始接触的时候觉得它不过是一条命令, gcc xxx.c -o xxx 就完事了;可一旦手头的代码从单文件变成多文件,再变成带第三方库的工程,问题就接踵而来:头文件找不到、 undefined reference 、一堆 warning 不知道从哪儿下手。这篇就围绕 GCC 编译器本身,从最简单的单文件编译一路聊到多文件工程、静态库动态库和 Makefile 自动化构建,把我平时真正会用到的东西完整讲一遍。适合刚学 C/C++ 的读者,也适合那些一直依赖 IDE、想搞清楚“编译到底在干嘛”的开发同学。
1. 编译命令背后的四道工序
很多人把“编译”当作一个黑盒动作,实际上 GCC 处理一个 .c 文件要经过四个阶段:预处理、编译、汇编、链接。建议所有新手都亲手把四个阶段各跑一遍,因为很多玄学报错,等你理解了阶段划分,基本一眼就能定位。
1.1 预处理阶段
预处理在词法上做文本级处理。它做的事情包括:展开 #include 引入的头文件、处理 #define 宏替换、依据 #if / #ifdef / #ifndef 做条件编译裁剪、删除注释。举个最简单的例子,你对下面这个文件执行 gcc -E :
#include <stdio.h>
#define PI 3.14
int main(void) {
printf("%.2f\n", PI);
return 0;
}
运行:
gcc -E test.c -o test.i
生成的 test.i 会有几百行,因为 stdio.h 头文件的内容被整体替换进来了, PI 也变成了 3.14 。这一步并不会做语法检查,也不会管你的 printf 写法对不对,它只负责“文本替换”和“展开”。条件编译也是在这一步处理的,比如 #ifdef DEBUG 只在定义了 DEBUG 宏时才保留对应代码,一步就能裁剪出需要的内容。
提示:想快速查看某个宏预处理后的还原结果,
gcc -E是最直接的。配合-D参数可以在命令行预定义宏,例如gcc -DDEBUG -E test.c,很多大型项目就是通过这种方式在编译期注入配置的。
这个小节顺带提一下头文件守卫的重要性。头文件展开这一步,正是 #ifndef/#define/#endif 发挥作用的地方。没有守卫的话,同一个头文件被多次 include,文本会被重复粘贴进同一个 .c 文件,后面等着你的就是一堆重复定义。
1.2 编译阶段
预处理产物 .i 进入真正的编译器前端。GCC 会做词法分析、语法分析、语义分析,把 C 代码变成语法树,再做中间代码生成和优化,最后产出汇编代码。语法错误如漏分号、括号不匹配,都是在 error: 提示里出现的。怎么查看这一步的结果?
gcc -S test.c -o test.s
这一步是整个四阶段里信息量最大的一步,因为它决定了你的代码质量。GCC 会做大量优化,包括常量传播、循环展开、指令选择等。这也是为什么同一段 C 代码,不同优化级别跑出来的性能能差出几倍。如果你想了解一段代码在 CPU 层面长什么样, -S 生成的 .s 文件是很好的学习材料,汇编里的每条指令几乎都能对应回 C 源码的某个逻辑。
1.3 汇编阶段
汇编这一步用汇编器把 .s 翻译成机器指令,产物是 .o 目标文件,也叫可重定位目标文件。执行 gcc -c 会得到这个文件。它还没到能直接运行的状态,因为里面的函数地址、全局变量地址都没有最终确定,留了一堆“重定位信息”和“符号表”等待链接去修补。
Linux 下可以用 file 命令查看 .o 的格式,比如 ELF 64-bit LSB relocatable, x86-64 中的 relocatable 就说明它还需要被链接。很多初学者在 .o 和可执行文件之间划不上等号,其实一个 hello.o 就算你用十六进制编辑器读,也很难直接跑起来,它只是“半成品”。
1.4 链接阶段
链接器负责把多个 .o 文件以及你依赖的库文件合并在一起,完成符号解析和地址重定位,生成最终的可执行文件。 printf 是你写的吗?不是,它来自标准库。链接器就是从某个库文件里把 printf 的实现找出来,跟你的 main.o 拼在一起,或者记录下运行时从共享库加载的信息。
这里补充一个对多文件工程很重要的概念:链接器处理文件是有顺序的,这一点在后面的排查部分还会重点讲。另外静态库和动态库的差异,本质上也发生在链接阶段。把四个阶段用一张表总结一下,方便平时翻阅:
| 阶段 | 常用选项 | 产物 | 本质工作 |
|---|---|---|---|
| 预处理 | -E | .i | 头文件展开、宏替换、条件编译 |
| 编译 | -S | .s | 语法分析、优化、生成汇编 |
| 汇编 | -c | .o | 汇编指令转机器码,生成可重定位目标文件 |
| 链接 | 无(默认) | 可执行文件 | 符号解析、地址重定位、库合并 |
2. 单文件编译:从 hello.c 到可执行文件
先说最基础的。单文件编译是大多数人认识 GCC 的第一步,也是后面所有复杂构建的基础。很多教材只教你 gcc hello.c -o hello ,但并没有解释为什么要加 -o 。实际上,如果你直接写 gcc hello.c ,GCC 会默认生成一个叫 a.out 的可执行文件——这个名字来自几十年前 Unix 的汇编输出格式。不加 -o 的情况在单文件 demo 里勉强能用,在工程里就是个隐患,因为你很容易分不清哪个 a.out 是哪个程序。所以从一开始就养成习惯: gcc 源文件 -o 输出名 。
2.1 最小命令与输出命名
编译命令
1034




被折叠的 条评论
为什么被折叠?



