読者です 読者をやめる 読者になる 読者になる

6502逆アセンブラに関する考察 その2

前回述べた前提に基づいて解析フェーズを考えます。

簡単のため、解析はマルチパスで行うことにします。具体的には以下の通り:

  • アドレス単位のコード判定
  • 制御フローを考慮したコード判定(これ自体も複数パス。後述)

以前作ったC++版では解析を1パスで行っていましたが、これだとコードが複雑になる上、たまに致命的な誤判定が生じることがありました。簡単な例を見てみます:

80FF : db $02
8100 : db $60 ; 単独の RTS はデータとみなす(ヒューリスティック)
8101 : db $02

; ...

; ここは本来コードなのだが…
8200 : lda $00
8202 : ldx $01
8204 : ldy $02
8206 : jmp $8100 ; データ領域への JMP なので、ルーチン全体がデータとみなされてしまう!

このようなケースでは $8200- が全てデータとして出力されてしまいます。ここでは単独の RTS をデータとみなすというヒューリスティックが誤っていたためにこうなったわけですが、そうしたヒューリスティックと制御フロー解析が分離されていなかったために、一箇所の誤りが全体に波及し、しかもその修正が難しいという結果になりました。そこで今回は、ヒューリスティックを用いるのはアドレス単位のコード判定のみに限定し、制御フロー解析は誤判定を避けるため極力保守的に行うことにします。

アドレス単位のコード判定

最初のパスとして、制御フローを考慮せずに個々のアドレスに対するコード判定を行います。

といっても、実際にプログラムを動かすことなく100%の精度でコード判定を行うのは不可能です。例えばNESの場合は非公式命令は滅多に使われないだとか、ある範囲のアドレスは普通は読み書きしないなどといった事実からある程度の推測は可能ですが、例外は常にありえます。よって、このパスはハードコードされた基準ではなくユーザ設定に基づいて行います。出力がおかしくなったらここの設定を変えて再試行することになります。

なお、実際にプログラムを動かしたとしても、わかるのはあるアドレスが「コードである」ことだけです。このときあるアドレスが実行されなかったとしても、条件を変えれば実行される可能性があるわけで、あるアドレスが「コードでない」ことを100%保証することはできません。

とはいえ、設定としてある程度妥当なプリセットは考えられるので、それを以下で詳しく見ていきます。

NES用プリセット

最も保守的なものはこんな感じでしょうか:

  • KIL 命令 (12種類) は非コード
  • XAA 命令 (0x8C) は非コード
  • I/Oレジスタ ($2000-$4017) を実行するものは非コード
  • 書き込み専用レジスタ ($2000 など) を読み取るものは非コード
  • 読み込み専用レジスタ ($2002 など) に書き込むものは非コード*1

これは割と信頼できそうです。KIL, XAA はともに非公式命令で、前者はCPUを停止させ、後者はそもそも動作が不安定とされているので、少なくとも商用ゲームではまず使われないでしょう。I/Oレジスタに関しても妥当なところかと思います。

よりアグレッシブに行くなら、以下の条件を加えることが考えられます:

  • 全ての非公式命令は非コード
  • BRK 命令 (0x00) は非コード
  • RAMミラー ($0800-$1FFF) を読み書き/実行するものは非コード
  • PPUレジスタのミラー ($2008-$3FFF) を読み書きするものは非コード

これでも大抵はうまく行くと思いますが、非公式命令を使っている商用ゲームも存在します。BRK を禁止するとデータ領域がかなりきれいに出力されます*2が、これは公式命令で別に禁止されているわけでもないので、使っているゲームがあってもおかしくないと思います(私は見たことがありませんが)。ミラー領域に関してはそもそも仕様の範囲内なのかどうかよくわかりませんが、使っているのを見たことがないのでとりあえず入れてみました。

後はマッパーによっては $4018-$FFFF についても読み/書き/実行を制限できるので、メジャーなマッパーについてだけでもプリセットを作っておくと役立ちそうです。ただしこれにはちょっとした罠があります。例えばマッパー0のゲームは普通 $4018-$7FFF を読み取りませんが、任天堂『ゴルフ』ではオペコード兼オペランドのByteが存在するために $40A9 の読み取りが発生しています。

例外が全くない設定というのはなかなか難しいので、出力がおかしくなった場合は手動でコード判定を行い、残りの部分について自動解析を適用するという形になるかと思います。

実行記録の利用

実際にプログラムを実行することで、少なくとも実行された部分はコードだと判定できます。例えば FCEUX には Code/Data Logger (CDL) が付属しています。*3

ただし、FCEUX CDL はオペコードとオペランドを区別しないため、確実にコードと判定できるのはコード領域の先頭のみです。

次のパスでは、これらの手段で得られた結果を元に制御フローを考慮したコード判定を行います。具体的には次回

*1:2017/03/02 追記: NESアルカノイドはなぜか $2002 への書き込みを行っているので、これも確実とまではいえないようです。

*2:BRK は実質的に2Byte命令です。

*3:BizHawk の Code/Data LoggerNESには未対応のようです。