编译器工具链:GCC、Clang、MSVC、ICC、armclang

写了几百行代码,按过很多次"编译"按钮。灯亮不亮、程序跑不跑得起来,是后面的事;按下去之后第一个说话的东西,是编译器------它要么一声不吭地把代码变成能烧进芯片的文件,要么甩回来一串红字,告诉你哪一行它看不懂。

红字里有一类特别让人发懵:代码在上一个工程里明明能编过,换了个地方就开始报错,报的还是"声明不能出现在语句之后"这种听起来莫名其妙的理由。代码未必坏了,通常是编译器换了 ,或者编译器采用的语言标准换了。

编译不是一件事,是一整条流水线

"编译"是口语。真正发生的是好几步:把 #include 和 #define 展开、把 C 代码变成汇编、把汇编变成机器码目标文件、最后把一堆目标文件和库拼成一个可执行的映像。

干这些活的不是单个程序,是一组程序,加上随它们一起来的头文件、库、启动文件------这一整套叫工具链。你在 Keil 里点的那一下,是让工具链从头到尾跑一遍。

每一环的细节都能单独讲很久,这里先只记一件事:工具链是有品牌、有版本的。它像尺子一样,量同一段代码,不同牌子、不同版本的尺子给的答案可能不一样。

几个名字先认识

各家工具链的差别,先看它们平时出现在哪:

名字 是什么 常在哪见到
GCC 开源编译器集合,出现最早、移植最广 嵌入式上的 arm-none-eabi-gcc、Linux 上的 gcc
Clang / LLVM 新一代编译框架,报错友好、结构清晰 苹果开发工具、很多嵌入式工具链的内核
MSVC 微软 Visual Studio 的编译器 Windows 桌面程序
ICC Intel 的编译器 x86 平台上追求性能的场景
armclang ARM 官方编译器,基于 Clang / LLVM Keil MDK 的 AC6

看这张表能发现一件事:编译器是跟着目标芯片走的 。PC 上的 gcc 编出来的是给自己这台电脑跑的程序;STM32 跑不了 PC 的程序,所以嵌入式用的是另一套------同一个 GCC 家族里那个 arm-none-eabi- 前缀的版本。

Keil 里用的就是 ARM 自家的那一支,而且有两代。

手上这一套:AC5 和 AC6

Keil MDK 把 ARM 的编译器打包进来,于是选项里出现过两个名字:

  • AC5 :编译器程序叫 armcc,老一代,很多年前的标准库工程都是它编的,ARM 已经不再维护;
  • AC6 :编译器程序叫 armclang,基于 Clang 重写,支持现代 C 标准,优化也更好。

怎么知道自己在用哪一个?三个地方都能看到:

  1. 工程选项 :Options for Target → Target 页,ARM Compiler 下拉框里写着用的是哪个版本;
  2. 编译输出窗口 :每次编译都会打印编译器自己的名字和版本,例如 Armcc.exe V5.06 update 6 (build 750) ------这行就是 AC5;
  3. 帮助菜单 :Help → About µVision,看的是 IDE 的版本,和编译器版本是两件事。

为什么要在意?因为选择哪一个,直接决定你的代码要满足哪一套语言规则。

C 语言其实有好几个版本

我以前也把"C 语言"当成一个固定的东西,后来才发现它其实有一份份逐年发布的标准。标准管的是语法允许写什么、库提供哪些函数。版本一路走过来,写代码的手感一直在变。

C89 / C90:第一份正式标准。ANSI 在 1989 年发布,ISO 在 1990 年采纳,内容一样,所以两个名字指的是同一份东西。它的规矩很严:

  • 变量声明必须写在块的最前面,中间不能插;
  • 只有 /* */ 块注释,// 单行注释还没进标准(不少编译器把它当扩展先用着);
  • 没有固定宽度的整数类型那套头文件。

