📚

ファミコンエミュレーターのアドレッシングモード

12 min read

ファミコンの CPU (6502) にはアドレッシングモードという機能があります。アドレッシングモードは 6502 に限らない一般的な機能ですが、低レベルプログラミング初心者には難所の一つになります。

アドレッシングモードは意味を理解しなくても仕様さえわかれば実装できてしまいます。ほとんどのモードは数行で済むと思われるので、公開されている実装を書き写すだけでも十分です。あるいは、納得できないけど実装しないと先に進めないので仕方なく書き写した方もいるのではないでしょうか。アドレッシングモードを解説した記事はたくさん見つかるんですが、存在理由や用途まで書かれている記事はあまり見かけないんですよね...私は納得できないと正しく実装できない性質なので調べるのに時間がかかりました。私と同じようにアドレッシングモードで引っかかっている方の手助けになれば幸いです。

ファミコンエミュレーターを実装するだけならネットの情報だけで十分ですが、さらに理解を深めたい方は 6502とAppleⅡシステムROMの秘密 がお勧めです。アドレッシングモードと命令についての解説があります。令和の時代に 6502 の解説本とは一体誰得なのか...?

アドレッシングモードとは

アドレッシングモードとは、 CPU の命令が操作対象とする値を算出する方法です。といっても、一言では何のことだかわかりませんよね。私を含めアドレッシングモードがわからないと頭を抱える人は、アドレッシングモードの目的がわからないから理解が進まないのではないでしょうか。

6502 の大半の命令はメモリアドレスの情報を必要とします。たとえば LDA 命令はメモリの値をアキュムレーター (A) レジスタにコピーします (LDA = Load Accumelator from Memory) 。読み出すメモリのアドレスを算出する方法がアドレッシングモードです。 6502 のアドレッシングモードは 13 種類あります。数は多いですが、ほとんどは基準となるアドレスとプラスアルファの組み合わせです (まとめた表を最後に載せます) 。

6502 の命令はすべて命令コードとアドレッシングモードの組み合わせです。命令コードは 1 バイト、アドレッシングモードは最長 2 バイトなので、命令は最長 3 バイトです。命令一つとっても複数のアドレッシングモードに対応しなければならないので膨大な量を実装しなければならないように感じますが、どの命令もアドレスが決まればやることは同じなので、実際に実装すべき処理は見た目の印象ほど多くありません。たとえば先の LDA 命令は 8 種類のアドレッシングモードに対応しなければいけませんが、うち 7 種類はアドレス計算後の処理は同じです。アドレッシングモードは命令と独立した機能ですから、アドレッシングモードを実装できればあとは命令の実装に集中できます。というか命令はアドレッシングモードなしでは実装できないのでどのみち最初に実装しなければならないのですが。

アドレッシングモードの目的

6502 のレジスタは 6 つありますが、そのうち汎用レジスタはアキュムレーター (以下 A) のみです。任意の目的に使えるレジスタが 1 つしかないので、算術命令などの 2 つの値を扱う命令ではレジスタのみで処理を完結できません。そのためメモリを組み合わせて使います。たとえば論理積命令 (AND) であれば、指定したアドレスに格納されている値と A の値の論理積を計算し、結果を再び A にセットします。ですので、メモリアクセスはほぼ常に発生します。

6502 のメモリアドレスは 16 ビットです。完全なメモリアドレスを指定する命令は 3 バイトになります。「たかだか 3 バイトか」と思われるかもしれませんが、ファミコンだとわずかなバイト数の違いがばかになりません。普通のプログラムでは、命令を格納する領域は 0x8000 から 0xFFFF までの 32KB しかありません。 3 バイトの命令なら詰め込めるのは 約 11,000 です。しかし、メモリアドレスを 1 バイトで表す方法があれば数千バイトを節約できる可能性があります。詰め込む命令を増やせればそれだけゲームの幅が広がりますし、メモリアクセスが 1 回減る (8 ビット CPU なので 1 バイトずつ読み出します) ので負荷も減ります。

