PS2エミュレーター検出:1*XがXと等しくないとき
(fobes.dev)- PS2の VU浮動小数点乗算には1ビットの演算誤差があり、特定の値では
1 * XがXと異なる場合がある - VU開発者マニュアルによると、
X * 1は正確性が保証されているが、1 * Xには同じ保証がなく、この差が エミュレーター検出 のシグナルになる - 例では、ブルートフォースで見つけた問題発生値のうち 129.5f を使い、実機PS2とエミュレーターの挙動差を確認する
- 実装は、VU0マクロモードで
129.5fと1を乗算した後、元の入力と結果が異なるかだけを比較する単純な構造 - PCSX2、Play!、DobieStation、hps2x64は現在この挙動をエミュレートしておらず、検出難度は 1/5 と評価されている
PS2 VU乗算で生じる1ビット誤差
- この方法はPS2エミュレーター検出シリーズの2番目の項目で、VU1、VU0マイクロモード、VU0マクロモードで利用できる
- 例では実装を単純にするため VU0マクロモード を使用する
- VU0をコプロセッサのように使うため、EE CPUから直接実行できる
- 別途VUプログラムを扱う必要がない
- VU開発者マニュアルの
MUL、MULiなどの乗算命令には 1ビットの演算誤差 があるという注記がある1 * Xは元の値Xと異なる場合があるVF[fs]を被乗数として使うと、X * 1形式の結果の正確性が保証される
- なぜビットが失われるのかは正確には確認されていない
検出値と実装方法
- この誤差を検出するには問題を引き起こす数値が必要で、最も簡単な探索方法は ブルートフォース である
- 筆者は過去に0.5刻みで問題を起こす最初の250個の数値リストを作成し、そのリストを gist で公開している
- サンプルコードは 129.5f を検出対象の数値として使用する
QMTC2でVF1に129.5fを設定するVADDwでVF2に1を作るVMULでVF1 = 1 * 129.5fを計算するQMFC2で結果をEE側へ取り込み、入力と比較する
- 戻り値は
in[0] != out[0]で、元の値と乗算結果が異なればVU乗算誤差が存在すると判断する
エミュレーター別の影響
- 現在 PCSX2、Play!、DobieStation、hps2x64 はこのPS2 VU乗算の挙動をエミュレートしていない
- 数値1つと
1を乗算して結果だけを確認すればよいため、この検出方式の難度は 1/5 程度である
1件のコメント
Hacker Newsのコメント
実際のARMはパイプラインのため、PCで実行しつつPC+4をデコードしている間にPC+8を読むので、新しく保存した命令は影響しないはず。ハードウェアのパイプラインをエミュレートしていないエミュレータなら、その命令を実行してしまう
2004年のさまざまなエミュレーション妨害手法とともに、より詳しく説明した記事: https://mgba.io//2014/12/28/classic-nes/
おそらく数サイクルの間、レジスタ値は未定義だったのだと思う。こういうチップ向けにぎちぎちに最適化したアセンブリコードを書くのは、ひときわ趣味の悪いZachtronicsクローンを遊んでいるようで、かなりひどいものだった
ずっと後になって、誰かが検出されない別の境界ケースを見つけた。それは自分自身を上書きする反復文字列命令だった
https://silviocesare.wordpress.com/2009/02/02/anti-debugging...
x86にはそうした仕組みがあるが、64ビット版で最終的に取り除かれたのかはよく分からない
元のハードウェアとソフトウェアのあらゆる特異な挙動を知っている必要があるだけでなく、どれほど奇妙でもそのまま再現しなければならない。それ自体も難しいが、性能への影響まで考えなければならない
JIT再コンパイラを使うことは、元のハードウェアと完全にサイクル単位で正確にはなり得ないが、ゲームコードが意図的にエミュレータを壊すよう作られている場合でなければ、たいてい問題にはならない
Dolphinも、いくつかの市販Wiiゲームが実際のWii CPUのキャッシュ挙動の細部を悪用したエミュレータ対策コードを入れていたとき、このバランスに対処しなければならなかった。理論上は実CPUのキャッシュをエミュレートしてゲームを滑らかに動かすこともできたが、性能オーバーヘッドはおそらく10倍遅くなる水準で、遊べないものになっていたはずなので、回避パッチを選んだ
https://dolphin-emu.org/blog/2017/02/01/dolphin-progress-rep...
事実上、電子工学と深いプログラミングの魔法を理解する必要があるように思える
始めるときも、たいていは終えるときでさえも、深い魔法を理解する必要はない。基本的には仕様を見て、その通りに実装すればよい。めちゃくちゃにならないようにコード構造を作る能力は必要だが、よくあるパターンがあり、エミュレータを1つか2つ作ってみればずっと簡単になる
電子工学を理解しなければならない場合もほとんどない。エミュレートするのは挙動だけだ。元のハードウェアの挙動にバグが見つかれば、普通はエミュレータに特別処理を入れればよい。そうした挙動がなぜ生じたのかを理解するのに電子工学の知識が役立つことはあるが、実用的というより歴史的な興味に近い
固有の難しさはある。問題が起きると、たいていハードウェアへの理解、エミュレータ実装、エミュレート中のゲームという3つを同時にデバッグすることになる。正確な原因を絞り込むのは難しいことがある。それでも、とにかくざっくり作ってみることを勧める。きれいではないが、どのエミュレータも人気ゲームをどうにか動かすための特別処理でいっぱいだ。汚いハックをいくつか入れてゲームが動くなら、それでいい。元のハードウェアの挙動を正確に実装する必要はなく、ゲームが動くようにすればいい
8ビットCPUは、数バイトの状態、つまりレジスタだけを持つ単純な状態機械だ。プログラムを1バイトずつ読み、CPUがそのバイトを読んだ後に行う動作をまねればよい。数を足したり引いたり、バイトを読んで保存したりするような、とても単純な演算だ
http://www.6502.org/users/obelisk/6502/registers.html
http://www.6502.org/users/obelisk/6502/instructions.html
6502 CPUエミュレータは、プログラムの次の数バイトを読み、そのバイト列を命令として解釈し、命令を実行する。この過程でCPUのレジスタやカウンタをいくつか更新し、算術演算やビット演算を行い、必要ならある位置から別の位置へデータ1バイトを読み書きする。この過程を無限ループで繰り返す
これがフェッチ・デコード・実行サイクルのシミュレーションであること
https://en.wikipedia.org/wiki/Instruction_cycle
以前、SID音楽ファイルを再生するために6502インタプリタをUNIXからClassic Macintoshへ移植したことがある。十分に速く動けばよかったので、クロックサイクル精度は重要ではなかった
インタプリタからCコードを呼び出す方式で動作していた
今でもやってみたい気持ちはあるが、時間がない
それ以外については、@xcv123の兄弟コメントが本当にその通り
いつかFPGAでPS2を複製できるようになれば、この動作がどのように起きたのかを解明するのは、誰かにとって面白いプロジェクトになるだろう
PS2のFPGA版だからといって、同じ、または似たようなバグを実装しない保証はない
ソフトウェア浮動小数点は遅いだろうが、一般的な解法はおそらくPS4のPS2エミュレータに倣うことになる。ゲームごとに、ソフトウェア浮動小数点経路を許可するコード区間をホワイトリスト指定する方式だ
これがマウスとキーボードを接続するPersonal System/2ポートではなく、PlayStation 2の話だと分かるまでに時間がかかりすぎた
3文字の略語は文脈を見つけるのを本当に難しくすることがある。略語だけをGoogleに入れると、かなり頻繁にほとんど関係のない結果ばかりが出てくるからだ