Meltdown启示录:深入分析Xbox 360中的CPU设计漏洞

阅读量    58447 |   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

前言

近期爆出的Meltdown和Spectre漏洞,让我突然想起曾在Xbox 360的CPU中发现的一个设计漏洞。造成该漏洞的原因是一个新增加的指令,而这个指令的存在产生了潜在的风险。

Xbox 360的CPU结构

早在2005年,我就开始对Xbox 360的CPU进行研究。当时,我夜以继日地对那个小小的芯片进行分析,时至今日我的墙上还嵌着一个30厘米的CPU晶圆(Wafer),并且挂着一个长1.2米的CPU设计图纸。我用了大量时间来弄清楚CPU流水线(CPU Pipeline)的工作原理,在我遇到一些看似不可能出现的崩溃情况时,我能敏锐地判断出其根本原因在于设计上的缺陷。首先,我们先来共同了解一下相关的背景知识。
Xbox 360的CPU是由IBM制造的三核PowerPC(Performance Optimization With Enhanced RISC – Performance Computing,也可以简称为PPC)架构的芯片。其中的3个核分别位于3个独立的模块之中,第四个模块中包含1MB的二级缓存(L2 Cache)。如下图所示,我们可以直观地看到CPU不同的组件。其中,每个内核都有一个32KB的指令缓存(Instruction Cache)和一个32KB的数据缓存(Data Cache)。

Xbox 360的CPU对所有内容都有非常高的延迟,其中内存的延迟尤为严重。并且,其拥有的二级缓存大小仅为1MB,这对于三核CPU来说非常小。因此,就需要尽量节约二级缓存空间,以尽可能地减少缓存未命中(Cache Misses)现象的发生,这一点非常重要。

空间和时间的局部性

由于空间和时间的局部性,促使CPU缓存不得不努力提升其性能。其中的空间局部性(Spatial Locality)是指,如果你使用了某位置的一个字节,那就意味着你可能很快就使用掉其附近的数据字节。其中的时间局部性(Temporal Locality)是指,被使用过一次的内存位置,很可能在未来会被多次使用。
然而在实际中,时间局部性并不会经常发生。如果我们每次都处理大量的数据,便可以证明时间局部性的概念,这些数据将全部从二级缓存中消失,直至下一次再需要使用它们的时候。事实上,我们仍然希望能够在一级缓存中使用这些数据,在这里,就体现出来了时间局部性的优势。但与此同时,二级缓存占用了稀缺的空间,也就意味着其他数据会被覆盖,从而有可能会减慢其他两个内核的运行速度。

内存一致性机制

