GCC 编译四阶段与常用选项
理解预处理→编译→汇编→链接四阶段与 -o/-I/-l/-L/-g/-O 常用选项,能定位编译错误与链接错误的阶段归属
- 来源
- 补充
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 180 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.1.1 GCC 编译四阶段与常用选项
本章来源:四阶段划分与常用选项来自《第一阶段讲义》3.1-3.5 节【来源】,经重新组织表述;示例代码重新编写并本机实测【补充】(-E/-S/-c 分阶段产物与链接错误均为 gcc 15.2.0 真实运行结果)。平台适用性 universal(MinGW 与 Linux 同为 GCC,命令一致)。
① 学习目标
- 说出 gcc 把 .c 变成可执行文件的四个阶段与各阶段产物(.i/.s/.o);
- 用 gcc -E/-S/-c 分步生成并查看三个中间产物;
- 解释 undefined reference 报错发生在哪个阶段、为什么"编译过了"程序还是出不来;
- 说出 -o/-I/-L/-l/-Wall/-Wpedantic/-Werror/-g/-O0/-O2 各选项的作用;
- 独立完成分步编译并说清编译错误与链接错误的阶段归属(实践任务)。
② 前置知识
- 必修:0.2.4(会用一条 gcc 命令完成编译与运行);
- 建议:0.2.1 环境(本章全部演示在本机 MinGW 实测,Linux 命令相同)。
③ 核心概念【来源】
| 概念 | 说明 |
|---|---|
| 四阶段 | 预处理 → 编译 → 汇编 → 链接;gcc 默认一条命令串起全部,用 -E/-S/-c 可以停在中间 |
| 预处理 | gcc -E x.c -o x.i:展开 #include/宏、处理条件编译;头文件在这一阶段"消失"(.i 通常上万行) |
| 编译 | gcc -S x.c -o x.s:把 C 翻译成汇编代码(可读:能看到函数标签与指令) |
| 汇编 | gcc -c x.c -o x.o:把汇编翻译成机器码目标文件;含符号表,但还不能运行 |
| 链接 | gcc x.o -o x:合并多个 .o 与库、解析符号引用;undefined reference 就发生在这一阶段 |
| 常用选项 | -o 输出名;-I 头文件搜索目录;-L 库搜索目录;-l 库名(libm.a → -lm);-Wall/-Wextra/-Wpedantic 警告全开;-Werror 警告当错误;-g 调试信息;-O0/-O1/-O2/-O3 优化级别 |
④ 通俗解释【补充】
把编译想象成餐厅出餐:
- 预处理 = 备菜:把菜谱里的"引用卡"(#include)换成真正的菜谱内容(头文件展开);
- 编译 = 翻译菜谱:把中文菜谱翻译成大厨看得懂的工作笔记(汇编);
- 汇编 = 备成半成品:把笔记变成可以直接下锅的配菜盒(.o 目标文件);
- 链接 = 合成上桌:把前菜、主菜、甜点(多个 .o + 库)按菜单拼成完整的一桌(可执行文件)。
undefined reference = 菜单上写了"香煎 missing_func",但后厨根本没人会做这道菜——问题出在"上桌拼装"环节,而不是前面的备菜翻译。这就是为什么它叫"链接错误"而不是"语法错误"。
⑤ 示例代码【补充,代码新写并本机实测】
代码示例与验证记录
- examples/ex1-four-stages.c单文件四阶段演示(-E/-S/-c 分步产物)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex1-four-stages.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
Hello from four stages add(3, 4) = 7
差异说明:-E/-S/-c 分步与 gcc ex1.o -o ex1b 链接均已实测(本机产物:.i 79423 字节 / .s 1103 / .o 1118 / exe 58444), ex1b 运行输出与 ex1 一致;文件大小随头文件版本变化,数量级相近即可
完整源码见 /code 代码示例页
- examples/ex2-main.c多文件链接(main.c 调用 util.c 的 square)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex2-main.c ex2-util.c -o app -std=c11 -Wall -Wextra -Wpedantic适用环境:LinuxWindows(MinGW)展开预期输出(实测)
square(2) = 4 square(5) = 25 square(9) = 81
差异说明:分步编译(gcc -c ex2-main.c / ex2-util.c 后再 gcc main.o util.o -o app2)也已实测,输出一致; 两个源文件放在 examples/ 顶层(flat),因 verify:examples 只拷贝 examples/ 顶层文件到临时目录编译
完整源码见 /code 代码示例页
- examples/ex3-link-error.c链接错误演示(diagnostic)诊断示例(预期报错/警告)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15)编译:
gcc ex3-link-error.c -o ex3 -std=c11 -Wall -Wextra -Wpedantic(预期失败)适用环境:LinuxWindows(MinGW)展开预期输出(实测)
D:/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/15.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:\Users\<用户>\AppData\Local\Temp\ccXXXXXX.o:ex3-link-error.c:(.text+0xe): undefined reference to `missing_func' collect2.exe: error: ld returned 1 exit status
差异说明:预期编译失败(diagnostic):报错文本为 gcc 15.2.0 实测;临时对象文件名(ccXXXXXX.o)与偏移每次不同, 稳定核心是 undefined reference 与 collect2 两行。language 用 text 使 verify:examples 不把本示例当作必须编译通过的示例
完整源码见 /code 代码示例页
⑥ 编译与运行方法
四个阶段可以用一条命令一次跑完,也可以分步停下来看产物(三处命令一致:示例卡 / 源文件头注释 / 这里):
gcc ex1-four-stages.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic # 一条命令直达可执行文件
gcc -E ex1-four-stages.c -o ex1.i # 预处理:展开头文件(本机实测 .i 约 7.9 万字节)
gcc -S ex1-four-stages.c -o ex1.s # 编译:得到汇编代码(本机 1103 字节)
gcc -c ex1-four-stages.c -o ex1.o # 汇编:得到目标文件(本机 1118 字节)
gcc ex1.o -o ex1 # 链接:目标文件 → 可执行文件多文件工程(ex2)两种编译方式等价:
gcc ex2-main.c ex2-util.c -o app -std=c11 -Wall -Wextra -Wpedantic # 一起编译
gcc -c ex2-main.c -o main.o -std=c11 -Wall -Wextra -Wpedantic # 或:分步
gcc -c ex2-util.c -o util.o -std=c11 -Wall -Wextra -Wpedantic
gcc main.o util.o -o app运行:Linux ./ex1;Windows ex1.exe。查看产物内容:head ex1.i(Windows 用 type 或编辑器)、file ex1.o(Linux 显示 ELF relocatable)。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
undefined reference to 'xxx' | 链接阶段找不到实现:只声明未定义 / 实现文件没参与编译 / 库没链接 | 补实现文件;检查 -l 库名与顺序(被依赖库放后面) |
| 编译阶段报 syntax error、unknown type name | 语法错误或类型未声明(预处理没把声明带进来) | 按报错行号修语法;检查头文件与 -I 路径 |
cannot open output file ... Permission denied | Windows 上旧 exe 还在运行,文件被占用 | 关闭正在运行的程序再编译 |
'gcc' 不是内部或外部命令 | MinGW 未加入 PATH | 回看 0.2.1 的环境验证命令 |
| 开 -O2 后行为异常 | 程序依赖了未定义行为(未初始化变量、越界等) | 先用 -O0 -g 调试定位;修复 UB 再谈优化 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-1-1-1.gcc -c hello.c -o hello.o 生成的文件是?(单选)
◌ 未作答ex-1-1-1-2.undefined reference to 'xxx' 这个错误发生在哪个阶段?(单选)
◌ 未作答ex-1-1-1-3.判断:gcc -O2 与 gcc -O0 生成的程序,运行结果一定完全相同。(判断)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
分步编译 ex1 并解读三个中间产物
对 ex1-four-stages.c 依次执行 gcc -E/-S/-c 并完成链接,查看并说明 .i/.s/.o 三个产物 分别来自哪个阶段;随后故意制造一个链接错误,说明它在哪个阶段被报出。
输入与输出
输入:ex1-four-stages.c 源码。交付物:.i 的查看记录(前 20 行与总行数)、.s 中 main/add 的 汇编片段、.o 的文件类型说明,以及一份"四阶段 + 错误阶段归属"小结。
功能要求
- 用 -std=c11 -Wall -Wextra -Wpedantic 完成分步编译且零警告
- 记录三个产物的大小(本机参考:.i 约 7.9 万字节、.s 1103 字节、.o 1118 字节,数量级相近即可)
- 在 .s 中找到 add 函数对应的汇编标签
- 把 ex2 的 main.c 单独链接(不带 util.c),记录 undefined reference 报错并说明它属于哪个阶段
限制条件
- 中间产物用 -o 重命名,实验完成后删除(.i/.s/.o 不入库)
- 小结中必须分别给出"编译错误"与"链接错误"各一例并说明阶段归属
验收步骤(自检清单 0/4)
验收标准
- 四步命令零警告且三个产物齐全(验收步骤 1)
- 阶段说明正确:.i=预处理 / .s=编译 / .o=汇编(验收步骤 2)
- 报错含 undefined reference 且被正确归为链接阶段(验收步骤 3/4)
常见失败原因
- gcc -E 不加 -o:预处理结果刷屏,应 -o ex1.i
- 误以为 .o 可以直接运行(.o 是目标文件,还需链接)
- 把 undefined reference 当语法错误去改代码(应先检查实现文件/库是否参与链接)
可选扩展
- 用 gcc -v 观察四阶段各自调用的子程序(cc1/as/collect2)
- 比较 -O0 与 -O2 下 ex1.s 的大小与内容差异
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
从 .c 源文件到可执行文件经历了哪些阶段?每个阶段的产物是什么?高频C/C++ · medium
要点:预处理(.i)→ 编译(.s)→ 汇编(.o)→ 链接(可执行文件);头文件与宏在预处理阶段展开消失。
预处理:展开 #include/宏、处理条件编译,产出 .i(gcc -E);编译:把 .i 翻译成汇编,产出 .s(gcc -S); 汇编:把汇编翻译成机器码目标文件 .o,含符号表(gcc -c);链接:把多个 .o 与库合并、解析符号引用 (undefined reference 就出在这里),产出可执行文件。补充亮点:头文件在预处理后就不存在了—— 所以改头文件必须重新编译所有包含它的 .c(工程上用构建系统处理依赖,阶段 2.1 展开)。
追问:- 追问:-E/-S/-c 分别让 gcc 停在哪一步?
- 追问:改了一个头文件,为什么多个 .c 都要重新编译?
评分要点:- 四阶段名称与顺序
- 各阶段产物(.i/.s/.o/可执行)
- 链接阶段职责(符号解析)
undefined reference to 'xxx' 是什么错误?如何排查?高频C/C++ · medium
要点:链接阶段的符号缺失:有声明/引用但找不到实现。排查实现文件是否参与编译、库是否链接、-l 顺序、C/C++ 混编名字修饰。
编译阶段只检查语法与声明,链接阶段才把"对 xxx 的引用"与"xxx 的实现"对上号;对不上就报 undefined reference。排查顺序:① 实现所在的 .c 是否被一起编译/链接;② 是否忘了链接库 (如数学库 -lm)或 -l 顺序不对(被依赖的库要放后面);③ C/C++ 混编时 C 函数被 C++ 名字修饰 (extern "C" 解决);④ 声明与定义签名不一致(如参数个数)。可以顺带提 nm 查看 .o 的符号表。
追问:- 追问:为什么 C 与 C++ 混编容易报 undefined reference?
- 追问:nm 命令能帮什么忙?
评分要点:- 阶段归属(链接而非编译)
- 至少两类排查手段
- extern "C" 加分
-O0 与 -O2 有什么区别?开发调试和发布分别应该怎么选?C/C++ · easy
要点:-O0 不优化、编译快、便于调试;-O2 开启大量优化、程序更快但与源码对应关系弱;调试用 -O0 -g,发布用 -O2。
-O0 关闭优化,机器码与源码一一对应,断点/单步行为直观,适合开发调试(配合 -g 带调试信息); -O1/-O2/-O3 逐级开启更多优化(常量折叠、死代码消除、内联、向量化等),代码更快但可能被重排、 变量被优化掉,调试体验变差。关键点:优化不改变符合标准的程序的语义——如果开 -O2 后结果变了, 优先怀疑程序依赖了未定义行为(如未初始化变量、越界)。工程实践:Debug 用 -O0 -g,Release 用 -O2 (嵌入式资源受限场景还会配合 -Os 减小体积)。
追问:- 追问:为什么开 -O2 后某个变量在调试器里看不见了?
- 追问:-Os 与 -O2 的区别?
评分要点:- O0/O2 的行为差异
- 调试与发布的选择
- 优化不改语义 + 未定义行为意识
⑫ 延伸阅读
- 《程序员的自我修养——链接、装载与库》第 2 章:编译与链接全过程(找工作面试高频);
- GCC 官方手册(gcc.gnu.org/onlinedocs,只引名称不复制内容);本地
gcc --help与man gcc; - C11 标准 5.1.1.2 翻译阶段(translation phases):四阶段的标准依据;
- 下一章预告:1.1.2 预处理指令深入——四阶段第一站的放大镜。
迁移训练(migration training)
把本章技能迁移到 Linux / Mac / 开发板:
| 环节 | Windows(本机实测) | Linux | 开发板(阶段 7) |
|---|---|---|---|
| 编译器 | gcc(MinGW-w64 15.2.0) | gcc(命令一致) | 交叉编译器(如 arm-linux-gcc) |
| 四阶段命令 | 一致 | 一致 | 一致(gcc 换成交叉编译器前缀) |
| 产物 | ex1.exe(PE 格式) | ex1(ELF 格式,无后缀) | 交叉目标二进制,需拷贝到板子运行 |
| 运行 | ex1.exe | ./ex1 | scp/烧录后执行 |
不变的:预处理→编译→汇编→链接四阶段、-E/-S/-c 用法、全部选项、错误阶段归属判断;要改的:编译器名字与产物文件名。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| 编译四阶段划分与常用选项(-o/-I/-L/-l/-g/-O) | 第一阶段讲义 | 3.1-3.5 节 | 【来源】 | 正文重新组织表述 |
| 示例代码(ex1/ex2/ex3)与分阶段实验 | 无 | 【补充】 | 重新编写;gcc 15.2.0 本机实测(-E/-S/-c 产物与链接错误均为真实运行结果) | |
| 练习/测验/面试/实践任务 | 无 | 【补充】 | 全部新编 |