上に戻る
6.FC命令を走破してみる
概要
 前回『ぼけぇっと』ディスアセンブル結果を見てみましたが、とりあえず、ファミコンのCPU『6502』の命令を一通り説明してみようと思います。
 ざっと、こういった命令がある……とわかればなぁ、と。んなことで、流し読み推奨。
ぶっちゃけた話……
 今自分の持っているこの手の知識は、ネット上の情報を集めただけです。何もないところからからそういった情報を調べていこうと思った先人たちには頭が上がりません。(ま、“商業的に”調べた人も少なからずいるんだろうけど……)
 もし、このサイトの情報と他所で食い違いがあるときは、このサイトが一方的に間違ってます^^
 というわけで、このサイトに存在意義はないのですが、一応続けます。

 コンシューマのゲーム機の資料は、以下のサイトに充実しています。
   すずめ愛好会
     http://vsync.org/
               今年初め、このサイトが消えていて、泣きそうになった……
もう一度レジスタ
 http://computers.yahoo.co.jp/dict/semicon/mpu/architecture/inner/1509.html
 この辺によると……やっぱりレジスタってのは『CPUの内部にある高速・低容量な記憶装置』という解釈で問題ないみたいですね。←お前が確かめてどうする
 ファミコンのCPU『6502』のレジスタは、前回の汎用的に使えるレジスタ『汎用レジスタ』a,x,yの他に、あと3つあるみたいです。それが、sレジスタ、pレジスタ、pcレジスタです。
 sレジスタはa,x,yと同じく8ビットの記憶容量がある、スタックの位置を記憶しておくレジスタです。スタックについてはこちらを
 次の、pレジスタも8ビットです。CPUの状態や、演算結果の一部が格納されます。
 pcレジスタは、16ビットの容量があります。これは、プログラム中で実行されている位置を指すものです。
 それぞれ、汎用的じゃないので、勝手に書き換えちゃダメです……が、
 sレジスタはtxs命令で書き込み、tsx命令で読み込み可能で、
 pレジスタはplp命令で書き込み、php→pla命令で読み込み可能で、
 pcレジスタは適当な場所にジャンプすれば勝手に書き換わります。pcを意識して書き換えるメリットはないと思うが……

 アルファベットは、それぞれ、Stack pointer,Program status,Program Counterの略みたいです。
 よくまぁ、こんだけ似た頭文字の単語が並んだもんだ……pレジスタが意味から考えてやたら不自然な感じ。
pレジスタ
 すぐ上でも書きましたが、pレジスタは、CPUの状態や、演算結果の一部を格納するレジスタです。
 8ビット分の、各ビットに意味があります。それぞれONかOFFかのフラグと見るらしいです。
 あるビットを、1にすることを『セット』、0にすることを『クリア』といいます。
 分岐命令の条件に使う他、命令の結果に影響を与えることもあります。
 8ビットを上から
 7654 3210
 と識別番号を振って……(ビットに識別数字を振るときはこういう振り方が一般的らしい)
●7……Nフラグ。ネガティブフラグ。直前の演算の結果の最上位ビットが格納されることが多い。
      ストア命令以外の、値を操作するほとんどの命令で書き換わる。
●6……Vフラグ。オーバーフローフラグ。(oVerflow?)
      値を符号付と見て、足し算(adc)や引き算(sbc)をしたときに、
      値が7Fh(127)より大きくや、80h(-128)より小さくなったときにフラグが立つ。
      あと、なぜかbit命令でも使う。
      あんまり書き換わらないフラグ。
●5……未使用?
●4……なんか割り込みの種類を特定するのに使うらしい。
      2つの別原因の割り込みの割り込み先が共有で、1つしか指定できないから必要らしい。
●3……Dフラグ。2進化10進数を扱うか?
      なんでもファミコンのCPUでは実装していないから、常に、0であるべきみたい。
      2進化なんたらってのは、例えば、ビットを4ビットずつ区切って、それぞれ1桁の10進数と見る整数管理方法です。
●2……Iフラグ。インタラプトフラグ。
      割り込みを許可“しない”かのフラグ。
      cli命令でクリア、sei命令でセットできます。
●1……Zフラグ。ゼロフラグ。命令結果が0だとセットされる。
      ストア命令以外の、値を扱うほとんどの命令で書き換わる。
●0……Cフラグ。キャリーフラグ。
      キャリーは、たぶん足し算の『繰り上がり』の意味……
      加算(adc)減算(sbc)比較(cmp,cpx,cpy)
      あと、シフト(asl,lsr)ローテート(ror,rol)命令をするとき、
      その命令の、演算結果に影響する上に、これ自体も書き換わる。
      ロード・ストアでは変わらないのがミソ……かも。

 どのフラグも、plp命令やrti命令で書き換わる。
