
2026/03/13 22:40
Xbox 360(2018)におけるCPU設計上のバグの検出
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要
Xbox 360のPowerPC CPUは、3つのコアと1MBの単一L2キャッシュ、各コアに32KBの命令/データキャッシュ(Core 0は低いL2レイテンシ)を備えていますが、微妙なプリフェッチバグがあります。新しい
xdcbt 命令はデータを直接L1へ取得し、必須のL2在住ルールを回避します。著者が広く使用していたメモリコピー例では PREFETCH_EX の下で xdcbt を利用しており、これがゲーム開発者から報告されたヒープ破損クラッシュを引き起こしました。対策としては、xdcbt がバッファの終端を越えてプリフェッチしないようにすることと PREFETCH_EX を無効化することで、両方ともクラッシュを止めました。
後に同じクラッシュが再び発生しました。すべての明示的な
xdcbt を削除したにも関わらずです。調査では、CPU の長いパイプラインとコンパクトな2ビット分岐予測子によって誤って予測された間接ブランチが、隠れた xdcbt を実行していたことが判明しました。すべての xdcbt をブレークポイントに置き換えるとクラッシュが消え、スペキュラティブ実行が原因であることを証明しました。
間接分岐は任意のアドレスを予測できるため、
xdcbt を配置する「安全な場所」は存在せず、スペキュラティブ実行により任意のメモリをプリフェッチし、システムを不安定化させる可能性があります。この問題は Meltdown/Spectre の根底にある脆弱性と同様であり、Hacker News、r/programming、r/emulation、Twitter などで議論されています。
Xbox 360 を対象とするゲーム開発者やエミュレーターのプロジェクトは、スペキュラティブ
xdcbt の使用または類似のプリフェッチ命令をコード上で監査しなければならず、そうしない場合リリースが遅延したりパッチが必要になる可能性があります。本文
最近の Meltdown と Spectre の暴露は、Xbox 360 CPU に関する関連設計バグを発見したあの時期を思い出させました。実際に存在するだけで危険な新しい命令が追加されていたのです。
2005 年には Xbox 360 CPU を担当していました。そのチップを生き、呼吸し、まるで自分自身の一部だったと言っても過言ではありません。壁に 30 cm の CPU ウェーハと四フィートのレイアウトポスターを飾り、CPU のパイプラインがどう動くかを理解するために多大な時間を費やしました。その結果、実際に起きているはずのないクラッシュを調査するとき、設計上の欠陥が原因だと直感できました。まず、背景を少し説明します…
Xbox 360 CPU の概要
- IBM が製造した 3 コア PowerPC チップ。
- 各コアは別々のクワドラントに配置されており、4 番目のクワドラントには 1 MB の L2 キャッシュがあります。右側の図と自分の CPU ウェーハで各構成要素を確認できます。
- 各コアには 32 KB の命令キャッシュと 32 KB のデータキャッシュが備わっています。
トリビア: コア 0 は L2 キャッシュに最も近く、L2 レイテンシが統計的に低いことが確認されていました。
CPU は全体的に高レイテンシで、特にメモリアクセスの遅延が顕著でした。3 コアの CPU に対して 1 MB の L2 キャッシュは小さく、L2 内でキャッシュミスを最小化するためにスペースを節約することが重要でした。
キャッシュとローカリティ
- 空間局所性: あるバイトを読み取ったら、その直近のバイトもすぐに使われる可能性が高い。
- 時間局所性: 何かメモリを使用したら、比較的短時間で再度アクセスされる可能性が高い。
しかし時間局所性は必ずしも成立するわけではありません。フレームごとに大きな配列を一度だけ処理する場合、そのデータは L2 からすべて消えてしまうことが証明できます。L1 に残しておくことで空間局所性の恩恵を受けるため、必要なのは L1 のスペースです。しかし L2 にそのまま放置すると他のデータを排除し、残りの 2 コアの速度が落ちる原因になります。
通常これは避けられません。PowerPC CPU のメモリ整合性機構は、L1 キャッシュにあるすべてのデータも L2 に存在する必要があります。MESI プロトコルでは、あるコアがキャッシュラインを書き込むと、他のコピーを持つ全てのコアがそれを破棄します。さらに L2 はどの L1 がどのアドレスをキャッシュしているかを追跡しています。
しかしこれはビデオゲーム機であり、性能が最優先でした。そのため新しい命令 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 からフラッシュされる前に書き込むと、2 つのコアはメモリを異なる状態で見ることになり、両者のビューが結合する保証はありません。Xbox 360 のキャッシュラインは 128 バイトであり、私のコピー関数はソースメモリの終端までプリフェッチしていました。その結果 xdcbt は隣接するデータ構造(典型的にはヒープメタデータ)に属するキャッシュラインの後半部分にも適用されてしまい、クラッシュが発生しました。整合性を保つためにロックを慎重に使っていたにも関わらず、古いデータでクラッシュし、ダンプには実際の RAM 内容しか書き出されませんでした。
したがって
xdcbt を安全に使用する唯一の方法は、バッファ末尾まで 1 バイトでもプリフェッチしないよう注意深く制御することです。私はルーチンを修正して過剰プリフェッチを避けました。ゲーム開発者が PREFETCH_EX フラグを渡さなくなると、クラッシュは消えました。
本当のバグ
ここまで普通に聞こえるでしょうか? ゲーム開発者は火遊びし、太陽に近づきすぎた… などということです。問題を時間内に捕まえてゲームを出荷でき、すべて順調でした。しかし同じゲームが再びクラッシュを起こしました。症状は同一で、ただ
xdcbt を使っていない状態でした。コードをステップ実行して確認したのです。
私は古いデバッグ手法を使いました:空白の頭脳で画面を見つめ、CPU パイプラインが潜在意識に流れ込むのを待ち、突然問題点に気づきました。IBM に短いメールを送って確認すると、私の疑念は正しかったことが分かりました。それは Meltdown と Spectre の根底にある微妙な内部 CPU の詳細と同じ原因でした。
Xbox 360 CPU は 順序実行 CPU です。高周波(10 FO4 にも関わらず期待ほどではありません)を性能のために頼っていますが、分岐予測機構も備えています。長いパイプラインにより分岐予測は不可欠です。公開されたパイプライン図ではすべてのパイプラインが示されており、分岐予測器はフロントに位置し、パイプラインは非常に長く(図では幅広)あります。これにより、誤予測された命令でも順序実行で速度を維持できます。
つまり、分岐予測器が予測を立てると、予測された命令はフェッチ・デコード・実行されますが、予測が正しいことが判明するまでリタイアしません。ここに気づいたのは、プリフェッチをスぺキュレート実行した場合も実際に実行したのと同じという点です。レイテンシーが長いので、プリフェッチ取引をバス上でできるだけ早く開始することが重要でした。一度プリフェッチが始まればキャンセルは不可能です。そのため、
xdcbt のスぺキュレート実行は実際に xdcbt を実行したのと同じ効果を持ちました(スぺキュレートロード命令は単なるプリフェッチでした)。
この問題は分岐予測器が時折
xdcbt 命令をスぺキュレート実行し、結果として本当に実行したのと同じ危険な副作用をもたらすということです。私の同僚(Tracy ありがとう!)は巧妙なテストを提案しました:ゲーム内の全 xdcbt をブレークポイントに置き換える。この操作で次の二つが起こりました。
- ブレークポイントはヒットせず、ゲームは
命令を実行していないことが証明された。xdcbt - クラッシュは消えた。
結果は予想通りでしたが、それでも驚きました。何年も経ってから Meltdown を読んだ後でも、実際に実行されていない命令がクラッシュを引き起こしていたという確固たる証拠を見るのはとてもクールです。
分岐予測器の洞察は、この命令がゲームコード全体で危険すぎることを明らかにしました。スぺキュレート実行されるタイミングを制御するのは非常に難しく、間接分岐の分岐予測器は理論上任意のアドレスを予測できるため、安全な場所は存在しませんでした。そしてもし
xdcbt がスぺキュレート実行されたら、レジスタが指す任意のメモリに対して拡張プリフェッチが発生します。リスクを減らすことはできましたが完全には排除できず、結局価値がありませんでした。Xbox 360 のアーキテクチャ議論ではまだこの命令について言及されますが、実際にゲームで使用されたケースはほぼないと考えています。
面接の時に「これまでに最も難しいバグを調査した経験」を聞かれたとき、「Alpha プロセッサで似たような問題に直面しました」と答えると、インタビュアーは驚きました。変化は続いています…
Michael の編集のお礼を言いたいところです。
追伸
「決して取られない分岐がどのように採択されるか?」
簡単です。分岐予測器は実行ファイル内のすべての分岐について完全な履歴を保持しません ― それは非現実的です。代わりに、シンプルな分岐予測器はアドレスビットと(必要なら)分岐履歴ビットを組み合わせ、2 ビットエントリの配列へインデックスします。その結果、他の無関係な分岐が影響し、時には誤った「採択」が生じます。しかしそれは問題ありません ― それは単なる予測であり、正しくある必要はないからです。
この投稿に対する議論は Hacker News(2021)、r/programming、r/emulation、Twitter などで見られます。数年前にここで議論された Xbox‑キャッシュバグと多少関連があります。