编译命令

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

363.3.2 编译命令

本节视频全程在思维导图中讲解 GCC 选项,只展示了一条命令 gcc main.c -o main,没有打开、创建或修改任何 C 源文件,也没有在终端中实际执行命令。因此,本篇不虚构 main.c 的内容,不收录后续 Makefile 课程中的源码,也不编造运行结果。

一、GCC

GCC 的全称是 GNU Compiler Collection。在命令行中使用的 gcc 则是编译器驱动程序:它根据输入文件和选项,协调 C 预处理器、编译器、汇编器和链接器完成相应阶段。日常所说的“用 GCC 编译”,可能指狭义的 C 到汇编转换,也可能泛指从 .c 一直生成可执行文件的完整构建过程,需要结合命令判断。

1.1 编译文件的指令

视频画面中的命令是:

gcc main.c -o main

各部分含义如下:

内容含义
gcc启动 GCC 的 C 编译器驱动程序
main.c输入的 C 源文件
-o main把这次构建的主要输出文件命名为 main

这条命令没有用 -E、-S 或 -c 提前停止,所以会依次完成预处理、编译、汇编和链接。在普通 GNU/Linux 本机构建环境中,成功后通常得到名为 main 的 ELF 可执行文件;若使用的是交叉编译器,输出则面向相应的目标平台。视频没有真正执行该命令,因此没有可核对的终端输出。

特别注意:小写字母 -o 与大写字母 -O 完全不同。命令行选项区分大小写:

  • -o:指定输出文件名;
  • -O0、-O1、-O2、-O3、-Os:选择优化级别,其中 O 是大写字母,不是数字 0。

如果想观察构建的各个阶段,可以使用以下命令:

命令停止位置典型输出
gcc -E main.c -o main.i只做预处理main.i
gcc -S main.c -o main.s预处理并编译,不汇编main.s
gcc -c main.c -o main.o预处理、编译并汇编,不链接main.o
gcc main.c -o main完成全部阶段,包括链接main

所以,视频把 -c 说成“预编译”是不准确的。只做预处理应使用 -E;-c 的准确含义是编译并汇编,但不链接。 同样,-o 也不是“链接开关”,它只负责指定主要输出文件的名字;是否执行链接,由是否出现 -E、-S、-c 等停止选项以及输入类型共同决定。

1.2 GCC编译原理

视频中的选项可以先这样总览:

选项准确作用
-c生成目标文件,不执行链接
-o file指定主要输出文件名
-Dname、-Dname=value在命令行中预定义宏
-O0、-Og、-O1、-O2、-O3、-Os选择不同优化策略
-g生成供调试器使用的调试信息
-I dir把一个目录加入头文件搜索路径
-Wall启用一组常用警告,而不是关闭警告
-Werror把已启用的警告按错误处理
-std=c99选择以 ISO C99 为基础的 C 语言方言

其中 -std=c99 出现在视频思维导图的“Makefile 基本语法”分支中,但本节没有口头展开。它实际上是传给编译器的语言标准选项,不是 Makefile 自身的语法;Makefile 只是经常把它保存在 CFLAGS 一类变量中,再传给 gcc。

1.2.1 定义宏

-D 是预处理选项:

  • -DLED_ENABLE 相当于在源文件最前面预定义 LED_ENABLE,其默认替换值为 1;
  • -DMAX_COUNT=12 相当于预定义宏 MAX_COUNT,替换值为 12;
  • 如果宏定义含有空格、括号或其他对当前 Shell 有特殊意义的字符,还需要按所用 Shell 的规则正确引用。

因此,视频所说的“宏定义链接”并不准确。-D 的作用发生在预处理阶段,不是把宏交给链接器。它常与源码中的 #if、#ifdef、#ifndef 等条件编译指令配合,让同一份源码按构建配置选择不同代码分支,而不必每次手工修改源文件。

视频用 -DLED_ENABLE 表示“使能 LED 功能”,这只说明对应条件编译分支会参与后续编译,并不意味着一个命令行宏就能直接点亮 LED。真实硬件是否动作,还取决于被选中的代码是否完成时钟、GPIO 模式、输出电平和有效电平等配置,也取决于板卡引脚与 LED 的实际连接。本节没有展示开发板原理图或 LED 驱动源码,不能进一步断言具体接线和电平逻辑。

命令行宏很适合区分板型、功能和调试配置,但应把这些选项固化在 Makefile 或其他构建配置中,确保同一版本可以被准确复现;如果只靠开发者临时手敲参数,很容易出现“源码相同、生成物不同”却无法追踪的情况。

1.2.2 编译优化

优化并不是简单地“级别越高,文件一定越小”。不同级别启用的是不同优化集合,而且具体集合会随 GCC 版本和目标架构变化:

选项主要定位
-O0关闭绝大多数优化;未指定 -O 时通常采用此级别,源码与执行过程较容易对应
-Og保留较好的调试体验,同时启用部分不会明显妨碍调试的优化
-O1启用一组基础优化,兼顾编译时间与生成代码
-O2在 -O1 基础上增加更多优化,常用于需要性能的正式构建
-O3在 -O2 基础上采取更激进的性能优化,可能扩大代码体积
-Os以减小代码体积为目标,基于 -O2 调整并关闭部分容易增大体积的优化

因此,视频中“从 O0 到 O3,代码体积会从 5 KB 一直降到 2.5 KB”只能算未经验证的个别经验,不能当作普遍规律。-O3 可能因为函数内联、循环展开等行为生成更大的代码;-Os 也不保证任何程序都得到固定比例的缩减。判断效果应针对同一工具链、同一目标和同一链接配置,使用 size、链接映射文件以及真实性能测试进行比较。