アドレッシングモード
 CPUの命令に付けられた名前をニーモニックというそうです。staとかadcとか。
 命令は、ニーモニックだけでなく、作用対象と共に指定されます。
 例えば、足し算命令なら、『何を』足し算するか……ということ。
 『何を』と言っても、整数定数かも知れませんし、どっかしらメモリから読み込んだ値かもしれません。
 『何を』の指定方法の種類を、アドレッシングモードというみたいです。(やぶれかぶれな説明だなぁ……)
 以下でアドレッシングモードを網羅してみます。なんか、まんま転載になりそうですが……
●無し:none
   単体で動作する命令。(これはアドレッシングモードの一つに分類されるのかなぁ)
   あと、作用対象がメモリじゃなくて、aレジスタの時、形式的にコレに分類?
   (それは、 アキュムレータ:accumulatorって分類したほうが良いのか?)
●直値:immediate
#**
   1バイトのオペランドの値そのもの。
   C言語表現:op0
●ゼロページアドレス:zero page
**
   1バイトのオペランドで表されるアドレス(上位は00)から読み出した値。
   C言語表現:*op0
●ゼロページアドレスX:zero page X
**,x
   1バイトのオペランドで表されるアドレスにxレジスタの値を加算した場所から読み出した値。
   加算によって256を超えた分は無視するらしい。(というか、そんなアドレスは普通は指定しない)
   C言語表現:*( (op0+x)%0x100)
●ゼロページアドレスY:zero page Y
**,y
   ゼロページXと同じなのだが、サポートしている命令が少ない。
   C言語表現:*( (op0+y)%0x100)
●絶対アドレス:absolute
****
   2バイトのオペランド(順番は低位→高位)で表されるアドレスから読み出した値。
   C言語表現:*( (op1<<8) | op0 )
●絶対アドレスX:absolute X
****,x
   同じく2バイトオペランドに、xレジスタの値を足したアドレスから読み出した値。
   C言語表現:*( ( (op1<<8) | op0 ) + x )
●絶対アドレスY:absolute Y
****,y
   絶対アドレスXと同じなのだが、サポートしている命令が少ない。
   C言語表現:*( ( (op1<<8) | op0 ) + y )
●間接アドレス:indirect
(****)
   2バイトのオペランドで、指定したアドレスから、2バイト分読み出し、
   そのアドレスから読み出した値。
   jmp命令のみサポート。
   C言語表現:ptr=(op1<<8)|op0 , *( ( *(ptr+1)<<8 ) | *ptr )
●間接アドレスX:indirect X
(**,x)
   1バイトのオペランドにxレジスタを足したアドレスから2バイト分読み出し
   そのアドレスを読み出した値。
   C言語表現:ptr=op0+x , *( ( *(ptr+1)<<8 ) | *ptr )
●間接アドレスY:indirect Y
(**),y
   これはindirect Xとは全然違う!表記もよく見ると違うし。
   1バイトのオペランドで指定されるアドレスから2バイト分読み出し、
   そこにyレジスタの値を足したアドレスから読み出した値。
   C言語表現:*( ( ( *(op0+1)<<8 ) | *op0 ) + y )

 機械語は、『ニーモニック×アドレッシングモード』で決まります。
 命令により(サポートしていてもよさそうなのに)サポートしていないアドレッシングモードがあります。
ロード・ストア命令
#******,x**,y********,x****,y(**,x)(**),y
lda×
ldx××××
ldy××××
sta××
stx××××××
sty××××××
 とりあえず、可能なアドレッシングモードのリストを作ってみた。
 ……機械語も載せておいたほうが良いかなぁ。でも、あんま丸転載しても……ね。
 上にある○印の全てに、違うオペコードが割り当てられています。

 前回もやりましたが、ロードはメモリ→レジスタ、ストアはレジスタ→メモリとデータ転送する命令です。
 ロードのときは、pレジスタのフラグが書き換わります。
 ・読まれた値の最上位ビット→Nフラグ
 ・読まれた値がゼロかどうか→Zフラグ
 
演算命令
 以下の命令は、#** ** **,x **** ****,x ****,y (**,x) (**),yが使えます。**,yは使えない……
●adc( ADd with Carry?)
 加算命令です。『aレジスタと、対象、cフラグ』を加算し、aレジスタに書き込みます。
 また、演算結果の8ビット目(256の位)がcフラグに入ります。
 cフラグを加算するので“フツーに”足し算するときは、cフラグをクリアしてからでないとマズイことが起きます。何故こんな仕様かといいますと、たぶん、16ビット以上の計算を考慮しているからだと思います
 計算結果の……
 ・aレジスタが0かどうか →zフラグ
 ・aレジスタの最上位ビット→nフラグ
 ・演算結果の8ビット目  →cフラグ
 ・aと対象を符号付の数と見たとき、加算結果が、-128を下回る、もしくは、127を超えるかどうか→vフラグ
