在相继出现 Meltdown 和 Spectre 漏洞之后,花一些时间研究造成漏洞的根本原因是值得的。这两个漏洞都涉及处理器绕过某种访问检查直接执行指令,让攻击者可以通过侧通道观察执行结果。导致这些漏洞的原因让 C 语言程序员相信他们正在使用的是一门低级的编程语言,但几十年来,情况并非如此。
不仅仅是处理器厂商,这件事情与我们这些从事 C/C++ 编译器研究的人也不无关系。
什么是低级编程语言?
计算机科学先驱 Alan Perlis 对低级编程语言的定义:
“当一门编程语言的程序要求把注意力放在不相关的内容上时,那它就是低级的编程语言”。
或许这个定义适用于 C 语言,但它并不能准确表达人们对低级语言的认识。人们通过多种属性来判断一门编程语言是否是低级的。我们假设将编程语言视为一个连续的整体,一端是汇编,另一端是星际级的计算机接口。低级语言“接近金属”,而高级语言更接近人类的思维方式。
对于“接近金属”的语言,必须提供一个抽象机,以便轻松映射到目标平台公开的抽象上。人们很容易认为 C 语言是 PDP-11 的低级语言。在 C 语言模型中,程序都是按顺序执行,内存是一个扁平的空间,甚至预增量和后增量运算符都与 PDP-11 寻址模式完全一致。
PDP-11 模拟器
Spectre 和 Meltdown 漏洞的根本原因在于,处理器架构师不仅试图构建出快速的处理器,他们还试图构建与 PDP-11 一样的抽象机。这样就可以让 C 语言程序员相信他们的语言是接近底层硬件的。
C 语言的代码提供了一个几乎串行的抽象机(直到 C11,如果排除非标准的厂商扩展,那么它就是完全串行的抽象机)。众所周知,创建新线程是一种昂贵的库操作,因此希望保持执行单元忙于运行 C 语言代码的处理器不得不依赖 ILP(指令级并行)。它们检查相邻的操作,然后并行执行独立的操作。在这种情况下,为了让程序员编能够按照串行的方式编写代码,增加复杂性(和功耗)是不可避免的。相比之下,GPU 在没有这种逻辑的情况下实现了非常高的性能,但代价是要求程序代码必须是并行的。
追求高 ILP 是导致 Spectre 和 Meltdown 漏洞的直接原因。现代英特尔处理器一次最多可以执行 180 条指令(与串行 C 语言抽象机形成鲜明的对比,后者希望每个操作在下一个操作开始之前完成