とほほの8ビットアセンブラ入門

目次

はじめに

JavaScript, PHP, Ruby, Python, Rust, Scala, Go... などいろいろな高級言語がありますが、これらはすべて 機械語(マシン語) で実行されます。Rust, Go などのコンパイル型言語はプログラムが機械語に翻訳(コンパイル)されて実行されます。PHP, Ruby, Python などのインタープリタ型言語はインタープリタがプログラムを実行しますが、インタープリタ自体が機械語に翻訳(コンパイル)されています。

下記が機械語のサンプルです。要は16進数(コンピュータ的には2進数)の羅列です。

1F 0F 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 06
02 02 06 03 E8 38 18 00 32 00 32 01 06 01 00 03
00 02 05 03 00 12 02 12 03 15 01 02 27 1F 36 01 
36 00 39 00 ...

16進数の機械語をいきなりプログラミングできるひとは少ない(昔は居ましたけどね)ので、下記の MOV ような命令(ニーモニック)を用いた アセンブリ言語 でプログラムを記述し、これを機械語に翻訳(アセンブル)して実行します。

    JMP start            ; Jump to start

hello:
    DB "Hello World!"    ; Variable
    DB 0                 ; String terminator

start:
    MOV C, hello         ; Point to var 
    MOV D, 232           ; Point to output
    CALL print
    HLT  

アセンブラの命令や文法は対象とするCPUによって異なります。今回は最もシンプルな8ビットアセンブラを題材に、コンピュータの基本的な動作の仕組み、機械語、ニーモニック、アセンブリ言語、アセンブラの感触を説明してみたいと思います。

Simple 8-bit Assembler Simulator

カリフォルニア在住の Marco Schweighauser さんが作成された、8ビットアセンブラシミュレーターです。8ビットCPUの代表格である 8080/Z80 CPU よりもさらにシンプルな機能です。

スペックは下記の通り。

画面を表示すると、左側にプログラム(Code)、右側にアウトプット確認部(Output)、レジスタ表示、メモリ表示、ラベル表示があります。[Run] ボタンを押すとプログラムが実行されて、Output 欄に Hello world! が表示されます。[Step] を押すと1ステップずつ実行します。

レジスタ

レジスタ は高級言語で言うところの変数に相当します。ただし、Simple 8-bit Assembler Simulator では汎用的に使用できるのは A, B, C, D の4つしかありません。

種別名称ビット数説明
汎用レジスタA8ビットAレジスタ。別名アキュムレータ。
B8ビットBレジスタ。ベースと呼ばれることも。
C8ビットCレジスタ。カウンターとして使われることが多い。
D8ビットDレジスタ。データを示す値として使われることが多い。
特殊レジスタIP8ビットインストラクションポインタ。現在、プログラムの何番地を実行しているかを示す。
SP8ビットスタックポインタ。現在、スタックデータが何番地まで格納されているかを示す。

フラグ

Simple 8-bit Assembler Simulator では下記のフラグが実装されています。

フラグ名ビット数説明
Z1ビットゼロフラグ。計算結果がゼロの時に TRUE(1) となる。
C1ビットキャリーフラグ。200 + 200 など計算結果が 256 の範囲をオーバーした時に TRUE(1) となる。
F1ビットフォールトフラグ。0 で除算したなどのエラーが発生した時に TRUE(1) となる。

メモリ構成

Simple 8-bit Assembler Simulator は256バイトのメモリを有します。

    00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
---------------------------------------------------
00: 1F 0F 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 06
10: 02 02 06 03 E8 38 18 00 32 00 32 01 06 01 00 03
20: 00 02 05 03 00 12 02 12 03 15 01 02 27 1F 36 01
30: 36 00 39 00 00 00 00 00 00 00 00 00 00 00 00 00
40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
D0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
E0: 00 00 00 00 00 00 00 17 48 65 6C 6C 6F 20 57 6F
F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

データ領域 (02-0E)