また、絶対アドレスだけでプログラムを組めるはずもありません。ジャンプ命令で相対アドレスを求めたり、連続した領域に順にアクセスするなどの目的で動的にアドレスを加減しなければならない場合もあります。

そこで活躍するのがアドレッシングモードです。アドレッシングモードには 1 バイトの数値からアドレスを算出したり、レジスタやメモリを利用して動的にアドレスを算出するモードがあります。多様なアドレッシングモードがあるおかげで、アドレス算出のための命令を節約できるようになります。アドレッシングモードがなければアドレス計算だけでプログラムが埋まってしまうでしょう。

アドレッシングモードの種類

それでは 13 種類のアドレッシングモードを順に紹介していきます。なお、具体例はあえて出しませんので自分で考えてみてください。本でも書くことになったら具体例も載せようと思います。

各アドレッシングモードの仕様は次のテーブルで表します。

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
  • 長さ: 命令コードを含む命令のバイト数
  • B1: 命令コードの次のバイト
  • B2: B1 の次のバイト
  • レジスタ: 加算するレジスタ
  • 範囲: B1 と B2 の値の範囲
  • 位置: B1 と B2 で表されるアドレスの位置 (絶対アドレス、相対アドレス、間接アドレス)
  • メモリ: メモリアクセスの有無
  • 加算: レジスタの加算方法

Absolute

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
3 下位アドレス 上位アドレス なし 0x0000-0xFFFF 絶対 なし なし

アドレッシングモードのうち、最もわかりやすいのが Absolute です。絶対アドレスの 16 ビットを「命令コード、下位アドレス (8 ビット) 、上位アドレス (8 ビット) 」の順に並べるだけです。 「下位、上位」の順 なので注意してください。

当然、 Absolute で算出できるアドレスは上位アドレスと下位アドレスを組み合わせた数値です。アドレッシングモードで算出したアドレスは「 実効アドレス (effective address) 」と呼びます。 Absolute ではメモリアクセスもレジスタの加算もありませんので簡単です。

ただし、アドレスが 0x0000-0x00FF の範囲に収まる場合は Absolute を使う必要がありません。使えない、使ってはいけないという意味ではなく、次に紹介する Zero Page が適役です。

Zero Page

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
2 下位 なし なし 0x00-0xFF 絶対 なし なし

6502 で言う「ページ」とは、 256 バイト単位のメモリ領域を指します。 0 ページ目は 0x0000-0x00FF 、 1 ページ目は 0x0100-0x01FF 、 2 ページ目は 0x0200-0x02FF を意味します。 Zero Page は 0 ページ目のアドレスを算出するアドレッシングモードです。以下、 0 ページ目をゼロページと表記します。

実効アドレスはゼロページを示すので、 上位アドレスは 0x00 で固定されます。 B1 が 0xFF であれば実効アドレスは 0x00FF になります。上位アドレスの指定が不要になるので、命令は「命令コード、下位アドレス」の 2 バイトで済み、 Absolute と比べて負荷も下がります。このアドレッシングモードのおかげで、ゼロページを 256 本の汎用レジスタとして気軽に使うことができます。プログラムのパフォーマンスは Zero Page の使い方にかかっていると言えます (エミュレーターを実装するだけならあまり関係ありません。現在のコンピュータは桁違いに高速になってますし) 。

Relative

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
2 オフセット なし PC 0x80-0x7F (-128, 127) 相対 なし 符号あり (16 ビット)

Relative はプログラムカウンタレジスタ (PC) からの相対的な位置のアドレスを求めるモードです。条件分岐命令で使われます。

実効アドレスの算出は、このモードの実行時の PC と B1 が加算されます。このとき B1 は 符号つき 16 ビット整数として加算されます。 したがって PC に -128 (0x80) から 127 (0x7F) を加算した値が実効アドレスになります。

なお、オフセットを 16 ビットで指定するモードはありません。

Indexed Absolute

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
3 下位アドレス 上位アドレス X または Y 0x0000-0xFFFF 絶対 なし 符号なし (16 ビット)

