夢想メモリ

ゲームの攻略、データ、感想、縛りプレイのこと等を中心に、ゲームとは無関係の雑記も書きます。

レトロゲームのROM解析・逆アセンブルの方法メモ

今年に入ってレトロゲームのROM解析・逆アセンブルを行うようになった。まだ1つのハード、1つのソフトの解読しかやったことがないのだが、概ね他のレトロゲームでも同じ方法が通じるだろうと考えメモを残しておく。誰かの参考になったり、好奇心を満たしたりできれば幸いである。

アセンブルとは

アセンブルとは、CPUの命令と1対1に対応している機械語(バイナリコード)を、アセンブリ言語に戻すこと、のように言われる。このようなことができるツールを逆アセンブラと言う。しかし、変数名などは復元できない為、逆アセンブラでできるのは機械語を適切に区切ったり、対応する命令を表示したり、ジャンプの処理を追えたりする程度である。機械語の解読支援ツールという感じがする。

レトロゲームにおいては、逆アセンブルによって、乱数の変わり方、ゲーム内の数値(例えばダメージとか)の算出方法、バグの発生条件や原因などを解明することができる。

アセンブルの流れ

ROMを吸い出す

アセンブルは普通PCで行うので、ゲームソフトのデータを吸い出す必要がある。これにはROM吸い出し機と言われるハードウェアが必要である。数千円で手に入るので、ネット通販で購入するのが早いと思う。ROMの吸い出し方は吸い出し機によって異なる。

例えば、GBAダンパーは基板剥き出しのハードウェアであり、吸い出し機をUSBでPCと接続→吸い出し機にゲームカセットを取り付け→製造元の用意したソフトを実行、という手順でROMを吸い出せる。レトロゲームの吸い出しでは標準的な方法ではないかと思う。

Superufo pro 8というSFCの吸い出し機はカセット状のもので、これにSDカードを入れる→吸い出し機を直接SFC本体にセット→上部にゲームカセットを接続→SFCを起動してSDカードに書き出す、という手順だったと思う。

いくつか例を挙げたが、ここがメインでない為、詳しくは述べないので、別途調べて欲しい。

このように自分でROMを吸い出すのは私的複製に当たり、合法であると考えられる。このデータをアップロードする行為、ネット上にアップロードされていたとしても、それをダウンロードする行為は違法である。

エミュレータによる事前調査

ゲームの処理を全て把握したいのではなく、特定の処理について知りたいなら、エミュレータで特定の値が格納されているメモリアドレス等を知っておくと役に立つ。例えば、戦闘中の処理を知りたければ、攻撃力や防御力、得られるEXPなどの値が配置されているメモリ番地を知っておくと良い。これらを把握する上では、エミュレータのチート検索機能や、有志が公開しているチートコードを利用できる。ゲームボーイのチートコードの検索については以前の記事に書いている。

また、〇〇の状態では××が2倍になるとか、知りたい処理について論理的に少しでも分かる部分があると後で解読しやすい。エミュレータのステートセーブ、チートなどを用いてある程度実験しておくと良い。例えば、ダメージ計算式が知りたければ、ステータスの値をチートで改竄し、ステートロードを繰り返して何度も攻撃し、最小値、最大値などを計測し、最小値だけでも計算式が予想できれば解読の助けになる。

CPU仕様と命令セットの把握

まず、アセンブリ言語がどういうものであるか、CPUの仕組みなどを知らなければ簡単に調べておこう。次に、解析したいゲームのハードで使われているCPUの種類を調べ、レジスタが何種類あって何ビットのデータを記憶できるのか、といった解析に必要な情報を調べておく。また、オペコードの一覧、ニーモニックの意味などの表を探しておく。ゲームボーイの場合、これらの情報が日本語ではあまり見つからなかったので、英語の資料を参考にした。

アセンブリツールを用いて解読する

解析したいソフトのハードに対応した逆アセンブラをダウンロードし、ROMを逆アセンブルして実際に読んでいく。

2進数の計算(2の補数、シフト演算など)、スタック等のデータ構造の理解は必要なので、覚束ない点があればちゃんと調べる。また、必須ではないが、高水準言語でもプログラミング経験があるとアセンブリ言語の解読に役立つだろう。

アセンブル等のリバースエンジニアリング自体は合法だと考えられる。ソフトウェアの使用許諾などにおいてリバースエンジニアリングを禁止する文言が記載されている時もあるが、法的効力は分からない。説明書やパッケージが無くて確認できないものも多いが、古いゲームでリバースエンジニアリングの禁止が明言されているものは少ないと思う。

解読のコツ

