アセンブリ言語とは何か
CPU が分かる言語を、人間が読める形で書いたものである。プログラミング言語には抽象度の階段があり、アセンブリはその一番下の近くにいる。
層をクリック → 右に詳細
上ほど抽象度が高く人間に近い / 下ほど機械に近い
a = 12
b = 7
result = a + b
print(result) # → 19 と表示
人間が読み書きしやすい。でも CPU はそのまま実行できない。
LOAD A, 12 ; A = 12 LOAD B, 7 ; B = 7 ADD A, B ; A = A + B → 19 OUT A ; 19 と表示 HALT ; 終了
CPU への指示書き。1行 = 1命令 = CPU が1回やることである。
レジスタ ― CPU の「手元メモ帳」
計算のたびにメモリへ読み書きしていては遅すぎる。だから CPU の中に、超高速で小さな置き場を持っている。
どんな計算にも使える「変数箱」。8ビット(0〜255 の整数)を入れられる。
例:LOAD A, 42 → A の箱に 42 を入れる。
次に実行する命令の番号を常に指している特殊レジスタ。通常は 0→1→2→… と自動的に増えるが、JMP 命令で強制的に変えられる。
直前の演算結果の「性質」を記録する1ビットのスイッチ。CMP の後で JEQ / JGT が参照する。
| フラグ | 1 になる条件 |
|---|---|
| Z | 結果が 0(=同じ値を比較したとき) |
| N | 結果が負(Rx < Ry のとき) |
| C | 加算で桁あふれ(255 を超えた) |
メモリ ― 「引き出しだな」
レジスタより遅いけれど、たくさん置ける。このシミュレーターでは 16 マスある。
STORE A, 0x00 → アドレス 0x00 に A の値を保存
メモリの各マスには住所番号(アドレス)がついている。0x00〜0x0F は16進数の表記で、10進数の 0〜15 のことである。
LOAD A, 42 ; 即値 → レジスタ STORE A, 0x00 ; レジスタ → メモリ保存 LOAD B, [0x00] ; メモリ → レジスタ読出し
このシミュレーターでは LOAD B, [0x00] と角括弧で書くとメモリから読み込む。
メモリ階層構造
レジスタとメモリの間には、実は何段階もの中間層がある。各層をクリックすると速度・容量・用途が開く。 (詳しくは第3章の 03.3 主記憶 で扱う。)
| 層 | 速度 | 容量 | 単価 |
|---|---|---|---|
| レジスタ | ~1 cy | ~512 B | 最高 |
| L1 | ~4 cy | 32–64 KB | 非常に高 |
| L2 | ~12 cy | 256 KB–2 MB | 高 |
| L3 | ~40 cy | 4–64 MB | 中 |
| RAM | ~100 ns | 4–128 GB | 低 |
| SSD | ~100 µs | 256 GB–8 TB | 非常に低 |
| HDD | ~8 ms | 1–20 TB | 最低 |
02 節のレジスタ(A/B/C/D)が最上位の超高速層、03 節のRAM(0x00〜0x0F)が STORE / LOAD でアクセスするメインメモリ層である。キャッシュは CPU が自動的に管理するので、アセンブリから直接は操作しない。
命令セット(ISA)全解説
このシミュレーターの命令は12個だけである。すべて例つきで見ていく。
データ転送命令
レジスタ Rx に即値(数字)を入れる。
LOAD A, 10 ; A ← 10 LOAD C, 255 ; C ← 255(最大値)
メモリのアドレス addr から値を読み込む。
STORE A, 0x00 ; まず保存 LOAD B, [0x00] ; B ← mem[0x00]
Rx の値をメモリアドレス addr に保存する。
LOAD A, 42 STORE A, 0x03 ; mem[3] = 42
Ry の値を Rx にコピーする。Ry は変わらない。
LOAD A, 99 MOV B, A ; B = 99(A も 99 のまま)
算術命令
Rx = Rx + Ry
LOAD A, 3 LOAD B, 5 ADD A, B ; A = 8
Rx = Rx − Ry
LOAD A, 10 LOAD B, 3 SUB A, B ; A = 7
Rx = Rx × Ry(8ビットの下位)
LOAD A, 6 LOAD B, 7 MUL A, B ; A = 42
ADD A, B の後、A の値は変わるが B は変わらない。
比較・分岐命令
「もし〜なら」を実現するために使う。CMP で比較 → JEQ / JGT で分岐という流れが基本である。
Rx と Ry を比較してフラグを更新する。値は変わらない。
| Rx = Ry | Z = 1 |
| Rx < Ry | N = 1 |
| Rx > Ry | Z = 0, N = 0 |
無条件で label の行へジャンプする(ループに使う)。
loop: ADD A, B JMP loop ; 永遠にループ
Z = 1(直前の比較が「等しい」)なら label へ。
CMP A, B JEQ equal ; A == B ならジャンプ
N = 0 かつ Z = 0(直前の比較が「より大きい」)なら label へ。
CMP A, B JGT bigger ; A > B ならジャンプ
出力・終了命令
Rx の値を出力欄に表示する。Python の print() に相当。
LOAD A, 42 OUT A ; 「42」と出力
プログラム終了。必ず最後に書くこと。
OUT A HALT ; ← これがないと暴走する
サンプルプログラムを1行ずつ読む
シミュレーターに用意されている3つの例を、実際に追いかけてみましょう。
LOAD A, 12 ; 1 A = 12 LOAD B, 7 ; 2 B = 7 ADD A, B ; 3 A = A+B = 19 OUT A ; 4 「19」出力 HALT ; 5 終了
ADD A, B → A = 12 + 7 = 19。B は 7 のまま変わらない。LOAD A, 0 ; 1 sum = 0 LOAD B, 1 ; 2 i = 1 LOAD C, 5 ; 3 limit = 5 loop: ADD A, B ; 4 sum += i LOAD D, 1 ; 5 D = 1 ADD B, D ; 6 i += 1 CMP B, C ; 7 i > 5? JGT done ; 8 なら終了へ JMP loop ; 9 繰り返す done: OUT A ; 10 「15」出力 HALT
sum=0; i=1; limit=5 に相当。sum += i — A に B を足す。CMP B, C で B と 5 を比較。B > 5 なら JGT done で出口へ。JMP loop で先頭に戻る。LOAD A, 15 ; 1 A = 15 LOAD B, 20 ; 2 B = 20 CMP A, B ; 3 A > B? JGT a_win ; 4 A>B なら a_win へ OUT B ; 5 B が大きい → 出力 HALT a_win: OUT A ; A が大きい → 出力 HALT
CMP A, B → 15 − 20 = −5 → N フラグが立つ(負)、Z は 0。JGT a_win — N=1 なのでジャンプしない(A は B より大きくない)。OUT B を実行 → 20 を出力。よく使うパターン集
コピーして改造すればすぐ使えるテンプレートである。
for i in range(1, N+1) に相当
LOAD B, 1 ; i = 1(開始値) LOAD C, N ; 上限 N に変更 LOAD D, 1 ; インクリメント用 loop: ; ← ここにループ処理 ADD B, D ; i++ CMP B, C JGT done JMP loop done: HALT
大きいほうを使う分岐
CMP A, B JGT a_bigger ; A>B ならジャンプ ; B が大きい場合 OUT B HALT a_bigger: ; A が大きい場合 OUT A HALT
レジスタが足りないときのテクニック
LOAD A, 42 STORE A, 0x00 ; A を保存 ; A は別の計算に使う LOAD A, 100 ; 後で復元 LOAD B, [0x00] ; B ← 42(復元)
等しいかどうかの分岐(JEQ を使う)
LOAD A, 7 LOAD B, 7 CMP A, B JEQ equal ; A == B ; 等しくない場合 HALT equal: ; 等しい場合 OUT A HALT
よくあるミスと対処法
最後に HALT がないと、命令リストの外を読み続けて予期しない動作になる。必ず最後の行に HALT を入れましょう。
ADD A, B は「A を更新する」命令である。B には何も書き込まれない。「B に A を足した結果を入れたい」なら ADD B, A と書く。
loop: と定義して JMP lop と書くと「ラベルが見つからない」エラーになる。定義と参照のスペルを一致させましょう。
フラグは直前の演算の結果である。JEQ / JGT の直前には必ず CMP を置きましょう。
; 以降はコメントとして無視される。各行に「何をしているか」を書いておくと、後で見直したときに理解しやすくなる。
シミュレーターの「1ステップ」ボタンを使えば、1命令ずつ実行してレジスタの変化を確認できる。バグを見つけるのに最適である。
準備できたら、シミュレーターへ
実際に命令を入力して、CPU がどう動くか体感してみましょう。
1命令が Fetch → Decode → Execute → Memory → Write-back と進む実行サイクルが、1段ずつ光って見える。
メモリに触らない命令が Memory 段を素通りするところも観察できる。