スーパーバグファミコンをエミュで再現する

※まだ完全ではないので世界中のチャレンジャー求む。
 おそらく完全再現できました。

 先に動画を見ておくが吉。

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…は現時点では未確認ですが、理論上ダメな理由はないので、おそらくこれでハック完遂ではないかと思われます。