ここまでに必要な知識があれば、逆アセンブラを見て、どのレジスタでどんな計算をしている、メモリのどの番地にどの値を代入しているといった処理を読み解くことはできる。しかし、ハードウェアに近い所の数値の扱いが分かった所で、ゲーム内のどんな処理をしているのかは分からない。

メモリへの操作をもとに知りたい処理を探す

まず、自分が知りたい処理がどこにあるのかを見つけなくてはならない。ここでは、エミュレータによる事前調査で得たメモリに関する情報が使える。知りたい処理、あるいはそれに関連する処理で、書き込まれる/読み込まれるメモリ番地への操作でROM内を検索するのだ。

例えば、所持金を表す値が0x1000番地に格納されているとして、その値をあるレジスタの値の分だけ減らす処理で検索を掛けていくつかの結果がヒットしたとすれば、この中に道具屋でアイテムを買った時の処理が含まれていることが予想できる。道具屋で売られるアイテムが決まる仕組みを知りたければ、まずはこのようにアイテムを買った時の処理を見つけ、そこからサブルーチンの呼び出し元を辿っていく方法が考えられる。

ただし、メモリへのアクセスはレジスタに格納している値を使って行われることもあるので注意。例えば、0x2001番地に1人目の仲間の攻撃力、0x2011番地に2人目の仲間の攻撃力が格納されているとしたら、恐らく0x2001に即値でアクセスすることはないと予想できる。あるレジスタに0x2000を入れておき、そこに、1人目の仲間が攻撃するとしたら0x01を、2人目の仲間が攻撃するとしたら0x11を加えるなどして、レジスタの値を使って相対的にメモリにアクセスしないと、コードが冗長になるからである。例えば、RPGにおける戦闘処理を見つけたいのなら、戦闘終了後に得られるお金など、一通りしかない値を格納するメモリ番地への操作で検索をかける方が良いと思う。

何を行っている処理なのかを把握する

上述のメモリへのアクセスをもとに処理を辿れたとして、それが何を行っているかを判断するにはどうするべきか。ここでは、メモリに関する情報だけでなく、エミュレータでの事前調査、あるいは実機でのプレイで知った論理的な仕組みも手掛かりになる。

例えば、0x3000番地の値が普通は0だが、弱体化デバフを受けると1になるとして、弱体化のデバフを受けるとダメージが2倍になることを知っていたとしよう。この時、レジスタ1に0x3000番地の値を入れる→レジスタ1の値が0なら、レジスタ2の値を左に1ビットシフトする処理を飛ばす、という処理があれば、レジスタ2に入っているのがダメージであることが予想できる。このように、操作対象のメモリだけでなく、論理的な仕組みから、処理の内容を把握できることもある。

また、バイナリエディタでROMを書き換えてエミュレータで実行してみることで、何の処理なのかを確定させることもできる。例えば、強化バフが掛かっている時にダメージを2倍にするという処理だと予想できる部分があったとする。ここで、2という部分を20に書き換え、エミュレータでゲームを起動して強化バフをかけて攻撃してダメージを見れば、その処理なのかを確定できる。

解読する範囲を区切る

ある値の変化の仕組みを知りたかったとして、その値が格納されているメモリアドレスが分かっていても、その値を変化させる処理はサブルーチンとして呼ばれており、呼び出し元の前後に別のサブルーチンがあり、その呼び出し元もサブルーチンとして呼ばれている……というような複雑な状況は往々にしてある。こういう時はサブルーチン毎に区切って考えるのも良いが、そのメモリ番地への値の代入、あるいは初期化処理を見つけて、そこを区切りとして考えると良いこともある。

ところで、初期化処理は、高水準言語の発想では分かりにくい書き方をしていることがある。実際にあった初期化処理なのだが、あるメモリ番地に即値で0を代入するのではなく、レジスタ1とレジスタ1の排他的論理和レジスタ1に代入する→あるメモリ番地にレジスタ1の値を代入するということが行われていた。命令毎のクロック数を見ると、確かにこちらの方が早かった。

サブルーチン毎のまとめを作る

アセンブリ言語には関数やクラスの概念は無く、代わりにCALL命令とサブルーチンが用いられる。CALL命令では現在のROMのアドレスをスタックに入れ、指定したアドレスにジャンプし、RET命令によってスタックの値を取り出し、そのアドレスにジャンプすることで戻って来る。アセンブリ言語では、かけ算、割り算に相当する命令も無い(ことが多いと思う)ので、サブルーチンによって実現しており、同じサブルーチンが様々な箇所で呼ばれることが多々ある。サブルーチン毎に、どのレジスタの値が使われるのか(入力)、何を行っているのか、結果はどのレジスタに入っているのか/影響を受けるフラグはどれか(出力)、レジスタに入っていた値は保存されるのかといった情報をまとめておけば、次に出て来た時すぐに理解できる。