借り初めのひみつきち

仮ブログです。

UEFI自作OS日記 v0.6.1 ゆ。

前回の記事で紹介したように v0.6 は大幅なリメイクとなりましたが、それには大きな目的がありました。

github.com

現代の PC ではペリフェラル接続に主に USB を利用しています。
キーボードやマウスもデスクトップ PC では USB 接続が主流です。
モバイル PC は 10 年くらい前は内部的に PS/2 接続のものが多かったですが、ここ数年は USB 接続のものが増えてきています。
BIOS の時代は PS/2 ポートは正義でしたが、まもなく完全に消滅してしまうでしょう。

自作 OS も BIOSPS/2 の時代が終わって UEFI と USB の時代がきます。

USB が使えるようになれば色々なペリフェラルが接続できるようになります。 USB には未来があります。 USB に対応しないと未来はありません。

実は v0.5 の非公開バージョンで USB 対応を初めていてある程度のところまで動いていたものの、あと一歩のところで不可解なバグに悩まされる日々が続いていました。

そんなわけで、余計な肉を削ぎ落として安定させた上で USB 対応するのが v0.6 の最大の目的でした。

f:id:neriring16:20190825114723p:plain

PS/2 の時代は決まった I/O ポート (0x60 など) から入出力するだけでかんたんにキーコードを取得できましたが、 USB はいくつかの層に分かれた比較的複雑な構造になっており、「デバイスと1ビットのデータをやりとりする」状態にいくまでが大変です。

一方で「デバイスと1ビットのデータがやりとりできる」状態になってしまえばキーボードなんてたかだか数バイトのデータやりとりするだけので特別難しいことはありません。

USB キーボードは HID という規格の一部です。 HID 規格自体は人間とデバイスの間のあらゆるインターフェースを定義しようとした巨大な規格なので全てを網羅するのはけっこう大変ですが、多くの機種ではキーボードとマウスはブートプロトコルという機能を制限したモードで使えるようになっているので、そちらを使えば比較的かんたんに利用できます。*1

というわけで USB 対応のうちの多くの時間は xHCI との格闘の歴史だったわけですが、筆者がはまった箇所を覚えてる限りいくつか紹介しておきます。

ポート ID とスロット ID のオリジン

xHCI では USB デバイスはルートハブの各ポートに接続しており、これをポート ID で区別します。ルートハブのポートは全て筐体の外に出てるとは限らず内蔵のデバイスの接続にも利用しています。
各ポートに接続されたデバイスは適切な初期化をすると xHCI 内部のスロットという内部データ構造に割り当てられ、スロット ID で区別します。
初期化以降はスロット ID を通して通信する事になります。

初期化時やデバイスの抜き差しでポートの状態に変化があると Port Status Change Event というイベントが発生しますが、このイベントで通知されるポート ID は1から始まります。
一方、実際の xHCI のポートに相当する PORTSC などのレジスタ

ベース + 0x10 * (ポートID - 1)

のような計算式で計算します。実質0オリジンです。
この「-1」を忘れると、イベントの発生したポートと実際にレジスタを操作するポートが1ずれる事になっておかしな事になります。

また、スロット ID も1から始まりますが、実際のスロットに相当する DCBAA という配列の項目も1から始まります。
0番目の項目は Scratchpad Buffer という xHC が内部で利用する内部で利用するバッファーの指定に使います。
これが指定されていないと、ポートの初期化などは一見普通に動作するものの xHC が Scratchpad Buffer に依存しているコマンドを実行しようとした時に再起動などの怪しい挙動します。

コントロール転送の Status Stage

USB のコントロール転送には Setup, Data, Status という3つのステージがあります。
USB プロトコルでは全ての転送命令の方向が決まっていて、 Setup Stage は常にホストからデバイスに対する出力となります。
Data Stage の方向は Setup Stage で指定する値によって異なりますが、入力したいのか出力したいのかはあらかじめ決まっているのでそんなに難しくないでしょう。
問題は最後の Status Stage ですが、なんと Data Stage の方向によって変わります。また Data Stage は存在しないこともよくあります。

これをまとめると以下のようになります。

f:id:neriring16:20190825124100p:plain

方向を正しくを設定しないと USB Transaction Error となって以降の通信がうまくいかなくなります。

キーコードの変換

