[CS講座 #01] レジスタとフラグの正体 — なぜ8ビットで255までしか扱えないのか?

[CS講座 #01] レジスタとフラグの正体 — なぜ8ビットで255までしか扱えないのか?

はじめに

JavaScriptやPythonのような高位言語を触っていると、「変数に数値を代入する」操作に上限を意識することはほとんどありません。

しかし、ハードウェアのレベルに一歩足を踏み入れると、すべての数値は「物理的な配線の本数(ビット幅)」という強烈な制約を受けることになります。

本講座は、JavaScriptでZ80 CPUエミュレーターをフルスクラッチ実装した経験をもとに、「コンピューターが内部でどうやって数値を保持し、計算し、その結果を判断しているのか」を解き明かすシリーズです。

第1回となる今回は、CPUの最小記憶領域である「レジスタ」と、計算結果の状態を記録する「フラグ」の正体に迫ります。

Z80 EMULATOR PROJECT

6502 EMULATOR PROJECT

1. 8ビットの壁:なぜ255までしか扱えないのか?

1-1. ビット幅と数値表現

8ビットCPUにおける「8ビット」とは、CPU内部で一度に処理できるデータバスの配線が8本あることを意味します。1本の配線は「電圧が高い(1)」か「低い(0)」の2状態しか表現できません。

つまり、8ビットで表現できる状態の総数は 28=2562^8 = 256 通りとなります。

  • 符号なし8ビット整数(Unsigned 8-bit):0 〜 255 (0x00 〜 0xFF)
  • 255(1111 1111)に 1 を加算すると、9ビット目に溢れて オーバーフロー が発生し、0 に戻ります。

1-2. 負の数をどう表現するか?(2の補数表現)

8ビットの枠組みの中でマイナスの値を扱うため、コンピューターは「2の補数(2’s complement)」という仕組みを使用します。

最上位ビット(Bit 7)を「符号ビット(Sign Bit)」とし、1 であれば負数とみなします。

  • 0000 0001 = +1
  • 1111 1111 = -1
  • 1111 1110 = -2
  • 1000 0000 = -128

2の補数の最大の利点は、「加算回路だけで減算(引き算)が実現できる」点にあります。 例えば 1 + (-1) を計算してみます。

   0000 0001 (+1)
+  1111 1111 (-1)
--------------
 1 0000 0000 (9ビット目の1は捨てる -> 結果は 0)

特別な引き算回路を作らなくても、ビットを反転して1を足した値(2の補数)を加算するだけで、正しく 0 が導き出せます。

2. Z80のレジスタ構造と対レジスタ

Z80 CPUには、演算の主役となる8ビットレジスタが複数存在します。

レジスタ主な役割
A (Accumulator)演算の基本となるアキュムレータ。四則演算や論理演算の結果はここに入る
F (Flags)直前の演算結果の状態(キャリーやゼロなど)をビット単位で保持する
B, C, D, E, H, L汎用レジスタ。データの一時保存やカウント処理に使用

16ビット「ペアレジスタ」のテクニック

Z80は8ビットCPUですが、アドレス指定(64KB空間=16ビット必要)などのために、2つの8ビットレジスタを組み合わせて16ビットレジスタ(BC, DE, HL)として扱うことができます。

JavaScriptでこれを表現する場合、DataView や TypedArray の構造を活用するか、次のようにビットシフトを用いて合成・分解します。

// 8ビットレジスタ B と C から 16ビット値 BC を取得
const getBC = () => (b << 8) | c;

// 16ビット値 val を 8ビットレジスタ B と C に書き込み
const setBC = (val) => {
  b = (val >> 8) & 0xff;
  c = val & 0xff;
};

3. フラグレジスタ(F)の解剖学

CPUが命令を実行した直後、その結果が「0だったか」「溢れたか」「負数になったか」といったメタ情報が F(フラグ)レジスタ に保存されます。条件分岐(JP Z, addr や CALL C, addr など)は、すべてこのFレジスタのビットを参照して動きます。

Z80のFレジスタのビット配置は以下の通りです。

Bit:  7   6   5   4   3   2   1   0
    [ S | Z | Y | H | X | P/V| N | C ]

主要4大フラグの論理

  1. C (Carry Flag - Bit 0)
  • 加算で8ビット(255)を超えたとき、または減算で引ききれずに借り(Borrow)が発生したときに 1 になる。
  1. Z (Zero Flag - Bit 6)
  • 演算結果が完全に 0(0x00)になったときに 1 になる。
  1. S (Sign Flag - Bit 7)
  • 演算結果の最上位ビット(Bit 7)が 1(負数)のときに 1 になる。
  1. P/V (Parity / Overflow Flag - Bit 2)
  • Overflow (V): 符号付き演算で、正数同士の足し算なのに結果が負数になった場合など、符号の矛盾が起きたときに 1 になる。

4. エミュレーターでの実装(JavaScript)

実際にJavaScriptでZ80の8ビット加算命令(ADD A, r)とフラグ更新を実装したコード例を見てみましょう。

// CPU状態オブジェクト
class Z80Core {
  constructor() {
    this.a = 0; // Accumulator
    this.f = 0; // Flags (S Z Y H X P/V N C)
  }

  // フラグ操作用定数
  static FLAG_C = 0x01;
  static FLAG_N = 0x02;
  static FLAG_PV = 0x04;
  static FLAG_H = 0x10;
  static FLAG_Z = 0x40;
  static FLAG_S = 0x80;

  // ADD A, val 命令の実行
  addA(val) {
    const src = this.a;
    const res = src + val;
    const res8 = res & 0xff; // 8ビットに収める

    let flags = 0;

    // 1. Sign Flag (Bit 7が1ならセット)
    if (res8 & 0x80) flags |= Z80Core.FLAG_S;

    // 2. Zero Flag (結果が0ならセット)
    if (res8 === 0) flags |= Z80Core.FLAG_Z;

    // 3. Half Carry Flag (Bit 3からの繰り上がり)
    if (((src & 0x0f) + (val & 0x0f)) > 0x0f) flags |= Z80Core.FLAG_H;

    // 4. Overflow Flag (同符号同士の加算で結果の符号が変わったらオーバーフロー)
    if (((src ^ val) & 0x80) === 0 && ((src ^ res8) & 0x80) !== 0) {
      flags |= Z80Core.FLAG_PV;
    }

    // 5. Carry Flag (9ビット目に値があればキャリー)
    if (res > 0xff) flags |= Z80Core.FLAG_C;

    // 結果を格納
    this.a = res8;
    this.f = flags;
  }
}

このように、高位言語では1行で済む a + b という計算の裏側で、CPUは「繰り上がり」「符号の変化」「ゼロ判定」をビット演算(&, |, ^)を用いて正確に評価し、Fレジスタへセットしています。

まとめ

  • 8ビットCPU は 28=2562^8 = 256 通りの状態しか保持できず、0 〜 255(または -128 〜 127)の範囲で動作する。
  • 2の補数 を使うことで、CPUは加算回路だけで安全に引き算を実行できる。
  • フラグレジスタ(F) は直前の演算結果のメタ情報をビット単位で記憶し、次に来る条件分岐(IF 文の根拠)を提供する。

普段何気なく書いているプログラミング言語の条件分岐や変数代入も、すべてはこのレジスタとフラグの泥臭いビット操作の上に成り立っています。

次回は、「第2回:メモリ空間とバス制御 — CPUはどうやって外部と会話するのか?」をお届けします。

シリーズ目次

  • 第1回:レジスタとフラグの正体 — なぜ8ビットで255までしか扱えないのか?(本記事)
  • 第2回:メモリ空間とバス制御 — CPUはどうやって外部と会話するのか?
  • 第3回:Fetch-Decode-Execute — CPUが命を宿す無限ループ
  • 第4回:ビット演算の極意 — 複雑な命令セットを美しく捌く