- 6o6は保護機能の弱い NMOS 6502 上でさらに 6502 をソフトウェア実行し、古い 8 ビットシステムに制御可能な仮想実行レイヤーを追加するプロジェクト
- ゲストコードの命令実行とメモリアクセスを途中で制御し、アドレス再マッピング、不正な読み取り/書き込みの遮断、jam opcode トラップのような機能を提供
- 中核はホスト 6502 の ALU をそのまま借用する方式で、ゲストレジスタとフラグをホストに載せて同じ命令を実行し、結果を再保存する
- 検証には Klaus Dormann の 6502 functional test suite と lib6502 ベースのテスト環境が使われ、最適化構成は 1,602,516,769 命令で非最適化構成より 36.5% 少ない命令を実行
- 同時公開された The Incredible KIMplement 1.0 と複数の例は、KIM-1 エミュレーション、ネストした仮想化、タスク切り替え、geoRAM ベースの外部メモリシステムまで 6o6 の活用範囲を示している
6o6 と KIMplement が公開したもの
- The Incredible KIMplement 1.0 は 1KB・1MHz の MOS/Commodore KIM-1 6502 シングルボードコンピュータをエミュレートする
- 拡張なしの Commodore 64 で動作する
- KIM の内蔵 TTY をサポートし、実機のシリアルポート経由でもアクセス可能
- アドレス空間は 16K に拡張されている
- 6o6 は “6502-on-6502” の略で、6502 CPU 上で動作する完全なソフトウェア NMOS 6502 仮想 CPU
- ゲストコード実行を制御する
- 文書化されていない opcode と jam opcode をトラップする
- すべてのメモリアクセスを抽象化する
- アドレス再マッピング、不正な読み取り/書き込みの横取り、仮想メモリベースの実行をサポートする
- Commodore 64 と Apple IIe でゲスト
hello world を実行し、さらに 6o6 の中で 6o6 を実行する ネストした仮想化 も動作する
- stage 1 はほぼ即時に実行される
- stage 2 はより遅い
- stage 3 は非常に遅いが動作する
6502 に仮想化が必要な理由
- 初期のパーソナルコンピュータでは通常 1 つのプログラムがマシン全体を制御しており、プログラムが誤動作しても再起動で対処できた
- マルチユーザーやマルチタスク環境では、誤ったコードが他のアドレス空間を破壊したり、危険な命令を実行したり、資源を独占したりする問題が生じる
- NMOS 6502 は約 4,000 個未満のトランジスタで構成された単純な CPU のため、保護機能には限界がある
- 多くの歴史的な NMOS 6502 システムでは zero page やプロセッサスタック位置を自由に移動しにくかった
- 任意の位置へコードアドレスを再マッピングして fixup なしで実行する機能がなかった
- 特定メモリ位置へのアクセスを包括的に禁止できなかった
- 文書化されていない jam または
KIL opcode が実行されるとプロセッサが完全停止することがあった
- 一部の問題はハードウェアで緩和できる
- NMI を定期的に発生させれば、割り込みフラグを設定してシステム独占を狙うプロセスを外部から停止できる
- 一部の 6502 マルチタスクカーネルはこの方法でプリエンプティブなタスク切り替えを実装した
- Eastern House Software の “Trap65” のような in-circuit emulator は不正な opcode をトラップ可能な BRK に置き換えられたが、高価で複雑なバス操作には限界があった
6o6 の実行方式
- 単純なインタプリタ方式も 6502 では実用的になりうる
- 6502 はレジスタ数が少ない
- 命令は 56 個で、アドレッシングモードもそれほど多くない
- プロセッサ状態の追跡が比較的容易
- 6o6 の「仮想化」の要点は、ホスト 6502 の ALU をゲスト内部演算に使うことにある
- ゲストのアキュムレータとフラグをホスト CPU にロードする
- ゲストが実行するのと同じ命令をホストで実行する
- 結果とフラグを保存し、ホスト状態を整理する
- 算術処理やフラグ処理を直接再実装しなくてよいことがこの方式の利点
- decimal mode、つまり BCD 演算も自然に動作する
- 実際の 6502 が計算するため、結果は 6502 と一致する
- メモリ値の読み取りやレジスタ転送時も、負フラグと zero フラグ処理のために同じ方式を使う
- 実装には 自己書き換えコード が使われるため、ROM に入れる場合は別途注意が必要
VM、ハーネス、カーネル構造
- 6o6 VM はシステム全体ではなく エンジン の役割を担う
- 実行環境は 3 つの部分に分かれる
- VM: 実際の 6502 上で動作するハードウェア非依存の仮想 CPU
- ハーネス: ゲストメモリと管理対象ハードウェアへのインターフェース
- カーネル: VM を呼び出し、例外とゲスト状態を処理する制御ループ
- ハーネス は標準化された jump table でバイナリインターフェースを提供する
- 特定アドレスの load/store を実装する
- instruction fetch を処理する
- ハードウェアスタックとスタックポインタを維持する
- VM はページサイズやメモリページングの有無を前提にしない
- ハーネスは単純な加算・ビットシフトベースのアドレス変換からページド仮想メモリまで実装できる
- page fault を外部へ渡さず、ハーネスが load/store の過程で paging in/out を実行できる
- 保護例外もハーネスが発生させられる
- カーネル は VM 実行を開始し、VM が返した状態コードを解釈する
- ハーネスや 6o6 自体で発生した例外を処理する
- ゲストレジスタや PC を検査または修正する
- 特定のサービスルーチンをネイティブに処理できる
- VM 呼び出しの合間はゲスト CPU が「停止」状態なので、状態キャプチャやコンテキストスイッチが可能
- VM は仮想 IRQ、NMI、reset を直接発生させない
- こうしたイベントの発生タイミングはカーネルが決める
- BRK はサポートされるが、VM はスタックを設定して新しい PC へ移動するのではなく例外を返す
KIMplement における 6o6 活用
- KIMplement のハーネスは標準 6502 スタックと KIM-4 拡張装置を仮想化する
$0000-$17ff: 読み書きメモリ
$1800-$1fff: ROM
$2000-$3fff: RAM
$4000-$fff7: 未マップの書き込み不可領域
$1ff8-$1fff はベクタ用として $fff8-$ffff にミラーされる
- ホスト Commodore 64 は下位 16K を
$4000-$7fff に保持し、それ以外はハーネスが合成する
- KIMplement カーネルは RRIOT エミュレーション、LED 表示、TTY サービス、stop と Single-Step Switch 用 NMI 挿入、KIM-1 ROM モニタの一部トラップを処理する
- VM 実行後、KIMplement カーネルは 6o6 の PC を調べて現在のルーチンを横取りするかどうかを判断する
性能最適化
- 6o6 とハーネス間の呼び出しは大きなボトルネックになりうる
- 単純な命令でも最低 1 回の fetch が必要
- 間接アドレッシングはさらに多くのメモリアクセスを生む場合がある
- KIMplement 0.2 ではメモリ load の一部をプリプロセッサマクロで インライン化 した
- 任意仮想アドレス用の load ルーチンと zero page 最適化 load ルーチンを VM に直接接続した
- 速度は大きく改善したが VM サイズは増えた
- store は頻度が低くより複雑なためサブルーチン呼び出しのまま維持した
- 最新の反復では、インラインマクロが program counter にアクセスする方法の非効率も整理し、instruction fetch をさらに改善した
- KIMplement 0.3 では “extra helpings” と呼ぶ原始的な instruction fusion を追加した
- メモリに触れない命令はカーネルへ即座に戻る必要がない
- 即値命令、アキュムレータ中心命令、大半の implied 命令、分岐しなかった branch が対象
- load/store、PC の非順次変更、例外が起きると、VM は命令束ねの試行をやめる
- extra helpings は VM 自体を高速化するわけではない
- KIMplement のように PC 位置で機能を制限する場合はやや遅くなることもある
- その代わり、カーネルが観測すべき変化のない命令ごとに不要に実行されないため、システム全体の他部分が高速化する
- PC を精密制御したいアプリケーションでは妨げになることがあり、段階的または完全無効化のオプションがある
検証とテスト結果
- 検証には Klaus Dormann の functional test suite が使われた
- 提供バイナリはハードウェアへの前提を持たない
- 成功終了は特定位置の無限ループで通知する
- テストは Ian Piumarta の lib6502 CPU emulator を使い、シェルから直接実行できるよう構成された
- lib6502 は decimal mode のエッジケースのため当初失敗したが、パッチ後に通過した
- Klaus 提供バイナリは全体で 64K のため、標準 6502 アドレス空間内に 6o6 と同居させられなかった
- lib6502 に 32K の bank-switched 最小システムパッチを追加した
$7000-$efff 領域を使い、テストバイナリ前半 32K と後半 32K を別バンクに置いた
- 3 つの構成でテストした
- extra helpings なし、inline fetch macro なし
- inline fetch macro あり、extra helpings なし
- inline fetch macro と extra helpings の両方あり
- 3 構成すべてが Klaus suite を通過した
- 命令数の結果は以下の通り
- 6o6 なしの lib6502: 30,646,178 命令
- 最適化なしの 6o6: 2,188,322,914 命令
- inline fetch macro 適用: 1,713,350,225 命令
- inline fetch macro と extra helpings 適用: 1,602,516,769 命令
- 最速の 6o6 構成は、もっとも最適化の少ない構成より 36.5% 少ない命令 を実行した
- 最速構成はゲスト命令 1 つあたり平均 52.3 命令を実行した
- この値にはハーネス、カーネル、6o6 実行がすべて含まれる
- 命令ごとに cycle count が異なるため、速度倍率として解釈すべきではない
含まれる例
- hello world 例は、同じプログラムを最初にネイティブ CPU で実行し、その後 6o6 経由で実行する
- Commodore 64 では
$ffd2、Apple II では $fded の文字出力ルーチンにマップされる
- カーネルは PC が文字出力ルーチンを指すのを検知すると、ゲストのアキュムレータを取得してネイティブ ROM ルーチンを呼び出し、スタックから return address を取り出してループへ戻る
- inception 例は、同じハーネスとカーネルで 6o6 が自身をペイロードとして実行する
- 各 stage は固有の zero page と stack を持つ
- 現在の 6o6 は自己書き換えコードを使うため、stage ごとに VM のコピーが必要
- stage 3 ではメモリの大半が VM 3 コピーに使われ、inline fetch macro を含む VM は 1 個あたり 10KB 超
- ネスト実行での
CHROUT 呼び出しは、stage 3 から stage 2、stage 1 を経由して最後にネイティブルーチンへ渡される
- ペイロード終了には RTS 命令を「キック」のように使う
- 開始時スタックに return address がないため、RTS は stack underflow を引き起こす
- ハーネスがこれを例外として報告すると、カーネルは正常終了として扱う
- 深い stage でも同じ方法で上位カーネルまで伝播する
- Apple II では実行後
CALL 2051、Commodore 64 では RUN で再実行できる
- Apple II 版は
$9000 より上の常駐 DOS 領域まで使用するため、実行後は再起動が推奨される
タスク切り替え例
- tasks 例は、2 つの独立タスク間を往復する小さなタスクスイッチングカーネル
- 各タスクは自分専用の zero page、stack、小さなコードアドレス領域を持ち、互いにも VM の存在にも気づかない
- 1 つのタスクはアルファベットを表示し、もう 1 つは数字を表示する
- 数字は視覚的に区別するため reverse video で表示される
- キーを押すたびにタスクが切り替わる
- 2 つのタスクは zero page の同じ位置を状態保存に使うが、zero page が独立しているためそれぞれ最後の位置から続行される
- コンテキストスイッチに必要なのは、現在タスク情報と各タスクの A、X、Y、P、S、PC 状態保存領域だけ
- ハーネスは「on CPU」のタスクを見て zero page、stack、実行コードの物理アドレスを選ぶ
- カーネルは切り替え時に別の状態を保存・ロードし、別タスクを「on processor」として記録する
geoRAM ベース 64K 外部メモリ例
- vmgr 例は Commodore 64 専用で、システム自身の RAM を使わない 64K アドレス空間を外部メモリとして提供する
- geoRAM は Commodore 公式 REU とは異なる paged RAM デバイス
- REU は DMA 中心で、MOS 8726 REC を用いて main memory に対し読み取り・書き込み・交換を行う
- geoRAM は I/O 範囲
$de00 の 256 バイト window page にメモリをマップする
- 制御レジスタは
$dffe、$dfff にある
- 現代の互換 clone は最大 4MB 容量もある
- VICE は geoRAM エミュレーションをサポートする
- 例では RC2014 Z80 kit computer 向けに提供されていた 6502 processor module の ROM を使う
- ROM には monitor と Lee Davison の EhBASIC が入っている
- 使用 ROM は GitHub にある pre-built ROM で、6551 版を使う
- ハーネスは guest ROM 領域である
$c100 以降への write を無視する
- 16K 未満の書き込みは高速経路を使う
- それ以上は mask と shift で geoRAM bank を調整する
- 現在の geoRAM page をキャッシュし、同一 page へのアクセスでは設定処理を省く
- カーネルと main program はこの例では 1 つに統合されている
- geoRAM の存在と動作を確認する
- ROM イメージを geoRAM にコピーする
- BRK は monitor に戻す
- illegal instruction、user-defined instruction trap なども BRK と同様に扱う
- RC2014 ROM の serial vector を横取りして簡易ターミナルをエミュレートする
- PETSCII とターミナル文字の変換を行う
- 小さなカーソルを維持する
- ゲストレジスタとフラグを結果に合わせて調整する
CTRL-SHIFT-Commodore でエミュレートされたシステムを reset でき、メモリは保持される
- EhBASIC の cold start では、メモリサイズを手入力しないと C64 と geoRAM の組み合わせで 32768 バイト free を見つけるまで約 1 分かかる
- ROM は
$8000 hard cap でビルドされているため、実際にはもっとあっても 32768 バイトに制限される
$8000-$c0ff には別のものを置ける
- EhBASIC は小文字 command や keyword を受け付けないため、すべて大文字で入力する必要がある
- 実機 Commodore 128DCR と 512K geoRAM cartridge でも動作する
- 浮動小数点演算も正常動作する
- bad instruction は制御された形で即座に横取りされる
- 256 バイト window を除けば、画面上のシステムは 6502 自体のアドレス空間で動作していない
- 512K geoRAM でも 64K 6502 システムタスクを 8 個個別に置ける
今後の改善と用途
- 6o6 を ROM 上で実行可能にする改善は可能だが、リファクタリングが必要で、さらに遅くなる可能性があるため選択肢に近い
- 65816 エミュレーションは範囲外とされるが、NMOS システムで CMOS 命令をエミュレートすることは可能かもしれない
- ALU を使うため、NMOS 6502 が CMOS 65C02 をエミュレートしてもフラグは NMOS 方式で設定される
- 逆方向でも同様
- アドレッシングは現在 NMOS CPU 方式で書かれている
- inline memory macro 方式には peephole optimization の余地がある
- 実際の assembly 前に「post-preprocessor」pass を置ける
- ツールチェーンがより複雑になるため、一般的な利得の確認が必要
- ダウンロードしたコードを現在作業を壊さずに実行する用途は、6o6 の明示的な活用先の 1 つ
- Gopher client の一部として使い、ダウンロードしたものを動的実行するアイデアがある
- 新しい 6502 システムを自作するなら、必要機能をハードウェアで実装する方が速いかもしれない
- NMOS CPU で保護機構が少ない、あるいは追加シリコンを最小化したい場合、6o6 は柔軟で適応性の高い代替手段になる
配布とライセンス
- The Incredible KIMplement は ホームページ と GitHub で提供されている
- 6o6 は GitHub で提供され、記事で扱った 4 つの例も含まれる
- KIMplement 1.0 アップデートは公開向け整理と小さなバグ修正が中心
- KIMplement には Dave Hassler 提供の Tiny PILOT も含まれる
- Tiny PILOT は 1979 年の MICRO magazine に Nicholas Vrtis が書いた実装に、Bob Applegate と Dave Hassler のパッチが加えられたもの
- Dave Hassler は Carol Shaw と Harry Stewart による 1980 年 Atari PILOT 実装から ELIZA も移植した
- KIMplement と 6o6 はどちらも Floodgap Free Software License で配布される
1件のコメント
Hacker Newsのコメント
6502 はシンプルで制約も多いのに、ほぼ50年前のアーキテクチャが今なお新たな限界まで押し広げられていくのを見るのは、いつも興味深い
超低価格・大量生産市場向けの一部SoCには、今でも6502コアが載っている
10セントのRISC-Vコアに勝つのは簡単ではなさそう
最初は6502コアが6502個入ったマルチコアSoCを想像して笑ったけれど、FPGAでやれば面白いプロジェクトになりそう
Apple 2で16ビット後継の 65816 を可変速の拡張カードとして挿して使っているが、ほとんどは8ビットモードで動かしている。問題なく動くし、ライブラリコードの大半も8ビットだからだ
この速度域ではチップは高速だ。特にRAMとCPUが1:1でクロックされる単純なモデルを考えると、なおさらそうだ。私の場合は1MHzバス経由でコードを実行できるので、多くのマルチサイクル演算がメモリfetchあたり単一バスサイクルのようになる
あるいはカードに1MBのRAMがあり、このRAMはCPU速度(0.15〜16MHz)で動作する。こうすると高級言語で書かれた大きなプログラムでも、実用的な速度で動かせるだけの速さになる。もちろんアセンブリはとんでもなく速い
いろいろハックしてみるにはかなり楽しい環境だ
GEOSのウィンドウの中で GEOS を実行できるのか?
この記事はしばらくコメントなしで上がっていたので、あとで見てみようと思っていたが、要点はCommodore 64がまったく別の6502ベースのシステムをエミュレートする仕組みにある
“6o6”、つまり “6502-on-6502” は、6502 CPU上で動作する完全な仮想化ソフトウェアNMOS 6502 CPUで、文書化されていないopcodeやjam opcodeトラップまで含めてゲストコードの実行を完全に制御し、メモリアクセスもすべて抽象化している
そのためアドレスの再マッピング、不正な読み書きのフック、さらには完全な仮想メモリ実行まで可能だ。完全な機能テストに合格するだけでなく、自分自身を仮想化する自分自身まで仮想化できるというのだから、6502の観点を超えて、どの観点から見てもすごい仕事だ
Zilog Z80に保護モードがあるという動画も思い出した: https://www.youtube.com/watch?v=DLSUAVPKeYk
最初に 6502アセンブリ を独学していた頃を思い出す。“The Visual Computer” という本があって、フロッピーディスクにエミュレータが付属していたのだが、本当に目が開かれる体験だった
本のPDFは見つけたが [1]、フロッピーに入っていたソフトウェアがどこかに残っているかは分からない
[1] https://files.commodore.software/reference-material/books/c6...
“Visual 6502” は現代のゲート・トランジスタレベルの6502シミュレータの名前だ: http://visual6502.org/JSSim/index.html
https://archive.fo/2u3Y8