[CS講座 #07] 高級言語によるHLEとレガシーの再現 — BIOS不要起動(Direct Boot)のからくり

[CS講座 #07] 高級言語によるHLEとレガシーの再現 — BIOS不要起動(Direct Boot)のからくり

はじめに

【実践編】も折り返しを迎える第7回テーマは、エミュレーター開発において避けては通れない技術的・法的ハードル、「BIOS(Basic Input/Output System)」の攻略です。

実機のゲーム機やコンピューターを完全に模倣しようとすると、ハードウェアの電源投入直後に実行されるファームウェア(BIOS ROM)が必須となります。しかし、著作権で保護された実機BIOSのバイナリファイルをユーザーに準備させる設計は、Webブラウザ上で手軽に遊べるポータブルなエミュレーターを作る上で大きな障害となります。

この問題を解決するのが、高級言語側でBIOSの処理を代行する HLE(High-Level Emulation) と、BIOSの初期化手順をスキップしてROMを即座に立ち上げる Direct Boot(ダイレクトブート) の技術です。

今回は、ColecoVisionやSega Master System(SMS)などのレトロハードを題材に、BIOSレス起動のカラクリとJavaScriptによるシステムコール・ハイジャックの実装手法を解き明かします。

前回の記事

Z80 EMULATOR PROJECT

6502 EMULATOR PROJECT

1. LLE vs HLE:抽象化レベルによる2つのエミュレーションアプローチ

エミュレーターの設計思想は、再現するレイヤーの深さによって大きく2つに分類されます。

[LLE (Low-Level Emulation) - 低レイヤー再現]
 CPU  -->  [ BIOS ROM (機械語命令) ]  -->  ハードウェア操作
  * 実機のBIOSバイナリを1命令ずつFetch-Decode-Executeで愚直に実行する。

[HLE (High-Level Emulation) - 高レベル機能再現]
 CPU  -->  [ JS側HLEフック (システムコール代行) ]  -->  JS側でメモリ/レジスタを書き換え
  * BIOSの処理結果や目的(例: 文字列描画、VRAM転送)を高級言語で直接計算して完了させる。

A. LLE(Low-Level Emulation)

  • 原理: 実機に搭載されているファームウェア/BIOS ROMの機械語を、CPUコアが1命令ずつ解釈してそのまま実行します。
  • メリット: 実機の挙動を100%忠実に再現でき、未確認のタイマー処理やBIOS内のバグを利用した特殊なソフトも動作します。
  • デメリット: 著作権のある実機BIOSファイル(ROM吸出しデータ)が別途必要になります。また、BIOSの初期化ループやメモリチェックを律儀に実行するため、起動までに無駄な時間がかかります。

B. HLE(High-Level Emulation)

  • 原理: BIOSが提供する「システムコール(API)」や「初期化ルーチン」を検知し、その処理内容をJavaScriptなどの高級言語側のプログラムで直接代行・計算して結果だけをCPUへ書き戻します。
  • メリット: 著作権のある実機BIOSが一切不要(完全リーガル)になり、起動時間をゼロに短縮できます。また、CPUの無駄な計算ステップを省略できるため非常に軽量・高速動作します。
  • デメリット: BIOS内部の細かいタイミングや undocumented な挙動に依存しているゲームソフトで互換性問題が発生するリスクがあります。

2. BIOS不要起動(Direct Boot)のからくり

電源を入れたとき、実機のBIOS ROMは具体的に何を行っているのでしょうか?

ColecoVisionやMaster Systemなどの8ビットハードにおいて、実機BIOSの役割は主に以下の4つです。

1. レジスタの初期設定  : SP(スタックポインタ)の設定、各レジスタの初期化
2. 周辺機器の初期化    : VDP(画面)、PSG(音源)のレジスタ初期値の書き込み
3. RAM空間の初期構築    : システム用ワークRAMのクリア、スプライト属性テーブルのセット
4. カセットROMの検証    : ROMヘッダのタイトル表示やチェックサム検証後、ROMのエントリーポイントへジャンプ

Direct Boot(ダイレクトブート)の動作原理

Direct Bootとは、上記の1〜4の処理をエミュレーターの初期化処理(JavaScript)の中で一瞬で完了させ、CPUのプログラムカウンタ(PC)をROMの開始アドレスへ直にセットして実行を開始する手法です。

 [実機起動フロー (LLE)]
 電源ON -> BIOS実行(1~3秒) -> ロゴ画面表示 -> メモリ初期化 -> カセットROMへジャンプ

 [Direct Bootフロー (HLE)]
 電源ON -> JS側でレジスタ/VRAM初期値を1ミリ秒でセット -> PC = ROMエントリーポイント (即起動!)

CPUからは「あたかもBIOSが正しく実行され、初期化が完了した直後の状態」に見えるため、ゲームソフトは何の疑いもなくメイン処理を開始できます。


3. システムコールのフック(HLEトラップ)の仕組み

ゲームソフトによっては、起動時だけでなくゲームプレイ中もBIOS内の関数(サブルーチン)を呼び出す(CALL $001F や RST $10 など)設計になっているものがあります。

これを実機BIOSなしで動作させるため、「CPUが特定のBIOSアドレスをフェッチしようとした瞬間にJavaScript側で割り込んで処理を横取り(フック)する」 トラップ構造を構築します。

HLEフックの実行手順

  1. アドレス検出: CPUの命令実行ループ(step())で、PC(プログラムカウンタ)がBIOS関数アドレス(例: 0x1F7D)を指したかを判定する。
  2. 処理の代行: CPUで命令を実行させず、JavaScript側でZ80のレジスタ(A, HL, DE など)から引数を読み取り、同等の処理(例: メモリ転送、フォント展開)を実行する。
  3. レジスタ復帰とRET: 戻り値となるレジスタを書き換え、スタックから戻り先アドレスをPOPして PC を更新(RET 命令の動作をJavaScript側で模倣)する。

この仕組みにより、Z80 CPUは1命令もBIOSコードを実行することなく、1クロックのオーバーヘッドのみでBIOSシステムコールを完了できます。


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

ColecoVisionおよびSG-1000/SMS環境を想定した、Direct BootとBIOSシステムコール・フックの具体的な実装例です。

Direct Boot & HLEトラップの実装コード

export class DirectBootEmulator {
  constructor(cpu, bus) {
    this.cpu = cpu;
    this.bus = bus;
  }

  // BIOSレスでゲームROMを即座に起動する(Direct Boot)
  bootROM(romData) {
    // 1. カセットROMをアドレス空間(0x8000〜)へマッピング
    this.bus.loadROM(romData);

    // 2. 実機BIOSが完了した時点の「CPUレジスタ初期状態」を再現
    this.cpu.sp = 0x73ef; // スタックポインタをワークRAM最上部へ
    this.cpu.a = 0x00;
    this.cpu.f = 0x00;
    this.cpu.h = 0x70;
    this.cpu.l = 0x00;

    // 3. VDP(画面チップ)レジスタの初期値を直接セット
    this.bus.vdp.writeRegister(0, 0x00); // モード選択
    this.bus.vdp.writeRegister(1, 0xe0); // 画面有効化 / 16KB VRAM
    this.bus.vdp.writeRegister(2, 0x0e); // パターンネームテーブルアドレス
    this.bus.vdp.writeRegister(3, 0x9f); // カラーテーブルアドレス
    this.bus.vdp.writeRegister(4, 0x00); // パターンジェネレータテーブル
    this.bus.vdp.writeRegister(5, 0x36); // スプライト属性テーブル
    this.bus.vdp.writeRegister(6, 0x07); // スプライトジェネレータ

    // 4. システム領域(RAM)の初期化と基本フォントの展開(HLE)
    this.setupSystemRAM();

    // 5. ROMのヘッダからエントリーポイント(開始アドレス)を取得してPCに直接セット
    // 例: ColecoVisionの場合、0x800A〜0x800B に開始アドレスが記録されている
    const entryPoint = this.bus.readByte(0x800a) | (this.bus.readByte(0x800b) << 8);

    // エントリーポイントへ直接ジャンプ!
    this.cpu.pc = entryPoint;

    console.log(`Direct Boot完了: 0x${entryPoint.toString(16)} から実行を開始します`);
  }

  // システムRAMへBIOS代行データを注入
  setupSystemRAM() {
    // ワークRAMのクリア (0x7000 - 0x73FF)
    for (let addr = 0x7000; addr <= 0x73ff; addr++) {
      this.bus.writeByte(addr, 0x00);
    }
  }

  // 1ステップ実行とBIOSフックの判定
  step() {
    // --- BIOS HLE フック判定 ---
    // PCが特定のBIOS関数アドレス(例: 0x1F7D = スプライト更新ルーチン)に達した場合
    if (this.cpu.pc === 0x1f7d) {
      this.hleUpdateSprites();
      return 10; // HLE処理消費クロック
    }

    // 通常の CPUステップ実行
    return this.cpu.step();
  }

  // BIOSの「スプライト更新処理」をJavaScriptで超高速代行
  hleUpdateSprites() {
    // レジスタ HL が指しているRAM上のスプライトテーブルを参照
    const hl = (this.cpu.h << 8) | this.cpu.l;

    // JS側で一括してVRAMのスプライト属性テーブルへ高速転送
    for (let i = 0; i < 128; i++) {
      const data = this.bus.readByte(hl + i);
      this.bus.vdp.writeVRAM(0x1b00 + i, data);
    }

    // RET 命令の代行(スタックから戻り先PCを復元)
    const low = this.bus.readByte(this.cpu.sp);
    const high = this.bus.readByte(this.cpu.sp + 1);
    this.cpu.sp = (this.cpu.sp + 2) & 0xffff;
    this.cpu.pc = (high << 8) | low;
  }
}

このアプローチにより、著作権のある実機BIOS ROMを一切保持することなく、完全に合法で超高速なレトロハード・エミュレーション環境をWebブラウザ上に構築できます。


まとめ

  • LLE(Low-Level Emulation) は実機BIOSバイナリを忠実に実行するが、BIOSの著作権問題と起動の遅さが課題となる。
  • HLE(High-Level Emulation) はBIOSの初期化やシステムコールの処理結果を高級言語側で代行・計算する手法。
  • Direct Boot(ダイレクトブート) は、CPUレジスタやVDPの初期化をJavaScript側で即座にセットし、ROMのエントリーポイントへPCを直接書き換えることでBIOSレス起動を実現する。
  • BIOSのサブルーチン呼び出しは、step() ループ内で PC のアドレスをトラップし、JS側で高速代行処理を行った後に RET 動作(スタック復帰)させることで互換性を維持できる。

次回は、【実践編】の締めくくりとなる「第8回:クロック同期とメインループ — Webブラウザ上で正確な周波数を刻む」をお届けします。


シリーズ目次

  • 【基礎編】
  • 第1回:レジスタとフラグの正体 — なぜ8ビットで255までしか扱えないのか?
  • 第2回:メモリ空間とバス制御 — CPUはどうやって外部と会話するのか?
  • 第3回:Fetch-Decode-Execute — CPUが命を宿す無限ループ
  • 第4回:ビット演算の極意 — 複雑な命令セットを美しく捌く
  • 【実践編】
  • 第5回:割り込み(Interrupt)の仕組み — 非同期イベントを検知するハードウェアの割り込み
  • 第6回:メモリバンキング(Bank Switching) — 64KBの壁を超えて巨大ROMを読み込む
  • 第7回:高級言語によるHLEとレガシーの再現 — BIOS不要起動(Direct Boot)のからくり(本記事)
  • 第8回:クロック同期とメインループ — Webブラウザ上で正確な周波数を刻む(次回)