ステージ遷移クラッシュの調査と修正

mGBAでの再現、原因の切り分け、ROMパッチ作成まで

ヒント結論

ステージ終了時には、別のROMバンクにある処理が2回呼ばれます。2回目の呼び出しに渡す値を DE=1400 に修正したところ、mGBA 0.10.3でステージ1から2へ正常に遷移しました。

また、mGBAがこのROMを正しい特殊カートリッジとして認識できるように、ROMヘッダーは変更せず維持しました。

このレポートについて

Pokémon Jade (Special Pikachu Edition) をmGBA 0.10.3で実行すると、ステージのゴール後に画面が崩れて停止します。このレポートでは、次の順に原因を切り分け、修正版のIPSパッチを作成するまでを説明します。

  1. ROMを静的に解析し、ステージ終了処理の位置を特定します。
  2. mGBAのデバッガーで、特定した処理を実際に追跡します。
  3. 2回目のバンク呼び出しに渡される値を検証します。
  4. ROM内の空き領域を使って修正処理を追加します。
  5. mGBAによるROMの識別方法を保ったまま、IPSパッチを生成します。

完成した修正だけでなく、E=00 やバンク 06 を試した結果も残しています。これらは最終的な修正ではありませんが、原因を絞り込むうえで必要な検証でした。

対象と安全上の方針

対象ROM

項目
サイズ 524,288 bytes(512 KiB)
元ROM SHA-256 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45
mGBA 0.10.3
mGBAが使うヘッダーCRC32 8628A287
mGBA上のマッパー GB_UNL_NT_NEW

SHA-256はファイル全体から計算する識別値です。内容が1 byteでも変わると通常は別の値になるため、同名の別ROMを誤って修正する事故を防げます。

Get-FileHash -Algorithm SHA256 pokemon_jade_pikachu.gb
node tools/verify-analysis.mjs pokemon_jade_pikachu.gb
ノートコマンドの意味

Get-FileHash は、元ROMのSHA-256を計算するPowerShellコマンドです。続く node tools/verify-analysis.mjs は、このレポート用の検証スクリプトを実行します。このスクリプトはハッシュに加えて、解析対象の命令列が期待した位置にあることも確認します。

管理方針

  • 元ROMは上書きしません。
  • ROM本体、セーブ、ステートはGitに含めません。
  • 生成した修正版ROMとIPSパッチはローカル検証専用とし、このリポジトリでは配布しません。
  • 修正スクリプトは、対象ROMのハッシュが一致しない場合に処理を中止します。
ノート公開版について

公開版では、再現条件と表示異常を論評するために不可欠なゲーム画面2枚だけを、出所と引用目的を明示して掲載します。確認動画、セーブステート、抽出素材、広範な逆アセンブルは掲載しません。実行結果はレジスター値、ハッシュ値、状態遷移などの事実情報でも記録します。全体方針は公開方針と権利表示をご覧ください。

最低限の用語

用語 この調査での意味
ROM ゲームのプログラムとデータが入った読み取り専用イメージです。
バンク 大きなROMを16 KiB単位に分けた区画です。CPUから見える区画を必要に応じて切り替えます。
マッパー CPUから見えるROMバンクを切り替える、カートリッジ側の仕組みです。
レジスター CPU内部の小さな作業領域です。このレポートでは ADE などを扱います。
DE 8-bitレジスター DE を組み合わせた16-bit値です。対象処理では D がバンク選択、E が処理番号として使われます。
PC Program Counterです。次に実行する命令のアドレスを示します。
SP Stack Pointerです。関数の戻り先などを記録するスタックの位置を示します。
ブレークポイント 指定したアドレスを実行する直前に、ゲームを自動で一時停止する設定です。
不正命令 CPUが命令として扱えないbyteを実行しようとした状態です。処理が本来のコードから逸れた手掛かりになります。
$240A 16進数で表したアドレスです。デバッガーへの入力では同じ値を 0x240A と書きます。

CPUアドレスとROM内の位置

Game BoyのCPUが見るアドレスと、ROMファイル内の位置は常に同じではありません。固定バンク0の $0000-$3FFF は一致しますが、$4000-$7FFF に見える内容は、現在選択されているROMバンクによって変わります。

