[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によるシステムコール・ハイジャックの実装手法を解き明かします。
前回の記事
[CS講座 #06] メモリバンキング(Bank Switching) — 64KBの壁を超えて巨大ROMを読み込む // PROTOCOL.LAIN
16ビットアドレス空間の制限、物理メモリと論理メモリのマッピング原理、MSXのメガROMマッパー(Konami/ASCII)やセガのバンク切り替え構造、JavaScriptエミュレーターでの参照切り替えによる$O(1)$メモリアクセス実装コードを解説。
lain-lab.comZ80 EMULATOR PROJECT
Z80 EMULATOR PROJECT
フルスクラッチ JavaScript Z80 コアと、その上に構築されたレトロシステムエミュレーター群
lain-lab.com6502 EMULATOR PROJECT
6502 EMULATOR PROJECT
フルスクラッチ JavaScript MOS 6502 コアと、その上に構築されたレトロシステムエミュレーター群
lain-lab.com1. 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フックの実行手順
- アドレス検出: CPUの命令実行ループ(
step())で、PC(プログラムカウンタ)がBIOS関数アドレス(例:0x1F7D)を指したかを判定する。 - 処理の代行: CPUで命令を実行させず、JavaScript側でZ80のレジスタ(
A,HL,DEなど)から引数を読み取り、同等の処理(例: メモリ転送、フォント展開)を実行する。 - レジスタ復帰と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ブラウザ上で正確な周波数を刻む(次回)