基礎知識
目次
まず、逆アセンブラのdis65816、デバッガのSnes9X v1.43.ep9r8 - Geiger's Snes9x Debuggerをダウンロードして、解析したいゲームのROMイメージを逆アセンブルしましょう。
風来のシレンでは、プログラムは全て000000〜06FFFFにあり、070000〜3FFFFFは全てデータになっているので、070000以降の逆アセンブル結果は意味がないので無視して構いません。 他のゲームでも、大抵プログラムは最初の方にあると思います。 プログラムかそうでないかは、慣れれば逆アセンブル結果を見れば分かります。
プログラム中で070000以降のデータが読み込まれていれば、その度にバイナリエディタで読み込み先を調べます。 但し、プログラムにはROMイメージ中のアドレスではなく、メモリに読み込まれた時のアドレスで書かれているので、後述の、ROMイメージ中のアドレスと、メモリでのアドレスの関係に注意する必要があります。
Snes9X v1.43.ep9r8 - Geiger's Snes9x Debuggerは、ヘッダのあるROMを読み込むとヘッダの削除を求められます。 ヘッダは解析には意味がなく、逆アセンブル結果を512バイトずらしたりして邪魔なだけなので削除してしまいましょう。
解析したいゲームがあれば、まずは数値を検索するなり、メモリビューアを見るなりして、沢山改造コードを集めて下さい。 改造するには面白みのない値を含めて、値の意味が分かったものは全てメモして、メモリリストのような表を作って下さい。 いきなり解析を始めると、見たこともないアドレスが次々と出て来て、それを一々調べることになり大変です。
デバッガにはブレークポイントといって、アドレスを指定しておくと、そこで実行、読み込み、書き込みが行われる直前にプログラムが停止して、そこから1行ずつ実行したり、レジスタの変化を追ったりできる機能があります。 解析したいことがあれば、それに関するアドレスにブレークポイントを設定して、プログラムが停止したら、その地点を含むプログラムを逆アセンブル結果から取り出して調べたり、分からなければ1行ずつ実行していったりするというのが解析の主な作業になります。
Snes9X v1.43.ep9r8 - Geiger's Snes9x Debuggerの機能の説明を簡単に書いておきます。 これ以外はあまり使いませんし、私もよく分からないので興味がある方は色々と試してみて下さい。
アセンブリでは、メモリのアドレスの前には$を付けます(それに対して、値の前には#$を付けます)。
風来のシレンでは、ROMは$C00000〜$FFFFFFに読み込まれます。 ROMイメージ中のアドレスと、メモリでのアドレスには、$(メモリでのアドレス)=$((ROMイメージ中のアドレス)+C00000)という関係があります。 他のゲームではそうは単純にはいかないものもありますが、メモリでの数値の列をバイナリエディタで検索すれば分かります。 プログラムにはメモリでのアドレスで書かれていますが、dis65816は自動的にROMイメージ中のアドレスに修正してくれます。
更に、どういう訳か$008000〜$00FFFFと$808000〜$80FFFFにも、$C08000〜$C0FFFFと同じ内容が読み込まれています。 同様に$818000〜$81FFFFにも、$C18000〜$C1FFFFと同じ内容が読み込まれています。 違いはよく分かりませんが、気にする必要はないと思います。
RAMは$7E0000〜$7FFFFFですが、$000000〜$001FFFにも$7E0000〜$7E1FFFと同じ内容があり、片方が変わるともう片方も変わります。 $7E0000〜$7E1FFFの読み書きをする場合は$000000〜$001FFFを指定するようです。 やはり違いはよく分かりません。
セーブデータの内容は風来のシレンの場合、$B06000〜$B07FFF、$B16000〜$B17FFF、$B26000〜$B27FFF、$B36000〜$B37FFFに読み込まれていますが、何故かメモリビューアで値を直接確認できなかったり、ブレークポイントを設定するとアドレスが10000違いでも停止したりするよく分からないものです。
レジスタ | バイト | 意味 |
---|---|---|
"A" | 1 or 2 | 演算等、自由に使われる。サイズは"P"のbit5によって変わる。 |
"X","Y" | 1 or 2 | 演算等、自由に使われる。配列の読み込みにも使われる。サイズは"P"のbit4によって変わる。 |
"S" | 2 | スタック(後述)の現在位置の下位2バイト(上位1バイトは00)を示す。 |
"PC" | 2 | プログラムの実行位置の下位2バイト。 |
"PB" | 1 | プログラムの実行位置の上位1バイト。引数2バイトでジャンプする時の上位1バイトにもなる。 |
"D" | 2 | 引数1バイトでメモリのアドレスを指定する時の基点(上位1バイトは00)。0000以外になっているのを見たことがない。 |
"DB" | 1 | 引数2バイトでメモリのアドレスを指定する時の上位1バイト。 |
"P" | 1 | 様々なフラグを示す。内訳は下の表を参照。 |
Snes9X v1.43.ep9r8 - Geiger's Snes9x Debuggerでは、"P"の状態がenvMXdizcのように表示されます(小文字が0、大文字が1)。 普通の解析で出て来るのは殆どC,Z,X,M,Nだけです。
ビット | 文字 | 意味 |
---|---|---|
0 | C | 演算で更新される。 加算・2倍では結果が"A"のサイズを超えた時に1、そうでないときに0。 減算では結果が負にならなかった時に1、そうでないときに0。1/2倍では余りが入る。 |
1 | Z | 値の読み込みや演算で更新される。結果が0になったときに1、そうでないときに0。 |
2 | I | 何でしょう。 |
3 | D | 何でしょう。 |
4 | X | "X","Y"のサイズを決定する。0のとき2バイト、1のとき1バイト。 1にすると"X","Y"の上位バイトは00になり、変更できなくなる。 |
5 | M | "A"のサイズを決定する。0のとき2バイト、1のとき1バイト。 1にしても"A"の上位バイトはそのままで、XBA命令を使うと上位バイトを利用することができる。 この状態では"A"の下位バイトの演算で繰り上がり・繰り下がりの影響を受けない。 |
6 | V | 何でしょう。 |
7 | N | 値の読み込みや演算で更新される。結果の最上位ビットが入る。 すなわち、結果を符号付きの数値と見なした時の符号を示す。 |
- | E | 何でしょう。直接は変更できず、XCE命令によってのみ値を変更できる。 |
スタックとは、針に円盤を通すようなもの(他に良い例えを思い付かない)で、最後に入れたデータを最初に取り出せる仕組みです。 スタックにデータを入れることをプッシュ、データを取り出すことをプルと言います。 針に円盤を通すようなものと書きましたが、プッシュしても元の数値が消えてしまうことはありません。 アセンブリ言語にはローカル変数などというものはないので、サブルーチンに入る前にレジスタの値を退避させるのによく使われます。
スタックの正体はメモリの一部で、普通は$001FFF(=$7E1FFF)から前にデータが入っていきます。 スタックの現在位置のアドレスはレジスタ"S"が示しています。 これを変更してスタックを複数持つようなこともできます。
スタックの仕組みは、1バイトのデータがプッシュされると、"S"が示すアドレスにそれを書き込み、"S"から1を減じます。 2バイトだと、"S"が示すアドレスにその上位バイトを、("S"が示すアドレス)-1にその下位バイトを書き込み、"S"から2を減じます。 3バイトでも同様です。 プルされた時はその逆の処理を行います。
実はサブルーチンも、その命令の末尾のアドレスをプッシュしてジャンプし、戻る時にプルしてそれに1を加えたアドレスから再開することによって実現しています。 従って、戻る直前にはジャンプした直後と"S"を一致させておかなければなりません。 しかし、この仕組みを利用して、プッシュされたアドレスをサブルーチン中でプルし、2つ以上上のルーチンに戻ることもできます。
サブルーチンの命令には、引数が2バイト(上位1バイトは現在のアドレスと同じ)のJSRと、3バイトのJSLの2種類がありますが、上の理由により、JSRでジャンプした場合は必ず2バイトプルするRTSで、JSLでジャンプした場合は必ず3バイトプルするRTLで戻らなければなりません。 上位1バイトがどこからでも呼ばれるような、広く使われるサブルーチン(例えば乱数生成)はRTLで戻るようにしておき、上位1バイトが同じアドレスからジャンプする場合でも必ずJSLでジャンプしなければなりません。 解析をするだけなら気にする必要はありませんが、プログラムを改造する場合は覚えておかなければなりません。
2010年2月28日更新