CPU $0000-$3FFF  -> 常に固定バンク0
CPU $4000-$7FFF  -> 現在選択されているROMバンク

そのため、同じCPUアドレス $4000 でも、選択中のバンクが 0414 では別のbyteが見えます。

調査の全体像

flowchart TD
  A["静的解析で終了処理を特定"] --> B["状態6の入口 $240A で停止"]
  B --> C["次の状態 $242A には到達しない"]
  C --> D["区間内の2回のバンク呼び出しを確認"]
  D --> E["1回目の復帰後に E が C1 へ変化"]
  E --> F["E=00 に直して再検証"]
  F --> G["バンク05の $4000 はデータ"]
  G --> H["バンク06も不一致"]
  H --> I["セレクター14で有効なコードを確認"]
  I --> J["2回目を DE=1400 に修正"]
  J --> K["ヘッダーを維持してパッチを生成"]
  K --> L["ステージ1から2への遷移に成功"]

1. 静的解析で停止位置を決める

最初にROMの命令を読み、ステージ終了時に実行される処理を探しました。ここで処理の位置を先に特定したため、デバッガーで無作為に停止位置を探す必要はありませんでした。

状態番号から処理アドレスを特定する

$2366 から始まる処理は、メモリ C21C の値を状態番号として読み、対応するアドレスへ移動します。分岐先の一覧は $2563 にあります。

2366: FA 1C C2    ld   a,[$C21C]  ; 現在の状態番号を読む
236D: 21 63 25    ld   hl,$2563   ; 分岐先一覧の先頭
2370: C3 1E 38    jp   $381E      ; 状態番号に対応する処理へ移る

256F: 0A 24       dw   $240A      ; 状態6
2571: 2A 24       dw   $242A      ; 状態7

この表から、C21C=06 のときは $240A、次の C21C=07 では $242A が実行されると分かります。

$240A$242A が何をするか

$240A からの状態6は、終了演出中に使われる値 C213 を確認します。この値が0になるまでは演出の更新を続け、0になると C21C を7へ進めます。

240A: FA 13 C2    ld   a,[$C213]  ; 終了演出中に使われる値を読む
240D: B7          or   a
240E: 28 14       jr   z,$2424    ; 0なら状態7へ進める
      ...
2424: 3E 07       ld   a,$07
2426: EA 1C C2    ld   [$C21C],a
2429: C9          ret

続く $242A からの状態7は、論理レベル番号 C27D を1増やし、次の画面遷移を開始します。

242A: FA 7D C2    ld   a,[$C27D]
242D: 3C          inc  a
242E: EA 7D C2    ld   [$C27D],a
      ...
2439: 3E 05       ld   a,$05
243B: EA 19 C2    ld   [$C219],a  ; 次の画面遷移を開始

したがって、次の2か所を観測すれば、問題の範囲を切り分けられます。

  • $240A に到達する場合、ステージ終了処理の入口までは正常です。
  • その後に $242A へ到達しない場合、状態6の処理 $240A-$2429 に問題があります。

これが、最初に $240A で停止した理由です。再検証に必要な範囲の命令例はステージ遷移の静的解析と本レポート内にまとめています。

2. $240A で実行を止める

毎回ステージを最初から進めずに済むように、ゴールへ触れる直前でmGBAのステートをローカルに保存しました。図1は、再現起点となる看板とプレイヤーの位置関係を文章だけでは特定しにくいため、その条件を示す目的で引用しています。ステートファイル自体は非公開です。

ステージ1で、プレイヤーが画面右側の看板に接触する直前のゲーム画面

ステージ1のゴール直前。プレイヤーが画面右側の看板に接触する直前のゲーム画面

引用図1:ステージ1のゴール直前

出典:『Pokémon Jade (Special Pikachu Edition)』ゲーム画面。投稿者がmGBA 0.10.3上で取得。看板への接触後に始まる終了処理の再現条件を特定するため、画面全体を掲載しています。対象は非公認作品であり、制作・販売主体および個々の素材の権利帰属は未確認です。

