将Linux内核转移到现代C

2022-02-25 05:02:49

LWN订户已向您提供以下仅限订阅的内容。成千上万的订阅者依靠LWN从Linux和自由软件社区获得最好的消息。如果您喜欢这篇文章,请考虑订阅LWN。感谢您访问LWN。网

尽管内核项目通常运行速度很快,但它依赖于大量旧工具。而批评者喜欢关注社区#39;电子邮件的过度使用,一个可能更严重的时代错误是使用了1989年版的C语言内核代码标准——该标准在30多年前的内核项目开始之前就已被编纂。随着5.18内核的发布,长期以来的实践似乎即将结束,预计将于今年5月发布。讨论从Jakob Koschel的这个补丁系列开始,他试图防止与内核相关的推测性执行漏洞';s链接的列表原语。内核广泛使用structlist_head定义的双链表:这种结构通常嵌入到其他结构中;通过这种方式,可以制作任何感兴趣的结构类型的链表。除了类型之外,内核还提供了一系列函数和宏,可用于遍历和操作链表。其中之一是list_for_each_entry(),这是一个伪装成某种控制结构的宏。要了解这个宏是如何使用的,假设内核包含这样一个结构:list成员可以用来创建foo结构的双链接列表;单独的列表头结构通常被宣布为此类列表的开头;假设我们有一个叫做foo_list的。可以使用如下代码遍历此列表:struct foo*iterator;列出每个条目(迭代器和foo_list,list){do_something_with(迭代器);}/*这里不应该使用迭代器*/

list参数告诉宏list_head结构在foo结构中的名称。该循环将对列表中的每个元素执行一次,迭代器将指向该元素。Koschel在USB子系统中修复了一个错误,在该子系统中,迭代器传递给该宏,在退出宏后使用该宏,这是一件危险的事情。根据列表中发生的情况,迭代器的内容可能会令人惊讶,即使在没有推测执行的情况下也是如此。Koschel通过重新编写相关代码,在循环结束后停止使用迭代器,解决了这个问题。莱纳斯·托瓦尔兹没有';我非常喜欢这个补丁,而且没有';我看不出它与特定的执行漏洞有什么关系。然而,在科舍尔进一步解释了情况之后,托瓦尔兹同意";这只是一个普通的bug,简单明了#34;并且saidit应该独立于较大的系列进行修复。但随后他深入到了问题的真正根源:传递给列表遍历宏的迭代器必须在循环本身之外的范围内声明:这种非推测性错误可能发生的全部原因是我们在历史上没有';没有C99风格";在循环中声明变量;。因此,为每个条目列出条目()——以及所有其他条目——基本上总是将最后一个头部条目漏出循环,仅仅因为我们不能';t在循环本身中声明迭代器变量。

如果可以编写一个列表遍历宏来声明自己的迭代器,那么该迭代器在循环之外就不可见,也不会出现这种问题。但是,由于内核是基于C89标准的,在循环中声明变量是不可能的。Torvalds说,也许是时候考虑使用C99标准了——它已经有20多年的历史了,但至少现在允许块级变量声明。正如他所指出的,这项运动没有';过去没有这样做过";因为我们在一些古老的gcc版本中遇到了一些奇怪的问题,这些版本打破了已有的初始值设定项;。但是,与此同时,内核已经将其最低GCC要求移到了5.1版,因此这些错误不再相关。倾向于密切关注跨架构编译器问题的Arnd Bergmann同意,内核应该可以向前发展。事实上,他暗示,在进行更改时,有可能达到C11标准(从2011年开始),尽管他不是';t请相信C11将带来任何对内核有用的新功能。甚至有可能转移到C17,甚至是尚未完成的C2X版本。然而,这也有一个缺点,那就是它#34;将打破gcc-5/6/7的支持";,内核目前仍然支持这些版本。将GCC的最低版本提高到8。x很可能是一个跳跃,而不是用户社区愿意接受这一点。不过,转移到C11不需要更改最低GCC版本,因此可能更容易实现。托瓦尔兹支持这个观点:";考虑到它';它酝酿了很多年;。在伯格曼证实应该可以这样做后,托瓦尔兹宣布:";好的,有人请提醒我,让';让我们在5月初试试这个。18合并窗口";。距离5.18合并窗口还有不到一个月的时间,所以这是一个可能在不久的将来发生的变化。不过,值得记住的是,在合并窗口和5.18版本之间可能会发生很多事情。迁移到新版本的语言标准可能会在内核中不显眼的地方发现任意数量的问题;现在,不需要太多的改变就可以恢复。但是,如果一切顺利,到C11的转换将在下一个内核版本中发生。将list_for_each_entry()和变体(内核中有超过15000个)的所有用户转换为一个新版本,该版本不';不过,显示内部迭代器似乎需要更长的时间。(登录发表评论)

请注意,如果出于某种原因需要继续使用c89,则始终可以在for()语句周围添加一个块来保存循环变量。

你';我需要对每个调用者这样做,这是一个很大的代码搅动。

由pbonzini于2022年2月24日16:22 UTC(Thu)发布(✭ 支持者✭, #60935)[链接]

而不是泄漏一个不一定有效的指针,可以';宏是否在末尾将其设置为空?实际上,我';我很惊讶没有';除非编译器将其作为死代码消除,否则这是执行将是错误的赋值的标准技巧。

费用在某些情况下,购买一些本不必要的东西的成本相当高。

在迭代变量为'的任何情况下,它都应该是免费的;t在循环之后访问,因为编译器将消除死存储。代码更改也相当简单:只需从"编辑条件&;位置->;会员!=(负责人)和#34;至";(&;pos->;member!=(head))| |((pos=NULL))和#34;。不幸的是,仅此一点并不能';t处理由于"而提前退出的循环;休息";或";转到";。34岁;后藤";案件不可避免,但";休息";这种情况可以通过将宏包装在第二个简单循环中来处理,如本例[0]所示。请注意,生成的代码(对于带有-O2的gcc 5.1)在带有额外循环(traverse1)的版本和在循环(traverse2)之后未将迭代器设置为NULL的原始版本之间是*相同的*。迭代器初始化为标志状态(-1)、外部循环的条件,以及循环后对迭代器的NULL存储都已成功消除。

它可能有以下功能:外部无符号长列表迭代器循环之后的活动循环;和";|124;((pos=(void*)list_iterator_live_after_loop),0)和#34;我没有';我没有试着那样修改内核宏,但我的小测试代码没有';t link,如果迭代器在循环之后使用,但是如果它',则链接并工作;它没有被使用。我记得,内核已经在使用这种技巧,只有在编译器能够反驳错误消息时,才使用编译器优化来删除错误消息。

哦,我的意思是,编译器会消除所有这些写操作,除了那些暴露bug的写操作,但后来我想知道,如果编译器不消除写操作,你是否可以使内核甚至不链接,这让我左右为难。无论如何,它不会';除非编译器能够';我说不出代码是正确的。

我的想法是';要求与足够旧的标准兼容是愚蠢的,即使是实现新版本的软件也会失去长期支持。还有其他一些案例可以证明';这有可能导致卖旗日,但离开C89不是';我不是他们中的一个。

如果计划在下一个周期开始时采取这一行动,那么';下一个内核现在就采用它吗?

是的,终于!像其他编程语言一样,组合声明和初始化。更多';常数';更少";这个变量';5月'日;在未初始化的情况下使用";猜测/愚蠢。再也没有反向的圣诞树了。用更高级的语言';常数';是默认值,但让';不要忘乎所以;过多的数学运算可能会让硬件工程师在情感上对寄存器产生恐惧。