●sbc( SuBstract with Carry?)
 減算命令です。aレジスタから、対象を減算し、さらに、cフラグを逆転したものを引きます。
 “フツーに”引き算したいときは、cフラグをセットしてからでないとまずいことが起きます。
 これも、16ビット以上の演算を考慮しているのだと思います。
 計算結果の……
 ・aレジスタが0かどうか →zフラグ
 ・aレジスタの最上位ビット→nフラグ
 ・aと対象を符号無しと見たとき、結果が正か0であるかどうか→cフラグ
 ・aと対象を符号付の数と見たとき、減算結果が、-128を下回る、もしくは、127を超えるかどうか→vフラグ
●and(これはそのまま)
 aレジスタと対象をand演算し、結果をaレジスタに格納します。
 ・結果0かどうか →zフラグ
 ・結果の最上位ビット→nフラグ
●ora(OR with A?)
 aレジスタと対象をor演算し、結果をaレジスタに格納します。
 ・結果が0かどうか →zフラグ
 ・結果の最上位ビット→nフラグ
●eor(ExOR?)
 aレジスタと対象をexor演算し、結果をaレジスタに格納します。
 ・結果が0かどうか →zフラグ
 ・結果の最上位ビット→nフラグ
インクリメント・デクリメント命令
 演算に含めるべき?
●inx(INcrement X)
 xに1加算します。
●dex(DEcrement X)
 xから1を引きます。
●iny●dey も同じ。
●inc,dec(INCrement,DECrement)
 inc、decはアドレッシングモードとして、** **** **,x ****,xが使えます。やっぱり**,yは使えない……
 対象に1を足したり引いたりします。なぜか、inc aとはできません。
 よって、aに1加算したいときは、adcを使うなり、いったんxレジスタにコピーしてinxして戻すなりしないとダメらしい。

 上のどの命令も以下のフラグが変わります。
 ・結果の最上位ビット→nフラグ
 ・結果が0かどうか →zフラグ
レジスタコピー命令
 txa,tya,tax,txaと、あと、txs,tsx命令があります。
 例えばtxaは、 Transport(?) X to A かなんかの略かと思います。つまり、xをaにコピーする。
 他のものも同じです。sはスタックポインタです。
 txyやtyxがないのが惜しい……(スーファミCPUにはあるんですが……)

 これら命令では、以下のフラグが変わります。
 ・結果の最上位ビット→nフラグ
 ・結果が0かどうか →zフラグ
シフト・ローテート命令
 対象のビット配置をずらします。
 アドレッシングモードは、a(none?) ** **** **,x ****,xです。やっぱりyインデックスは使えない……
●asl(A Shift Left?)
 対象のビット配列を左に1個ずらし(シフトし)ます。
 “左にはみ出た”部分がcフラグに入ります。
●lsr(Logical Shift Right?)
 対象を右にシフトします。対象は符号なしと見ます。
 “右にはみ出た”部分がcフラグに入ります。
●rol(ROtate Left?)
 aslと同じく、対象をビット配列を左に1ビット分ずらします。
 “左にはみ出た”部分がcフラグに入り、かつ、もともとcフラグに入っていた値が、対象の“右から現れ”ます。
●ror(ROtate Right?)
 lsrと同じく、対象をビット配列を右に1ビット分ずらします。
 “右にはみ出た”部分がcフラグに入り、かつ、もともとcフラグに入っていた値が、対象の“左から現れ”ます。

 以上のようにcフラグが書き換わりますが、加えて、以下のフラグが変化します。
 ・結果の最上位ビット→nフラグ
 ・結果が0かどうか →zフラグ
フラグ書き換え命令
 全て単体命令です。
 clc,cli,clv,cld,sec,sei,sed命令があります。
 最初の2文字は、CLearかSEtからきていて、残り1文字が対象フラグです。
 ファミコンCPUは2進化10進数対応していないので、プログラム開始時にまずcldして、sedすることはないかと思われます。
 sevはありませんが、必要になるシチュエーションもないでしょう……
 clcとsecは頻繁に使います。何しろ加算・減算に影響を与えますので……
比較命令
#******,x********,x****,y(**,x)(**),y
cmp
cpx×××××
cpy×××××
 cmp(CoMPare),cpx(CoMPare with X?),cpy(CoMPare with Y?)があります。
 それぞれ、aレジスタ、xレジスタ、yレジスタを比較します。
 比較の仕方は、『レジスタ − 対象』を評価し、フラグを書き換えます。
 ・評価式の7ビット目 →nフラグ
 ・評価式が0かどうか →zフラグ
 ・評価式が正かどうか →cフラグ
 この比較命令の後に、フラグを調べて分岐すれば、いろいろできるわけですな。