C99:1999 年的版本,是写嵌入式的人最该认识的一版。它带来的变化,几乎每一条都用得着:

  • 变量可以随用随声明,不必挤在块首;
  • // 单行注释正式写进标准;
  • for 里面可以直接声明循环变量;
  • 多了固定宽度整数类型(int8_t、uint32_t 这一家)和布尔类型;
  • 指定初始化器:结构体可以只写要赋值的字段,.speed = 30 这样点名;
  • 支持变长数组等特性。

C11 :2011 年的版本,主要加了些"编译期就能检查"和并发相关的东西,例如编译期断言、匿名结构体与联合体、_Generic 泛型选择。

C17 / C18 :2018 年出的修补版,主要是澄清和勘误,没有加新语言特性。

C23:更新的版本,工具链支持还在陆续跟上。

看这一串能明白一件事:"编译器报错"很多时候不是代码错,而是它按的版本比你想的旧。

同一段代码,两种命运

举个最典型的例子。C90 要求一个块里的声明全部挤在开头,语句一旦开始,后面就不许再声明变量。写成这样就通不过:

c 复制代码
void Work_Once(void)
{
    uint8_t mode = 0U;        /* 声明写在块首,合法 */
    mode = 1U;                /* 从这里开始是语句 */
    uint8_t count = 0U;       /* 语句之后又出现声明,C90 不允许 */
    count = mode;
}

在按 C99 检查 的编译器眼里,这是正常写法。在按 C90 检查 的编译器眼里,mode = 1U; 已经是一条语句,后面再冒出 uint8_t count = 0U;,就属于"语句之后再出现声明",直接拒绝:

text 复制代码
error: #268: declaration may not appear after executable statement in block

#268 这个编号是 ARM 老编译器(AC5)的报错风格。同一段代码换到 AC6 上编译,通常一句话都不会说------因为 AC6 默认就按现代标准来。

所以那个经典的困惑就有了答案:代码没变,标准变了。AC5 默认按 C90 的口径检查,AC6 默认按更现代的标准检查。

怎么让它们讲同一种语言

三种做法,按你手上的工具选:

Keil 的 AC5 :Options for Target → C/C++ 页,把 C99 Mode 勾上。勾选前后编同一份代码,上面那个 #268 会直接消失。这是把老编译器"放宽到 C99"的开关。

GCC / Clang :命令行上加 -std=c99 或 -std=c11,明确告诉它按哪一版检查。

MSVC :用 /std:c11 或 /std:c17 指定。这里补一个背景:MSVC 对 C 语言的支持长期停在 C89 那一套上,C11、C17 是近几年才通过这两个选项补进来的------同一段代码在 MSVC 上编不过,原因可能只是它的 C 支持起步晚。

反过来看,AC6、GCC、Clang 之间通常更接近:它们都认现代标准,符号也相似。真正常见的组合是:老工程(尤其是随工程附带的标准外设库那种)在 AC5 下顺顺当当,换成 AC6 就冒出一片错------那些错误多半来自编译器内置关键字和头文件里的分支,而不是你的业务逻辑写错了。

优化等级:编译器会替你改代码

编译器还有一个会改变现象的权力:优化。

在 Keil 的 Options for Target → C/C++ 页里,Optimization 分级从 Level 0 (-O0) 到 Level 3 (-O3)。级别越高,编译器越"主动":把能算的提前算掉、把重复的搬出去、把没用的删掉。换来的通常是更小的体积、更快的速度,代价是单步调试时跳来跳去,不好对应源码。

这里有个必须知道的坑。想用空循环凑一段延时,写成这样:

c 复制代码
for (uint32_t i = 0; i < 1000000U; i++)
{
    /* 什么都不做 */
}

AC6(armclang) 开了优化以后,编译器一眼看出这个循环没有任何可见效果,于是整段删掉 。你想要的这段延时就不存在了(一百万次空循环在 72MHz 上本来也只有约 0.1 秒),程序跑过去比眨眼还快。AC5(armcc )不会删它-------O2、-O3、-O3 -Otime 下这个空循环都还在,所以在 AC5 上看不到"整段删掉"这一幕。