ここから少し複雑になってきます。モード名の "Indexed" は インデックスレジスタ (X または Y のどちらか) の加算 を意味します。 X を加算するモードと Y を加算するモードで 2 つの Indexed Absolute モードがあります。

Indexed Absolute はその名前通り Absolute と同じく絶対アドレスを扱いますが、その絶対アドレスに X または Y を加算した値が実効アドレスになります。この加算は符号なし 16 ビット整数同士の演算となり、下位アドレスの繰り上がりは上位アドレスに反映されます。つまり、 X の内容が 1 のとき 0x00FF に加算した結果は 0x0100 になります。演算結果が 16 ビットを超えた分は切り捨てられます。キャリーとオーバーフローが発生した場合は無視され、両フラグは変更されません。複雑な演算に見えますが、ステータスフラグを考えずに済むので実際は単純です。

なぜレジスタを加算するようなややこしいモードがあるのかと言うと、特定のメモリアドレスを基準とした任意のメモリアドレスにアクセスしたい場合があるからです。たとえば、配列の要素に順にアクセスするケースがあります (厳密に言えば配列という型はなく、連続するメモリ領域の意味で使っています) 。 X をアクセスしたい要素のインデックスとすると、 X に 1 を加算して更新すれば次の要素にアクセスできます。

X と Y で演算方法に違いはありませんが、命令によってはどちらか一方のモードしか対応していません。たとえば ASL, LSR, ROL, ROR などの命令では X のモードに対応していますが、 Y のモードには対応していません。どうも中心となるのは X で、 Y は X の補助的な役割を与えられているようです。エミュレーターを実装する分には深く考えなくてもいいでしょう。

Indexed Absolute に対応した命令は Absolute にも対応しています。 Indexed Absolute のみに対応して Absolute に対応していない命令はありません。

Indexed Zero Page

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
2 下位アドレス なし X または Y 0x00-0xFF 絶対 なし 符号なし (8 ビット)

Indexed Zero Page は Zero Page の Indexed 版です。指定されたアドレスに X または Y を加算した値を実効アドレスとします。こちらも X と Y の 2 つのモードがあります。

Indexed Absolute と異なり、 X, Y は符号なし 8 ビット整数として加算されます。 8 ビットを超える桁あふれは切り捨てられます。 上位アドレスは変化しない (0x00 のまま) ので注意してください。たとえば B1 が 0xFF で X が 0x01 の場合、算出される実効アドレスは 0x0000 になります。ゼロページにアクセスするためのモードですから、ページが進んでしまうと意味がありません。

Indirect

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
3 下位アドレス 上位アドレス なし 0x0000-0xFFFF 間接 あり 符号なし (16 ビット)

レジスタを加算するモードの次は、メモリが発生するモードです。メモリが発生するモードは他にもいくつかありますが、このモードが使われるのは JMP 命令のみです。実効アドレスを間接的に算出するので "Indirect" ですが、一般的に "Indirect" が必ずメモリを示すとは限りません。 B1 + B2 で表されるアドレスが絶対アドレスなので、 Indirect Absolute または Absolute Indirect とも呼ばれます。

Indirect は、 B1 と B2 で指定されたアドレスと、そのアドレスに 1 を加算したアドレス (つまり隣のアドレス) にあるメモリの値を読み出します。加算の結果が 16 ビットを超えてもキャリーフラグは変化しません。実効アドレスは前者の値を下位アドレス、後者の値を上位アドレスとします。さすがに文章だとわかりにくいので、擬似コードで例を示します。

m = B2<<8 | B1
lower = read(m)
upper = read(m+1) // キャリーは無視される
addr = upper<<8 | lower

たとえば B1 = 0x01 、 B2 = 0x00 であれば、最初に使うアドレス ('a とします) は 0x0001 です。まず、メモリの 0x0001 にある値を読み出します。その値を 0x34 とします ('b) 。次に、 'a の次のアドレスである 0x0002 のメモリの位置から値を読み出します。その値を 0x12 とします ('c) 。 'b を下位アドレス、 'c を上位アドレスとし、最終的に 0x1234 を実効アドレスとします。

