NEE's Blog

在 Xbox 360 CPU 中发现的设计缺陷

March 17, 2026

本文翻译自 Finding a CPU Design Bug in the Xbox 360,原载于 Hacker News。


2018 年初 Meltdown 和 Spectre 漏洞的曝光,让我想起了一段往事——我也曾在 Xbox 360 CPU 中发现过一个相关的设计缺陷:一个新添加的指令,它的存在本身就是危险的。

背景:Xbox 360 的 CPU 架构

2005 年,我是 Xbox 360 的 CPU 专家。我对那块芯片了如指掌——我的墙上至今还挂着一块 30 厘米的 CPU 晶圆,还有一张四英尺长的 CPU 布局海报。正是因为花了大量时间理解那个 CPU 流水线的工作原理,当被要求调查一些”不可能发生”的崩溃时,我才能够直觉地判断出设计缺陷是根本原因。

Xbox 360 的 CPU 是由 IBM 制造的三核 PowerPC 芯片。三个核心分别位于三个象限,第四个象限包含 1MB 的 L2 缓存。每个核心都有 32KB 的指令缓存和 32KB 的数据缓存。

冷知识:Core 0 距离 L2 缓存更近,其 L2 延迟明显更低。

Xbox 360 CPU 的各项延迟都很高,内存延迟尤其糟糕。而且,1MB 的 L2 缓存(已经是能塞进去的最大容量了)对于三核 CPU 来说实在太小了。因此,节省 L2 缓存空间以尽量减少缓存未命中非常重要。

CPU 缓存之所以能提升性能,依赖于空间局部性和时间局部性。空间局部性意味着如果你使用了某个字节的数据,那么你很快可能会使用附近的其他字节。时间局部性意味着如果你使用了某些内存,你可能在不久的将来还会再次使用它。

但有时候,时间局部性并不会发生。如果你每帧处理一次大型数据数组,那么很容易证明当你再次需要它时,它早就从 L2 缓存中消失了。你仍然希望这些数据在 L1 缓存中以受益于空间局部性,但让它占用宝贵的 L2 缓存空间只会驱逐其他数据,可能会拖慢其他两个核心。

xdcbt:危险的扩展预取指令

通常情况下,这是无法避免的。我们的 PowerPC CPU 的内存一致性机制要求 L1 缓存中的所有数据也必须存在于 L2 缓存中。用于内存一致性的 MESI 协议要求,当一个核心写入某个缓存行时,其他拥有相同缓存行副本的核心需要丢弃它——而 L2 缓存负责跟踪哪些 L1 缓存正在缓存哪些地址。

但是,这是一款游戏主机的 CPU,性能高于一切,因此添加了一个新指令——xdcbt

普通的 PowerPC dcbt 指令是典型的预取指令。而 xdcbt 指令是扩展预取指令,它直接从内存获取数据到 L1 数据缓存,跳过 L2。这意味着内存一致性不再有保证,但是嘿,我们是游戏程序员,我们知道自己在做什么,不会有问题的。

大错特错。

我编写了一个广泛使用的 Xbox 360 内存复制函数,它可以选择性地使用 xdcbt。预取源数据对性能至关重要,通常它会使用 dcbt,但传入 PREFETCH_EX 标志就会使用 xdcbt 预取。这个设计考虑得并不周全:

if (flags & PREFETCH_EX)
    __xdcbt(src+offset);
else
    __dcbt(src+offset);

一位使用这个函数的游戏开发者报告了奇怪的崩溃——堆损坏崩溃,但内存转储中的堆结构看起来是正常的。盯着崩溃转储看了一会儿后,我意识到了自己犯的错误。

使用 xdcbt 预取的内存是”有毒的”。如果另一个核心在它从 L1 刷新之前写入了这块内存,那么两个核心就会对内存有不同的视图,而且无法保证它们的视图会趋于一致。Xbox 360 的缓存行是 128 字节,我的复制函数的预取会一直进行到源内存的末尾,这意味着 xdcbt 被应用到了一些缓存行,而这些缓存行的后半部分属于相邻的数据结构。通常这是堆元数据——至少这是我们看到崩溃的地方。不一致的核心看到了过时的数据(尽管我们小心翼翼地使用了锁),然后崩溃了,但崩溃转储写出了 RAM 的实际内容,所以我们看不到发生了什么。

所以,安全使用 xdcbt 的唯一方法是非常小心,不要预取超出缓冲区末尾哪怕一个字节。我修复了内存复制函数,避免预取过远,但在等待修复期间,那位游戏开发者停止传递 PREFETCH_EX 标志,崩溃就消失了。

