上周,Linux 内核邮件列表上关于“社区最近讨论了是否为内核采用现代 C 语言标准”的信息引发业内关注。刚刚,Linux 开源社区已正式宣布:内核 C 语言版本将在未来升级到 C11,且预计将在今年 5 月份的 5.18 版本之后生效。
这个突然的决定,也终于让拥有 30 年历史的 Linux 内核 C 语言迎来了升级。
众所周知,想要说服固执的 Linux 之父 Linus Torvalds 绝非易事。那么,这一次 Linus Torvalds 为何终于松口了呢?这里面,似乎还真有那么一点偶然因素。
事件起因还是要回到上周的那次的 Linux 社区讨论。
一条 Bug 引发的“连锁反应”
据悉,当时一位名叫 Jakob Koschel 的博士生正在研究与内核链表原语相关的推测性执行漏洞,过程中他发现了一个问题:Linux 内核广泛使用 struct list_head 定义的双链表:
struct list_head {
struct list_head *next, *prev;
};
通常,开发者通过将此类结构嵌入其他结构里的方式,来使任何相关的结构类型都可以创建链表。同时,该内核还提供了大量可用于遍历和操作链表的函数和宏。其中一个就是 list_for_each_entry(),这是一个伪装成控件结构的宏。
恰巧,问题出在了这个宏上。
我们假设该内核包含以下结构:
struct foo {
int fooness;
struct list_head list;
};
List 中的元素则可用于创建 foo 结构的双链接列表。
假设有一个名为 foo_list 的结构声明作为此类链表的头,则可以使用以下代码遍历此链表:
struct foo *iterator;
list_for_each_entry(iterator, &foo_list, list) {
do_something_with(iterator);
}
/ Should not use iterator here /
list 参数告诉宏 foo 结构中 list_head 结构的名称。对于迭代器指向的列表中的每个元素,该循环将执行一次。
而这样就会导致 USB 子系统中出现错误:在退出宏后,传递给该宏的迭代器仍可使用。当然,这是一件非常“危险”的事情。
所以,Koschel 提交了一个补丁,重新编写了有问题的代码,通过在循环结束后停止使用迭代器来修复这个错误。随后,Jakob Koschel 将(投机性安全列表迭代器建议)修复的与内核链接表相关的预测执行漏洞的补丁提交给了 Linus Torvalds。
Linux 之父终于被说服
最初,Linus Torvalds 本人似乎对这个补丁并不是很喜欢,也不知道该补丁与推测性执行漏洞有什么关系。但经过 Koschel 详细解释之后,Linus 承认了这只是一个常见的 Bug。
然而,事情并非那么简单,Linus 很快就意识到了真正的问题:传递给链表遍历宏的迭代器必须在循环本身之外的范围内声明。
而出现这种不可预测的错误的原因是 C89 中没有“在循环中声明变量”。
我们知道,虽然 Linux 内核正在快速发展,但它也依赖于一些非常古老的工具,其中之一就是其内核代码仍在使用 1989 年版的 C 语言标准,也就是说,该标准是在内核项目启动 30 多年前编写的。
像 list_for_each_entry()这样的宏,基本上总是将最后一个 HEAD 条目泄漏出循环,就是因为不能在循环本身中声明迭代器变量。
如果可以编写一个迭代器列表遍历宏来声明自己,那么迭代器在循环外就不可见,也不会出现这样的问题。
然而,由于内核停留在C89标准上,因此不可能在循环中声明变量。
因此,Linus 决定,“让我们升级一下”,也许是时候升级到 C99 标准了,尽管 C99 也有 20 多年的历史了,但它至少比 C89 更新一点,且可以在循环中声明变量。
既然 C89 已经过时了,为什么这么多年都没有改变呢?Linus 解释称,“这是因为我们在一些旧的 gcc 编译器版本上遇到了一些奇怪的问题,这些版本不能随意升级。”
然而,现在 Linux 内核已经将 gcc 的最低要求提高到了 5.1 版,过去那些奇怪的 Bug 应该消失了。
另一位核心开发者 Arnd Bergmann 也对此事比较关注,他认为可以升级到 C11 甚至更高版本,但升级到 C17 或 C2x 会破坏 gcc-5/6/7 支持,因此升级到 C11 更容易实现。
最终,Linus Torvalds 支持了这个想法,并宣布将“在 5.18 版合并窗口的早期尝试一下”。
虽然接下来转移到 C11 可能会导致一些意想不到的 Bug 也说不定,但如果一切顺利,下一个 Linux 内核版本将正式转移到 C11。您对此次升级事件有何看法呢?也欢迎在下方交流互动。