要は、 事前にメモリに書き込んでおいたアドレスを読み出す モードです。 JMP 命令を使う際はジャンプ先のアドレスをあらかじめメモリに書き込んでおき、命令の実行時にそのアドレスを読み出してジャンプします。ジャンプ先を動的に決定します。

アドレスを動的に決定するだけであれば、前節で説明した Indexed 系や Relative でもできます。しかし、それらのモードは特定のアドレスを基準とした 8 ビット整数分の前後のアドレスしか求められません。プログラムの先頭 0x8000 か最後尾 0xFFFF など、遠く離れたアドレスのいずれかにジャンプしたい場合は、ジャンプ先アドレスをメモリに書き込んでおく Indirect 系のモードを使う必要があります。

Indexed Indirect

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
2 下位アドレス なし X 0x00-0xFF 間接 あり 符号なし (8 ビット)

Indexed Indirect はレジスタとメモリを併用する最も複雑なモードです。他のモードと比べると限定的な状況で使われます。

Indirect と同様に、 Indexed Indirect はメモリから読み出したアドレスを実効アドレスとします。読み出し元のアドレスは上位アドレスを 0x00 とし、 B1 に X を加算した値が下位アドレスになります (他の Indexed 系のモードと異なり Y は使われません) 。 X は符号なし 8 ビット整数として加算され、繰り上がりは無視されるので上位アドレスは常に 0x00 です。 読み出し元のアドレスは必ずゼロページ内 になります。モード名には含まれていませんが、 Indexed Zero Page の派生としてとらえても問題ありません。実際 Indexed Zero Page Indirect と書かれている記事もあります。

Indirect と同様に、メモリから読み出すアドレスは 16 ビットです。読み出し元アドレスのメモリの値を下位アドレス、読み出し元アドレスに 1 を加算したアドレスのメモリの値を上位アドレスとしたアドレスが実効アドレスになります。文章で説明するとややこしいですが、 名前の通り Indexed してから Indirect する だけなので、順を追えば難しくないはずです。次節で説明する Indirect Indexed と比べたとき、 Indexed Indirect は最初に X を加算するので Pre-Indexed Indirect とも呼ばれます。完全な名前をつけるなら Pre-X-Indexed Zero Page Indirect でしょうか。

以下に擬似コードで手順を示します。

// ゼロページ。先に X を加算する
m = B1 + X
lower = read(m)
upper = read(m+1)

// 16 ビットアドレス
addr = upper<<8 | lower

このややこしい Pre-X-Indexed Zero Page Indirect が何のために使われるのかというと、たとえば「ゼロページに書き込まれている 16 ビットアドレスの配列の要素へのアクセス」が考えられます。 B1 で配列のアドレスを指定し、 X で要素の位置を指定すると、 16 ビット分のアドレスを読み出すことができます。 Indexed Indirect の挙動は他のいくつかの命令とモードを組み合わせれば実現できますが、たぶん頻出するパターンなので 1 つのモードにまとめて最適化されています。

Indirect Indexed

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
2 下位アドレス なし Y 0x00-0xFF 間接 あり 符号なし (16 ビット)

Indirect Indexed は、前節の Indexed Indirect の順序とレジスタが入れ替わったモードです。こちらも読み出し元アドレスはゼロページになります。

まず、読み出し元アドレスの上位アドレスを 0x00 (ゼロページ) 、 B1 を下位アドレスとします。このアドレスと、このアドレスに 1 を加算したアドレスのメモリを読み出し、それぞれを下位アドレス、上位アドレスとして 16 ビットのアドレスを用意します。そのアドレスに Y を加算した値が実効アドレスになります (キャリーは無視されます) 。 Indirect 後に Y を加算する ので Post-Indexed Indirect とも呼ばれます。完全な名前をつけるなら Post-Y-Indexed Zero Page Indirect でしょうか。

以下に擬似コードで手順を示します。