通常情况下,这种场景是不可避免的。在PowerPC中,CPU的内存一致性机制(Memory Coherency Mechanism)要求一级缓存中的所有数据也要存储在二级缓存之中。用于确保内存一致性的MESI协议(Modified Exclusive Shared Or Invalid,也称伊利诺斯协议,是一种广泛使用的支持写回策略的缓存一致性协议,参考https://en.wikipedia.org/wiki/MESI_protocol) 要求:当一个内核写入缓存行(Cache Line,CPU的最小缓存单位)时,任何其他具有相同缓存行副本的内核都需要将其丢弃,并且二级缓存负责跟踪一级缓存所缓存的地址。
然而大家不要忘记,我们的CPU是要用在游戏机上的,而游戏机对性能的要求非常高,几乎超过了其他所有的设备。因此,一个新的指令应运而生——那便是xdcbt。

xdcbt指令

在普通型号的PowerPC中,dcbt指令是一个典型的预取指令(Prefetch Instruction)。而这里的xdcbt指令则是一个扩展的预取指令,可以直接将指定地址的内存中预取到一级数据缓存中,跳过了二级数据缓存。这就意味着,在这里将不会再保证内存一致性。因此,游戏开发者们应该充分理解这个概念并在编程过程中特别注意,然而实际上,一些开发者并没有重视这一指令所产生的问题。

内存复制例程的问题与修复过程

针对这一问题,我曾写过一个Xbox 360通用的内存复制例程,会利用到xdcbt指令。对原始数据进行预取,能够极大地提升性能。通常来说,预取操作会使用dcbt指令,但当其传入PREFETCH_EX标志后,便会开始使用xdcbt。在这一点上,我并没有太过深思熟虑。
随后,一些使用该函数的开发工程师向我反馈说,他们编写的游戏会发生莫名其妙的堆损坏崩溃(Heap Corruption Crashes)。然而经过分析,内存转储中的堆结构看起来却是正常的。在经过一段时间的仔细研究之后,我意识到我犯了一个错误。
这一崩溃的原因在于,用xdcbt指令预取的内存是有问题的。如果该内存是由另一个内核写入,然后又被一级缓存刷新,那么两个内核将具有两种不同的内存视图,并且我们无法保证这两种视图会相互同步。Xbox 360的缓存行大小为128字节,而我的复制例程会直接预取到原始内存的结尾,这也就意味着xdcbt会被应用在一些缓存行之中,而它们是相邻数据结构中的一部分。通常,这些位置是堆的元数据(Heap Metadata),我们也可以从崩溃产生的错误信息中推断出来。尽管我们已经谨慎使用了内存的锁定,但由于某个内核使用了未更新的数据,所以导致了崩溃。然而,在内存报错(Crash Dump)文件中所写入的却是随机存取存储器(RAM)的真实数据,所以我们之前看到的堆结构是正常的。
因此,我们必须要注意,在使用xdcbt指令的时候,不要堆超过缓冲区末尾的字节进行预取操作。为修复这一问题,我修改了我的内存复制例程,以避免预取的内容过多。但就在我修改例程的同时,开发者们也对其游戏进行了改动,他们不再传递PREFETCH_EX这一标志,最终也成功解决了这个问题。

再一次出现的崩溃

到目前为止,一切似乎重回了正轨。游戏开发者们在开发的过程中,出现过各种严重的问题( https://literarydevices.net/hubris/ ),然而我们都逐一攻破,成功解决,并且愉快地准备将这些游戏正式公开发行。
然而好景不长,这些游戏很快又再次出现了崩溃的情况。尽管现在的这些游戏中,已经不再使用xdcbt指令,但出现的崩溃却与之前一模一样。
我非常疑惑,并且预感到我们正面对一个严重的问题,开始逐行分析所有的代码。
我使用的是传统的调试方法,并且不断思考着CPU流水线的工作原理,终于在某一天灵光一闪,意识到了问题的所在。
于是,我给IBM发送了一封邮件,并很快得到了答复。在答复中,证实了我对于CPU内部构造问题的怀疑,而这一问题也是产生Meltdown和Spectre漏洞的罪魁祸首。

CPU流水线与分支预测器

Xbox 360的CPU,是顺序执行的CPU(in-order CPU)。其原理非常简单,就是依靠它的高频率来执行。然而,由于其流水线较长,因此该CPU具有一个分支预测器(Branch Predictor)。下图是CPU流水线的示意图,其中展现了所有的流水线:

由于受到英特尔NDA保密协议的限制,我无法披露更精确的示意图,但大家可以参考下图所展示的结构:

我们在图中可以看到分支预测器,并且可以看到其流水线非常长。因此,针对错误预测的指令,其加速的时间就非常长,即使顺序执行也是如此。
所以,分支预测器将会不断对程序的分支流程进行预测,其预测的指令将会被提取、解码和执行,这一过程将持续进行,直至已知的预测是正确的。以我对这一过程的理解,它会先进行预测,然后根据预测的结果执行预取操作。由于延迟的时间很长,所以尽快向总线发送预取指令是非常重要的。并且,一旦执行了预取操作,就无法再取消。因此,一个预测后再执行(Speculatively-Executed)的xdcbt指令和一个真正的xdcbt指令是一样的。然而我们知道,一个预测后再执行的加载指令,本质上还是一个预取指令。
这就是问题的所在。分支预测器有时会导致xdcbt指令在预测后被执行,这实际上和此前直接执行该指令在本质上是一样的。在我的同事Tracy的建议下,我们有一个非常巧妙的方法可以验证这一点——我们用断点来替换游戏中的每一个xdcbt指令。这样,可以实现两件事情:
1、假如运行过程中没有中断,那就能证明游戏没有执行xdcbt指令;
2、就算执行了xdcbt指令,也不会真正地发生崩溃。
我知道,最后的结果让人非常惊讶。然而经过了这么多年,时至今日,在阅读了关于Meltdown漏洞的文章之后,我们仍然发现没有被执行的指令能够导致崩溃,这个漏洞还是很酷的。

总结

分支预测器的实现过程清楚地说明,这个指令非常危险,因为我们很难去控制能够预测后执行指令的位置。间接分支的分支预测器理论上可以预测任何地址,所以并没有一个“安全位置”可以用来放置xdcbt指令。我们只能通过各种手段来降低风险,但依然不能消除风险。
在针对Xbox 360结构的讨论中,我们提到了这一指令,我非常怀疑所有使用该指令的游戏都可能受到了这一问题的影响。
我曾在一次面试中曾经问过别人这样的问题:“你在研究过程中遇到的最棘手的问题是什么?”而面试者回答的是“我们之前在Alpha处理器上,曾经遇到过……”
现在看来,恐怕如果今后还有面试者,他们的回答应该会不一样了。
最后,感谢Michael对本文提供的帮助,相关文章请参考:http://michaelbrundage.com/project/xbox-360-emulator/

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多