その後、mGBAの ツール → デバッガーコンソールを開く から次のコマンドを入力しました。

break 0x240A
continue
ノートコマンドの意味

break 0x240A は、CPUが $240A を実行する直前で停止するブレークポイントを設定します。continue は、停止中のゲームを再開するコマンドです。

ゴールへ触れると、次のように停止しました。

Hit breakpoint 1 at 0x0000240A
PC: 240A  SP: C1FD
ROM: 02
00:240A: FA13C2  ld a, [$C213]

これはクラッシュではありません。Hit breakpoint は、設定したブレークポイントへ正常に到達して停止したことを示します。この結果から、ゴールとの接触判定から状態6の入口までは実行できていると分かります。

停止中に状態値も確認しました。

x/1 0xC219 4
ノートコマンドの意味

x/1 0xC219 4 は、アドレス $C219 から1 byte単位で4個の値を表示するコマンドです。

0x0000C219: 00 02 01 06

4 bytesは順に C219=00C21A=02C21B=01C21C=06 を表します。最後の C21C=06 は、静的解析で確認した状態6と一致します。

3. 次の状態へ進む前にクラッシュすることを確認する

次に、状態7の入口 $242A へブレークポイントを置きました。

break 0x242A
continue
ノートコマンドの意味

break 0x242A は、状態7の入口 $242A にブレークポイントを設定します。continue で実行を再開し、この位置へ到達するかを確認します。

しかし、$242A のブレークポイントには到達せず、CPU状態が次のように壊れました。

PC: 0000  SP: 0002
HL: 0000
00:0000: E9  jp hl

PC=0000SP=0002HL=0000 は通常の関数実行では考えにくい組み合わせです。ここでは、ブレークポイントによる意図的な停止ではなく、処理が本来の経路から逸れたクラッシュだと判断しました。

以上から、調べる範囲を状態6の $240A-$2429 に絞れます。

listb
delete 1
ノートコマンドの意味

listb は登録済みのブレークポイントを一覧表示し、delete 1 は番号1のブレークポイントを削除します。mGBA 0.10.3では disable 1 が認識されないため、不要なブレークポイントは delete で削除します。

4. 状態6にある2回の「別バンク処理」を確認する

問題の範囲 $240A-$2429 を読むと、共通ルーチン $FF90 が2回呼ばれています。

2413: 16 04       ld   d,$04
2415: 1E 00       ld   e,$00
2417: CD 90 FF    call $FF90

241A: 16 05       ld   d,$05
241C: CD 90 FF    call $FF90

ここでの $FF90 は、D で指定されたROMバンクへ切り替え、そのバンクの $4000 にある処理を呼び出すための共通ルーチンです。

  • 1回目は D=04E=00 の両方を設定してから呼びます。
  • 2回目は D=05 だけを設定し、E は設定し直していません。

$FF90 の内容は次のとおりです。

FF90: FA 0A C2    ld   a,[$C20A]  ; 現在のバンク値を読む
FF93: F5          push af         ; 元の値を一時保存する
FF94: 7A          ld   a,d
FF95: EA 0A C2    ld   [$C20A],a
FF98: EA 00 20    ld   [$2000],a  ; Dで指定されたバンクへ切り替える
FF9B: CD 00 40    call $4000      ; 選択したバンクの処理を呼ぶ
FF9E: F1          pop af
FF9F: EA 0A C2    ld   [$C20A],a
FFA2: EA 00 20    ld   [$2000],a  ; 元のバンクへ戻す
FFA5: C9          ret

動作をまとめると、次のようになります。

  1. 現在選択されているバンク番号を保存します。
  2. D の値を使って別のROMバンクへ切り替えます。
  3. 切り替えたバンクの $4000 を呼びます。
  4. 呼び出しが終わったら、元のROMバンクへ戻します。

このルーチンは AF を保存しますが、DE は保存しません。そのため、$4000 から始まる処理が E を変更すると、呼び出し元へ戻った後も変更後の値が残ります。

5. 1回目のバンク呼び出しを追跡する

1回目の呼び出し直前 $2417 で停止し、trace で命令を1つずつ実行しました。

