← ブログに戻るCPUアーキテクチャ

プログラムカウンターを解説:CPUのしおり

プログラムカウンターが次の命令を示し、順次コードを進み、分岐、ジャンプ、例外、リセットでどう切り替わるかを解説します。

DennyDenny
読了まで約10分
Chalkboard feature illustration for “プログラムカウンターを解説:CPUのしおり”

要約: プログラムカウンター(PC)は、一部のアーキテクチャでは命令ポインターとも呼ばれ、次にフェッチする命令を示します。順次実行では復号された命令長だけ進み、分岐、ジャンプ、例外、割り込み、リセットでは異なる次PC値が選ばれます。

プログラムカウンターは、命令フェッチの中心にある専用のアーキテクチャ状態要素です。教材用CPUでは、命令メモリのアドレスを直接駆動するレジスタそのものかもしれません。現代のプロセッサーでは、予測、キュー、キャッシュ、アドレス変換、投機フェッチによって実装はより分散しますが、アーキテクチャは依然として次の命令アドレスを定義します。

この記事では、PCが保持する値、増分の方法、分岐とジャンプによるリダイレクト、そしてレジスタ、加算器、マルチプレクサーからPCを配線する方法を説明します。各部品は、順次命令実行器テンプレートで操作できる動作回路へリンクしています。

プログラムカウンターとは

プログラムカウンターは、メモリ内の次の命令を指すという一つの仕事に専念するレジスタです。ユーザーコードが自由に読み書きする汎用レジスタではありません。CPUの制御ロジックにより駆動される専用レジスタです。

最も単純な機械では、その幅とメモリのアドレス指定可能な単位がコードアドレス空間を制限します。

  • 8ビットPCは、28=2562^8 = 256個のアドレス指定可能な位置を選べます。
  • 16ビットPCは、216=65,5362^{16} = 65{,}536個の位置を選べます。
  • 32ビットPCには、2322^{32}通りのビットパターンがあります。
  • 64ビットPCには、2642^{64}通りのビットパターンがあります。

これらの位置がバイトなのは、バイトアドレス方式のアーキテクチャの場合だけです。実際のプロセッサーは、名目上のレジスタ幅より少ないアドレスビットしか実装しないことがあり、仮想命令アドレスを物理メモリへ変換し、アラインメントや正規アドレスの規則を課すことがあります。

DigiSimのPROGRAM_COUNTER_8BIT部品は8ビットの場合を実装します。シミュレーター内で実際のプログラムを実行できる十分な大きさでありながら、ビット単位で調べられるほど小さく保たれています。

PCはフェッチ段の先頭にある

フェッチ・デコード・実行サイクルのフェッチ段では、四つの信号を順に扱います。

  1. PCが現在値をアドレスバスへ出力する。
  2. メモリがそのアドレスの命令語を返す。
  3. 命令語が命令レジスタにラッチされる。
  4. 次のフェッチが次の命令を読むよう、PCを更新する。

手順1~3は、より広いフェッチの説明で扱っています。この記事の対象は手順4です。

なぜPCは自動的に増分しなければならないのか

PCが同じ場所にとどまれば、CPUは同じ命令を永遠にフェッチします。そのためハードウェアはPCに自動増分を配線し、標準では各フェッチサイクルの後に一歩進むようにします。

増分は専用加算器で計算されます。通常は2進加算を理解するで詳述したような小型リップルキャリー加算器であり、入力は現在のPCと定数オフセットです。その加算器の出力はPCのデータ入力へ戻り、PCは次のクロックエッジでそれをラッチします。

PC + 1とPC + 4:なぜ命令幅が重要か

PCに加える定数は、常に文字どおりの1ではなく、アドレス指定可能な単位で表した命令の幅です。

アーキテクチャ命令幅増分理由
DigiSim 8ビット教材用CPU1バイト(1語)PC + 1各アドレスに完全な命令が入る
圧縮命令なしのMIPS、RISC-V4バイトPC + 4メモリはバイトアドレス方式で、これらの符号化は4バイト幅
ARM(A32)4バイトPC + 4同じ理由
C拡張付きRISC-V、Arm Thumb/Thumb-22または4バイトPC + 復号済み長どちらも16ビット符号化を含み、32ビット符号化も現れる
x861~15バイトPC +(復号済み長)可変長命令であり、デコーダーが長さを報告する

