编译器概述

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

362.3.1 编译器概述

/* 定义对象式宏 MAX_CNT;预处理时,后续出现的 MAX_CNT 会被替换为 12。 */
#define MAX_CNT 12
/* 按视频画面原样保留;标准 C 的类型关键字是小写 char,除非另有 typedef,否则大写 Char 无法通过编译。 */
Char buf[MAX_CNT] = {0};

上面是本节视频唯一展示的源码片段,用来说明预处理阶段的宏替换。它是在 OneNote 中临时输入的概念示例,不是完整的 .c 文件,也没有在视频中实际编译。画面确实写成了大写 Char,所以这里按原样收录并指出错误;不能擅自补出 main()、头文件或下一节才演示的编译代码。

一、编译器

本节先用 test.c → test.i → test.s → test.o → 可执行文件 介绍 C 程序的构建流程,再比较 ARMCC、IAR、GCC、LLVM/Clang,最后浏览 GNU Binutils 中常见的二进制工具。视频建立的总体框架是有用的,但其中把 IDE、编译器和工具链混作一类,也有几处关键技术错误,下面按实际流程纠正。

1.1 编译器概念

人编写的 C 源文件是文本,处理器不能直接把 if、函数名、变量名等 C 语法当作机器指令执行。编译工具链需要根据目标处理器的指令集、ABI 和运行环境,把源程序变成目标机器可以加载或烧录的机器代码。

日常交流中,“编译”常被宽泛地用于整个构建过程;严格区分时,GCC 驱动的一次完整构建通常包含四个连续阶段:

test.c
  │  ① 预处理
  ▼
test.i
  │  ② 编译(狭义)
  ▼
test.s
  │  ③ 汇编
  ▼
test.o ──┐
         │ ④ 链接:还会读取其他 .o、库、启动文件和链接脚本等
         ▼
     可执行文件或固件映像
阶段主要输入与输出主要工作GCC 驱动的观察命令
预处理.c → .i处理 #include、宏、条件编译并移除普通注释gcc -E test.c -o test.i
编译.i → .s词法、语法和语义分析,优化并生成目标架构的汇编代码gcc -S test.i -o test.s
汇编.s → .o把汇编指令编码为机器码,生成符号和重定位信息gcc -c test.s -o test.o
链接.o、库等 → 最终文件解析跨文件符号、分配地址并完成重定位gcc test.o -o test

这些扩展名是 GCC/Unix 工具链的常见约定,不是所有语言、编译器和平台都必须采用完全相同的名称。视频图中的 test.exe 也只是示意:Windows 通常使用 .exe,GNU/Linux 可执行文件常常没有扩展名,嵌入式项目则通常保留带调试信息的 ELF 文件,再按烧录器要求生成 .bin 或 .hex。

1.1.1 预处理

预处理器是宏处理器,不理解完整的 C 类型和运行时语义。视频示例:

#define MAX_CNT 12
Char buf[MAX_CNT] = {0};

经过宏展开后,在概念上相当于:

Char buf[12] = {0};

这里只是把标识符 MAX_CNT 替换为它的替换列表 12,不会把代码“转化成宏”。同时,大写 Char 仍然保持原样;预处理器不会自动把它纠正成标准关键字 char。如果程序没有另行声明名为 Char 的类型,后续编译阶段会报错。

预处理阶段主要完成:

  • 展开对象式宏和函数式宏;
  • 将 #include 指定的头文件内容纳入当前翻译单元;
  • 根据 #if、#ifdef、#ifndef 等指令选择参与后续编译的代码;
  • 处理源文件的续行等早期转换,并把普通注释替换为空格。

视频所说“预处理会删除未使用变量或无用函数”并不准确。普通未使用变量仍会进入编译阶段,并可能产生编译警告;在开启优化时,编译器或链接器才可能删除确认无副作用且不可达的代码或数据。只有被条件编译排除的内容,例如 #if 0 ... #endif,才会在预处理结果中消失。

实际的 gcc -E 输出还可能包含头文件展开内容、预定义宏产生的结果和行标记,不会总是像画面上的单行替换那样简短。

1.1.2 编译、汇编与链接

狭义的编译阶段分析经过预处理的 C 程序,检查语法和类型,执行所启用的优化,并生成针对目标架构的汇编代码 .s。汇编器 as 再把汇编文本编码成机器指令,形成可重定位目标文件 .o。

.o 中已经有机器码,但通常还存在三类未完成事项:

  • 对其他文件中函数或变量的引用尚未解析;
  • 代码和数据的最终装载地址尚未完全确定;
  • 程序入口、运行时启动代码和所需库尚未组合完成。

因此,视频所说“得到 .o 后程序就可以运行”是错误的。普通 .o 是链接器的输入,不是最终可执行程序。链接器会把本项目的多个目标文件、需要的静态库或动态库信息、启动文件等组合起来,解析符号并应用重定位,才生成可执行文件或固件 ELF。

使用:

gcc main.c -o main

