【编译器玄学研究报告】第五期——三十年老娘倒绷孩儿

2021-08-25 10:30:08 浏览数 (1)

【说在前面的话】


这是一件发生在我身上的真实事件,它根本不是一个故事——由于它差点就变成我人生的一次巨大乌龙——所以应该算是个事故。此前,我曾经发现过不下两位数的编译器Bug,所以一开始,当这一次事件到来的时候,我并未过多的产生过怀疑……

【诡异的错误信息】


那是个与往常无异的寂静夜晚,我像平常一样,关掉了小房间的灯,让黑暗如同浓厚的咖啡那样包裹着橘黄色台灯下的我。透过屏幕左上角的时钟,我注意到时间刚刚划过了子夜。作为一个独自在家办公已经一年多的老码农来说,此时才是一天中敲击代码最为惬意的时刻。

正当我抿了一口口味浓烈的82年矿泉水时,一个开源项目的微信窗口闪动了起来。群里的人熟识了太久,正因为无事不谈,搞得现在无事可谈——一个红色的“@”符号后赫然的写着我的名字——看来不是什么好兆头

“淦!”

我骂了一句,点开了聊天窗口。

你的模块在GCC下编译报错了”,雪白的窗口背后,此刻一定有一张不无嘲笑的嘴脸。

“怎么可能?” 我愤愤不平:“在clang和IAR下都测试过的代码怎么会在GCC中编译报错呢?

考虑到凡事不可把话说的太绝,我顿了顿补充道:“是不是你忘记打开-fms-extensions了?这是常见错误。”

“加了,因为这的确是你的代码出现编译故障的常见原因,所以我第一时间就处理了……呐!你看……”,聊天窗口里出现了截图,“我加了哦!”

“淦!”

我又骂了一句,由于想好的话被截图活生生压了回去——就好比哥斯拉铆足了力气、张大了嘴巴准备吐息时被人堵上了嘴——我一时不知道如何应对才好。

“编译报什么错误呢?”

当这句话从指尖流向屏幕时,我敢打包票,这完全是出于聊天的本能而不是本意——因为大脑此时正在飞速旋转,思考符合这一切的合理解释,换句话说,我对如下的事实其实完全丈二和尚摸不着头脑:

  • 同一段代码,在clang、IAR以及Arm Compiler 6下编译是没问题的,然而现象表明GCC报告了错误;
  • Clang以及Arm Compiler 6同根同源,它们都使用了GCC的语法前端,因此几乎可以这么断定:GCC里可以编译的代码,只要不涉及特殊的#pragma或者__attribute__在clang中几乎肯定是可以正确编译的;反之亦然。
  • 实践中经常会发现,clang比gcc的语法要严格,gcc很多时候在语法风格上更加“放飞自我”,因此clang中可以通过编译的代码,怎么会在GCC中无法编译通过呢?

“Bug!一定是编译器Bug!” 我几乎失去了理智一般脱口而出!同时这一想法马上又让子夜时分脑前叶近乎梦游的我觉得亢奋不已——难道我终于要在大佬云集的GCC界出人头地了?

此时,你一定非常好奇,究竟是怎样的代码让我如此笃定这是编译器Bug呢?尽管原本的应用代码结构看起来要复杂的多,但为了隔离问题,实际上最终能稳定复现问题的代码片断被简化的只剩下了一行:

代码语言:javascript复制
#include <stdint.h>

static const uint32_t s_wTest = (0, 0x12345678);

对于我是如何使用逗号表达式产生如此骚操作而感到好奇的小伙伴,可以阅读这篇文章《【为宏正名】99%人都不知道的"##"里用法》。


这里的关键是右边的逗号表达式,关于它的用法很多人都并不陌生,简单说就是“依次执行逗号左边的表达式,并把最右边表达式的结果作为整个逗号表达式的返回值”。这里:

  • 无论是“0”还是“0x12345678”都是常数
  • 整个逗号表达式的结果怎么看在编译时刻都是确定的

究竟是谁给了GCC一个胆子在众目睽睽之下信口雌黄,扔出如下的错误信息?

代码语言:javascript复制
reproducer.c:3:33: error: initializer element is not constant
    3 | static const uint32_t s_wTest = (0,0x12345678);
      |                                 ^

睁着眼睛说瞎话么?

你敢说这个表达式不是常量?!!!!

【交叉验证】


为了验证我的想法,我又在clang下做了同样的事情:

