SPC解析入門 - アセンブリ読解への誘い

ものすごく難しい内容を取り扱うつもりはないのですが、内容が内容だけに、ある程度の知識と忍耐は必要です。

上記の条件を満たす方が対象です。そうでない方は適宜調べながら読んでみてください。

アセンブリの読み方に関しては、永久UnderConstructionに数多くの資料へのリンクがあります。入門には、鬼畜王マリオで知られるCarolさんの墓場にある資料が、頻出命令を太字で示してくれていたりしておすすめです。その他、SPC700のメモリマップドIO周りの仕様は、SPC700仕様書のテキストにあります。あまりアセンブリそのものの読み方の細かいところは説明しないので、適宜こういった資料を眺めながら読解してください。

SPCファイルからの逆アセンブル

SPCファイルの仕様上、ファイル中にはARAMの内容64kBが丸ごと保存されています。特殊なケースを除いては、曲データから演奏プログラムまで、ここに曲を演奏するためのデータがすべて入っています。そういうわけで、演奏プログラムがどんな挙動をするのか調べるために、まずはプログラムを逆アセンブルして、機械語の羅列からアセンブリの一覧を得ることにしましょう。

65816やSPC用の逆アセンブラはいくつかありますが、ここでは以下のようないくつかの理由からspcdas(の修正版)を用いることにします。

  • ソースコード付属
    • 不具合があった場合、自分でプログラムを修正することができる
    • 単純なコンソールアプリケーションなので、各種プラットフォーム間での移植性が高い
  • 作者があのbsnesのbyuuさんだけに、なんとなく信頼できる(←)

spcdasのもっとも簡単な使い方は次のようになるはずです。

  1. SPCファイルからARAM領域($00100-$10100)をヘキサエディタで切り出して保存(例: a.bin)
  2. spcdas a.bin a.sのようにしてファイル先頭から逆アセンブル(出力例: a.s)

とっても簡単ですね。-load-pcをオプションに指定して、一部のみを逆アセンブルすることもできます。慣れてきたら活用してみましょう。

SPC700アセンブリの特徴

SNES CPU側の65816とそこまで大きな違いはありません。

  • $f0-$ffは特殊なアドレスで、ここへの読み書きは通常の読み書きとは意味が違う(詳細は資料参照)。
    • $f2,$f3を通して各種DSPレジスタ(音量、エコーなどの情報)に書き込みをおこなう。
  • 乗算用の命令など、一部65816にはない命令がある。資料や先人のアセンブリを見ながら覚えるのが吉。

その他、ありがちな特徴。

  • 最初の若い数$100バイトは通常データ領域として使われる。コード領域は早くても$0200付近から。
  • DSPレジスタYに値Aを書き込む」のような関数をどこかに持っていることが多い。
  • DSPレジスタの特定レジスタに1対1対応する変数(shadow)が存在することが多い。たとえば$42の内容を常にFLGに書き込むようになっていて、$42を変更すれば近いうちにFLGにその値が反映される……といった具合。レジスタとshadowのアドレスの対応はテーブルに記述されていることが多い。
  • 主に曲データを表現するコマンド(vcmd, voice cmd, 例: $e0 $04で鳴らす波形を4番に変更)と、演奏状態の指示をportを介しておこなうためのコマンド(cpu cmd, 例: $ffで再生中の曲を停止)を持つ。
    • いずれも、byte値に応じた実行内容を定めたアドレステーブルを持つことが多い。また、コマンドのサイズを羅列したテーブルを持つことも多い。
      • 実行方法はいろいろ。単純にテーブルに基づいてjmpする場合、call先を動的に書き換える場合、行き先をpushしてretする場合、いろいろあります。このあたり少なからずアセンブリ慣れしてないと意味わからないかも?
  • その他、音符(note)の長さ、周波数、エコーのFIR係数など、ありがちなテーブルというのはいくつかある。

処理テーブルの例:

