DIVE

Part 1: 物理・電子・論理回路

第 3 章

CPUを組み立てる — フェッチ・デコード・実行の無限ループ

前章までで、部品は揃った。計算する論理ゲート、記憶するレジスタ、時を刻むクロック。この章では、これらを組み上げて、ついにCPU(中央処理装置)を動かす。

CPUは魔法の箱ではない。「メモリから命令を1つ読んで、実行する」を、ただひたすら繰り返すだけの機械だ。その繰り返しの正体を、部品レベルから組み立てていこう。


CPUの5大部品

最小構成のCPUは、次の部品でできている。

部品役割厨房の比喩
ALU計算する(加減算・論理演算)実際に調理する手
レジスタ群計算中のデータを保持まな板の上の食材
プログラムカウンタ (PC)次に読む命令の位置を指すレシピの「今どの行か」を指す指
命令レジスタ (IR)今読んだ命令を保持今読んでいるレシピの一行
制御装置 (Control Unit)命令を解読し各部に指示レシピを読んで手に指示する脳

これらがクロックの号令で連携する。


ALU — 計算の心臓

ALU(Arithmetic Logic Unit)は、前章までの加算器・論理ゲートを束ねた「計算専門ユニット」だ。2つの入力と「何をするか」を指示するオペコードを受け取り、結果を出す。

結果=ALU(A, B, 操作)\text{結果} = \text{ALU}(A,\ B,\ \text{操作})
def alu(a: int, b: int, op: str) -> tuple[int, dict]:
    """簡易ALU。結果とフラグを返す"""
    a &= 0xFF; b &= 0xFF
    if   op == "ADD": r = a + b
    elif op == "SUB": r = a - b
    elif op == "AND": r = a & b
    elif op == "OR":  r = a | b
    elif op == "XOR": r = a ^ b
    else: raise ValueError(op)

    result = r & 0xFF                 # 8ビットに収める
    flags = {
        "zero":  result == 0,         # 結果が0か(分岐命令で使う)
        "carry": r > 0xFF or r < 0,   # 桁あふれ
    }
    return result, flags

print(alu(200, 100, "ADD"))  # (44, {'zero': False, 'carry': True})  200+100=300→桁あふれ
print(alu(50, 50, "SUB"))    # (0,  {'zero': True,  'carry': False}) 結果0

注目はフラグだ。ALUは計算結果だけでなく「結果がゼロだったか」「桁あふれたか」という付随情報を出す。この zero フラグが、後述の条件分岐を可能にする。


フェッチ・デコード・実行 — すべての根源にあるループ

CPUの動作は、3ステップの無限ループに集約される。

   ┌──────────────────────────────────┐
   │                                  │
   ▼                                  │
[Fetch]  PCが指すメモリから命令を取得    │
   │                                  │
   ▼                                  │
[Decode] 命令を解読(何をする命令か?)  │
   │                                  │
   ▼                                  │
[Execute] ALUやレジスタを操作して実行    │
   │      PCを次の命令へ進める          │
   └──────────────────────────────────┘
  1. フェッチ(Fetch):プログラムカウンタ(PC)が指すアドレスから命令を読み、命令レジスタ(IR)へ。
  2. デコード(Decode):制御装置が命令を解読。「これは加算命令だ」「対象はレジスタ2番だ」と各部品への配線を決める。
  3. 実行(Execute):ALUで計算、結果をレジスタへ。そしてPCを次へ進める。

この3ステップが、クロックに合わせて延々と回る。あなたが今使っているPCも、スマホも、この瞬間に数十億回/秒でこのループを回している。


自作CPUをコードで組む

概念だけでは腑に落ちない。動く最小のCPUを書いてみよう。数種類の命令だけを持つ、教育用の仮想CPUだ。

まず命令セット(このCPUが理解できる言葉)を定義する。