break 0x2417
continue
trace 8
status
ノートコマンドの意味

trace 8 は次の8命令を1命令ずつ記録しながら実行します。status は、現在のレジスター、実行位置、選択中のROMバンクなどを表示します。breakcontinue は前述と同じです。

段階 実行位置 起きたこと
呼び出し前 $2417 DE=0400 の状態で $FF90 を呼びます。
バンク切替 $FF94-$FF98 D=04 をバンク選択レジスターへ書きます。
バンク内呼び出し $FF9B 選択したバンクの $4000 を呼びます。
呼び出し先 バンク 04$4000 E を処理番号として分岐します。今回は E=00 です。
復帰 $FF9E バンク内処理が終わると、元のバンクを復元します。

トレースでも、$FF9B の次にバンク 04$4000 へ移ったことを確認できました。

PC: 00:FF9B | call $4000
PC: 04:4000 | ld a,e
ROM: 04

バンク 04$4000 は、E の値に応じて処理を選ぶ入口です。今回は E=00 なので、比較が一致して $4406 へ分岐します。

4000: 7B          ld   a,e
4001: FE 00       cp   $00
4003: CA 06 44    jp   z,$4406

なお、mGBAの status 末尾には、実メモリと異なる命令が表示される場合がありました。そのため、バンク切替付近では次の3つを照合しました。

  • trace が実行した命令
  • x/1 で読み出した生のbyte
  • disassemble で逆アセンブルした結果

6. 1回目の復帰後に E が変わる

バンク内処理から戻った直後の $FF9E で停止しました。

break 0xFF9E
continue
ノートコマンドの意味

ここでは、バンク内処理から戻った直後の $FF9E にブレークポイントを設定し、そこへ正常に戻るかを確認します。

1回目は正常に戻りましたが、レジスターは次の値になっていました。

D: 9B  E: C1  (DE: 9BC1)
PC: FF9E
ROM: 04

呼び出し前は E=00 でしたが、復帰後は E=C1 です。続くコードは D=05 だけを設定するため、2回目の $FF90 には DE=05C1 が渡されます。

その後、2回目の $FF9E へは戻らず、次の不正命令で停止しました。

Hit illegal opcode at 0x0000CFFB: 0x000000FC
PC: CFFA  SP: EFFB
ROM: 08

画面表示も図2のように崩れました。この表示は、単なるブレークポイント停止ではなく、処理がコード領域を外れて表示データも破損したことを視覚的に示します。その点を上記の停止位置とレジスター値と合わせて論評するため、異常発生時の画面を引用しています。

黄緑色の背景上でステージの一部と数値表示が崩れているゲーム画面

不正命令へ逸れた際に表示が崩れたゲーム画面

引用図2:不正命令へ逸れた際の表示異常

出典:『Pokémon Jade (Special Pikachu Edition)』ゲーム画面。投稿者がmGBA 0.10.3上で取得。不正命令停止時の表示異常を、停止位置 PC=CFFA および ROM=08 と対応付けて分析するため掲載しています。対象は非公認作品であり、制作・販売主体および個々の素材の権利帰属は未確認です。

この結果から、2回目の呼び出しで E を設定し直していないことが、最初の原因候補になりました。

7. E=00 だけでは直らない

$241C で停止し、2回目の呼び出し前に E=00 を手動で設定しました。

w/r e 0
continue
ノートコマンドの意味

w/r e 0 は、レジスター E00 を書き込むコマンドです。書き換え後、continue で処理を再開します。

これで2回目の呼び出しは DE=0500 になります。しかし、今度は別の場所で不正命令が発生しました。

Hit illegal opcode at 0x0000CD1B: 0x000000E3
PC: CD1A  SP: 0841

E の値には問題がありましたが、E=00 に直すだけでは正常な処理を呼べません。そこで、もう一方の値であるバンク選択 D=05 も調べました。

8. 0506$4000 は目的のコードではない

まず DE=0500 にした状態で、バンク 05$4000 を読みました。

x/1 0x4000 64
disassemble 0x4000 40
ノートコマンドの意味