; vcmd dispatch table (0C5D)
0ee9: dw $0c79  ; e0 - set patch
0eeb: dw $0cd2  ; e1 - pan
0eed: dw $0ce0  ; e2 - pan fade
0eef: dw $0cf9  ; e3
0ef1: dw $0d05  ; e4
0ef3: dw $0d20  ; e5 - master vol
0ef5: dw $0d25  ; e6 - master vol fade
0ef7: dw $0d37  ; e7 - tempo
0ef9: dw $0d3c  ; e8 - tempo fade
0efb: dw $0d4e  ; e9 - global transpose
0efd: dw $0d51  ; ea - per-voice transpose
0eff: dw $0d55  ; eb
0f01: dw $0d61  ; ec
0f03: dw $0d82  ; ed - voice volume
0f05: dw $0d8b  ; ee - voice volume fade
0f07: dw $0da8  ; ef - call subroutine
0f09: dw $0d10  ; f0
0f0b: dw $0d64  ; f1
0f0d: dw $0d68  ; f2
0f0f: dw $0d7e  ; f3
0f11: dw $0da4  ; f4
0f13: dw $0dcb  ; f5 - set echo vbits, volume
0f15: dw $0e0e  ; f6 - disable echo
0f17: dw $0e18  ; f7 - set echo delay, feedback, filter
0f19: dw $0ded  ; f8 - echo volume fade
0f1b: dw $0e9a  ; f9 - portamento?
0f1d: dw $0e87  ; fa - set perc patch base

その他の基本的な注意点も少し述べておきます。

  • 減算ではキャリーフラグ(ボローフラグ)に注意する。

いきなりいろいろ言われても意味がわからないのではないかと思います。なにかコメントのついたアセンブリと一緒に読むなどするのがおすすめです。手前味噌ですが、わたしのMIDIコンバータの中にはいくらかアセンブリがごろごろおいてあります。あんな程度でも一応参考にはなるかなあと。

アセンブリの読み方

わたしが取る手順をいくつかご紹介します。たいした内容ではないです。

不要な部分を削る

プログラムが格納されているのはARAMの一部なので、最初にいらないところを切り捨てようということです。

方法は、なんとなくコードっぽいところを探します。非常に抽象的ですね。どういうところがコードっぽくないかと言えば、普段出てこないようなレアな命令がたくさん並んでいるとか、どう見ても意味のない挙動をしているとか、そんな命令の並びです。その他、「clrpのあとにspをセットしている」など、開始位置にありがちなことというのはないでもないですが、絶対的でもないのであまり大声で言えません。

あくまで無駄を削るのが目的なので、わかる範囲で適当に削ることができれば十分です。過不足があると思ったら、あとで調整すればいいだけの話です。

定数領域を書き直す

アセンブラはあらゆるものを命令列とみなして変換をおこないますが、プログラムの中にはコード領域だけではなく、プログラムで用いる数値を書き並べた定数領域だってあるわけです。まず最初は、これらの存在を把握して、書き直すことをおすすめします。

方法はいろいろあるかもしれませんが、とりあえずコードらしくないコードを探してみるのが効果的です。よく定数がコードと誤爆されて出てくる命令の例を示します。

  • 00 : nop
  • 01 : tcall 0
  • 7f : reti
  • ff : stop

その他、普段あまり見かけない命令が無意味に何度も出てきていたりと、見た目どこか妙な箇所は大抵誤爆です。せっかくなので実例をひとつ示します。

0ee8: 6f        ret
0ee9: 79        cmp   (x),(y)
0eea: 0c d2 0c  asl   $0cd2
0eed: e0        clrv
0eee: 0c f9 0c  asl   $0cf9
0ef1: 05 0d 20  or    a,$200d
0ef4: 0d        push  psw
0ef5: 25 0d 37  and   a,$370d
0ef8: 0d        push  psw
0ef9: 3c        rol   a
0efa: 0d        push  psw

retの後ろ、比較したかと思えば無視してシフト演算をおこなったり、push pswを連呼していたりと、とっても怪しいアセンブリです。バイナリに着目すると、1バイトおきに0cや0dが並んでいます。答えを先に言ってしまえば、これはコマンドの処理テーブルの例です。コード領域のアドレスを2バイトで羅列しているため、このような特徴が現れるのです。