アーキテクチャ上の原則は同じです。順次の次アドレスは、現在の命令の符号化に続くものです。 固定長ISAは定数の増分を使えます。可変長ISAでは、高性能フロントエンドが復号完了前にその境界を越えて予測・フェッチする場合でも、現在の命令長を決定しなければなりません。

ブール式による記述

命令幅が固定でWWのアーキテクチャでは、標準の次PC値は次のとおりです。

PCn+1=PCn+W\text{PC}_{n+1} = \text{PC}_{n} + W

DigiSimの8ビットの場合はW=1W = 1なので、増分は単に+1加算器です。

分岐とジャンプはPCをどのように上書きするか

順次実行は退屈な場合です。実際のプログラムにはループ、条件分岐、関数呼び出し、リターンが必要で、いずれもPCを単なる次の順次アドレス以外へ移動させます。

代表的な二つの制御転送クラスが、この変化を示します。

  • ジャンプ(無条件): 条件にかかわらず、PCを対象アドレスで置き換える。
  • 分岐(条件付き): ISAで定義された条件が真のときだけ対象を選び、それ以外では順次アドレスを選ぶ。一部のISAはゼロ、キャリー、符号、オーバーフローなどのステータスフラグをテストします。ベースRISC-Vを含む別のISAはレジスタを直接比較し、算術条件コードレジスタを持ちません。CPUフラグのガイドでは、フラグベース設計を説明しています。

どちらの操作も、非順次値をPCへ書き込むことに帰着します。

対象アドレスはどこから来るか

対象は三つの方法のいずれかで符号化されます。

  1. 即値(PC相対): 命令語にオフセットを含みます。次PCはPC + offsetです。RISC-VのBEQ、ARMのB、x86の短いジャンプがこの形式を使います。利点は位置独立コードです。
  2. 直接または領域相対: 命令が対象ビットを提供し、固定ベースまたは現在のPCの上位ビットと組み合わせる場合があります。古典的なMIPSのJがよく知られた例です。完全な絶対アドレスは、通常の固定幅命令にはほとんど収まりません。
  3. レジスタ間接: 対象は汎用レジスタまたはALU出力にあります。RISC-VのJALR、ARMのBX、x86のJMP raxが例です。この形式は関数ポインター、仮想ディスパッチ、計算されたgotoを支えます。

小さな単一サイクルのデータパスでは、これらの候補をマルチプレクサーへ入力できます。より高度な実装では、選択したアーキテクチャ上の経路がリタイアする前に、複数のユニットで対象を計算、予測、キューイングすることがあります。

実装:レジスタ + 増分 + MUX

PCのハードウェア構造は、CPUで最も分かりやすいデータパスの一つです。

                        ┌──────────────┐
       PC current ─────▶│  Adder (+W)  │──── PC + W ──┐
                        └──────────────┘              │
                                                      ▼
                                              ┌───────────────┐
                                              │ MUX (2:1)     │
                target_addr ─────────────────▶│  sel = take?  │──── next_PC
                                              └───────────────┘
                                                      │
                                                      ▼
                                              ┌───────────────┐
                  clock ─────────────────────▶│   PC register │──── current_PC
                                              └───────────────┘

構成要素は三つです。

  1. 現在のアドレスを保持するレジスタ。教材用データパスでは、これはアドレスバスに合わせて幅を広げたエッジトリガー記憶素子から作る8ビットレジスタです。
  2. 一方の入力をPC、もう一方を定数幅WWとして配線した加算器。
  3. 加算器からの入力(標準の順次経路)と分岐対象からの入力(分岐取得時の経路)を持つ2:1マルチプレクサー。選択線は制御装置により駆動され、現在の命令がジャンプ、または条件が真になる分岐であるときにアサートされます。マルチプレクサーについてはデータ通信の交通整理役で扱っています。

各クロックエッジでPCは、そのときMUXが選んでいる値をラッチします。この一つのラッチが「進む」と「分岐する」を区別する仕組みのすべてです。

MUXへの任意の入力

実際の設計では、同じMUXにさらに多くの入力元を加えます。

入力元選択されるとき
PC + W標準:順次実行
PC + offset(PC相対)条件分岐、短いジャンプ
直接対象/ベクター直接ジャンプ、リセット、またはアーキテクチャ定義の制御転送
レジスタ/ALU出力間接ジャンプ、関数リターン、switchディスパッチ
リセット/ブートベクター電源投入後または外部リセット後
例外/割り込みベクタートラップ後

選択線は1ビットから3ビットへ広がりますが、構造上の考え方は変わりません。

リセット時の動作:PCはどこから始まるか

