※まだ完全ではないので世界中のチャレンジャー求む。 おそらく完全再現できました。
先に動画を見ておくが吉。
Youtube : 4ST 次々とゲームをバグらせる恐怖のスーパーファミコン【実態調査編】
Youtube : 4ST 次々とゲームをバグらせる恐怖のスーパーファミコン【原因究明編】
—
4STシイナさんが調査した結果、スーパーバグファミコンはCPUに実装されたDIV 16/8(除算エンジン)が故障していたのが原因と判明。
でも、そんな都合よくイイ感じに壊れたスーパーファミコンなんて奇跡でも起きないかぎり手に入りませんし、CPUだけピンポイントで壊すなんてもっと無理。
ですが、もっと都合良くCPUレベルで破壊できるスーパーファミコン実行環境が身近にあります…そう、エミュレータ(SNES9X)です。
SFCの除算命令はコード4206…というわけで、まずはSNES9Xのソースコードを眺めます。
case 0x4206: // WRDIVB { uint16 a = Memory.FillRAM[0x4204] + (Memory.FillRAM[0x4205] << 8); uint16 div = Byte ? a / Byte : 0xffff; uint16 rem = Byte ? a % Byte : a; // FIXME: The update occurs 16 machine cycles after $4206 is set. Memory.FillRAM[0x4214] = (uint8) div; Memory.FillRAM[0x4215] = div >> 8; Memory.FillRAM[0x4216] = (uint8) rem; Memory.FillRAM[0x4217] = rem >> 8; break; }
上記ルーチンを書き換えてソースコードをビルドすればスーパーバグファミコン完成~…と出来れば良いのですが、ぶっちゃけSNES9XをWin32ビルドする環境を組むのはアホほど大変です。
今回は狙いのルーチンだけぶっ壊せれば良いので、SNESエミュレータをハックして解決してみることにします。
私が解析に使っている、デバッガ搭載のSNES9X亜種「Geiger’s Snes9x Debugger」を逆アセンブルして、00004214、00004215、00004216、00004217に連続アクセスするような怪しい処理を探すと…
--------- :005BC8FB 8B353C056C00 mov esi, dword[006C053C] :005BC901 660FB69605420000 movzx dx, byte[esi+00004205] :005BC909 660FB68604420000 movzx ax, byte[esi+00004204] :005BC911 8A5D08 mov bl, byte[ebp+08] :005BC914 66C1E208 shl dx, 08 :005BC918 6603D0 add dx, ax :005BC91B 84DB test bl, bl :005BC91D 0FB7CA movzx ecx, dx :005BC920 7411 je 005BC933 :005BC922 0FB7C1 movzx eax, cx :005BC925 0FB6CB movzx ecx, bl :005BC928 99 cdq :005BC929 F7F9 idiv ecx :005BC92B 0FB7C0 movzx eax, ax :005BC92E 0FB7CA movzx ecx, dx :005BC931 EB08 jmp 005BC93B --------- :005BC933 B8FFFF0000 mov eax, 0000FFFF :005BC938 0FB7C9 movzx ecx, cx --------- :005BC93B 888614420000 mov byte[esi+00004214], al :005BC941 8B153C056C00 mov edx, dword[006C053C] :005BC947 88A215420000 mov byte[edx+00004215], ah :005BC94D A13C056C00 mov eax, dword[006C053C] :005BC952 888816420000 mov byte[eax+00004216], cl :005BC958 8B153C056C00 mov edx, dword[006C053C] :005BC95E 88AA17420000 mov byte[edx+00004217], ch :005BC964 A13C056C00 mov eax, dword[006C053C] :005BC969 881C07 mov byte[edi+eax], bl :005BC96C 5F pop edi :005BC96D 5E pop esi :005BC96E 5B pop ebx :005BC96F 8BE5 mov esp, ebp :005BC971 5D pop ebp :005BC972 C3 ret
というわけで無事に発見。
ここをエミュレータが停止しない程度に都合よくゲームだけバグるように壊すという、なんとも力加減の難しい作業をすることになります。
・パッチ1「EAXレジスタにFFFFが入ったまま突き抜けるよう分岐破壊
:005BC931 EB08 jmp 005BC93B << ここを90(nop)で壊す --------- :005BC933 B8FFFF0000 mov eax, 0000FFFF << 必ず実行される :005BC938 0FB7C9 movzx ecx, cx
本来であればEB08(jmp 005BC93B)でeaxレジスタにFFFFをセットする処理をEB08→9090とすることで直後の4214と4215への書込がFFになり、除算ルーチンが破壊されます。
ただし効果が強すぎるためか、モンスター闘技場のダメージが異常値になる傾向が強く、FF6の戦闘獲得EXPが16,777,216(0xFFFFFF)固定で仲間の脳力がバグったりと、4STの映像とはかなり異なる挙動でした。
バイナリエディタ用パッチ1 001BC931: EB 90 001BC932: 08 90
・パッチ2「空きメモリに転送してedx+4215だけにFFFF書き込み
説明すると長くなりすぎるので超ざっくりと説明。
:005BC941 8B153C056C00 mov edx, dword[006C053C] :005BC947 88A215420000 mov byte[edx+00004215], ah :005BC94D A13C056C00 mov eax, dword[006C053C]
これを相対ジャンプで00400500に強制分岐。
:005BC941 8B153C056C00 mov edx, dword[006C053C] :005BC947 E9B43BE4FF jmp 00400500 :005BC94C 90 nop :005BC94D A13C056C00 mov eax, dword[006C053C]
飛んだ先の空きメモリで4215に2バイトFFFFを書き込んでから元の処理へ再び相対ジャンプで戻る。
// 本来 :00400500 66C78215420000FFFF mov word[edx+00004215],FFFF :00400509 E93FC41B00 jmp 005BC94D // 元のルーチンに戻る
これで4215への書き込みがFFFFに強制化され、こちらもモンスター闘技場で勝つ都度に11111111ゴールドが入手できるようになります。
ただし「防御に向かって会心の一撃でダメージ5桁になる」が観測できておらず、FF6も獲得EXP1700万オーバーのため、こちらも不完全な可能性が高いです。
バイナリエディタ用パッチ 00000500: 00 66 00000501: 00 C7 00000502: 00 82 00000503: 00 15 00000504: 00 42 00000507: 00 FF 00000508: 00 FF 00000509: 00 E9 0000050A: 00 3F 0000050B: 00 C4 0000050C: 00 1B 001BC947: 88 E9 001BC948: A2 B4 001BC949: 15 3B 001BC94A: 42 E4 001BC94B: 00 FF 001BC94C: 00 90
~~
そんなわけで、エミュレータでスーパーバグファミコンを[理論上、再現できる」ことは確定しました。
皆様は是非とも「DQ5でコイン11111111枚が手に入り、防御に向かって攻撃すると会心の一撃で5桁ダメージが出て、FF6で獲得EXPが1500万程度で、マリオカートでCPUが蛇行運転しまくるdiv16/8破壊パターン」を見つけてみてください。
・2024.3.20追記
ねこかぶさんトコでSNESエミュレータAresを改造&ビルドするハック方法が公開されました。
スーパーバグファミコン再現したエミュを作成する
しかも都合の良いことにAresはSNES9Xとは異なる実装になっているおかげで、4215を実行した時に返る値まで自由に調整が可能です。
case 0x4215: return io.rddiv.byte(1); //RDDIVH を case 0x4215: return 0xff; //io.rddiv.byte(1); //RDDIVH
ねこかぶさんは上記のように書き換えを提案していましたが、4STシイナさんのところに届いたスーパーバグファミコンはFF6で15728672ポイントを取得していたので、試しに0xffではなく0xf0に書き換えてAresをビルド。
その結果…
無事にスーパーバグファミコンの完全再現に成功しました。
ドラクエ5の1111111…は現時点では未確認ですが、理論上ダメな理由はないので、おそらくこれでハック完遂ではないかと思われます。