真正的 Bug

到目前为止还算正常,对吧?傲慢的游戏开发者玩火、飞得太靠近太阳、娶了自己的母亲,一台游戏主机差点错过圣诞节。

但是,我们及时发现了问题,侥幸逃过一劫,一切准备就绪,游戏和主机即将发货,大家都可以开开心心回家了。

然后,同一个游戏又开始崩溃了。

症状完全相同。只是这个游戏不再使用 xdcbt 指令了。我可以单步执行代码确认这一点。我们遇到了一个严重的问题。

我使用了古老的调试技术:盯着屏幕发呆,让 CPU 流水线填满我的潜意识。突然,我意识到了问题所在。给 IBM 发了一封邮件,证实了我对一个从未想过的微妙 CPU 细节的怀疑。

这与 Meltdown 和 Spectre 的罪魁祸首是同一个。

Xbox 360 CPU 是一个按序执行的 CPU。它相当简单,依靠高频率(尽管没有达到预期的 10 FO4)来获得性能。但它确实有分支预测器——它非常长的流水线使这成为必要。下面是我制作的一个公开分享的 CPU 流水线图:

Xbox 360 CPU Pipeline

你可以看到分支预测器,也可以看到流水线非常长(在图中很宽)——足够长,即使按序处理,错误预测的指令也能”起跑”。

投机执行的代价

所以,分支预测器做出预测,被预测的指令被取指、解码和执行——但直到预测被确认正确才会提交(retired)。听起来很熟悉?

我当时的顿悟——对我来说是全新的认识——是投机执行一个预取指令意味着什么。延迟很长,所以尽快把预取事务放到总线上很重要,而且一旦预取启动,就没有办法取消它。

所以,投机执行的 xdcbt 和真正执行的 xdcbt 是一模一样的!(顺便说一句,投机执行的加载指令只是一个预取。)

这就是问题所在——分支预测器有时会导致 xdcbt 指令被投机执行,这和真正执行它们一样糟糕。

我的一位同事(谢谢 Tracy!)提出了一个巧妙的测试方法来验证这一点——把游戏中所有的 xdcbt 替换成断点。这实现了两件事:

  1. 断点没有被触发,证明游戏没有执行 xdcbt 指令。
  2. 崩溃消失了。

我知道会是这个结果,但亲眼看到还是很震撼。多年后的今天,即使在读过了关于 Meltdown 的文章之后,看到没有被执行的指令导致崩溃的确凿证据,仍然让我觉得很酷。

教训

分支预测器的发现让事情变得清楚:这个指令在任何游戏的代码段中都太危险了——控制一条指令何时可能被投机执行太困难了。间接分支的分支预测器理论上可以预测任何地址,所以没有”安全的地方”可以放置 xdcbt 指令。而且,如果被投机执行,它会很乐意地对指定寄存器恰好随机包含的任何内存进行扩展预取。可以降低风险,但无法消除,这真的不值得。虽然 Xbox 360 架构讨论中继续提到这个指令,但我怀疑是否有任何游戏带着它发货了。

我曾经在一次面试中提到过这个——”描述你遇到过的最棘手的 Bug”——面试官的反应是”是的,我们在 Alpha 处理器上也遇到过类似的问题”。事物变化越多……


附言:分支预测器如何”凭空”预测?

一个从未被采取的分支怎么会被预测为采取?很简单。分支预测器不会为可执行文件中的每个分支维护完美的历史记录——那是不切实际的。相反,简单的分支预测器通常会将一堆地址位、可能还有一些分支历史位”挤压”在一起,然后索引到一个两位条目的数组。因此,分支预测结果会受到其他不相关分支的影响,导致有时出现虚假的预测。

但这没关系,因为这”只是一个预测”,它不需要是正确的。

总结

这个故事给我们的启示:

  1. 投机执行的副作用是真实存在的——即使指令最终没有被提交,它的某些效果可能已经无法撤销。
  2. 性能优化与正确性的权衡需要谨慎——像 xdcbt 这样绕过安全机制的”聪明”优化,可能在最意想不到的地方埋下隐患。
  3. 分支预测器行为难以预测——你无法完全控制何时何地会发生投机执行。
  4. 底层 Bug 调试需要深厚的硬件知识——如果不是对 CPU 流水线了如指掌,这个 Bug 可能永远找不到。

这个故事发生在 2005 年,而 2018 年的 Meltdown 和 Spectre 漏洞再次证明:投机执行带来的安全问题是一个持续的挑战。历史总是惊人地相似。

comments powered by Disqus