※まだ完全ではないので世界中のチャレンジャー求む。
おそらく完全再現できました。
先に動画を見ておくが吉。
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…は現時点では未確認ですが、理論上ダメな理由はないので、おそらくこれでハック完遂ではないかと思われます。