借り初めのひみつきち

仮ブログです。

WASM自作PCエミュレータ制作日記/5

ついに某OSの起動画面が出ました。

f:id:neriring16:20190721020613p:plain

まだ動作が怪しいですが...

この辺ならキーボードも普通に動きます。(マウスは死んでます

f:id:neriring16:20190721020631p:plain

某OSも途中で止まります。
cpuid実装してるはずなのに認識されてないのが気になります。

f:id:neriring16:20190721020652p:plain

FreeDOS (32bit) はまだよくわからない動作をします。

f:id:neriring16:20190721020703p:plain

WASMで自作PCエミュレーター Part4

自作エミュレーターVGA に(一部)対応しました!

github.com

今までは UART 経由で端末エミュレーターに接続していました。
DOS やその上で動く多くのソフトウェアは標準入出力という概念があるので、今まではこの方法が簡単に実装できてうまく動いていました。
しかし、世の中には VRAM を直接操作してテキスト表示したり、グラフィックスを扱うソフトもたくさんあります。
また、使っていた端末エミュレーターにも色々と不満が出てきていたので置き換えたいと考えていました。
そんなわけで VGA の実装をはじめたのでした。

V・G・A!

VGA を実装するにあたって問題がひとつありました。このエミュレーターMMIO に対応していません。
要するに VRAM にメモリアクセスしたことを検知して画面を更新することができません。

UART は IO 命令を契機に画面更新ができましたが、それができなくなりました。
画面のレンダリングはとても重い作業なので毎フレーム再描画するわけにもいきません。
解決策としては一定間隔で VRAM 領域をスキャンして変更があった場合に再描画するようにしました。
徹夜で調整した結果、まぁまぁのパフォーマンスが出てるようです。

モード 3 とモード 13 のみ実装しています。モード 12 は VGA = 640x480 を象徴するメジャーな画面モードのひとつではありますが、 MMIO がないと実現不可能なのでおそらく未来永劫実装しません。

PS/2 と JS のキーボード処理の闇

端末エミュレーターを廃止したのでキーボード入力もきちんと実装する必要が出てきました。
JavaScript ではキーイベントで結構細かいところまでキー入力の値が取れるのですが、そこは闇だらけでした。
環境によって取れる値が異なったり、別のキーに対して同じキーコードが割り当てられていたりします。
さらに、英語配列を基準にキーコードを決めてるようで、日本語モードだと記号の対応がぐちゃぐちゃでした。
完全に PS/2 キーボードをエミュレーションしようとすると記号キーの対応が死んでしまうので、 ASCII コードが取れるところは文字が入力したいと判断して ASCII コードを優先するようにしました。

TL; DR

これらの改修でゲームなんかもそれなりに動くようになりました。

f:id:neriring16:20190715001621p:plain

f:id:neriring16:20190715000906p:plain

f:id:neriring16:20190715001010p:plain

f:id:neriring16:20190715001050p:plain

ロングモードと64ビットモードの違い

ロングモードと64ビットモードの違い、分かりますか?

おそらく多くの人が混乱していると思うのでブログにまとめます。(そしてぼくは混乱してないことを祈る

極論を言うと、現代の x86 系プロセッサにはリアルモード、プロテクトモード、ロングモードの3つのモードしかありません *1
仮想8086モード、互換モード、64ビットモードなどというモードは存在しないのです。(え?

リアルモード、プロテクトモード、ロングモードの3つのモードを切り替えるにはシステムレジスタの CR0 の特定のフラグを操作する必要があります。逆にいうとそれ以外の方法では切り替わりません。

では、仮想8086モード、互換モード、64ビットモードはどうでしょう?
これらは eflags やコードセグメントのフラグを操作することで切り替わります。
つまり、割り込みで切り替わります。

一方、リアルモード、プロテクトモード、ロングモードはそれぞれ割り込みの仕組みが違います。(IDT からディスクリプタをロードする大まかな仕組みはよく似ていますが・・・)
割り込みでこれらのモードを行き来することはできません。

ここに大きな違いがあります。

システムとしてはリアルモード、プロテクトモード、ロングモードのどれかで動作しているのに対し、仮想8086モード、互換モード、64ビットモードの3つは単なるコンテキストの違いでしかありません。

最初の x86 系プロセッサの 8086 には制御フラグが flags レジスタしかありませんでした。
このレジスタの中には演算命令の結果でばたばた変わるフラグもあれば、割り込みを禁止するとても危険なフラグまですべてひとつのレジスタで管理していました。
今考えるととてもおろそしい仕様ですが、当時はこれが普通だったようです。

8086 が成功してマルチタスクのための大幅な拡張を施した 80286 が出たとき、この常識が覆されました。
システムの基本的な制御は MSW というレジスタで管理し、 flags レジスタはコンテキストごとの状態を保持するように明確に分離されました。
のちに MSW は 80386 が出たときに CR0 という名前に変わりましたが、基本的な路線は踏襲されました。

仮想8086モードに移行するには必ずプロテクトモードから切り替える必要があり、例外や割り込みが発生するとプロテクトモードの仕組みを使って基本的にはプロテクトモードに戻ります。*2
このことから eflags で切り替わる仮想8086モードはプロテクトモードで動作するシステムの中のコンテキストのひとつということがわかります。

一方、互換モードと64ビットモードはセグメントディスクリプタの L ビットの値で切り替わります。
ロングモードに移行直後は互換モードですが、例外や割り込みが発生するとロングモードの仕組みを使って64ビットモードに切り替わります。
やはりこの2つはロングモードで動作するシステムのコンテキストのひとつです。

プロテクトモードの時代にも16ビットで動作するモードと32ビットで動作するモードがありましたが両者を区別する正式な名前はありませんでした。*3
ロングモードの時代になってわざわざ64ビットモードだけ特別扱いするのはどうなのかな?と思いました。

*1:VMM や SMM は除く

*2:あくまで基本的なので割り込みでプロテクトモードに戻らない拡張機能があるし、タスクゲートで仮想8086モードのタスクに切り替えることは可能な気がします

*3:混在していたwin9Xは悪者扱いでした

WASMでPCエミュレータ作った話 / Part 3

github.com

さいきんはずっとエミュレーター作ってるます。

FreeDOS

ついに、FreeDOS (16bit版) が起動しましたヽ(•̀ω•́ )ゝ✧

f:id:neriring16:20190706200753p:plain

MSDOSはまだどこかおかしいようです。

f:id:neriring16:20190706202805p:plain

Web MIDI

Web MIDI に対応しました。
MPU-401 UART モード互換インターフェースのつもりなので、いろんなソフトから MIDI 機器を制御できるはずです。
また、 iPad で Web MIDI に対応したブラウザと Sound Canvas for iOS などがあれば直接鳴らすこともできます。

f:id:neriring16:20190707202126j:plain

Mac と無線で繋ぐこともできますが、タイミングが厳しくて雑音になりました(´・ω・`)

また、とある MIDI プレイヤーがタイマー割り込みの関係でうまく再生できなかったのでタイマーを補正するようにしました。従来より負荷が少し上がってるかもしれません。

テストたくさん

テストも書き始めましたが、演算命令のテスト項目が山のようにあってどうやってまとめるか試行錯誤中です:;(∩´﹏`∩);:

IBM PC のタイマー事情

IBM PCBIOS は通常、約 55ms / 18.2Hz ごとにカウントしています。
この数値は一体どこから来てどんな意味があるのでしょうか?

まず、 IBM PC で使われているタイマーIC 8254 PIT の動作クロック入力が約 1193182Hz となっています。
すごく中途半端な値に見えますが、これはよく使われている周波数で部品が入手しやすいらしいです。

この 8254 PIT で設定可能な 16bit の最大値である 65536 を設定してみます。
すると約 55ms (約18.2Hz) の周期でタイマー割り込みが発生するようになります。

さらにこのタイマー割り込みで BIOS データエリアの 40:6C にある WORD 値をインクリメントすると、 16bit の限界である 65536 回カウントしたところでおよそ 3600 秒、つまり1時間になります。
40:6C の値がオーバーフローしたら 40:6E にある WORD 値をインクリメントし、そこが 24 になると 24 時間ということで 40:70 に 1 をセットします。(ここだけなぜかカウンターではなくフラグです。)

なるほどうまくできてますね。

55ms という謎の数字にはこんな意味があったのでした。

続・WASMでPCエミュレータ作った。

neriring.hatenablog.jp

前回の記事の時点では、対応する命令も少ないしペリフェラルは UART のみでとても OS が起動する状態ではありませんでした。
その後色々実装して前回動かなかったブラウザにも対応し、ついに自作 OS の osz が起動しました!

f:id:neriring16:20190623234557p:plain

f:id:neriring16:20190623224830p:plain

基本的な操作は問題なく動作しているように見えます。

続いて FreeDOS を見てみましょう。

f:id:neriring16:20190623224957p:plain

ダメですね。

お次は PC-DOS

f:id:neriring16:20190623225015p:plain

動いてるように見えます。

最後は elks

f:id:neriring16:20190623225024p:plain

この画面のまま止まってしまいます・・・

動くプログラムが出てくるとやる気出てきますねヽ(•̀ω•́ )ゝ✧

WASMでPCエミュレータ作った。

WASM で PC エミュレータ作りました☆(ゝω・)vキャピ

f:id:neriring16:20190618000038p:plain

github.com

※ 現時点では自作のOSすら起動しません。

すでにいろんなエミュレータが存在しているので今更感あるかもしれないですが、このエミュレーターは WebAssembly を使っているのが大きな特徴です。

もともと Web ブラウザーで動作するPCエミュレーターを探していて、いくつかあることはあるのですが、どれも不満がありました。

筆者は数年前に Web で動作する PC エミュレーターを試作したことがありました。
サーバーサイドで既存のエミュレーターを動作させて WebSocket で入出力だけブラウザ側が担当する実装でした。
クライアントの処理が軽いので軽快に動作しましたが、動作するサーバーの問題があって公開前に開発中止になりました。

クライアント単体で完結できるものとして JavaScript を利用したものがよくありますが、パフォーマンス的に満足できるものはひとつもありませんでした。

WebAssembly の環境も整ってきたので自作するかーということになりました。

SharedArrayBuffer と Atomics

技術調査をしていて一点気になっていたことがありました。

一般に GUI 環境では GUI イベントを処理するためのスレッドがあります。
JavaScript の場合は GUI イベントとメインのスクリプトが同じスレッドで動作するので、重たいスクリプト処理が走っていると GUI イベントに素早く反応できずに操作性が悪くなります。かといって GUI イベントのために小刻みに動作を中断するとエミュレーターのパフォーマンスが非常に悪くなります。
こういう場合、重い計算処理は GUI スレッドとは別のスレッドで処理するのが普通です。

JavaScript では Worker という仕組みを使ってマルチスレッドを実現できます。
Worker スレッドで動作するスクリプトGUI スレッドとは別のスレッドで動作するため、 GUI スレッドは GUI イベントに、エミュレータスレッドはエミュレータ処理にそれぞれ専念することができます。

しかし一方で、エミュレーターは常に処理しているわけではありません。
全力で処理したい時もあるけど、ユーザーの操作待ちなどで休んでる時もあります。
こういう場合、 JavaScript では SharedArrayBuffer と Atomics を使って待ち合わせを行うことができます。
エミュレータースレッドで入力待ちが発生した場合は Atomics.wait を使ってスリープし、 GUI スレッドで入力イベントを受け取ったら Atomics.notify を使ってエミュレータスレッドを起こすことができます。

これで、全力で処理するときは全力で処理し、入力待ちで休んでるときはほとんど CPU を消費しないようになり、ぼくの要求するパフォーマンスを満たすことができるようになりました。

しかし、 SharedArrayBuffer はセキュリティ上の理由により Chrome 以外のほとんどのブラウザーでは無効になっているという悲しいニュースがありました。これを解決するには別の手段を探す必要があるかもしれません。

ModR/M とフラグ

実装上の問題としては、筆者は過去に 80 系 CPU のような単純なエミュレーターは自作したことあるのですが、 x86 系の本格的なエミュレーターを作るのは初めてでした。
8086 の特徴として多くの汎用命令で ModR/M と呼ばれるエンコーディングが使われています。
しかしこの ModR/M が曲者で、 8086 エミュレーターで一番面倒な場所だと思います。ここだけで1日くらい消費しました。

ModR/M デコーダーを実装して多くの命令が動作するようになりましたが、今度はフラグの動作にハマりました。
OF や CF が実機と結果が異なるのです。
これらのフラグは条件分岐でよく使われるフラグなので正しく実装しないとほとんどのプログラムが動作しません。
自作 VM なら比較命令と条件分岐命令を統合させてフラグの動きを考えないようにできますが、実際の CPU をエミュレーションするので真面目に実装する必要があります。

そんなこんなでとりあえず簡単なタイプライター的動作をする BIOS が動く程度にはなったので公開します:;(∩´﹏`∩);: