ステージ遷移の静的解析

ROMの命令から、ステージ終了後の処理を読み解く

ヒント結論

ROMの命令を調べた結果、ステージ終了後は「終了演出」「レベル番号の更新」「次の画面への切り替え」という順に処理が進むことが分かりました。

一方、mGBA 0.10.3で実際に確認すると、終了演出の途中で本来の命令から外れ、次のステージへ進めなくなっていました。 2回目のバンク呼び出しに渡す値を DE=1400 に修正すると、ステージ1から2へ正常に移動しました。

このレポートについて

このレポートでは、Pokémon Jade (Special Pikachu Edition) のROMに記録された命令を読み、ステージ終了後の処理を整理します。 このように、ゲームを実行せずにプログラムやデータを調べる方法を「静的解析 (Static analysis)」と呼びます。

アドレスや命令などの技術情報は、調査結果を再確認できるように残しています。先に処理の意味を平易な言葉で説明し、その後に根拠となる値を示します。 用語については、用語集を参照してください。

実際にゲームを動かして原因を確認した手順については、ステージ遷移クラッシュの調査と修正をご覧ください。

対象ROM

項目
サイズ 524,288 bytes(512 KiB、16 KiBのバンク32個分)
SHA-2561 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45
ヘッダー上の情報 CGB対応、MBC1、512 KiB ROM、外部RAMなし
元ROMのチェックサム ヘッダー、グローバルともに一致

このROMはMakon/NT系カートリッジに固有の動作を必要とする可能性があります。 そのため、ヘッダーに「MBC1」と書かれていることだけでは、実機やエミュレーター上の動作を断定できません。

処理の全体像

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

flowchart TD
  A["C21C = 6<br>終了演出を処理"] --> B{"C213 = 0 ?"}
  B -- "いいえ" --> A
  B -- "はい" --> C["C21C = 7"]
  C --> D["C27D を 1 増やす"]
  D --> E{"C27D < 5 ?"}
  E -- "はい" --> F["C219 = 5"]
  E -- "いいえ" --> G["C219 = 8<br>最終ステージ後の処理"]
  F --> H["C219 = 6<br>画面切り替え"]
  H --> I["論理レベル番号を<br>内部レベルIDへ変換"]
  I --> J["通常のゲーム処理へ戻る"]

1. 終了演出から次の状態へ進む

ステージをクリアすると、すぐに次のステージへ移るわけではありません。 まず終了演出を最後まで再生し、その後で次のステージ番号を決定します。

この処理の進行状況は、メモリ C21C に保存されています。

(i) C21C = 6: 終了演出を待つ

C21C6 のときは、$240A の処理が実行されます。

ここでは、終了演出に使われている値 C213 を確認しています。 C213 が 0 になると終了演出が完了したと判断し、C21C7 に変更します。

(ii) C21C = 7: 次のレベルへ進む

C21C7 になると、$242A の処理が実行されます。 まず、現在の論理レベル番号 C27D1 増やします。 その値によって、次に表示する画面を決めます。

flowchart TD
  A["C27D を確認"] --> B{"C27D < 5 ?"}
  B -- はい --> C["C219 = 5"]
  B -- いいえ --> D["C219 = 8"]

つまり、通常のステージでは次のステージへ進み、最後のステージまで到達した場合には別の画面へ移る仕組みになっています。

このように、$240Aステージ終了演出の完了を待つ処理$242Aレベル番号を更新して次の画面を選ぶ処理と考えることができます。

2. 次の画面への2段階切り替え

最終ステージ以外では、C21956 の順に変化しながら、次のステージへ移るための画面切り替え処理が進みます。

(i) C219 = 5

$0C98 から始まる処理が実行され、最後の $0D9FC2196 に変更されます。

(ii) C219 = 6

続いて $0DA9 から始まる処理が実行されます。 最後の $0E64C27D が内部レベルIDへ変換され、C2190 に戻ります。

これにより、画面切り替えが完了し、通常のゲーム処理へ戻ります。

つまり、レベル番号を更新した直後に次のステージを開始するのではなく、C219 = 5C219 = 6 の2つの状態を経てから次のステージへ移る仕組みになっています。

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 から別のROM領域を呼び出した際に、コードではないデータを命令として実行し、不正命令で停止しました。

2回目の呼び出しで異常が起きる

$241C 付近では、別のROM領域を呼び出す処理が2回行われます。