时,gcc 是编译器驱动程序。没有 -E、-S 或 -c 限制阶段时,它会依次协调预处理器、编译器、汇编器和链接器,-o main 只是在这里指定最终输出名。直接调用 ld 往往还要手工处理启动文件、库路径和运行时选项,因此一般应用程序更适合让 gcc 驱动链接。

1.2 主流编译器

先区分三个容易混淆的概念:

概念作用例子
IDE编辑、工程管理、构建配置、下载和调试的图形环境Keil MDK、IAR Embedded Workbench、Visual Studio
编译器把某种高级语言翻译为较低级表示或目标代码GCC、Arm Compiler、IAR C/C++ Compiler、Clang
工具链编译器、汇编器、链接器、库及二进制工具的组合Arm GNU Toolchain、Arm Compiler for Embedded

一个 IDE 可以支持多套编译器,同一套编译器也可以被不同 IDE 或命令行构建系统调用。因此,不能因为某个 IDE 能使用 GCC,就说这个 IDE“底层就是 GCC”。

ARMCC 与 Arm Compiler 6

原笔记中的 “MCC” 是识别错误,视频说的是 ARMCC。更准确地说,armcc 是 Arm Compiler 5 中的 C/C++ 编译器可执行程序,该工具链曾长期用于 Arm 嵌入式开发;Keil MDK 和 Arm Development Studio 是开发环境,不是 armcc 本身。ADS(ARM Developer Suite)则是更早期的历史产品。

Arm Compiler 6 的编译器程序叫 armclang,以 Clang 前端和 LLVM 代码生成框架为基础。它不是把 “LLVM” 简单改名成 “AC6”,也不能把所有 Keil MDK 工程一概说成已自动切换:实际选用 Arm Compiler 5、Arm Compiler 6 或 GNU 工具链,要看 MDK 版本、已安装工具链和具体工程配置。

IAR

IAR Embedded Workbench 是 IDE 与商业嵌入式工具链的组合,包含 IAR 自己的编译器、汇编器、链接器和调试器。视频按当时资料称它“只支持 Windows”,这种平台结论具有版本时效性,不能作为永久规则。截至本笔记精校时,IAR 官方已经提供 Linux 和 Windows 原生版本,并提供面向 Linux/Windows 的 Arm 命令行构建工具。因此应依据项目所用的具体产品、版本和目标架构查阅官方支持矩阵。

“项目要在 Linux 上运行”与“编译器在 Linux 主机上运行”也不是同一件事:

  • 宿主平台(host):执行编译器的电脑系统;
  • 目标平台(target):生成的程序最终运行的平台或处理器。

例如,可以在 Windows 主机上运行 arm-none-eabi-gcc,为没有桌面操作系统的 Cortex-M 单片机生成代码,这就是交叉编译。

GCC

GCC 的全称是 GNU Compiler Collection,不只支持 C,还包含多种语言前端,并支持许多本机与交叉目标。课程选择 GCC,主要因为它开源、资料丰富,并且 GNU/Linux 与嵌入式交叉编译环境中很常见。

从宿主平台看,GCC 可用于 GNU/Linux 和多种 Unix 类系统;Windows 上常通过 MinGW-w64、MSYS2、Cygwin 或厂商交叉工具链使用,macOS 也可以另行安装真正的 GCC。尤其要注意,现代 macOS 上名为 gcc 的命令可能实际调用 Apple Clang,判断工具链不能只看命令名,应检查 gcc --version 等版本信息。

GCC 可以在不同宿主平台上构建或以预编译工具链形式安装,但不能据此断言所有 Eclipse 系 IDE 或厂商 IDE 都固定使用 GCC。视频提到的 TI Code Composer Studio、STM32CubeIDE、NXP S32 Design Studio 等产品,其可用编译器会随产品版本和目标芯片变化,应查看对应版本的官方文档。

视频称 “Visual Studio 的 C 开发底层也是 GCC” 是错误的。Visual Studio 的原生 C/C++ 工具链通常是微软的 MSVC;它也可以配置 Clang 或其他工具链。不要再把 Visual Studio 与轻量编辑器 Visual Studio Code 混为一谈,后者本身不自带固定 C 编译器,可以扩展连接 MSVC、GCC 或 Clang。

LLVM 与 Clang

原转录中的 “C-Launch” 实际是 Clang。LLVM 与 Clang 关系密切,但不是两个可以随意互换的名称:

  • LLVM 是一组模块化、可复用的编译器与工具链技术,核心部分提供中间表示、优化和代码生成能力;
  • Clang 是 LLVM 项目中的 C、C++、Objective-C 等语言前端,负责解析源代码、诊断并生成 LLVM 中间表示。

LLVM 最初是美国伊利诺伊大学的研究项目,并非由苹果公司发起。苹果后来聘用核心开发者、投入开发并在自家工具链中广泛采用 LLVM/Clang,因而成为重要推动者。Arm Compiler 6 的 armclang 也建立在 Clang/LLVM 基础上。

