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ビットアセンブラを題材に、コンピュータの基本的な動作の仕組み、機械語、ニーモニック、アセンブリ言語、アセンブラの感触を説明してみたいと思います。
カリフォルニア在住の 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つしかありません。
種別 | 名称 | ビット数 | 説明 |
---|---|---|---|
汎用レジスタ | A | 8ビット | Aレジスタ。別名アキュムレータ。 |
B | 8ビット | Bレジスタ。ベースと呼ばれることも。 | |
C | 8ビット | Cレジスタ。カウンターとして使われることが多い。 | |
D | 8ビット | Dレジスタ。データを示す値として使われることが多い。 | |
特殊レジスタ | IP | 8ビット | インストラクションポインタ。現在、プログラムの何番地を実行しているかを示す。 |
SP | 8ビット | スタックポインタ。現在、スタックデータが何番地まで格納されているかを示す。 |
Simple 8-bit Assembler Simulator では下記のフラグが実装されています。
フラグ名 | ビット数 | 説明 |
---|---|---|
Z | 1ビット | ゼロフラグ。計算結果がゼロの時に TRUE(1) となる。 |
C | 1ビット | キャリーフラグ。200 + 200 など計算結果が 256 の範囲をオーバーした時に TRUE(1) となる。 |
F | 1ビット | フォールトフラグ。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番地はデータ領域です。固定値が格納されます。
00-01番地および 0F-32番地はプログラム領域です。プログラムを機械語に翻訳したものが格納されます。
E4-E7番地はスタック領域です。データをひとつずつ格納(Push)したり、取り出したり(Pop)します。最後に Push したものが最初に Pop されます。スタック領域は E7 番地からはじまり、番地の若い方向に向かって伸びていきます。スタック領域が増えて、プログラム領域や後述のヒープ領域を壊す事態になるとスタックオーバーフローエラーが発生します。
E8-FF番地はアウトプット領域です。CPU や実装によって様々ですが、例えば関数電卓では E8-FF番地の値が電卓のディスプレイに表示されるように設定されていたりします。
今回は使用していませんが、プログラム領域とスタック領域の間にヒープ領域があります。動的に確保されるデータなどが格納されます。
プログラムは下記のような命令(ニーモニック)を用いたアセンブリ言語で記述されています。
; 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分右シフト。
Simple 8-bit Assembler Simulator に比べて、8ビットCPUの代表格である 8080 やその拡張版である Z80 ではもっと高度な機能を備えています。
Z80アセンブラの詳細は下記などを参照してください。