02-0E番地はデータ領域です。固定値が格納されます。

プログラム領域 (00-01, 0F-32)

00-01番地および 0F-32番地はプログラム領域です。プログラムを機械語に翻訳したものが格納されます。

スタック領域 (E5-E7)

E4-E7番地はスタック領域です。データをひとつずつ格納(Push)したり、取り出したり(Pop)します。最後に Push したものが最初に Pop されます。スタック領域は E7 番地からはじまり、番地の若い方向に向かって伸びていきます。スタック領域が増えて、プログラム領域や後述のヒープ領域を壊す事態になるとスタックオーバーフローエラーが発生します。

アウトプット領域 (E8-FF)

E8-FF番地はアウトプット領域です。CPU や実装によって様々ですが、例えば関数電卓では E8-FF番地の値が電卓のディスプレイに表示されるように設定されていたりします。

ヒープ領域 (33-??)

今回は使用していませんが、プログラム領域とスタック領域の間にヒープ領域があります。動的に確保されるデータなどが格納されます。

アセンブリ言語

プログラムは下記のような命令(ニーモニック)を用いたアセンブリ言語で記述されています。

; Simple example
; Writes Hello World to the output

    JMP start
       
hello:
    DB "Hello World!" ; Variable
    DB 0              ; String terminator

start:
    MOV C, hello      ; Point to var 
    MOV D, 232        ; Point to output
    CALL print
    HLT               ; Stop execution

print:                ; print(C:*from, D:*to)
    PUSH A
    PUSH B
    MOV B, 0
.loop:
    MOV A, [C]        ; Get char from var
    MOV [D], A        ; Write to output
    INC C
    INC D  
    CMP B, [C]        ; Check if end
    JNZ .loop         ; jump if not

    POP B
    POP A
    RET

上記のコードを今っぽく表現すると下記の様になります。

var A, B, C, D             // 使用できる変数は A~D の4個のみ
start()                    // start()にジャンプ

const hello = "Hello World!\0"   // データ定義
const output = 232               // 出力領域の番地(固定値)

start() {
    C = &hello             // Cに転送元(helloのアドレス)を格納
    D = output             // Dに転送先(output)を格納
    print()                // print()サブルーチンを呼び出し
    exit()                 // プログラム終了
}

// 文字列(終端0まで)を転送するサブルーチン
//   C: 転送元アドレス
//   D: 転送先アドレス
//   A, Bの値は維持される
print() {
    push(A)                // Aレジスタを壊さないように退避
    push(B)                // Bレジスタを壊さないように退避
    B = 0                  // Bに文字列の終端を示す0を代入
loop:
    A = *C                 // AにC番地の中身を格納
    *D = A                 // D番地の中身にAを格納
    C++                    // Cをインクリメント
    D++                    // Dをインクリメント
    if (B != *C) goto loop // Cの中身が0でないならloop:に戻る
    B = pop()              // 退避していたBレジスタを復元
    A = pop()              // 退避していたAレジスタを復元
    return                 // 呼び出し元に復帰
}

ニーモニック

アセンブリ言語 で使用される下記の MOV のような命令を ニーモニック と呼びます。下記は、Cレジスタが示す番地の中身を Aレジスタに格納(MOVe)するという意味を持ちます。

    MOV A, [C]

ニーモニックには下記などがあります。

MOV x, y      // y を x に代入する
INC x         // x をインクリメントする
JMP x         // x番地にジャンプする
CALL x        // x番地のサブルーチンを呼び出す

アセンブル

ニーモニックを下記のような変換表で16進数に変換することを アセンブル と呼びます。