x/1 0x4000 64 は、現在CPUから見えている $4000 から64 bytesを表示します。disassemble 0x4000 40 は、同じ位置から40命令を逆アセンブルして、人が読めるCPU命令として表示します。

先頭のbyte列は次のとおりです。

10 60 90 65 8E 6D C4 75 5F 7C 5F 7C ...

これをCPU命令として解釈すると、関数の入口としては不自然な並びになります。

4000: 10          stop
4001: 60          ld   h,b
4002: 90          sub  b
4003: 65          ld   h,l

どのようなbyte列でも、逆アセンブラーは何らかの命令として表示します。しかし、この並びには通常の関数入口や一貫した分岐が見られません。画像などのデータを命令として読んでいる可能性が高く、目的のコードではないと判断しました。

単純な1バンクずれも考え、セレクターを 06 に変更しました。

w/1 0xC20A 6
w/1 0x2000 6
x/1 0x4000 16
ノートコマンドの意味

w/1 ADDRESS VALUE は、指定アドレスへ1 byteを書き込むコマンドです。ここでは管理用のバンク値 $C20A と、マッパーが受け取るバンク選択アドレス $2000 の両方へ 06 を書きます。最後に x/1 で、切り替え後の $4000 を確認します。

06 でも $4000 には同じデータが見えました。そのため、単純に次の番号を選ぶ方法では解決しません。

9. セレクター 14 で目的の処理を確認する

なぜ通常のバンク番号として考えられないか

ROMヘッダー上のカートリッジ種別はMBC1ですが、mGBA 0.10.3はこのROMをヘッダーCRC32 8628A287 で個別に識別し、非公認カートリッジ用の GB_UNL_NT_NEW マッパーを適用します。

つまり、この調査では一般的なMBC1のバンク番号を前提にせず、mGBA上で各セレクターを書き込んだときに $4000 へ実際に何が見えるかを確認する必要があります。mGBAがNT-newマッパーを選ぶ登録値は、公式ソースの src/gb/overrides.c で確認できます。

この説明は実機カートリッジの電気的な挙動を断定するものではありません。ここではmGBA 0.10.3上で観測した挙動を対象にしています。

14$4000 は処理の入口になっている

セレクター 14 と処理番号 00 を設定し、$4000 を確認しました。

w/1 0xC20A 0x14
w/1 0x2000 0x14
x/1 0x4000 16
disassemble 0x4000 12
ノートコマンドの意味

前節と同じ書き込みコマンドを使い、今度はセレクター 14 を指定します。その後、切り替え先のbyte列と逆アセンブル結果を確認します。

今度は、E を使ってジャンプ先を選ぶ一貫した処理が現れました。

4000: 16 00       ld   d,$00
4002: 21 D0 59    ld   hl,$59D0
4005: 19          add  hl,de
4006: 2A          ld   a,[hl+]
4007: 66          ld   h,[hl]
4008: 6F          ld   l,a
4009: E9          jp   hl

この処理は、$59D0 付近のジャンプ表から E に対応するアドレスを読み、その処理へ移ります。E=00 を渡すと表の先頭項目が選ばれます。

さらに、バンク内処理から戻った直後の $FF9E に正常に到達しました。

Hit breakpoint 2 at 0x0000FF9E
D: 00  E: 00  (DE: 0000)
PC: FF9E
ROM: 14

以上の結果から、mGBA 0.10.3では2回目の $FF90 を次の値で呼ぶ必要があると判断しました。

D=14  : セレクター14を選ぶ
E=00  : ジャンプ表の先頭処理を選ぶ
DE=1400

10. 5 bytesの場所から6 bytesの処理を呼ぶ

必要な命令は、合計6 bytesです。

ld   de,$1400  ; 3 bytes
call $FF90     ; 3 bytes

しかし、置換できる元の領域 $241A-$241E は5 bytesしかありません。

241A: 16 05       ld   d,$05       ; 2 bytes
241C: CD 90 FF    call $FF90       ; 3 bytes

そこで、固定バンク内の未使用領域に小さな補助ルーチンを置き、元の場所から呼ぶ方法を採用しました。このように既存プログラムの空き領域へ追加処理を置く手法を「コードケーブ」と呼びます。