ビットテスト?
 bit命令は、** ****のアドレッシングモードを取れます。
 対象と、aレジスタを比較して、フラグを書き換えます。
 ・『対象 AND aレジスタ』が0かどうか→zフラグ
 ・対象の最上位   →nフラグ
 ・対象の6ビット目  →vフラグ
 あんま使い道わかんない……
スタック関係
 スタックについてはこちらを
 ファミコンのCPUでは、スタックポインタsは8ビットしかありません。したがって、スタックのサイズは256バイトです。
 また、スタック用メモリは、01**という場所に固定されています。(**がsの値)
 スタックポインタsは、積むときにマイナス、下ろすときにプラスされます。(方向に注意)
 CPUの系統によって、スタックから取り出すことをポップといったりプルといったりするそうです。
 このCPUではプルみたいです。積むのはプッシュです。
●pha(PusH A)
 aをプッシュ
●php(PusH P)
 pをプッシュ
●pla(PuLl A)
 aにプル
 ・その最上位ビット→nフラグ
 ・それが0かどうか →zフラグ
●plp(PuLl P)
 pにプル
 ・当然全てのフラグが書き換わる
ジャンプ
 jmp命令は、プログラム実行位置を指定位置に変更します。
 アドレッシングモードは、**** (****)が使えます。唯一(****)が使える命令です。
分岐命令
 フラグ次第で処理位置を変える命令です。
 bmi,bpl,bvs,bvc,beq,bne,bcs,bcc命令があります。
 最初の1文字目はBranchだと思います。残りの2文字が条件です。
 bmi : マイナス     ; nフラグセットだと分岐
 bpl : プラス       ; nフラグクリアだと分岐
 bvs : Vセット      ; vフラグセットだと分岐
 bvc : Vクリア      ; vフラグクリアだと分岐
 beq : イコール     ; zフラグセットだと分岐  ※比較命令では2つの数値が等しい→差が0
 bne : ノットイコール  ; zフラグクリアだと分岐    けど、イコールの意味と違って使われることも多い
 bcs : キャリーセット  ; cフラグセットだと分岐
 bcc : キャリークリア  ; cフラグクリアだと分岐

 前回もなんか触れましたが、だいたい-128〜127バイト程度しかジャンプできません。
 それ以上ジャンプしたいときは、逆の条件をつけてjmp命令で飛べばOK。
サブルーチン
●jsr(Jump Sub Routine?)
 指定アドレスのサブルーチンに飛びます。
 もうちょっと細かく言うと、pcに2(!)を足した後、pcの上位、pcの下位という順番でスタックにプッシュ、そして、pcを指定アドレスに書き換えます。
 なんで2を足すんでしょうか……復帰後のpcインクリメントで次の命令に行くってこと?
●rts(ReTurn from Sub routine?)
 サブルーチンから元のルーチンに復帰します。
 細かく言うと、スタックから、pcの下位、pcの上位という順番でプルして、さらにpcに1を足した場所に復帰します。
●brk(BReaK?)
 ソフトウェア割り込みをかけます。
 よくわからんです^^
 噂によると、あんまり使わないほうが良いとか言う話も……?
●rti(ReTurn from Interrupt?)
 割り込みから復帰します。
 pレジスタもスタックに積まれているので、それも戻すらしいです。
その他
●nop(No OPeration?)
 何もしない命令です。しっかり処理時間は食います(当たり前か……)
 改造するときに大変お世話になります。
 たとえば、サブルーチンコールをこれで書き換えてみて、反応を見たり……
●db(Data Byte)
●dw(Data Word)
 いや、これらはCPUの命令じゃないけど一応……
 『これはプログラムじゃなくて、データですよ』という、アセンブラ上での印。
 逆アセンブルすると出てくるのは、未定義なオペコードに対し、形式的にこれを割り当てるから。
 全てのオペコードに対して命令が割り当てられているスーファミでは出てこない。たぶん。
 逆アセンブラによっては、『undefined operation;未定義命令』とか出力することも。
オチ?
 ファミコンとは言え、コンピュータですので、結構いろんな命令があります。
 覚えるのなんて無理です^^
 けど、なんか見ているうちに覚えてきちゃうんだよなぁ……自分程度でも。
 逆アセンブルしたコードを見るとき、わからん命令があったら参考にすると良いかも。
 あんま正確な記述じゃないけど^^
 ニーモニックを見てなんとなく命令の意味が予想できれば最高ですな。そんな能力私にはありませんが……
(21)2007年2月17日 プレさ兵衛
(289)2014年12月29日 改:誤植「分枝」→「分岐」の修正
inserted by FC2 system