CPUの電源投入時またはリセット時、PCは既知のアドレスを保持しなければなりません。そうでなければ実行はランダムな場所から始まります。

代表的な規約は二つあります。

  • PC = 0: 実行はアドレス0x0000…0から始まります。DigiSimの教材用CPUは、位置ゼロに最初の命令を置く、この単純な規約を使います。
  • ブートベクター: 固定された非ゼロアドレスです。x86はコールドリセット時に物理アドレス0xFFFFFFF0(4 GB空間の上端、BIOS ROMの近く)へリセットします。ARM Cortex-Mコアは、アドレス0x00000004に保存されたリセットベクターから初期PCを読みます。一部の6502系チップは、0xFFFC/0xFFFDからリセットベクターを読みます。

ハードウェアでは「リセット」は、PCの非同期クリア(または同期ロード)入力への配線です。アサートされると、MUXにかかわらずPCはブート値へ強制されます。

実例:五命令のループ

W=1W = 1の8ビットCPUにおける、次の擬似プログラムを考えます。

0x00: LDI R0, 5      ; load 5 into R0
0x01: LDI R1, 0      ; load 0 into R1
0x02: ADD R1, R0     ; R1 = R1 + R0
0x03: DEC R0         ; R0 = R0 - 1, sets Z flag if R0 hits 0
0x04: BNZ 0x02       ; if Z == 0, branch back to 0x02
0x05: HLT

PC値を順に追います。

サイクルフェッチ時のPC命令フェッチ後分岐を取るか次PC
10x00LDI R0, 5PC + 1 = 0x01該当なし0x01
20x01LDI R1, 0PC + 1 = 0x02該当なし0x02
30x02ADD R1, R0PC + 1 = 0x03該当なし0x03
40x03DEC R0 (R0=4, Z=0)PC + 1 = 0x04該当なし0x04
50x04BNZ 0x02PC + 1 = 0x05; target = 0x02はい (Z=0)0x02
60x02ADD R1, R0………

サイクル5では、Zフラグがクリアされているため、教材用データパスは順次の次アドレス(0x05)ではなく、分岐対象(0x02)を選びます。実際の制御フロー機構はより複雑ですが、アーキテクチャ上の結果は依然としてフォールスルー経路と制御転送対象の選択です。

よくある落とし穴

  • PC + Wのオフバイワン。 よくあるシミュレーターのバグは、命令幅が4バイトのアーキテクチャにPC + 1を使うことです。CPUは各命令の2バイト目を次の命令の先頭としてフェッチし、あらゆるプログラムをランダムデータのように扱います。
  • フェッチがメモリを読む前にPCを更新する。 メモリを読んでいる間、PCは現在のアドレスを保持しなければなりません。組合せ的にではなく、クロックエッジで更新してください。
  • リセットを忘れる。 リセット線のないPCは、電源投入時にフリップフロップがランダム化した値で起動します。必ずリセットを接続してください。
  • 無効なアラインメントへの分岐。 必要な命令アラインメントは、ISAと拡張に固有です。32ビット境界に揃った命令だけを持つ機械は0x1003のような対象を拒否しますが、16ビット符号化を持つISAはより多くの対象を許せます。普遍的な仮定を固定するのではなく、アーキテクチャの規則を適用してください。

DigiSimで作る

順次命令実行器テンプレートを開いてください。次が見えます。

  • フェッチ経路の先頭にあるPROGRAM_COUNTER_8BITブロック。
  • 各クロックサイクルで1を加えるよう固定配線された加算器。
  • PC + 1と制御装置が駆動する対象アドレスを選ぶMUX。
  • PCを0x00へ強制するリセットスイッチ。

クロックを手動で進め、PCが前進するのを観察してください。次に分岐命令を含むプログラムを読み込み、MUXが「順次」から「対象」へ切り替わる瞬間を見てください。その一つのビットが、最も純粋な形の制御フローです。

より大きな全体像でのPCの位置

プログラムカウンターは、より大きなCPUデータフローの一つのノードです。それが示す命令は、命令レジスタとデコード処理のために取り込まれます。算術結果は、フラグベース分岐設計が参照するCPUフラグを更新することがあります。

完全なループについてはフェッチ・デコード・実行のケーススタディを読み、その後に順次命令実行器テンプレートを読み込んで分岐命令を一ステップずつ実行してください。PCが初めて非順次に跳ぶのを見るとき、抽象化は図ではなく回路になります。

アイデアを実際に動く回路にしましょう。