呼び出し DE2 結果
1回目 0400 正常な処理を実行し、$FF9E へ戻る
2回目 05C1 コードではない領域へ進み、不正命令で停止する

なぜ DE=05C1 となるのか

1回目の呼び出しでは、最初に DE=0400 が渡されます。 しかし、呼び出された処理は DE の値を元に戻さないまま終了します。 実際に確認した例では、戻ってきた時点で DE = 9BC1 となっていました。

その後、呼び出し元は2回目の呼び出しに備えて D だけを 05 に変更します。 E は変更しないため、1回目の処理で残った C1 がそのまま使われます。

このため、2回目の呼び出しでは意図しない DE=05C1 が渡されます。

05 では正しいコードが選ばれない

さらに、このROMでは通常のMBC1とは異なるマッパーが使用されています。 mGBAでは、このROMを GB_UNL_NT_NEW という特殊なマッパーとして認識します。

このため、ここで使われる値は単純なROMバンク番号と考えることができません。

mGBA上で実際に確認すると、セレクター 05 を指定した状態の $4000 には、実行可能なコードではなくデータが配置されていました。 したがって、ここへ処理を移すと、そのデータをCPU命令として解釈してしまい、不正命令で停止します。

一方、セレクターを 14、処理番号を 00 として DE = 1400 を指定すると、$4000 に正しい処理が現れました。

この状態では処理が正常に実行され、$FF9E へ戻ることも確認できました。

したがって、2回目の呼び出しでは、

DE 結果
修正前 05C1 不正な領域へ進む
修正後 1400 正しい処理を実行する

となることから、2回目の呼び出しには DE=1400 を渡す必要があると判断しました。 なお、mGBAがこのROMを GB_UNL_NT_NEW として認識する際に使用するヘッダーCRC3238628A287 です。

5. 修正処理をROM内の空き領域に置く

元の $241A-$241E で、修正命令の置き換えに利用できる領域は5 bytesです。 一方、DE=1400 を設定して $FF90 を呼ぶには、合計6 bytesが必要です。 そのため、この場所だけでは修正後の命令を収められません。

そこで、固定バンク4内の未使用領域 $37F9-$37FF に、次の7-byteの小さな処理を置きます。

37F9: 11 00 14    ld   de,$1400
37FC: CD 90 FF    call $FF90
37FF: C9          ret
ノート命令の意味
命令 意味
ld 値をレジスタやメモリへ読み込む命令。ld de,$1400 では、16 bitレジスタ DE に値 $1400 を設定する。
call 指定したアドレスの処理を呼び出す命令。呼び出した処理が終了すると、call の次の命令へ戻る。
ret 呼び出し元へ戻る命令。直前の call で保存された戻り先へ処理を戻す。

なお、16 bitの値はリトルエンディアンで格納されるため、$1400 は機械語上では 00 14$FF9090 FF の順に記録されます。

元の5 bytesは、この処理を呼び出す命令と2個の NOP5 に置き換えます。

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点を確認できました。

  • クラッシュの原因となる呼び出し位置が正しいこと。
  • 追加した修正処理から正常に戻れること。
  • 修正後に実際のステージ遷移が完了すること。

付録: 用語集

用語 このレポートでの意味
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が命令として扱えないデータを実行しようとした状態です。本来の処理経路から外れた手掛かりになります。

付録: C27D を使う場所

論理レベル番号 C27D を読み出す場所は、次のとおりです。

アドレス 用途
$0E67 論理レベル番号から内部レベルIDを選びます。
$242A ステージ完了時に現在の番号を読み出します。
バンク3の $6061 レベル別データの表を参照します。

C27D へ書き込む場所は、次のとおりです。

アドレス 用途
$03E6$07AD$0A94$108E 値を初期化します。
$242E ステージ完了時に、1増やした値を書き込みます。

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

脚注

  1. SHA-256は、ROMを見分けるための識別値です。内容が1 byteでも変わると通常は別の値になるため、同名の別ROMと取り違えていないか確認できます。↩︎

  2. DE はGame Boy CPUの16ビットレジスタで、8ビットレジスタ DE を組み合わせたものです。この処理では、別のROM領域を呼び出す際の引数として使用されています。↩︎

  3. データから32 bitの短い値を計算するチェックサム方式。正式には Cyclic Redundancy Check 32-bit。↩︎

  4. バンク切り替えの影響を受けず、CPUから常に参照できるROM領域。↩︎

  5. No Operation。何も処理せず、次の命令へ進む1-byteの命令。↩︎