// ゼロページ
lower = read(B1)
upper = read(B1+1)

// 16 ビットアドレス。最後に Y を加算する
addr = (upper<<8 | lower) + Y

このモードの用途の説明も複雑になってしまいますが、たとえば「ある配列のアドレスがゼロページに書き込まれており、その配列の要素へアクセスする」が考えられます。 Indexed Indirect がゼロページにある 16 ビットアドレスの配列への直接的なアクセスで、 Indirect Indexed はゼロページ外にある 8 ビット整数の配列へのポインタ経由のアクセスとも言えます。説明を増やせば増やすほどドツボにはまりそうなのでこの辺でやめておきます。

Accumulator

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
1 なし なし A なし なし なし なし

Accumulator と次に説明する Immediate は、実効アドレスが計算用のデータとして使われる単純なモードです。 Accumulator はアキュムレーターレジスタ (A) の値を実効アドレスとします。レジスタの加算もメモリもないので簡単ですね。

Accumulator は A のみを使う命令で使われます。 A の値をビットシフトまたはビットローテートする ASL, LSR, ROL, ROR 命令です。

Immediate

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
2 即値 なし なし 0x00-0xFF なし なし なし

Immediate は B1 を直接計算用のデータとして使います。 B1 が実効アドレスになります。

実効アドレスだけを見れば Absolute と似ていますが、 Immediate を使う命令はメモリを行いません。たとえば LDX 命令の場合、 Absolute では実効アドレスのメモリの値を X に代入しますが、 Immediate では B1 の値をそのまま X に代入します。

Implied

長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
1 なし なし なし なし なし なし なし

最後に紹介する Implied は何もしないモードです。実効アドレスはありません。フラグをクリア・セットするだけとか、 2 つのレジスタの値を入れ替えるだけなど、実効アドレスを必要としない命令のモードが Implied になります。

一覧表

  • 略称: 仕様書などで用いられる略称です。
  • 文法: アセンブリの文法です。 "$" は 16 進数を表します。数値は例です。
モード 略称 文法 長さ B1 B2 レジスタ 範囲 位置 メモリ 加算
Implied impl なし 1 なし なし なし なし なし なし なし
Immediate # #$44 2 即値 なし なし 0x00-0xFF なし なし なし
Accumulator A A 1 なし なし A なし なし なし なし
Absolute abs $4400 3 下位アドレス 上位アドレス なし 0x0000-0xFFFF 絶対 なし なし
X-Indexed Absolute abs,X $4400,X 3 下位アドレス 上位アドレス X 0x0000-0xFFFF 絶対 なし 符号なし (16 ビット)
Y-Indexed Absolute abs,Y $4400,Y 3 下位アドレス 上位アドレス Y 0x0000-0xFFFF 絶対 なし 符号なし (16 ビット)
Zero Page zpg $44 2 下位 なし なし 0x00-0xFF 絶対 なし なし
X-Indexed Zero Page zpg,X $44,X 2 下位アドレス なし X 0x00-0xFF 絶対 なし 符号なし (8 ビット)
Y-Indexed Zero Page zpg,Y $44,Y 2 下位アドレス なし Y 0x00-0xFF 絶対 なし 符号なし (8 ビット)
Relative rel $44 2 オフセット なし PC 0x80-0x7F (-128, 127) 相対 なし 符号あり (16 ビット)
Indirect ind ($5597) 3 下位アドレス 上位アドレス なし 0x0000-0xFFFF 間接 あり 符号なし (16 ビット)
Indexed Indirect X,ind ($44,X) 2 下位アドレス なし X 0x00-0xFF 間接 あり 符号なし (8 ビット)
Indirect Indexed ind,Y ($44),Y 2 下位アドレス なし Y 0x00-0xFF 間接 あり 符号なし (16 ビット)

まとめ

HDD が壊れてローカルのデータがすべて飛んでしまいました。新しい HDD を買わないといけないのでサポートお待ちしています!

この記事に贈られたバッジ

Discussion

ログインするとコメントできます