Mnemonics        Code       Description
--------------------------------------------------------------
MOV r1, r2       01 r1 r2   MOVe。r2レジスタの値をr1レジスタに格納
MOV r1, [r2]     03 r1 r2   MOVe。r2レジスタが示す番地の内容をr1レジスタに格納
MOV [r1], r2     05 r1 r2   MOVe。r2レジスタの値をr1レジスタが示す番地に格納
MOV r1, nn       06 r1 nn   MOVe。固定値nnをr1レジスタに格納
MOV [r1], nn     08 r1 nn   MOVe。固定値nnをr1レジスタが示す番地に格納
INC r1           12 r1      INCrement。r1レジスタを1インクリメント
HLT              00         HaLT。プログラムを終了する
JMP nn           1F nn      JuMP。nn番地にジャンプ
CALL nn          38 nn      CALL。nn番地をサブルーチンとして呼び出す。
RET              39         RETuen。サブルーチンから戻る。
PUSH r1          32 r1      PUSH。r1レジスタをスタック領域に退避。
POP r1           36 r1      POP。スタック領域から値をひとつ取り出してr1レジスタに格納。
CMP r1, [r2]     15 r1 r2   CoMPare。r1レジスタとr2レジスタが示す内容を比較
JNZ nn           27 nn      Jump if No Zero。Z=Falseであればジャンプ。(Z=False)

r1, r2 は A~D のレジスタを意味し、下記の値で示されます。

Register    Code
-------------------
A           00
B           01
C           02
D           03

nn は 0~255 の固定値で下記などで表します。

200           // 10進数
200d          // 10進数
0xA4          // 16進数
0o48          // 8進数
101b          // 2進数

上記の表から MOV A, [C] は 03 00 02 となることがわかります。

MOV A, [C]     => 03 00 02

Simple 8-bit Assembler Simulator では下記のニーモニックをサポートしているようです。

Mnemonics        Code       Description
--------------------------------------------------------------
HLT              00         HaLT。プログラムを終了する

MOV r1, r2       01 r1 r2   MOVe。r2レジスタの値をr1レジスタに格納
MOV r1, [r2]     03 r1 r2   MOVe。r2レジスタが示す番地の内容をr1レジスタに格納
MOV [r1], r2     05 r1 r2   MOVe。r2レジスタの値をr1レジスタが示す番地に格納
MOV r1, nn       06 r1 nn   MOVe。固定値nnをr1レジスタに格納
MOV [r1], nn     08 r1 nn   MOVe。固定値nnをr1レジスタが示す番地に格納

ADD r1, r2       0A r1 r2   ADD。r2レジスタの値をr1レジスタに加算
ADD r1, [r2]     0B r1 r2   ADD。r2レジスタが示す番地の内容をr1レジスタに格納
ADD r1, nn       0D r1 nn   ADD。固定値nnをr1レジスタに格納
SUB r1. r2       0E r1 r2   SUBtraction。r2レジスタの値をr1レジスタから減算
SUB r1, [r2]     0F r1 r2   SUBtraction。r2レジスタが示す番地の内容をr1レジスタから減算
SUB r1, nn       11 r1 nn   SUBtraction。固定値nnをr1レジスタから減算
INC r1           12 r1      INCrement。r1レジスタを1インクリメント
DEC r1           13 r1      DECrement。r1レジスタを1デクリメント
CMP r1, r2       14 r1 r2   CoMPare。r1レジスタとr2レジスタの値を比較
CMP r1, [r2]     15 r1 r2   CoMPare。r1レジスタとr2レジスタが示す内容を比較
CMP r1, nn       17 r1 nn   CoMPare。r1レジスタと固定値nnを比較

