概要: この記事では 8ビット CPU 6502 を 使ったアセンブラプログラミングを紹介する。 「アセンブラプログラミング」とは、プログラミング言語を使わず、 CPU のネイティブ命令列を直接書くプログラミング方法である。 6502 はいまから約50年前に開発され、 ファミコンや Apple II など多くのハードウェアで利用された。 しかし、その原理は今日のコンピュータとほとんど変わっていない。 ここでは 6502 のプログラミングを通して、コンピュータの本質を学ぶ。
入力装置・出力装置・記憶装置の例を 2つずつ挙げよ。
本日使う CPU (のエミュレータ): MOS 6502
機械語 (machine language): Python や JavaScript のような現代的なプログラミング言語とは違い、 文字ではなく数値 (命令語) の列によって表現する。
6502 の機能:
CPU にある特別な変数: PC (プログラム・カウンタ)
// メモリの内容 (65536要素の配列) MEMORY = [0, 0, 0, 0, 0, ... ] // PCは現在実行する命令の位置。 PC = 0 // 以下を永久にくり返す。 while (true) { // 現在の命令を調べる。 command = MEMORY[PC] if (command == 1) { A = A + 1 // 変数 A に1を足す。 } else if (command == 2) { A = A - 1 // 変数 A から1を引く。 } else if ... } // 次の命令を実行。 PC = PC + 1 }
レジスタ (register): CPU における変数のこと。
6502 に装備されているレジスタ:
名前 | 大きさ | 機能 |
---|---|---|
PC | 16ビット | これから実行する命令のメモリ上の位置。 |
Aレジスタ | 8ビット | 計算のために使う。 |
Xレジスタ | 8ビット | メモリ上の位置を指すために使う。(後述) |
Yレジスタ | 8ビット | (今回は使わない) |
Zフラグ | 1ビット | 計算結果がゼロになったときに 1 になる。(後述) |
Cフラグ | 1ビット | 計算結果が桁あふれしたときに 1 になる。(後述) |
10進数 | 2進数 | 16進数 |
---|---|---|
0 | 0000 | 0 |
1 | 0001 | 1 |
2 | 0010 | 2 |
3 | 0011 | 3 |
4 | 0100 | 4 |
5 | 0101 | 5 |
6 | 0110 | 6 |
7 | 0111 | 7 |
8 | 1000 | 8 |
9 | 1001 | 9 |
10 | 1010 | a / A |
11 | 1011 | b / B |
12 | 1100 | c / C |
13 | 1101 | d / D |
14 | 1110 | e / E |
15 | 1111 | f / F |
以下の 16進数を 2進数に変換せよ:
以下の 2進数を 16進数に変換せよ:
http://visual6502.org/ は本物の 6502 の電子回路の動きをブラウザ上で仮想的に再現するエミュレータである。
最初のプログラムとして、メモリ上のある位置に数値を格納する処理をやってみる。
0000: A9 01 ; LDA #$01 - Aレジスタに $01 を格納。 0002: 95 10 ; STA $10 - Aレジスタの値をメモリの $10 番地に格納。 0004: 00 ; BRK - CPUの停止。
LDA #$01
」など … アセンブリ表記。
命令語を人間がわかりやすいように書いたもの。
$XX
は 16進数表記。
プログラムは、メモリ上の 16進数が書かれている部分を ダブルクリックして直接入力する。
命令語 (16進) | バイト数 | アセンブリ表記 | 機能 |
---|---|---|---|
A9 XX |
2 | LDA #$XX |
Aレジスタに値 16進数 XX を記録する。 |
95 XX |
2 | STA $XX |
Aレジスタの値をメモリの XX 番地に記録する。 |
00 |
1 | BRK |
制御装置を停止する。 |
上のプログラムは、JavaScript でいえば以下のような処理に等しい:
A = 1 // LDA #$01 MEMORY[0x10] = A // STA $10
(JavaScript では、0x64
や 0b1111
などと書けば
16進数・2進数 → 10進数に変換できる)
上のプログラム1を Visual 6502 エミュレータ上で実際に実行せよ。
次に足し算命令 ADC
命令 とジャンプ命令 JMP
を使ってみる。
0000: A9 01 0002: 95 10 0004: 69 02 ; ADC #$02 - Aレジスタに $02 を足す。 0006: 4C 02 00 ; JMP $0002 - $0002番地の命令にジャンプする。
「ジャンプ命令」を命令を実行すると、CPU は PC レジスタの値を書き換える。 つまり、指定されたアドレスの命令から実行できる。
命令語 (16進) | バイト数 | アセンブリ表記 | 機能 |
---|---|---|---|
69 XX |
2 | ADC #$XX |
Aレジスタの値に 16進数で XX を加える。 |
4C PP QQ |
3 | JMP $QQPP |
16進数で QQPP 番地から実行を開始する。 アドレスの上2桁、下2桁が逆になっていることに注意。 (リトルエンディアン) |
JavaScript でいえば、以下のような処理に(ほぼ)等しい:
A = 1 // LDA #$01 while (true) { MEMORY[0x10] = A // STA $10 A = A + 2 // ADC #$02 }
上のプログラム2をエミュレータ上で実際に実行せよ。 Aレジスタの値が FF を超えるとどうなるか?
6502 には数百種類の命令がある:
いちいち命令語の数値を調べるのは面倒くさいので、 ここでは アセンブラ (assembler) というものを使う。
LDA #$01 STA $0200
アセンブラではさらに、以下のような表記の決まりがある:
#$01
… $01
という値そのもの。
$0200
… $0200
という「メモリ上のアドレスに入っている」値。
このエミュレータでは、
メモリ上の $0200
〜 $05ff
の範囲が
画面の各ピクセルと連結している。
ここに値を格納すると、それが実際に画面に表示される。
つまり、ここではメモリへの書き込みが出力装置も兼ねている。
コンピュータにとって、画面やハードディスク (記憶装置) は、
どれもただの巨大な配列でしかない。
上のプログラム3をエミュレータ上で実際に実行せよ。
アセンブラを使うと、プログラム中のアドレスとしてラベルを使うことができ、 命令語のバイト数を考える必要がない。
LDA #$01 loop: ; ラベル "loop" をここに設定。 STA $0200 ADC #$02 JMP loop ; "loop" のアドレスにジャンプする。
注意:
ラベル (loop:
) 自体はただプログラム中の位置を表すもので、実際の命令ではない。
上のプログラム4をエミュレータ上で実際に実行せよ。
差分アドレッシングという機能を使うと、 メモリ上の可変の位置のデータを読み書きできる。
LDA #$01 LDX #$00 ; Xレジスタに $00 を格納。 loop: STA $0200,X ; Aレジスタの値を ($0200+X) の位置に格納。 ADC #$02 INX ; Xレジスタの値を 1だけ増やす。 JMP loop
命令語 | 機能 |
---|---|
LDX #$XX |
Xレジスタに値 $XX を記録する。 |
STA $ZZZZ,X |
Aレジスタの値を ($ZZZZ+X) の位置に格納する。 (差分アドレッシング) |
INX |
Xレジスタの値を 1だけ増やす。 |
以下、JavaScript 相当の処理:
A = 1 // LDA #$01 X = 0 // LDX #$00 while (true) { MEMORY[0x0200+X] = A // STA $0200,X A = A + 2 // ADC #$02 X = X + 1 // INX }
上のプログラム5をエミュレータ上で実際に実行せよ。 なぜ画面の一部しか更新されないのか?
「画面の特定の場所のみ、色を変える」にはどうするか?
LDX #$00 loop: CPX #$10 ; Xレジスタの値を $10 と比較。 BEQ on ; 等しければ、on に分岐する。 JMP off ; 等しくなければ、off に分岐する。 on: LDA #$01 JMP put off: LDA #$02 put: STA $0200,X INX JMP loop
6502 では、比較・演算命令
(ADC
, CPX
, INX
など) の結果によって
内部のフラグ (flag) が変化する。
命令語 | 機能 |
---|---|
CPX #$XX |
Xレジスタの値を $XX と比較し、
等しければ Zフラグを 1 にする。 |
BEQ ラベル |
Zフラグが 1 ならば、 指定されたラベルに分岐する。 |
CPX
命令は実際には何も計算してないように見えるが、
内部的には 2つの数の引き算をおこなっている。これによって、
2つの数が等しいときに結果が 0 になり、結果として Zフラグが 1 になる。
JavaScript 相当の処理:
X = 0 // LDX #$00 while (true) { if (X == 0x10) { // CPX #$10, BEQ on A = 1 // LDA #$01 } else { A = 2 // LDA #$01 } MEMORY[0x0200+X] = A // STA $0200,X X = X + 1 // INX }
上の条件分岐は、以下のようにも書ける:
LDX #$00
loop:
LDA #$02
CPX #$10
BNE put ; 等しくなければ、put に分岐する。
LDA #$01
put:
STA $0200,X
INX
JMP loop
命令語 | 機能 |
---|---|
BNE ラベル |
Zフラグが 0 ならば、 指定されたラベルに分岐する。 |
MOS 6502 ではほとんどの計算は 8ビットでしかできないが、
工夫することで 16ビットの計算が可能である。じつは "ADC
" 命令は
与えられた数に加えて C フラグの値も加える ようにできており、
これを使って 8ビットの数を 2回に分けて計算する。
CLC ; Cフラグをクリアする。 LDA $30 ; メモリ$30番地の値を Aレジスタに読み込む。 ADC #$01 ; A = A + 1 + 0 STA $30 ; Aレジスタの値をメモリ $30番地に書き込む。 LDA $31 ; メモリ$31番地の値を Aレジスタに読み込む。 ADC #$00 ; A = A + 0 + C STA $31 ; Aレジスタの値をメモリ $31番地に書き込む。
命令語 | 機能 |
---|---|
CLC |
Cフラグの値を 0 にする。 |
以上のテクニックと以下の「間接差分アドレッシング」を組み合わせると、 256バイト (=8ビット) 以上のメモリ領域にアクセスできる。 つまり、画面のより広い領域に描画できるようになる。
命令語 | バイト数 | 機能 |
---|---|---|
STA ($ZZ,X) |
2 (命令 1 + アドレス 1) | 間接差分アドレッシング。
|
LDA #$00 STA $30 LDA #$02 STA $31 loop: LDX #$00 LDA #$01 STA ($30,X) ; A をメモリ ($30+X) 番地に書かれている番地に書き込む。??? ; 16ビットの加算をおこなう ...JMP loop
上のプログラム 6. を完成させ、 画面全体を塗りつぶすようにせよ。
ファミリーコンピュータ (Nintendo Entertainment System、ファミコン) は 1983年に任天堂から発売されたゲーム機である。
ファミコンのハードウェアはかなり複雑で、 画面表示には CPU が PPU と通信する必要がある。 また、初期のゲームは最大 32KB までに制限されていた。 「スーパーマリオブラザーズ」もプログラム・ グラフィックス・音楽等込みで32KB の領域に収まっている。
アドレス範囲 | 機能 |
---|---|
$0000 〜 $07FF | メインメモリ |
$0800 〜 $1FFF | 未使用領域 |
$2000 〜 $2007 | PPU 制御レジスタ |
$2008 〜 $3FFF | 未使用領域 |
$4000 〜 $401F | APU レジスタ等 |
$4020 〜 $5FFF | カートリッジ拡張ROM |
$6000 〜 $7FFF | SRAM |
$8000 〜 $FFFF | プログラム用ROM |
現代の 最新鋭の演算装置でも、 基本的にやっていることは変わらない。ただ量は増えている。
1975年 | 2021年 | |
---|---|---|
レジスタの数 | 4 | 40 |
計算できるビット数 | 8 | 64 |
メモリの容量 | 65,536 | 34,359,738,368 |
1秒間の命令実行数 | 1,000,000 | 1,000,000,000 |
プログラムの大きさ | 10,000 | 10,000,000,000 |
他にも、現代のPC (サーバ、スマートフォン等) は以下のような違いがある:
現在、コンパイルされた「バイナリ」や「Docker イメージ」などと呼ばれているものの実体は すべてこれらのCPU用に書かれた機械語の命令列である。
現在のコンピュータでは、一般人が上で示したような プログラムを書く必要はない。文字表示やファイル処理など非常に基本的な部分は、 「オペレーティングシステム (OS, 基本ソフトウェア)」として 最初からPCと一緒に提供されているためである。
また、OS は多くの仮想化処理を実現している。
結局のところ、コンピュータはみな非常に単純な原理で動いている。 これをソフトウェアの力で複雑な処理をしているように見せかけているのが、 現代のコンピュータである。