RISC-Vを目標にCPUを自作する話(実装編2-2:8bit CPUで学ぶ命令実行の流れ fetch-decode-execute)


はじめに

前回は、最小構成の8bit CPUをRustで実装した。
今回は、そのCPUがどのように命令を実行するのか、fetch-decode-executeの流れを解説する。
CPUが命令を実行するためには、まず命令がメモリに配置されている必要がある。
そして、CPUは命令を順番に読み取って(fetch)、解釈して(decode)、実行する(execute)というサイクルを繰り返す。
この流れを理解することで、CPUがどのようにプログラムを実行しているのかが具体的に見えてくる。

命令はどのように配置されるのか

CPUはテキストのコードを直接実行しているわけではない。
命令はバイト列としてメモリに配置され、それをCPUが読み取って実行している。

例えば、以下のような命令を考える。

Instruction {
    opcode: Opcode::Add,
    op1: 0,
    op2: 1,
}

このようにRustの構造体で命令を表現しているが、実際にはバイト列としてメモリに格納される。

[0x00, 0x00, 0x01]

CPUはこのバイト列を読み取り、

opcode = Add
op1 = 0
op2 = 1

として解釈する。

fetch-decode-executeの流れ

CPUは、メモリ上の命令を以下のサイクルで実行している。

  • Fetch: PC(プログラムカウンタ)が指すアドレスから命令を読み取る
  • Decode: 読み取った命令を解釈する
  • Execute: 命令に従って処理を実行する

なお、このCPUでは命令は3バイト固定のため、次の命令に進むときはPCは3だけ進む。
では、先ほどのAdd命令がどのように実行されるかを見てみる。

Step 1: Fetch

PCが指すアドレスから、以下のバイト列を読み取る。

[0x00, 0x00, 0x01]

読み取った後、PCは次の命令を指すように進む。

Step 2: Decode

読み取ったバイト列を解釈する。

  • 0x00 → Add命令(opcode)
  • 0x00 → レジスタ0(op1)
  • 0x01 → レジスタ1(op2)

Step 3: Execute

レジスタ0とレジスタ1の値を加算し、結果をレジスタ0に保存する。

このようにCPUは、メモリ上のバイト列を1つずつ読み取りながら、命令を順番に実行していく。
CPUはテキストを実行しているのではなく、メモリ上のバイト列を順番に処理している。

私たちが書いたコードは最終的にこのバイト列に変換され、CPUはそれをただ順番に実行しているにすぎない。

初期状態:
PC = 0
R0 = 5
R1 = 3

メモリ:
0: [0x00, 0x00, 0x01] (ADD R0, R1)

Fetch:
命令を取得 → ADD R0, R1
PC = 3

Execute後:
R0 = 8
R1 = 3

人間にとっての命令の見え方

ここまで見てきたように、CPUはバイト列を処理して命令を実行している。
しかし、人間がこのバイト列を直接扱うのは非常に分かりづらい。

例えば、以下のようなバイト列があったとする。

[0x00, 0x00, 0x01]

これだけでは、どのような処理が行われるのか直感的には分かりにくい。

そこで登場するのがアセンブリ言語である。 アセンブリ言語は、バイト列を人間が理解しやすい形で表現したものだ。

例えば、先ほどの命令は以下のように表現される。

ADD R0, R1

このように表現することで、

  • どの命令か
  • どのレジスタを使うか

が一目で分かるようになる。

今回のCPUでは、このアセンブリの代わりにRustの構造体で命令を表現している。

Instruction {
    opcode: Opcode::Add,
    op1: 0,
    op2: 1,
}

つまり、

ADD R0, R1         (人間が書く)

Instruction {...}  (プログラム内部)

[0x00, 0x00, 0x01](CPUが実行する)

という対応関係になっている。
普段私たちが書いているCやRustなどの高水準言語も、最終的にこのような命令列に変換されて実行されている。

なお、乗算や除算のような複雑な処理も、これらの命令を組み合わせることで実現できる。

今回は、Add命令を例に説明したが、他の命令も同様の流れで実行される。
つまり、どのような処理であっても、CPUの基本的な動きはこのサイクルの繰り返しである。

falog_tiny_cpu (GitHub) のコードを見ると、他の命令も同様にfetch-decode-executeの流れで実装されていることが分かると思う。

まとめ

今回は、Rustで実装した最小構成の8bit CPUを題材に、命令がメモリに配置され、fetch-decode-executeで実行される流れを解説した。

CPUは、テキストのコードを直接実行しているのではなく、メモリ上のバイト列を順番に処理している。
私たちが書いたコードは最終的にこの形に変換され、CPUはそれを1つずつ実行しているにすぎない。

この基本的な仕組みを理解することで、CPUの動作だけでなく、今後の命令設計やアセンブラの理解にも繋がっていく。