ミクロン・シミュレータ
現実の実用規模のコンピュータを使って学ぶ前に,架空の,極端に小規模なコンピュータを使って,機械語プログラムとはどのようなものか学習しよう。
ミクロン・シミュレータは,架空のコンピュータ『ミクロン』をWebブラウザ上に表現したものである。 ミクロンは,Altair 8800 (1974) 等のような,キーボードもモニタ画面もない極めて簡素なコンピュータを模している。
Upper: Link / CC BY-SA 3.0 (cropped);
Lower: Todd Dailey / CC BY-SA 2.0
ミクロン上でできる操作は基本的に,主記憶装置(メモリ)に機械語プログラムやデータを書き込むことと,プログラムを実行することだけである。
本来,CPUの中のレジスタやメモリの内容は目に見えないが,シミュレータではこれらを表示している(いわば「透視能力」を使って見ている状態)。 レジスタやメモリの中身はビットの列であり,本来は0, 1の列(2進数)で表示した方が実態に合っているが,読みにくいので16進数で表示している(「どんなコンピュータでも,機械が扱う情報は必ずビット列である」ことを忘れずに)。
- 参考:10進・16進・2進 対応表
10進 | 16進 | 2進 |
---|---|---|
0 | 0 | 0000 |
1 | 1 | 0001 |
2 | 2 | 0010 |
3 | 3 | 0011 |
4 | 4 | 0100 |
5 | 5 | 0101 |
6 | 6 | 0110 |
7 | 7 | 0111 |
10進 | 16進 | 2進 |
---|---|---|
8 | 8 | 1000 |
9 | 9 | 1001 |
10 | a | 1010 |
11 | b | 1011 |
12 | c | 1100 |
13 | d | 1101 |
14 | e | 1110 |
15 | f | 1111 |
ミクロンの概要
- 8ビットコンピュータ(レジスタのビット幅が8ビット,かつ,8ビット同士の加減算が可能)
- メモリ容量 256バイト.番地のビット幅 8ビット
- 汎用レジスタ: A, B, Cの3つ
- 特殊レジスタ: IP(命令ポインタ),Zフラグ (zero flag),Cフラグ (carry flag)
- 実行開始番地: 0x00番地
ミクロンの命令セット
オペコード | ニーモニック | 意味 | 引数 |
---|---|---|---|
00 |
hlt |
停止 | なし |
01 |
mov_i |
r1 ← imm | r1, imm |
02 |
mov_r |
r1 ← r2 | r1, r2 |
03 |
add |
r1 ← r2 + r3 | r1, r2, r3 |
04 |
sub |
r1 ← r2 − r3 | r1, r2, r3 |
05 |
cmp |
r1 と r2 を比較 | r1, r2 |
06 |
b |
addr に移動 | addr |
07 |
bgt |
大きければ addr に移動 | addr |
08 |
bge |
以上ならば addr に移動 | addr |
09 |
beq |
等しければ addr に移動 | addr |
0a |
bne |
等しくなければ addr に移動 | addr |
0b |
ble |
以下ならば addr に移動 | addr |
0c |
blt |
小さければ addr に移動 | addr |
0d |
str |
r1 の値を r2 が指す番地に格納 | r1, r2 |
0e |
ldr |
r2 が指す番地の中身を r1 に代入 | r1, r2 |
最初のプログラム
演習0.1-1:
0x00〜0x03番地に 01 01 7b 00
を書き込み,Run (▶) ボタンを押して,結果を確認しなさい。
01 01 7b
は「レジスタAに 0x7b (=123) を代入せよ」という機械語命令である。これを実行するとレジスタAに0x7bが格納される。
次の 00
は「停止せよ (halt)」という機械語命令である。(Runボタンで実行開始すると,halt命令に到達するまで実行し続ける。)
演習0.1-2:
演習0.1-1に続けて,0x03〜0x06番地に 01 02 2d 00
を書き込み(つまり0x00〜0x06番地に 01 01 7b
01 02 2d
00
を書き込み),Runボタンを押して,結果を確認しなさい。
01 02 2d
は「レジスタBに 0x2d (=45) を代入せよ」という機械語命令である。
01 xx yy
という3バイトは「レジスタ xx
に8ビットの値 yy
を代入せよ」という機械語命令である。
先頭の 01
が命令の種類を表す番号で,これをオペコード (opcode, operation code) という。続く2バイトは引数(オペランド, operand)であり,代入先のレジスタの番号と代入する1バイトの値を表している。レジスタA, B, Cのレジスタ番号はそれぞれ 01
, 02
, 03
である。
(どのようなバイト列がどういう命令を表すかはCPUごとに異なる。上記はミクロンCPUに固有の事柄である。)
01 xx yy
の yy
は,代入したい値そのものを表す。このような,機械語命令中に埋め込まれたデータ値を即値 (immediate value) と言う。
ニーモニックとアセンブリ表記
01 01 7b
は「レジスタAに 0x7b を代入せよ」という命令,01 02 2d
は「レジスタBに 0x2d を代入せよ」という命令だが, 01 01 7b 01 02 2d
のようなバイト列だけでは意味を読み取るのに苦労するし,かと言って「レジスタAに 0x7b を代入せよ」のような説明文を毎回書くのも冗長だ。
もっと読み書きしやすい記法として,mov a, 0x7b
や mov b, 0x2d
のように,命令の種類を表す単語とオペランドを書いて機械語命令を表す記法がある。
命令の種類を表す単語をニーモニック (mnemonic) と言い,ニーモニックとオペランドで機械語命令を表す記法をアセンブリ表記 (assembly notation) と言う。
1〜2命令程度の短いプログラムを除けば,普通はまず紙の上などにアセンブリ表記でプログラムを書いてから,機械語命令であるバイト列に翻訳していく。
アセンブリ表記を機械語バイト列に翻訳することをアセンブル (assemble) という。
例. 演習0.1-2のプログラムをアセンブリ表記で書くと以下のようになる。
mov a, 0x7b
mov b, 0x2d
hlt
これをアセンブルすると以下のバイト列になる。
01 01 7b 01 02 2d 00
加算・減算
演習0.1-3:
演習0.1-2に続けて,0x06〜0x0a番地に 03 01 01 02 00
を書き込み,Runボタンを押して,結果を確認しなさい。
03 01 01 02
は「レジスタAとBの和をAに代入せよ」という機械語命令である。
演習0.1-3のプログラムのアセンブリ表記は以下のようになる(//
以降はコメント)。
mov a, 0x7b // a = 123
mov b, 0x2d // b = 45
add a, a, b // a = a + b
hlt
このプログラムを実行すると,レジスタAに 123 + 45 の結果 (168 = 0xa8) を格納して停止する。
03 xx yy zz
は「レジスタ yy
と zz
の和を xx
に代入せよ」という機械語命令である。
つまり,C言語やJavaでの xx = yy + zz
という文に類似している。
オペランドはすべてレジスタであることに注意。
例えば 123 + 45 を計算したい場合,被演算数 123, 45 をそれぞれレジスタに格納してからでなければできない。
演習0.1-4:
演習0.1-3に続けて,0x0a〜0x11番地に 01 02 43 04 01 01 02 00
を書き込み,Runボタンを押して,結果を確認しなさい。
04 01 01 02
は「レジスタAとBの差をAに代入せよ」という機械語命令である。
演習0.1-4のプログラムのアセンブリ表記は以下のようになる(//
以降はコメント)。
mov a, 0x7b // a = 123
mov b, 0x2d // b = 45
add a, a, b // a = a + b (= 123 + 45)
mov b, 0x43 // b = 67
sub a, a, b // a = a - b (= 123 + 45 - 67)
hlt
このプログラムを実行すると,レジスタAに 123 + 45 − 67 の結果 (101 = 0x65) を格納して停止する。
加算と同様に,減算命令は 04 xx yy zz
の4バイトであり,yy
− zz
の値をレジスタ xx
に代入する。
ニーモニックは sub
(subtract) である。
ファイルへの保存と復元
ミクロン・シミュレータには,メモリの内容をファイルに保存(ダウンロード)する機能,及び,ファイルから復元(アップロード)する機能がある。
準備 予め,Firefoxの設定で「ファイルごとに保存先を指定する」をオンにしておくことをお勧めする(ミクロン・シミュレータのマニュアルの最終ページを参照)。 オフになっていると保存先フォルダやファイル名を指定できない。
演習0.1-5
- 演習0.1-4までのメモリの内容をファイルに保存しなさい。ファイル名は何でもよい。
- 「メモリ編集」機能を使ってメモリ内の全セルを0にしなさい。
- 1で保存したファイルの中身をアップロードしなさい。メモリの内容が復元されたことを確認しなさい。
演算結果は0x00以上0xff以下
演習0.1-6
- 0x00〜0x0a番地に
01 01 64
01 02 c8
03 01 01 02
00
を書き込んで,実行しなさい。実行後のレジスタAの値はいくらか? - 0x06番地の
03
を04
に(つまりadd
をsub
に)書き換えて,実行しなさい。実行後のレジスタAの値はいくらか?
演習0.1-6の1のプログラムのアセンブル表記は以下の通り。
mov a, 0x64 // a = 100
mov b, 0xc8 // b = 200
add a, a, b // a = a + b
hlt
単純に考えると加算の結果は 300 (= 0x12c) だが,レジスタAには下位8ビットの 0x2c (= 44 = 300 − 256) が代入される。 各レジスタには8ビット分の情報しか格納できないことに注意。(そのような電子回路として作られているから当たり前。)
0110 0100 (= 0x64 = 100)
+) 1100 1000 (= 0xc8 = 200)
---------------
1 0010 1100 (= 0x12c = 300)
^^^^^^^^^
レジスタには8ビットのみ格納される
演習0.1-6の2のプログラムは,上記プログラムの add
を sub
に書き換えたものである。
つまり 100 − 200 の結果をAに代入する。
単純に考えれば減算結果は −100 だが,レジスタAには 0x9c (= 156 = −100 + 256) が代入される。
ここでもやはり,レジスタには8ビット分の情報(0, 1が8個並んだ列)しか格納できないことに注意。
減算において引く数が引かれる数より大きい場合,「下から9ビット目が1」と見なして(つまり引かれる数を 0x100 増やした上で)減算を行う。これを桁借り (borrow) と言う。
桁借り
1 0110 0100 (= 0x164 = 356)
-) 1100 1000 (= 0xc8 = 200)
---------------
1001 1100 (= 0x9c = 156)
(言い換えると,「0x00 と 0x100 は同一」つまり「下位8ビットが同じなら同じ数」と見なしている。 「0 と 0x100 (=256) は同一」なので「−100 と 156 は同一」。(角度の計算と似ている。0度と360度は同一なので,−100度と260度は同一である。))
メモリからの読み出し・メモリへの書き込み
演習0.1-7
- 0x00〜0x06番地に
01 02 f0 0e 01 02 00
を書き込み,かつ 0xf0 番地に55
を書き込み,実行しなさい。 実行後のレジスタAの値はいくらか? - 1に続いて,0x06〜0x0c番地に
01 02 f1 0d 01 02 00
を書き込み,実行しなさい。 実行後の 0xf1 番地の値はいくらか?
演習0.1-7の1のプログラムのアセンブリ表記は以下の通り。
mov b, 0xf0
ldr a, b // a = mem[b]
hlt
0e xx yy
は,「レジスタ yy
が指す番地の中身をレジスタ xx
に読み出せ」という機械語命令である(ニーモニックは ldr
(load register))。
上記のプログラムでは,ldr
を実行する時点でのBの値が 0xf0 なので,0xf0 番地の中身がAに読み出される。
演習0.1-7の2のプログラムのアセンブリ表記は以下の通り。
mov b, 0xf0
ldr a, b // a = mem[b]
mov b, 0xf1
str a, b // mem[b] = a
hlt
0d xx yy
は,「レジスタ yy
が指す番地にレジスタ xx
の中身を書き込め」という機械語命令である(ニーモニックは str
(store register))。
上記のプログラムでは,str
を実行する時点でのBの値が 0xf1 なので,Aの中身が 0xf1 番地に書き込まれる。
プログラム全体としては,0xf0 番地の中身を 0xf1 番地にコピーするプログラムとなっている。
ldr xx yy
において,xx
と yy
は同一でも構わない。
例えば演習0.1-7の1のプログラムを以下のように変えても,0xf0 番地の中身をレジスタAに読み出すという動作は変わらず,使うレジスタはAだけで済む。
mov a, 0xf0
ldr a, a // a = mem[a]
hlt
(str xx yy
も,xx
と yy
が同一でも構わないが,「ある番地にその番地自身を書き込む」(例えば 0xf0 番地に 0xf0 を書き込む)ことになるので,あまり使いみちはないだろう。)
練習問題
演習0.1-8 以下の各プログラムを作成し,プログラムを格納したメモリの中身をファイルに保存して提出しなさい。 提出ファイル名は各小問で指定する。異なる名前のファイルは提出できないので注意。 提出方法は後述。 期限は別途指示する。 小問4はオプション課題である(合否には影響しない。評価の高低には影響する)。
各小問とも,実行開始時の 0xf0〜0xff 番地の値を変更しても正しく動作するようにすること (なお,提出するhexファイルの 0xf0〜0xff 番地の値は何であっても良い)。
提出前に後述の検査プログラムを使って検査すること。検査に合格しない提出物は提出できない。
- 123 + 45 − 67 + 8 − 9 を計算し,結果を 0xf0 番地に格納して停止するプログラムを作成しなさい。(提出ファイル名:
123.hex
) - 0xf0 番地に格納されている値に 100 (= 0x64) を加えた値を 0xf1 番地に格納して停止するプログラムを作成しなさい。(提出ファイル名:
add100.hex
) - 0xf0 番地に格納されている値と 0xf1 番地に格納されている値を入れ替えて停止するプログラムを作成しなさい。例えば,実行開始時に 0xf0, 0xf1 番地にそれぞれ 0xab, 0xcd が格納されている場合,実行後にそれぞれ 0xcd, 0xab が格納されていればよい。(提出ファイル名:
swap.hex
) - (オプション課題)0xf0, 0xf1, 0xf2 番地に格納されている値の合計(の下位8ビット)を 0xf3 番地に格納して停止するプログラムを作成しなさい。
例えば,実行開始時に 0xf0, 0xf1, 0xf2 番地にそれぞれ 0x64, 0x65, 0x66 (= 100, 101, 102) が格納されている場合,実行後に 0xf3 番地に 0x2f (= 47 = 303 − 256) が格納されていればよい。(提出ファイル名:
add3mem.hex
)
(ヒント)
- 123 + 45 − 67 までは演習0.1-4でプログラムしたので,同様に残りの計算を書けばよい。 最後に 0xf0 番地に結果を格納することを忘れずに。
- 0xf0 番地の値の読み出し → 100加算 → 0xf1 番地に書き込み,を順に行うだけ。
- 0xf0, 0xf1 番地の値をそれぞれレジスタA, Bに読み出した後,0xf1 番地にAの値,0xf0 番地にBの値を書き込めばよい。番地を指定するのにCを使えばよい。
- 0xf0 番地の値をAに読み出す → 0xf1 番地の値をBに読み出してAに加算 → 0xf2 番地も同様 → 0xf3 番地にAの値を書き込む,の順に行えばよい.
検査プログラム
以下のコマンドで検査を行える.
$ ~y-takata/Public/pl2test018 123.hex
$ ~y-takata/Public/pl2test018 add100.hex
$ ~y-takata/Public/pl2test018 swap.hex
$ ~y-takata/Public/pl2test018 add3mem.hex
~y-takata/Public/pl2test018
がコマンド名(演習0.1-8の検査という意味).
引数は検査するファイル名.
どの小問の解答かはファイル名で判断される.
指定された提出ファイル名と異なる場合やファイルが存在しない場合はエラーメッセージが出力される.
検査に合格した場合は PASS
と出力される.
不合格の場合は !! FAIL(期待した値 ... 実際 ...)
と出力される.
機械語プログラムとして正しくない場合や実行が停止しない場合は,エラーメッセージを出力して終了する.
提出方法
以下のコマンドによって提出できる. 上記の検査プログラムと使い方が似ているが,一度に複数ファイルを指定することができる(まとめて提出してもファイルごとに提出してもよい).
$ ~y-takata/Public/pl2submit018 123.hex add100.hex
$ ~y-takata/Public/pl2submit018 swap.hex add3mem.hex
提出コマンドを実行するとまず検査が実行される. 検査に合格しなければ,提出を中止する.
提出に成功したときはその旨のメッセージを出力して終了する. 各人の提出済み課題一覧のありかは別途周知する.