视频给出的“原来两分钟,换 LLVM 后二十秒”只能视为某个项目的经验例子,不是通用性能保证。构建速度会受项目规模、增量构建、优化级别、并行度、缓存、链接方式、工具链版本和硬件影响;选择工具链还必须考虑生成代码质量、诊断、标准支持、调试信息、许可证和目标芯片支持,不能只比较一次计时。

1.3 GCC编译器工具

这里还要修正一个归属关系:GCC 与 GNU Binutils 是相互配合但彼此独立的 GNU 项目。gcc 驱动通常会调用汇编器和链接器,让使用者感觉它们是“一套工具”;as、ld、ar、addr2line、objcopy、objdump 等则属于 GNU Binutils。广义上可以把这些共同称为 GNU 工具链,但不能说它们全都由 GCC 本身“集成实现”。

普通本机编译常写:

gcc main.c -o main

交叉编译器通常带有目标三元组前缀,例如:

arm-none-eabi-gcc main.c -o main.elf

前者默认生成当前主机/系统目标的程序;后者面向 Arm 裸机 EABI 目标。文件名相似不表示生成物可以在编译电脑上直接运行。

1.3.1 binutils工具集

视频重点浏览的工具可以这样理解:

工具准确用途常见示例
as把汇编源文件汇编成可重定位目标文件as test.s -o test.o
ld组合目标文件和归档库,解析符号并重定位通常由 gcc 驱动间接调用
ar创建、修改和提取归档文件,静态库 .a 是其典型用途ar rcs libfoo.a foo.o
addr2line用调试信息把地址映射为函数、源码文件和行号addr2line -e app.elf -f -C 0x...
objcopy复制或转换目标文件,选择、删除或增加节objcopy -O binary app.elf app.bin
objdump查看目标文件头、节、符号、重定位并反汇编objdump -h app.elf、objdump -d app.elf

此外,嵌入式开发中还常见:

  • nm:列出目标文件中的符号;
  • readelf:查看 ELF 文件头、程序头、节、符号和重定位等结构;
  • size:汇总代码、只读数据、可写数据等节的大小;
  • strip:移除符号或调试信息以缩小发布文件。

视频对 ar 的描述有两处需要纠正:

  1. ar 不负责把 .c 编译为 .a。应先由编译器和汇编器得到一个或多个 .o,再由 ar 把这些成员归档成静态库;
  2. 静态库通常不包含原始 C 源文,所以能减少源码的直接暴露,但它不是加密或可靠的知识产权保护。目标代码仍可能保留符号,也可以被反汇编、逆向分析;即使执行 strip,也不能把机器码变成不可分析的“黑盒”。

objcopy 的核心用途是对一个输入目标文件进行复制、格式转换和节处理。例如,嵌入式工程经常把已链接的 ELF 转换成裸二进制 .bin。视频说可以直接用它“把 Boot 和 APP 两个程序整合在一起”过于笼统:两个镜像必须先有不冲突且正确的装载地址、空洞填充值、校验和及升级格式。通常应通过链接布局、支持地址的 HEX/S-record 工具或专用镜像打包工具合并;不能不看地址就把两个文件简单拼接。

objdump 也不只是查看含糊的“固件特有信息”。最常用的是查看文件头和节表、符号与重定位信息,以及把机器指令反汇编为汇编助记符。分析嵌入式 ELF 时,它常与 readelf、nm、size 配合使用。

1.3.2 addr2line

addr2line 不是汇编器;它是地址定位工具。它读取可执行文件或可重定位目标文件中的调试信息,把十六进制地址或“符号名 + 偏移”转换成源码文件和行号。一个典型流程是:

# 编译时保留调试信息;-O0 便于初学阶段让机器指令与源码保持较直观的对应关系
gcc -g -O0 main.c -o main

# 将崩溃地址映射到函数、文件和行号;-C 还会还原 C++ 修饰后的函数名
addr2line -e main -f -C 0x401156

要得到可靠结果,至少需要:

  • 使用发生崩溃时完全匹配的 ELF/可执行文件,不能拿另一构建版本查询;
  • 构建时保留调试信息,通常使用 -g,并保留未被剥离的 ELF;
  • 输入代码地址,而不是随意的栈数据地址;
  • 若程序采用 PIE、共享库、ASLR 或运行时重定位,先根据模块装载基址换算成工具所需地址;
  • 注意优化、内联和尾调用可能使一个地址对应多层内联位置,或无法直观对应某一条源码。

视频中的 “HardFold” 是转录错误,正确名称是 HardFault。在 Cortex-M 单片机发生 HardFault 时,处理器会在异常入栈帧中保存包括 R0~R3、R12、LR、PC 和 xPSR 在内的寄存器现场。排查时通常应提取保存的 PC,结合 CFSR、HFSR 以及可能有效的故障地址寄存器,再用生成该固件的精确 ELF 运行对应交叉工具链的 addr2line,例如 arm-none-eabi-addr2line。

addr2line 只能回答“这个地址对应哪段源码”,不能单独判断崩溃原因。空指针、栈溢出、总线错误、非法指令等仍要结合异常寄存器、调用栈、反汇编和现场数据分析。

参考资料