跳到主要内容
🔍
1.1.1已发布beginner · 约 3 课时 · P0

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,命令一致)。

① 学习目标

  1. 说出 gcc 把 .c 变成可执行文件的四个阶段与各阶段产物(.i/.s/.o);
  2. 用 gcc -E/-S/-c 分步生成并查看三个中间产物;
  3. 解释 undefined reference 报错发生在哪个阶段、为什么"编译过了"程序还是出不来;
  4. 说出 -o/-I/-L/-l/-Wall/-Wpedantic/-Werror/-g/-O0/-O2 各选项的作用;
  5. 独立完成分步编译并说清编译错误与链接错误的阶段归属(实践任务)。

② 前置知识

  • 必修: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 deniedWindows 上旧 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 生成的程序,运行结果一定完全相同。(判断)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

分步编译 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 --helpman 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./ex1scp/烧录后执行

不变的:预处理→编译→汇编→链接四阶段、-E/-S/-c 用法、全部选项、错误阶段归属判断;要改的:编译器名字与产物文件名。

内容来源映射

内容部分资料位置标记说明
编译四阶段划分与常用选项(-o/-I/-L/-l/-g/-O)第一阶段讲义3.1-3.5 节来源正文重新组织表述
示例代码(ex1/ex2/ex3)与分阶段实验补充重新编写;gcc 15.2.0 本机实测(-E/-S/-c 产物与链接错误均为真实运行结果)
练习/测验/面试/实践任务补充全部新编