JMP nn           1F nn      JuMP。nn番地にジャンプ
JC nn            21 nn      Jump if Carry。C=Trueであればジャンプ。(C=True)
JNC nn           23 nn      Jump if No Carry。C=Falseであればジャンプ。(C=False)
JZ nn            25 nn      Jump if Zero。Z=Trueであればジャンプ。(Z=True)
JNZ nn           27 nn      Jump if No Zero。Z=Falseであればジャンプ。(Z=False)
JE nn            25 nn      Jump if Equal。等しければ(=)ジャンプ。(Z=True)
JNE nn           27 nn      Jump if Not Equal。等しくなければ(!=)ジャンプ。(Z=False)
JA nn            29 nn      Jump if Abobe。大きければ(<)ジャンプ。(C=False/Z=False)
JNA nn           2B nn      Jump if Not Abobe。大きくなければ(not <)ジャンプ。(C=False/Z=False)
JAE nn           23 nn      Jump if Abobe or Equal。大きいか等しければ(<=)ジャンプ。C=False)
JNAE nn          21 nn      Jump if Not Abobe or Equal。大きくなく等しくもなければ(not <=)ジャンプ。(C=True)
JB nn            21 nn      Jump if Below。小さければ(>)ジャンプ。(C=True)
JNB nn           23 nn      Jump if Not Below。小さくなければ(not >)ジャンプ。(C=False)
JBE nn           2B nn      Jump if Below or Equal。小さいか等しければ(>=)ジャンプ。(C=True/Z=True)
JNBE nn          29 nn      Jump if Not Below or Equal。小さくなく等しくも無ければ(not >=)ジャンプ。(C=False/Z=False)

PUSH r1          32 r1      PUSH。r1レジスタをスタック領域に退避。
PUSH [r1]        33 r1      PUSH。r1レジスタが示す番地の内容をスタック領域に退避。
PUSH nn          35 nn      PUSH。固定値nnをスタック領域に退避。
POP r1           36 r1      POP。スタック領域から値をひとつ取り出してr1レジスタに格納。
CALL nn          38 nn      CALL。nn番地をサブルーチンとして呼び出す。
RET              39         RETuen。サブルーチンから戻る。

MUL r1           3C r1      MULtiplication。掛け算。Aレジスタ=Aレジスタ×r1レジスタ
DIV r1           40 r1      DIVision。割り算。Aレジスタ=Aレジスタ÷r1レジスタ
AND r1, r2       46 r1 r2   AND。論理積。r1 = r1 and r2レジスタの値
AND r1, [r2]     47 r1 r2   AND。論理積。r1 = r1 and r2レジスタが示す番地の内容
AND r1, nn       49 r1 nn   AND。論理積。r1 = r1 and 固定値nn
OR r1, r2        4A r1 r2   OR。論理和。r1 = r1 or r2レジスタの値
OR r1, [r2]      4B r1 r2   OR。論理和。r1 = r1 or r2レジスタが示す番地の内容
OR r1, nn        4D r1 nn   OR。論理和。r1 = r1 or 固定値nn
XOR r1, r2       4E r1 r2   XOR。排他的論理和。r1 = r1 xor r2レジスタの値
XOR r1, [r2]     4F r1 r2   XOR。排他的論理和。r1 = r1 xor r2レジスタが示す番地の内容
XOR r1, nn       51 r1 nn   XOR。排他的論理和。r1 = r1 xor 固定値nn
NOT r1           52         NOT。否定。r1 = not r1。ビットを反転。
SHL r1, r2       5A r1 r2   SHift to Left。r1 = r1 を r2レジスタの値分左シフト。
SHL r1, [r2]     5B r1 r2   SHift to Left。r1 = r1 を r2レジスタの示す内容分左シフト。
SHL r1, nn       5D r1 nn   SHift to Left。r1 = r1 を 固定値nn分左シフト。
SHR r1, r2       5E r1 r2   SHift to Right。r1 = r1 を r2レジスタの値分右シフト。
SHR r1, [r2]     5F r1 r2   SHift to Right。r1 = r1 を r2レジスタの示す内容分右シフト。
SHR r1, nn       61 r1 nn   SHift to Right。r1 = r1 を 固定値nn分右シフト。

Z80アセンブラ

Simple 8-bit Assembler Simulator に比べて、8ビットCPUの代表格である 8080 やその拡張版である Z80 ではもっと高度な機能を備えています。

Z80アセンブラの詳細は下記などを参照してください。