flowchart TD A["状態6: 終了演出を処理"] --> B["演出が終わると状態7へ進む"] B --> C["論理レベル番号を1増やす"] C --> D["2段階の画面切り替えを行う"] D --> E["論理レベル番号を内部レベルIDへ変換"] E --> F["通常のゲーム処理へ戻る"]
ステージ遷移の静的解析
ROMの命令から、ステージ終了後の処理を読み解く
ROMの命令を調べた結果、ステージ終了後は「終了演出」「レベル番号の更新」「次の画面への切り替え」という順に処理が進むことが分かりました。
一方、mGBA 0.10.3で実際に確認すると、終了演出の途中で本来の命令から外れ、次のステージへ進めなくなっていました。2回目のバンク呼び出しに渡す値を DE=1400 に修正すると、ステージ1から2へ正常に移動しました。
このレポートについて
このレポートでは、Pokémon Jade (Special Pikachu Edition) のROMに記録された命令を読み、ステージ終了後の処理を整理します。このように、ゲームを実行せずにプログラムやデータを調べる方法を「静的解析」と呼びます。
アドレスや命令などの技術情報は、調査結果を再確認できるように残しています。先に処理の意味を平易な言葉で説明し、その後に根拠となる値を示します。
実際にゲームを動かして原因を確認した手順については、ステージ遷移クラッシュの調査と修正をご覧ください。
対象ROM
| 項目 | 値 |
|---|---|
| サイズ | 524,288 bytes(512 KiB、16 KiBのバンク32個分) |
| SHA-256 | 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45 |
| ヘッダー上の情報 | CGB対応、MBC1、512 KiB ROM、外部RAMなし |
| 元ROMのチェックサム | ヘッダー、グローバルともに一致 |
SHA-256は、ROMを見分けるための識別値です。内容が1 byteでも変わると通常は別の値になるため、同名の別ROMと取り違えていないか確認できます。
ただし、このROMはMakon/NT系カートリッジに固有の動作を必要とする可能性があります。そのため、ヘッダーに「MBC1」と書かれていることだけでは、実機やエミュレーター上の動作を断定できません。
最低限の用語
| 用語 | このレポートでの意味 |
|---|---|
| byte(バイト) | データ量の小さな単位です。このレポートでは、命令や空き領域の長さを表すために使います。 |
| ROM | ゲームのプログラムとデータが入ったファイルです。 |
| バンク | 大きなROMを16 KiBずつに分けた区画です。必要な区画を切り替えながら使用します。 |
| マッパー | CPUから見えるROMバンクを切り替える仕組みです。 |
| CPU | ROMの命令を順番に実行する、ゲーム機の中心部分です。 |
| ROMヘッダー | ROMの種類や容量などを記録した管理情報です。 |
| チェックサム、CRC32 | データの内容から計算する確認用の値です。破損の検出やROMの識別に使われます。 |
| mGBA | Game Boy用のエミュレーターです。この調査ではバージョン0.10.3を使いました。 |
| メモリアドレス | プログラムやデータの場所を表す番号です。$240A のように、先頭の $ は16進数であることを表します。 |
| 状態番号 | 現在どの処理を行っているかを表す値です。このROMでは C21C や C219 に保存されます。 |
| 論理レベル番号 | プレイヤーから見たステージの順番を表す値です。C27D に保存されます。 |
DE |
CPU内部の小さな記憶領域 D と E を組み合わせた値です。この処理では、呼び出すバンクと処理を選ぶために使われます。 |
| 不正命令 | CPUが命令として扱えないデータを実行しようとした状態です。本来の処理経路から外れた手掛かりになります。 |
処理の全体像
ステージ終了後の処理は、次のようにつながっています。
1. 終了演出から次の状態へ進みます
メモリ C21C は、ステージ終了処理が今どの段階にいるかを表します。値が 6 のときは $240A、次の 7 では $242A の処理が実行されます。
C21C = 6 -> $240A
終了演出に使う値 C213 を確認します。
C213 が0になると、C21Cを7へ進めます。
C21C = 7 -> $242A
論理レベル番号 C27D を1増やします。
C27Dが5未満なら、画面の状態 C219を5にします。
C27Dが5なら、C219を8にします。
つまり、$240A は終了演出を待つ処理、$242A はレベル番号を更新して次の画面へ進む処理です。
2. 次の画面へは2段階で切り替わります
最終ステージ以外では、C219 が 5、6 の順に変化します。それぞれの状態には4段階の処理があります。
C219 = 5 -> $0C98から始まる4段階の処理
最後の $0D9F で、C219を6にします。
C219 = 6 -> $0DA9から始まる4段階の処理
最後の $0E64 で、C27Dを内部レベルIDへ変換します。
C219を0に戻し、通常のゲーム処理へ戻ります。
ここから、レベル番号を増やした直後に次のステージを始めるのではなく、画面の切り替えを終えてから通常のゲーム処理へ戻る設計だと分かります。
3. 表示上の順番を内部レベルIDへ変換します
C27D は、プレイヤーから見たステージの順番を表す「論理レベル番号」です。値 0 が最初のステージ、値 1 から 4 がステージ2から5に対応します。
ゲーム内部では、この番号をそのまま使わず、次の変換表で内部レベルIDへ置き換えます。
論理レベル番号: 0 1 2 3 4
内部レベルID: 3 4 2 1 0
この変換表はROM内の $0E7C にあり、byte列は 03 04 02 01 00 です。たとえば、論理レベル番号 1、つまりステージ2では内部レベルID 4 を使用します。
4. 実行時の確認で停止位置を絞り込みました
静的解析で見つけた処理をmGBA 0.10.3で追跡したところ、終了演出の入口 $240A には到達しました。しかし、次の状態の入口 $242A には到達せず、$241C から2回目に別バンクの処理を呼び出した後、不正命令を実行して停止しました。
2回の呼び出しには、次の違いがありました。
| 呼び出し | 渡された値 | 結果 |
|---|---|---|
| 1回目 | DE=0400 |
ROMバンク4の処理へ入り、正常に $FF9E へ戻りました。 |
| 2回目 | DE=05C1 |
コードではないデータへ進み、不正命令を実行しました。 |
1回目の呼び出し先は DE の値を元に戻しません。確認した例では、戻った時点で DE=9BC1 になっていました。呼び出し元は2回目の前に D=05 だけを書き換えるため、E=C1 が残り、結果として DE=05C1 になります。
さらに、mGBAはこのROMを、通常のMBC1ではなく GB_UNL_NT_NEW という特殊なマッパーで動かします。mGBAがこの設定を選ぶために使うヘッダーCRC32は 8628A287 です。
ここでいうセレクターは、mGBA上でどのROM領域を見せるか選ぶ値です。調査の結果、セレクター 05 の $4000 には命令ではなくデータがありました。セレクターを 14、処理番号を 00 にして DE=1400 とすると、$4000 には正しい移動先を選ぶ処理が現れ、正常に $FF9E へ戻りました。
以上から、2回目の呼び出しは DE=1400 で行う必要があると判断しました。
5. 修正処理をROM内の空き領域に置きます
元の $241A-$241E には5 bytes分の空きしかありません。一方、DE=1400 を設定して $FF90 を呼ぶには、合計6 bytesが必要です。そのため、この場所だけでは修正後の命令を収められません。
そこで、固定バンク内の未使用領域 $37F9-$37FF に、次の7-byteの小さな処理を置きます。
37F9: 11 00 14 ld de,$1400
37FC: CD 90 FF call $FF90
37FF: C9 ret元の5 bytesは、この処理を呼び出す命令と2個の NOP に置き換えます。NOP は何もせず次へ進む1-byteの命令です。
241A: CD F9 37 call $37F9
241D: 00 nop
241E: 00 nopこの方法なら、後続の $241F-$2423 にある C25C の更新処理と RET は変更せずに残せます。パッチを生成するときは、誤ったROMを書き換えないように、次の3点を確認します。
- 元ROMのSHA-256が一致することを確認します。
$241A-$241Eに、置き換え前の命令があることを確認します。$37F9-$37FFが、未使用を示すFF7 bytesであることを確認します。
6. mGBAによるROMの識別方法を維持します
mGBAは、ROMヘッダー $0100-$014F のCRC32を使って、このROMに適用する特殊なマッパーを選びます。ヘッダー内のグローバルチェックサム $014E-$014F もCRC32の計算対象です。
そのため、修正版ではグローバルチェックサムを再計算せず、元ROMのヘッダーをそのまま維持します。コードの変更後はROM全体の内容とグローバルチェックサムが一致しなくなりますが、ヘッダーCRC32 8628A287 は変わりません。これにより、mGBAは修正版にも GB_UNL_NT_NEW マッパーを適用できます。
7. 修正版でステージ遷移を確認しました
ヘッダーを維持したmGBA互換版の修正版ROMでは、ステージ1のゴール処理からステージ2へ正常に移動しました。
これにより、次の3点を確認できました。
- クラッシュの原因となる呼び出し位置が正しいこと。
- 追加した修正処理から正常に戻れること。
- 修正後に実際のステージ遷移が完了すること。
確認時にはローカルの画面記録も作成しましたが、公開版には含めていません。公開レポートでは、状態遷移、停止位置、ハッシュ値を検証記録とします。
付録: C27D を使う場所
論理レベル番号 C27D を読み出す場所は、次のとおりです。
| アドレス | 用途 |
|---|---|
$0E67 |
論理レベル番号から内部レベルIDを選びます。 |
$242A |
ステージ完了時に現在の番号を読み出します。 |
バンク3の $6061 |
レベル別データの表を参照します。 |
C27D へ書き込む場所は、次のとおりです。
| アドレス | 用途 |
|---|---|
$03E6、$07AD、$0A94、$108E |
値を初期化します。 |
$242E |
ステージ完了時に、1増やした値を書き込みます。 |
再検証に必要な命令列は、本レポートおよび調査と修正の全記録に必要最小限で掲載しています。広範な逆アセンブルは公開しません。