命令実行
CPU はプログラムカウンター PC の指す語を読み、次のアドレスを用意し、フィールドをデコードして、操作コードの指定する処理を繰り返す。本課の TinyCPU はフェッチ中に PC を 1 増やす。フェッチ、デコード、実行、書き戻しの順を示し、不要な段階は命令ごとに省く。
CPU はソースコードの関数、ループ、ファイル末尾を直接理解しない。アドレスから命令を取り、レジスターとメモリの状態を更新する。高水準の構造は命令配置と制御フローで実現する。
命令実行の仕組み
TinyCPU はレジスター 4 個、命令ごとに 16 ビット語一つを使い、命令とデータを同じメモリ列に置く。上位 4 ビットの op が操作、rd と rs の各 2 ビットが宛先とソースレジスター、下位 8 ビットの imm が即値フィールドだ。
imm の意味は op による。LDI r1, 2 は 2 を値、JNZ r1, 2 はアドレスとして読む。デコードは固定位置の切り分けで、ソース言語の構文解析や先読みではない。モデルは操作コードの有効性を調べる。ADD は imm を読まないため、この部分がゼロでなくても結果は変わらない。
PC も数値を保存する状態だ。デコード前のフェッチで 1 を加えるので、命令実行時には次の語を指す。分岐成立時は後の書き戻しで上書きし、不成立なら先に用意したアドレスを残す。
ステップ実行
既定値 n=2 のプログラムは静的には 7 命令で、2+1 を累積してメモリへ保存する。第三列は読者向けで、CPU は語だけを読む。
| アドレス | 語 | 解釈 |
|---|---|---|
| 0 | 0001 01 00 00000010 | LDI r1, 2 |
| 1 | 0001 10 00 00000000 | LDI r2, 0 |
| 2 | 0010 10 01 00000000 | ADD r2, r1 |
| 3 | 0011 01 00 00000001 | SUBI r1, 1 |
| 4 | 0100 00 01 00000010 | JNZ r1, 2 |
| 5 | 0101 00 10 00000111 | STORE r2, 7 |
| 6 | 1111 00 00 00000000 | HALT |
| 7 | 0000 00 00 00000000 | 命令ではないデータ |
LDI r1, 2:PC=0 の語を命令レジスターへ取り、PC を 1 にする。op=0001は LDI、rd=01、imm=00000010なので r1=2 を書く。値は命令中にあり、ALU が不要なため実行段階を省く。LDI r2, 0:r2=0 として累積器を準備する。ADD r2, r1:デコード前に PC は 3。ALU は r2=0 と r1=2 から 2 を作り、書き戻しで r2 へ入れる。imm=00000000は使わない。SUBI r1, 1:r1=2 から命令内の 1 を引き、r1=1 にする。JNZ r1, 2:フェッチが PC を 5 にする。r1 は非ゼロなので、書き戻しが PC=2 にする。特別なループ装置やスタックはなく、次のフェッチは PC の値に従いアドレス 2 を読む。- 再び
ADD r2, r1。r2=2+1=3。 - 再び
SUBI r1, 1。r1=0。 - 再び
JNZ r1, 2。同じ住所、語、フィールドでも、r1=0 なので条件は偽。PC を書かず、フェッチが入れた 5 を残す。フォールスルーは上書きをしない結果だ。 STORE r2, 7:r2=3、imm は今度はアドレス 7。プログラムと共用するメモリへ 3 を書く。HALT:op=1111で終了し、動的に実行した命令は計 10 個。
この正整数 n の例は 3n+4 命令。初期化 2、各周回 3、終了処理 2 となる。n=5 は 19 命令、n=3 は同じ周回が一つ増える。静的な 7 命令は変わらず、同じアドレスを何度実行するかが変わる。
ジャンプも状態の更新
LDI、ADD、SUBI はレジスター、STORE はメモリ、成立した JNZ は PC を書く。結果の宛先を変えると効果が変わる。教材の図は同類の書き戻しとして見せるが、実 CPU の全操作が同じ物理経路を使う意味ではない。
while は条件に応じて次のフェッチを後方へ戻し、if は一部を飛ばす形にできる。関数呼び出しには戻り先も保存し、復帰時に制御位置を戻す。実際の規約ではスタックやリンクレジスターを使う。本課は呼び出しを実装せず、アドレス更新が制御を支える点を示す。
分岐の判定に複数サイクルかかれば、待機するか次のアドレスを予測する必要がある。分岐予測とパイプラインへつながる仕組みだ。
境界ケース
- n=1 は本体を一度実行した時点で r1=0 となり、分岐は成立しない。
- No branch はジャンプを除く。PC は一語ずつ進み、初期化、ADD と SUBI 一回、保存、HALT を行う。n>1 なら後続の周回がなくなり、最終レジスターと保存値も変わる。
- Program edits itself は STORE でアドレス 4 の HALT を数値 3 に置換する。書き込み時は正常。その後二命令を実行し、4 をフェッチすると
0000 0000 0000 0011の op=0000が不正で停止する。実システムのコードページ権限は無許可の変更を防げる。 - Never returns はアドレス 1 の JNZ が 1 自身へ戻り、判定レジスターを減らさない。毎回正しく実行しても終了条件がない。アニメーションは 6 命令で切るが、それは CPU の自動終了条件ではない。
- プログラム末尾の先に特別な印はない。割り当てメモリ内ならデータやゼロなどをフェッチする。モデルのメモリ外なら不正アドレスで止まる。HALT は明示的な終了命令だ。
よくある間違い
- CPU が main や関数境界を理解すると考える。実行環境が入口と初期状態を設定し、ハードウェアは命令アドレスに従う。
- 命令とデータが必ず別にあると思う。この統合メモリでは PC が命令として取る語を決める。実機には分離した経路やアドレス空間もあり、OS の権限もアクセスを制限する。
- 分岐が途中の命令を一つずつ越えると考える。アドレスを書き換え、中間はフェッチしない。不成立なら既存の PC 増分を残す。
- imm に一つの意味を固定する。LDI は値、SUBI は減数、JNZ と STORE はアドレスとして読む。
- 未使用フィールドは必ずゼロだと思う。TinyCPU の ADD は imm を無視する。実 ISA は予約ビットの規則があり、ゼロでなければ例外にするものもある。
- 命令数をアニメーションのステップ数や時間と同一視する。一命令には複数の説明動作がある。
3n+4は命令数で、実時間はキャッシュ、予測失敗、処理の重なりにも依存する。
モデルの範囲
TinyCPU は一命令を最後まで終えてから次を始め、モデルのメモリを直接使う。実プロセッサーにはパイプライン、予測、順序変更、キャッシュがあり、この動作数を実クロック数とは読めない。
条件フラグ、可変長・複数語命令、計算によるアドレッシング、割り込み、メモリマップド I/O、特権レベルは省略する。レジスターのゼロ初期化は教材上の選択で、実際はアーキテクチャー、リセット、OS、呼出規約に依存する。
フェッチ、デコード、実行、結果更新は共通するが、フェッチ時に PC を 1 増やすのは TinyCPU の規則だ。実機の命令長や前段のタイミングは異なる。PC を汎用レジスターと分ける設計も多く、Arm A64 では PC を直接オペランドに指定できない。分岐も TinyCPU JNZ の絶対値ではなく相対オフセットを使うことが多い。共通するのは、分岐が次の命令取得位置を変える点だ。