静的検索により、$2799-$37FFFF で埋まった未使用領域であることを確認しました。その末尾7 bytesへ次の処理を配置します。

37F9: 11 00 14    ld   de,$1400
37FC: CD 90 FF    call $FF90
37FF: C9          ret

元の5 bytesは、補助ルーチンの呼び出しと2つの NOP に置き換えます。

241A: CD F9 37    call $37F9
241D: 00          nop
241E: 00          nop

NOP は何もせずに次へ進む1-byte命令です。これにより、後続のアドレスをずらさず、$241F からの処理もそのまま残せます。

ファイル位置 変更前 変更後 目的
0x0241A 16 05 CD 90 FF CD F9 37 00 00 補助ルーチンを呼びます。
0x037F9 FF FF FF FF FF FF FF 11 00 14 CD 90 FF C9 DE=1400$FF90 を呼びます。

11. 初版の修正版ROMが開かなかった理由

最初に作成した修正版では、ROM全体のグローバルチェックサムも変更後の内容に合わせて再計算しました。しかし、このROMはmGBAで正常に開けませんでした。

原因は、mGBAによる特殊マッパーの自動判定です。mGBAはROMヘッダー $0100-$014F のCRC32を計算し、値が 8628A287 の場合にNT-newマッパーを適用します。一方、グローバルチェックサムの保存位置 $014E-$014F も、このCRC32の計算範囲に含まれます。

グローバルチェックサムを更新する
  ↓
ヘッダーCRC32が変わる
  ↓
mGBAの個別登録と一致しなくなる
  ↓
NT-newマッパーが選ばれない
  ↓
ROMを正常に実行できない

mGBAがヘッダーCRC32を使って個別設定を探す処理は、0.10.3公式ソースの GBOverrideApplyDefaults で確認できます。

最終版では、グローバルチェックサムを含む $0100-$014F を元ROMのまま維持しました。そのため、ROM内に記録されたグローバルチェックサムはコード変更後の内容とは一致しません。一方、ヘッダーCRC32は 8628A287 のままなので、mGBAは修正版にもNT-newマッパーを適用できます。

12. 再現可能なパッチを生成する

修正版ROMとIPSパッチは、tools/build-stage-transition-patch.mjs で生成します。

node tools/build-stage-transition-patch.mjs `
  pokemon_jade_pikachu.gb `
  build/pokemon_jade_pikachu_stage_transition_fixed_mgba.gb `
  build/pokemon_jade_pikachu_stage_transition_fix_mgba.ips
ノートコマンドの意味

node tools/build-stage-transition-patch.mjs は、パッチ生成スクリプトを実行するコマンドです。後ろの3つの引数で、元ROM、生成する修正版ROM、生成するIPSパッチの順にファイルを指定します。行末のバッククォート ` は、PowerShellでコマンドを次の行へ続ける記号です。

スクリプトは、書き込み前後に次の項目を検査します。

  1. 元ROMが512 KiBであることを確認します。
  2. SHA-256が解析対象と一致することを確認します。
  3. $241A の置換元命令が期待値どおりであることを確認します。
  4. $37F9-$37FF が未使用の FF 7 bytesであることを確認します。
  5. ヘッダーCRC32が 8628A287 であることを確認します。
  6. 修正後もヘッダー $0100-$014F が元ROMと同一であることを確認します。
  7. 生成したIPSを元ROMへ適用すると、修正版ROMとbyte単位で一致することを確認します。

成功時には次のような要約が表示されます。

PASS source SHA-256 0E90D765...
PASS preserved header CRC32 8628A287
PASS source bytes at $241A and $37F9
PASS IPS round-trip reproduces patched ROM

IPSパッチとは

IPSは、元ROMと修正版ROMの差分だけを記録するパッチ形式です。今回のIPSにはROM本体は含まれず、変更した12 bytes分のレコードと、IPS形式の管理情報だけが入ります。

この調査では、適法に利用できる元ROMへローカルでIPSを適用し、修正版を検証しました。生成したROMとIPSファイルは公開・配布せず、修正内容は位置、長さ、byte列および生成スクリプトとして記録します。

13. 最終検証

静的検証