对一个没有未定义行为、并且正确表达了所有外部可观察行为的程序,优化器应保持其 C 语言语义。视频所说“优化器没有看到 extern 变量,于是把它优化掉并导致崩溃”并不是正确的普遍解释:规范的外部声明和定义不会仅因开启优化就被随意丢弃。

如果程序只在优化后出错,应优先排查被优化暴露出来的潜在问题,例如:

  • 越界访问、使用未初始化对象、空指针解引用、整数溢出规则误用等未定义行为;
  • 依赖空循环、指令条数或偶然时序的延时逻辑;
  • 中断、信号、线程或硬件在源码控制流之外修改数据,但程序没有使用适合该环境的同步方式;
  • 访问内存映射外设时缺少正确的 volatile 限定或屏障;
  • 内联汇编没有完整声明输入、输出、被破坏寄存器或内存影响;
  • 栈越界、链接脚本错误或其他内存布局问题,只是因优化改变布局后才显现。

volatile 可以告诉编译器某个对象的访问具有外部可观察性,常用于内存映射寄存器等场景,但它不是线程同步工具,不能代替原子操作、锁或平台规定的临界区机制。反过来,普通 extern 只是在声明对象或函数具有外部链接,并不自动表达“它会被硬件或并发执行流异步修改”。

开发阶段可以使用 -Og -g 获得较好的调试体验,但不应等到发布前最后一刻才首次开启正式优化级别。更稳妥的做法是持续测试与发布配置接近的构建,例如定期运行 -O2 -g 版本;-g 与优化并不冲突,保留调试信息还有助于分析正式配置下才出现的问题。

1.2.3 Debug调试

-g 的作用是让编译器和汇编器在输出文件中生成调试信息。在 ELF 平台上通常是 DWARF 信息,GDB、addr2line 等工具可以利用它建立机器地址与函数名、变量、源文件和行号之间的对应关系。

需要区分三件事:

  1. -g 不会启动 GDB,也不会让程序自动进入调试模式;它只是在构建产物中保留调试器可使用的信息。
  2. -g 可以与 -O 同时使用。优化后某些变量可能被消除,语句可能重排或合并,单步调试会显得“跳行”;初学调试通常可先用 -Og -g。
  3. -g 往往会明显增大未剥离 ELF 文件的磁盘占用,但视频把它直接说成“会让烧录的 .bin 显著增大”并不严谨。调试节通常不是运行时装载节,从 ELF 转换出的裸 .bin 往往不会包含它们;最终仍应以目标文件的节表和实际镜像大小为准。

发布时可以从交付给用户的可执行文件中剥离调试信息,但应归档与该版本完全匹配的未剥离 ELF、调试符号、映射文件和构建标识。发生崩溃后,地址只有配合精确对应的符号文件才能可靠定位;如果只保留剥离后的文件,往往会失去最有价值的现场分析依据。

1.2.4 添加头文件

-I dir 会把目录 dir 加到预处理器的头文件搜索列表中。例如:

gcc -I include main.c -o main

它的意思不是“把 include 目录里的所有头文件都包含进程序”,也不是递归遍历整个目录树。预处理器仍然只会处理源码通过 #include 实际请求的头文件;-I 只是告诉它还可以去哪里查找。

简化后的搜索顺序如下:

  • 对 #include "file.h",先搜索当前文件所在目录,再依次搜索 -iquote、-I 指定的目录,最后搜索系统目录;
  • 对 #include <file.h>,不会先搜索当前文件所在目录或仅供引号形式使用的 -iquote 目录,而是从 -I、系统头文件目录等搜索链继续查找;
  • 多个 -I 会按命令行从左到右搜索,每个要加入的目录通常分别写一个 -I;
  • 子目录不会被自动递归加入。如果源码写的是 #include "driver/gpio.h",预处理器会在各个搜索基目录下拼接并查找 driver/gpio.h。

项目自己的头文件一般用 -I。对于需要按系统头文件方式处理的第三方或工具链头文件,GCC 还提供 -isystem,它不仅改变搜索位置,也会给予该目录系统头文件的特殊诊断处理,不应与普通项目目录随意混用。

1.2.5 编译警告

视频口述中把 -Wall 说成“屏蔽警告”,这是本节最需要纠正的错误之一:

  • -Wall 会启用一组 GCC 认为常用且有价值的警告;
  • 名字虽然是 “all”,但它并不包含 GCC 能产生的全部警告;
  • -Wextra 会在 -Wall 基础上再启用一组附加警告;
  • -Wpedantic 结合 -std=... 检查所选 ISO 语言标准要求诊断的用法;
  • -w 才是抑制所有警告的选项,常规项目不应为了让输出“干净”而使用它掩盖问题。

-Werror 会把已经启用并实际产生的警告按错误处理,使本次编译失败;它不会自动启用 GCC 的每一种警告。还可以用 -Werror=return-type 只把某一类警告升级为错误,或用 -Wno-error=deprecated-declarations 让特定警告仍保持为警告。

针对本节出现的选项,一条适合练习阶段的严格构建命令可以写成:

gcc -std=c99 -Wall -Wextra -Wpedantic -Werror -Og -g main.c -o main

这里的 -std=c99 选择以 ISO C99 为基础的语言方言,-Wpedantic 会依据该标准诊断扩展用法,-Og -g 兼顾基础优化与调试。是否在真实项目中把所有警告都升级为错误,需要结合旧代码、第三方头文件和编译器版本决定;但不论是否使用 -Werror,都应认真理解并处理警告,而不是简单关闭它们。同时,“零警告”也不等于“零缺陷”,测试、静态分析和代码审查仍不可替代。

参考资料