解决办法是告诉编译器"这个变量每次都必须真的读写":

c 复制代码
for (volatile uint32_t i = 0; i < 1000000U; i++)
{
    /* 什么都不做 */
}

volatile 这个词上次出现,是讲寄存器的时候------它说的是"这个值可能被别的东西改变,不许替我优化掉"。空循环延时是它的另一个典型用武之地。

这个实验要拿 AC6(armclang) 做,只有它会把空循环删掉:让一盏灯亮、空循环、灭、空循环,先把 Optimization 放在 Level 0,看闪烁节奏;再改成 Level 3 重新编译,闪烁会明显变快;把循环变量加上 volatile 再编一次,节奏又回到原来那个速度。换成 AC5(armcc) 做同一个实验,Level 0 和 Level 3 的闪烁节奏看不出区别------它在 -O2、-O3、-O3 -Otime 下都没删这个空循环。同一份代码、同一个芯片,只改了编译选项------这就是工具链在动手脚。

顺带一句,编译输出的末尾会打印 Code、RO-data、RW-data、ZI-data 四个大小数字。改优化等级、加删代码的时候盯着它们,就能看到自己的程序占了多少 Flash 和 RAM。

查一遍自己的

三步,都在这台电脑上就能做完:

  1. 打开任意一个工程的 Options for Target,看 Target 页的 ARM Compiler:你用的是 AC5 还是 AC6;
  2. 编译一次,在输出窗口里找到编译器打印的那行版本号,记下来------以后跟别人对问题时,这行比"我用的 Keil"有用得多;
  3. 去 C/C++ 页看两处:C99 Mode 有没有勾、Optimization 在哪一级。

知道了这三件事,再遇到"同一个工程换个电脑就编不过""别人的代码我这儿报错",就先比这三个设置,而不是先怀疑自己的板子。

最后橘猫说

同一份代码换台电脑就编不过,先查三样,别先怀疑板子:编译器是哪一代、语言标准是哪一版、优化在哪一级。

AC5 默认按 C90 检查,变量声明插在语句中间就报 #268;AC6 默认按现代标准来,同一段代码一句话不说。优化等级还会改现象:AC6 开了优化,空循环连影子都不剩,加 volatile 才留得住。三样都查清楚,"玄学编译问题"就会少很多。

最后蜘蛛说

汝可曾听闻"交叉编译"?

相关推荐
凯基迪科技2 小时前
STM32G系列(G0/G4)芯片解密程序提取RDP读保护解析
网络·stm32·安全
硅基造物主3 小时前
STM32 从 0 到 1 做智能家居(五):工程结构与代码骨架
stm32·单片机·嵌入式硬件·智能家居
深圳老胡3 小时前
STM32F103 硬件 SPI 与软件 IO 模拟 SPI 最高速度差异详解
笔记·stm32·单片机·代码规范·1024程序员节
wuyk5554 小时前
TinyML 初探:在 MCU 上部署异常检测模型,实现本地振动故障诊断
stm32·单片机·嵌入式硬件·重构
新晨单片机设计4 小时前
S031A-基于STM32单片机指纹考勤系统【Proteus仿真+Keil程序+原理图】
stm32·单片机·嵌入式硬件·proteus·指纹考勤系统
元器件BOM超人4 小时前
STM32F103R8T6 中密度Cortex-M3 MCU 解析
stm32·单片机·嵌入式硬件
深圳老胡6 小时前
STM32C562RET6 外部中断详解:从 EXTI 原理到 GPIO 外部中断实战
笔记·stm32·单片机·代码规范·1024程序员节
wuyk55517 小时前
防逆向基础:在量产固件中关闭调试接口并设置读保护(RDP)的正确姿势
stm32·单片机·嵌入式硬件·性能优化·重构
lhbweilai21 小时前
分析FreeRTOS内核对象API的源码实现(八)
c语言·开发语言·stm32·单片机