写了几百行代码,按过很多次"编译"按钮。灯亮不亮、程序跑不跑得起来,是后面的事;按下去之后第一个说话的东西,是编译器------它要么一声不吭地把代码变成能烧进芯片的文件,要么甩回来一串红字,告诉你哪一行它看不懂。
红字里有一类特别让人发懵:代码在上一个工程里明明能编过,换了个地方就开始报错,报的还是"声明不能出现在语句之后"这种听起来莫名其妙的理由。代码未必坏了,通常是编译器换了 ,或者编译器采用的语言标准换了。
编译不是一件事,是一整条流水线
"编译"是口语。真正发生的是好几步:把 #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 标准,优化也更好。
怎么知道自己在用哪一个?三个地方都能看到:
- 工程选项 :
Options for Target→Target页,ARM Compiler下拉框里写着用的是哪个版本; - 编译输出窗口 :每次编译都会打印编译器自己的名字和版本,例如
Armcc.exe V5.06 update 6 (build 750)------这行就是 AC5; - 帮助菜单 :
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。
查一遍自己的
三步,都在这台电脑上就能做完:
- 打开任意一个工程的
Options for Target,看Target页的ARM Compiler:你用的是 AC5 还是 AC6; - 编译一次,在输出窗口里找到编译器打印的那行版本号,记下来------以后跟别人对问题时,这行比"我用的 Keil"有用得多;
- 去
C/C++页看两处:C99 Mode有没有勾、Optimization在哪一级。
知道了这三件事,再遇到"同一个工程换个电脑就编不过""别人的代码我这儿报错",就先比这三个设置,而不是先怀疑自己的板子。
最后橘猫说
同一份代码换台电脑就编不过,先查三样,别先怀疑板子:编译器是哪一代、语言标准是哪一版、优化在哪一级。
AC5 默认按 C90 检查,变量声明插在语句中间就报 #268;AC6 默认按现代标准来,同一段代码一句话不说。优化等级还会改现象:AC6 开了优化,空循环连影子都不剩,加 volatile 才留得住。三样都查清楚,"玄学编译问题"就会少很多。
最后蜘蛛说
汝可曾听闻"交叉编译"?