生成後のROMを逆アセンブルし、次の命令になっていることを確認しました。

241A: CD F9 37    call $37F9
241D: 00          nop
241E: 00          nop

37F9: 11 00 14    ld   de,$1400
37FC: CD 90 FF    call $FF90
37FF: C9          ret

成果物の識別値は次のとおりです。

成果物 SHA-256
元ROM 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45
mGBA互換修正版ROM 7463D30924B2827CFB824F986A5571B079B7F4952E1912023C5705B91E39CFCA

動的検証

mGBAを完全に閉じ、ヘッダーを維持した最終版ROMを開き直しました。ゴール直前から進めた結果、画面が崩れず、ステージ1から2へ遷移しました。

確認時には13秒の画面記録を作成しましたが、公開版には含めていません。公開レポートでは、修正版ROMのSHA-256、到達した状態、使用したエミュレーターの版を検証記録とします。

検証項目 結果
エミュレーター mGBA 0.10.3
修正版ROM SHA-256 7463D30924B2827CFB824F986A5571B079B7F4952E1912023C5705B91E39CFCA
開始条件 ステージ1のゴール直前
観測結果 画面が崩れず、ステージ2へ遷移

14. 調査結果から分かること

ブレークポイントによる停止とクラッシュは異なります

Hit breakpoint と表示された場合は、指定位置に正常に到達した結果です。一方、今回の PC=0000Hit illegal opcode は、処理が本来の経路から逸れたことを示します。停止時の表示、PC、現在命令を合わせて判断する必要があります。

E とバンク選択の両方に問題がありました

1回目の呼び出し後に E が変化し、2回目には E=C1 が渡されていました。ただし、E=00 に直すだけではクラッシュを解消できませんでした。バンク選択も 05 から 14 へ変更し、DE=1400 とする必要がありました。

逆アセンブルできることと、コードであることは同じではありません

バンク 05$4000 も命令として表示できましたが、実際には関数として不自然な並びでした。関数入口、分岐、戻り先などに一貫性があるかを確認する必要があります。

ROMヘッダーはmGBAのマッパー選択にも使われます

コード部分だけを正しく修正しても、ヘッダーCRC32が変わると、mGBAが別のカートリッジとして扱う場合があります。今回の修正版では、mGBAによるNT-newマッパーの選択を維持するためにヘッダーを変更していません。

15. 今後の確認候補

今回確認したのは、ステージ1から2への遷移です。追加で次の項目を確認できます。

  • ステージ2以降の各ゴールでも、同じ終了処理が正常に動くか確認します。
  • 最終ステージの別分岐 C219=08 に影響がないか確認します。
  • 通常セーブから起動した場合と、旧ROMのステートを読み込んだ場合の差を確認します。
  • mGBAの新しい版やhhugboyでの互換性を確認します。
  • IPS適用ツールを変えても、同じSHA-256の修正版が得られるか確認します。

付録A: mGBAデバッガーコマンド早見表

コマンド 説明
break ADDRESS 実行ブレークポイントを追加します。 break 0x241C
continue 実行を再開します。 continue
status CPUとバンクの状態を表示します。 status
trace N N命令を記録しながら実行します。 trace 8
disassemble ADDRESS N 指定位置からN命令を逆アセンブルします。 disassemble 0xFF90 20
r/1 ADDRESS 指定位置から1 byteを読みます。 r/1 0xC213
x/1 ADDRESS N 指定位置から1 byte単位でN個表示します。 x/1 0x4000 16
w/1 ADDRESS VALUE メモリへ1 byteを書き込みます。 w/1 0x2000 0x14
w/r REGISTER VALUE レジスターの値を書き換えます。 w/r e 0
listb ブレークポイントの一覧を表示します。 listb
delete ID 指定番号のブレークポイントを削除します。 delete 1

デバッガーコマンドはmGBAの版によって異なります。0.10.3では disable を使用できなかったため、delete を使用しました。

付録B: 関連ファイル

変更履歴

日付 内容
2026-08-13 初版を作成しました。
2026-08-13 敬体への統一、静的解析の前置、バンク呼び出しとNT-newマッパーの説明を改善しました。