ステージ遷移の静的解析

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では C21CC219 に保存されます。
論理レベル番号 プレイヤーから見たステージの順番を表す値です。C27D に保存されます。
DE CPU内部の小さな記憶領域 DE を組み合わせた値です。この処理では、呼び出すバンクと処理を選ぶために使われます。
不正命令 CPUが命令として扱えないデータを実行しようとした状態です。本来の処理経路から外れた手掛かりになります。

処理の全体像

ステージ終了後の処理は、次のようにつながっています。

flowchart TD
  A["状態6: 終了演出を処理"] --> B["演出が終わると状態7へ進む"]
  B --> C["論理レベル番号を1増やす"]
  C --> D["2段階の画面切り替えを行う"]
  D --> E["論理レベル番号を内部レベルIDへ変換"]
  E --> F["通常のゲーム処理へ戻る"]

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段階で切り替わります

最終ステージ以外では、C21956 の順に変化します。それぞれの状態には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点を確認します。

  1. 元ROMのSHA-256が一致することを確認します。
  2. $241A-$241E に、置き換え前の命令があることを確認します。
  3. $37F9-$37FF が、未使用を示す FF 7 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増やした値を書き込みます。

再検証に必要な命令列は、本レポートおよび調査と修正の全記録に必要最小限で掲載しています。広範な逆アセンブルは公開しません。