代码语言:javascript复制
clang reproducer.c

得到了肯定的回答:

代码语言:javascript复制
reproducer.c:3:34: warning: expression result unused [-Wunused-value]
static const uint32_t s_wTest = (0,0x12345678);
                                 ^
1 warning generated.

翻译下来,意思就是说,clang认为这个变量初始化是没问题的,只不过它发现你逗号表达式里有一个值其实没有真正被使用——没错,就是这个“0”——所以它产生了一个不痛不痒的warning:

  • 作为测试,这实际上告诉我们,clang是正常的认可了0x12345678作为逗号表达式的返回值
  • clang并没有认为这个表达式不是常量
  • clang也没有认为这个静态常量 s_wTest 的初始化有什么不妥;
  • 如果觉得这个warning不爽,我们大可以在命令行里加入 -Wno-unused-value来屏蔽它,比如:
代码语言:javascript复制
clang reproducer.c -Wno-unused-value

实际上类似的测试我在Arm Compiler 6以及IAR中多做了测试,并没有遇到什么问题。我忍不住看了一眼GCC的版本:

代码语言:javascript复制
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)

好家伙,虽然不是最新的,但也还是乌班图之类Linux系统的当红靓仔啊!

难道我真的发现了一个如此明显的Bug?

【越想越不对……】


在做好了截图,写好了邮件准备全网放送的时候,为了十足的仪式感以及为日后写回忆录时可以有更多的谈资,我决定沐浴更衣后再焚香一柱以纪念这个历史性的时刻。

就在热水哗啦啦的冲刷着我3个月没有打理过过的一头乌黑靓丽的秀发时,我的内心逐渐从兴奋变为开心、从开心变为平顺、从平顺变得冷静——最后从冷静变成恐惧:

  • 这种语法前端的解析bug太明显了,不可能到了9.0版本还存在——实际上我在写邮件时试图追溯这个Bug最早从哪个版本引入的,尝试过5.0、6.x、10.x等多个版本——问题似乎一直都在那里;
  • 逗号表达式如此常见,很难想象我是第一个发现者
  • 难不成这是一个“feature”而不是“bug”?如果真的是这样,我岂不成了全网笑柄?

怀揣着这种恐惧,我草草的擦干了身子,头都来不及吹就急忙冲到了屏幕前,急不可耐的打开搜索引擎,开始寻找类似的问题。果不其然,在StackOverflow上看到了一模一样的问题:

链接如下:

https://stackoverflow.com/questions/1737634/c-comma-operator

好险,果然小丑就是我自己,而且我差点还要把它广而告之……

【小丑居然是我自己】


正如这个帖子所指出的,在ANSI-C99标准中,Section 6.6/3节对于常量表达式做出了专门的规定:

Constant expressions shall not contain assignment, increment, decrement, function-call, or comma operators, except when they are contained within a subexpression that is not evaluated.

翻译一下就是:

常量表达式不应包含赋值、递增、递减、函数调用或逗号运算符……

问题似乎是水落石出了:这的确是一个由C99明确规定的“feature”而非编译器的"Bug"。

此时,仍然有一个疑问在我脑中挥之不去:

“为什么clang和IAR会允许在常量表达式中使用逗号运算符呢?”

在随后的搜索中,我大体找到了答案。实际上,也许正是如大家所感觉的那样——在一个常量表达式中禁用逗号运算符似乎并无必要——因此在随后的C 11标准中移除了对逗号表达式的禁令。clang和IAR显然因为某种原因(我猜是为了方便)在编译C代码(而非C 代码)时也同时移除了这一限制——这在某种程度上误导我们得出了“好学生GCC有Bug”的错误结论。更多细节和讨论,感兴趣的小伙伴可以看一下这篇帖子:

https://stackoverflow.com/questions/16576933/is-the-comma-operator-allowed-in-a-constant-expression-in-c11

【说在后面的话】


编译器是人类编写的,因此肯定会有Bug;但对于那些过于明显的“Bug”,如果对象是来自一个成熟的编译器,很可能反而是我们自己孤陋寡闻了。

这次事件给我的教训是:

  • 别着急下结论,多搜集证据
  • 作出重大决定前洗个澡可以让自己从盲目的情绪中清醒过来
  • 越是明显的东西,哪怕证据确凿,越是要小心可能有诈

对大部分常用的编译器来说,还是要给予足够的信任——因为有那么多人都在使用,如果有Bug,可能早就发现了。

0 人点赞