0ee8: 6f        ret
0ee9: dw $0c79  ; 
0eeb: dw $0cd2  ; 
0eed: dw $0ce0  ; 
0eef: dw $0cf9  ; 
0ef1: dw $0d05  ; 
0ef3: dw $0d20  ; 
0ef5: dw $0d25  ; 
0ef7: dw $0d37  ; 
0ef9: dw $0d3c  ; 

こんな感じに、定数であることがわかりやすい形に書き直します(書式は一例)。これがテーブルであることを確認するには、先頭アドレスの「$0ee9」をテキスト検索してみるのが有効です。しかし、常にテーブルが先頭から参照されているとは限らず、素直にヒットしてくれないこともあります。じつはこのサンプルもそういう例でした。

処理の区切りをわかりやすくする

ret命令とjmp命令の直後に空行を入れることで、サブルーチン間の分離がよくなります。分離しない方がいい箇所が分離されることもありますが、気づいたときに直せばいいでしょう。エディタの置換機能を用いてうまく自動処理するとよいです。正規表現が利用できるエディタがとくに便利です。誤爆には注意してください。

これはあくまで整形の嗜好のひとつなので、お好みに応じて適当にどうぞ。

アドレステーブルのジャンプ先にコメントをつける

; vcmd dispatch table
0ee9: dw $0c79  ; e0
0eeb: dw $0cd2  ; e1
0eed: dw $0ce0  ; e2
0eef: dw $0cf9  ; e3
0ef1: dw $0d05  ; e4
0ef3: dw $0d20  ; e5
0ef5: dw $0d25  ; e6
0ef7: dw $0d37  ; e7
0ef9: dw $0d3c  ; e8

このように、アドレステーブルのインデックスと内容がわかったら、

; vcmd e0
0c79: d5 11 02  mov   $0211+x,a
0c7c: fd        mov   y,a
0c7d: 10 06     ...

のように、その先のアドレスにもコメントを付加しておくといいですよということ。

vcmdの読解

上記のような作業を経てvcmd用のアドレステーブルの発見に至ると、それを用いている箇所も比較的容易に見つけられます。ジャンプのインデックスは楽曲データに含まれるデータバイトが元になっているため、周辺を調べれば、楽曲を読み進めるためのポインタの存在や、データバイトを用いたその他の分岐の存在などの情報がわかってきます。

あとは地道に読み進めていけば、データバイトを解釈するためにどのような遷移をすればいいのかわかってくるはずです。それらの持つ意味に関しては、適宜SPCファイルを書き換えて演奏したりしながら、ゆっくりと探っていくといいでしょう。

とりあえずは「読み方」と「音符の表現」のふたつが二大重要項目だとわたしは思います。

演奏中の曲の先頭アドレスを得る方法を知る

SPCファイルを対象にしたコンバータを作るときに、もっとも困ってしまうのがこれかもしれません。演奏の「現在位置」を知ることはできても、「先頭位置」を知るのには絶対的な方法がないことがほとんどです。なんとかして「楽曲位置一覧」から「現在位置」を元に検索して見つけるという方法が精一杯です。構造によっては、そのような手段すら用意できないかもしれません。

とにかく、不運でなければ、大抵は「各演奏データはここから開始」という情報もどこかに含まれています。「現在位置」の初期代入をおこなっている箇所を探して、なんとかうまく探ってください。このあたり、ノウハウがどうというよりはとにかく追っていくしかないので、あまりうまく解説できません……。

おしまい

これくらいの情報がそろえば、MIDIコンバータの初期版みたいなものが書けるかなあと思います。情報をそろえる過程に補助プログラムを書いて、徐々にそれを進化させていくのもひとつの有効な手段です。なんらかの補助ツールがあると、解析はスムーズに進めやすいです。

上達の秘訣は、やはり先達の方に倣うことかなあと思います(というかわたしがそうでした)。loveemu laboにもいくつかアセンブリを含んだコンバータが公開されています。あらゆるものを読んでしまう神々には足下にも及ばないですが、気が向いたら参照してみるのもいいかもしれません。

さらに活用できそうなノウハウがあればご教示ください。またいろいろ思うところがあれば、内容を大幅改訂するかも知れません。