実は moe は当初から USB 対応を見込んで設計されていてキーコードも PS/2 のコードではなく HID 規格のものをベースとして変換しているので、特に問題なく動作しました。

TL; DR

まだまだ問題も多いですが、とりあえず USB キーボードから入力できるようになりました。

*1:ブートプロトコルにしか対応してないキーボードや逆にブートプロトコルに対応していないキーボードもあります。

UEFI自作OS日記 v0.6 再始動

しばらく開発の停滞していた moe ですが・・・

github.com

思うところあってリファクタリングしながら再実装しています。

f:id:neriring16:20190812101940p:plain

既存のコードを再利用できるところは再利用していますが、完全に新規に書き直してる部分もあります。

これだけだと面白くないので具体的な違いをいくつか紹介します。

コンテキストスイッチを setjmp から専用の関数に変更しました。

moe は最低限のコストで最低限の物を作るというコンセプトだったので setjmp/longjmp の仕組みを流用することでコンテキストスイッチが簡単に実装できる予定でした。
しかし、実際に実装してみるとうまく動かすために工夫が必要で、無理やり setjmp/longjmp を使い続ける理由がないと判断して新しいバージョンでは完全に変更しています。

ウィンドウシステムの廃止

カーネルレベルでウィンドウ対応しているのは minimal と言えるのかどうかという問題もあり、将来ユーザーランドでウィンドウシステムを実装するとき邪魔になりそうだったのでウィンドウシステムに依存する処理をいったんリセットしました。

SSE の使用禁止

前述の問題と関連して、カーネルがコンテキストの怪しい部分で SSE を使用してしまうと SSE レジスタが破壊されてしまう懸念があり、最近のコンパイラは意図せず SSE 命令を生成している事があるので、根本的な対策としてカーネルでは SSE を完全に使用しないようにしました。

初期化時の 32bit コードの削除

SMP の初期化処理でリアルモードからロングモードに遷移する際、いったん 32bit モードを経由していたコードを見直して 32bit セグメントに依存しないようになりました。

この問題についてもう少し詳しく説明すると、ロングモードには 64bit モードだけではなく 16bit モードや 32bit モードも存在しています。

しかし、実は筆者は遥か昔のおれんじぺこを作っていた時代から 16bit モードから直接 64bit モードに遷移する方法が成功したことがありませんでした。ロングモードに 16bit モードは存在しないのではないかと疑った事があるレベルです。
そのため、今までは初期化時にいったん 32bit モードを経由したあと 64bit モードに遷移していました。

この問題の解決のヒントが少し前に開発していた PC エミュレーターにありました。

実は x86 の命令は 16bit モードと 32bit モードの2種類あるという単純なものではなく、デフォルトオペランドサイズによって 16bit モードと 32bit モードが切り替わる他に、 66 や 67 (名前はないけど有名なプレフィックス) でも切り替わり、命令によってはひとつのオペコードで最大4種類の命令になります。

例えば AB というオペコードは 8086 では STOSW ですが、 386 以降は以下の4種類の命令になります。

f:id:neriring16:20190812234346p:plain

STOSW 命令の場合 66 プレフィックスがあると STOSW/STOSD とニーモニックが変化しますが、 67 プレフィックスの有無はアセンブラレベルでは区別がつきません。アセンブラによっては特殊な表記で区別する場合もあります。

同様に FF /5 というオペコードは JMP FAR を指しますが、 66 プレフィックスや 67 プレフィックスによってニーモニックは変化しません。
JMP FAR 命令の場合は 67 プレフィックスの有無はメモリアドレスの指定方法が 16bit か 32bit かで変化し、アセンブラレベルで容易に区別できます。
問題は 66 プレフィックスの有無ですが、アセンブラレベルで正確に記述するのは容易ではありません。しかし、 16bit で動作するか 32bit で動作するかという重大な違いがあります。

16bit ロングモードから 64bit モードに遷移するときに 32bit の JMP FAR 命令を実行するには、 66 プレフィックスのついた命令を実行する必要があります。

この問題に気づくのに何年もかかってしまいました。

このように、 x86 の命令はひとつのオペコードに対して最大4種類の命令がありますが、アセンブラレベルでは識別が困難な命令も多く存在しています。

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 という謎の数字にはこんな意味があったのでした。