老编译器以及古老的 bug!
| 2021-01-29 16:24
内核项目花费了很多精力来帮助使用旧的工具链的开发者。在一个新系统上编译内核本身已经是一个挑战了,如果还要被迫先安装一套指定版本的工具链的话就是一个额外的负担了。因此,内核开发者们尽量保证大多数发行版中提供的工具链都可以成功编译内核。不过这个做法是有代价的,比如无法使用编译器里面的最新功能。并且,最近的一件事里看到,使用旧的编译器进行构建也会让开发者受到编译器中过去的 bug 的影响。
1 月 5 日,Russell King 报告了一个他调查了很久的问题。他有一些运行 5.4 或更高版本内核的 64 位 Arm 平台,极少数情况下会碰到 ext4 根文件系统校验失败。这个问题可能需要在正常测试中耗费长达三个月的时间才能复现一次,因此,正如 King 所描述的那样,“无法通过 bisect 二分法来调查是哪个补丁导致的问题”。不过,他后来找到了一种可以可靠重现的方法,这样一来就有可能确定出这个问题是何时引入的了。
在 King 报告这个问题之后,一些在 Arm 子系统中工作的开发人员研究了这个问题。他们花费了不少功夫之后指出这个提交是罪魁祸首。这个在 2019 年合并的改动,把 I/O 访问寄存器的函数中用到的内存分界线操作进行了优化,提升了 I/O 内存的访问效率。撤销这个补丁后,问题就消失了。
通常开发人员会打上撤销补丁之后,宣称问题已经解决,但是这次不一样。这个问题补丁的作者 Will Deacon 坚信此补丁是正确无误的。如果 Arm 架构的行为是跟规范定义一致的,那么就不应该需要更多的内存分界线,所以他认为这个问题有个其他尚未查清的真正来源。换句话说,撤销补丁使问题消失了,但是它掩盖了其他地方的真正问题。
这个“别的地方”可能在哪里?King 认为,它可能在内核、Arm 处理器本身,或者是 相干互连(就是把 CPU 集群和内存连接起来的部分)。他认为硬件存在问题的可能性相对来说很小,因此这个错误应该是隐藏在内核的某个地方。这就引起了大量的代码检查工作,特别是在 ext4 文件系统内的代码检查。
两天后,King 宣布问题已经查清了,这的确是 ext4 文件系统内部的问题,但不是人们所预期的那种。仔细看 ext4_chksum()
函数编译后生成的汇编代码,可以看到编译器会在函数本身结束前释放了函数的栈帧。该函数的最后一行是:
return *(u32 *)desc.ctx;
这里,desc
是一个局部变量,存放在栈之中。编译后的函数会在读取 desc.ctx
之前重置栈指针到这个变量之上。这就导致了有那么一个瞬间(刚好可以执行一条指令)中,这个函数在使用已经被释放的栈区域。
这是一个最严重类型的编译器错误。错误编译出来的代码几乎每次都能正常完成任务,毕竟没有其他代码试图在这一个指令窗口中来分配堆栈空间。但是,如果恰好在两条指令之间发生了一次中断,那么问题就会出现了,栈会被覆盖掉,后来的 desc.ctx
读取拿到的就会是个错误值,触发人们观察到的校验和失败错误。这几乎是永远不会发生的事情,但一旦发生,就会出大问题。
这个错误编译是由 2016 年 8 月发布的 GCC 4.9.4 产生的(4.9.0 是它所基于的主要版本,于 2014 年 4 月推出)。不过相关的 bug,在 2014 年就被报出来了,并在当年 11 月得到了修复。这个修复似乎从未从(当时的)开发分支也就是 5.x 版本分支反向移植回 4.9.x,所以 4.9.4 版本并不包含这个修复。有趣的是,像 Red Hat、Android 和 Linaro 这些发行版提供商所发布的 4.9.4 版本都有反向移植这个修复,所以它只影响了那些没有使用这些版本的开发者。这个 bug 在那里潜伏了好几年,直到最后出现在 King 的环境中。
这个事件在一方面清楚地展示了支持旧的工具链的潜在弊端。为了追踪一个事实上六年前就已经被修复的 bug,我们做了大量的工作。如果没有开发者还在使用 4.9.x 编译器的话,我们就不需要花费这个时间了。
恰好,GCC 4.9 是内核所支持的最老的编译器,但这也只是最近才公布的说法。直到 2018 年的时候,内核仍然宣称(不完全属实)可以使用 2002 年发布的 GCC 3.2 完成编译。2018 年的一些讨论之后,才将 GCC 最低版本提升到了 4.6,后来又变成了 4.9。
不可能再修复 GCC 4.9 来解决这个 bug 了,GCC 的开发者早就不在那个版本上工作了。所以,至少要把能用于编译 arm64 架构的编译器提升到 5.1 或以上了。但这马上就引出了一个问题,那就是是否应该将所有架构的编译器版本需求都提升上来。
Ted Ts'o 赞成这种改变,但他也指出,RHEL 7(包括衍生的 CentOS 7)系统仍然停留在 GCC 4.8 上。不过正如 Peter Zijlstra 所指出的,在这些系统上构建内核早就已经需要安装比发行版本身提供的编译器更新的版本了。Arnd Bergmann 说,GCC 4.9 还有其他一些地方在用,比如 Android 和 Debian 8。Android 后来改用 Clang 来构建内核了,而 Debian 8 在 2020 年 6 月底就不再继续支持了。所以看来将 GCC 最低版本提高到 5.1,只会给很少用户带来不便。
另一方面,除了解决掉一个 bug 之外,这样的举动也有一些其他好处。Bergmann 认为这样做的话就可以允许用 -std=gnu11
来编译内核了,从而可能可以使用那些依赖 C11 的前沿特性。目前,内核编译使用的是 -std=gnu89
,也就是基于 C89 标准,不是那么亮眼。Zijlstra 和 Deacon 都补充说,迁移到 5.1 就可以移除掉一些针对 GCC 4.9 上问题的临时措施了。
综上所述,似乎很少有人反对将内核整体转移到 GCC 5.1 或以上了。据说,Linus Torvalds 觉得这个改动的价值不是很明确,可能还需要一些更有说服力的信息。即使我们不会马上转向 GCC 5.1,但是 GCC 4.9 也不可能会有无限期的支持,这看起来是板上钉钉的事。当然,2015 年 4 月发布的 GCC 5.1 也不是什么新版本了。但我们希望它的隐藏 bug 少一些,同时也提供一些更受欢迎的新功能。支持老的工具链有它的价值,但有时候放弃最古老的工具链的做法也是有价值的。