オペコード命令意味
0x01LOAD r, valレジスタrに即値valを入れる
0x02ADD r, sr = r + s
0x03SUB r, sr = r - s
0x04JZ addr直前の結果が0ならaddrへジャンプ
0x05OUT rレジスタrを出力
0xFFHALT停止
class CPU:
    def __init__(self, program):
        self.mem = program      # 命令列(メモリ)
        self.reg = [0, 0, 0, 0] # レジスタ4本 (r0〜r3)
        self.pc = 0             # プログラムカウンタ
        self.zero = False       # ゼロフラグ
        self.running = True

    def step(self):
        op = self.mem[self.pc]  # ── Fetch: PCが指す命令を取得

        # ── Decode & Execute
        if op == 0x01:          # LOAD r, val
            r, val = self.mem[self.pc+1], self.mem[self.pc+2]
            self.reg[r] = val
            self.pc += 3
        elif op == 0x02:        # ADD r, s
            r, s = self.mem[self.pc+1], self.mem[self.pc+2]
            self.reg[r] = (self.reg[r] + self.reg[s]) & 0xFF
            self.zero = self.reg[r] == 0
            self.pc += 3
        elif op == 0x03:        # SUB r, s
            r, s = self.mem[self.pc+1], self.mem[self.pc+2]
            self.reg[r] = (self.reg[r] - self.reg[s]) & 0xFF
            self.zero = self.reg[r] == 0
            self.pc += 3
        elif op == 0x04:        # JZ addr
            addr = self.mem[self.pc+1]
            self.pc = addr if self.zero else self.pc + 2
        elif op == 0x05:        # OUT r
            print("OUT:", self.reg[self.mem[self.pc+1]])
            self.pc += 2
        elif op == 0xFF:        # HALT
            self.running = False

    def run(self):
        while self.running:
            self.step()         # フェッチ・デコード・実行を繰り返す

# 「3 + 4 を計算して出力する」プログラム
program = [
    0x01, 0, 3,   # LOAD r0, 3
    0x01, 1, 4,   # LOAD r1, 4
    0x02, 0, 1,   # ADD  r0, r1   → r0 = 7
    0x05, 0,      # OUT  r0
    0xFF,         # HALT
]
CPU(program).run()   # OUT: 7

たった数十行だが、これは本物のCPUと同じ原理で動いている。program という数字の列(=機械語)を、フェッチ・デコード・実行のループが解釈し、計算を実行する。


分岐 — CPUが「判断」する仕組み

上のCPUには JZ(Jump if Zero)命令がある。これが条件分岐、つまりプログラムの「もし〜なら」の正体だ。

仕組みはシンプルで、PCの値を書き換えるだけ。通常PCは命令1つ分ずつ増えるが、ジャンプ命令はPCを任意のアドレスに飛ばす。ゼロフラグを見て飛ぶかどうかを決めれば、if 文やループが実現できる。

# 「5 から 1 ずつ引いて、0になるまでループ」
program = [
    0x01, 0, 5,    # LOAD r0, 5   (カウンタ)
    0x01, 1, 1,    # LOAD r1, 1   (減算する値)
    0x05, 0,       # OUT  r0      ← ループの先頭 (アドレス6)
    0x03, 0, 1,    # SUB  r0, r1  → r0 = r0 - 1、ゼロならzeroフラグON
    0x04, 6,       # JZ   ...     実際は「0でなければ6へ戻る」ロジックを組む
    0xFF,          # HALT
]

高級言語の while も for も、機械語レベルでは「フラグを見てPCを書き換える」という、この単純な操作に還元される。「ループ」という抽象概念の物理的正体が、PCの巻き戻しなのだ。


まとめ — 「おまじない」の消し方

  1. CPU=計算+記憶+制御の統合体:ALU(計算)、レジスタ(記憶)、PC/制御装置(進行管理)の連携で動く
  2. フェッチ・デコード・実行:メモリから命令を読み、解読し、実行する。この無限ループがあらゆる処理の根源
  3. 機械語=ただの数字の列:CPUはその数字を命令として解釈しているだけ
  4. 分岐=PCの書き換え:if や for の正体は、フラグを見てプログラムカウンタを操作すること

「プログラムが動く」という現象の、最も物理に近い層が見えた。CPUは賢いのではなく、単純な操作を高速に繰り返しているだけだ。

次章からはPart 2へ。この機械語を、人間がどう書き、Cのような言語がどうやって機械語に翻訳されるのかを解体していく。


「コンピュータができることは、驚くほど少ない。ただ、それを驚くほど速くやる。」