借り初めのひみつきち

仮ブログです。

GPD MicroPC

正直、 GPD 社に期待はしてるけど今まで出した製品はあまり出来がいいと思ってない Nerry です。

エンジニア向けとして期待されていた GPD MicroPC ですが、やっと触る機会ができたので自作 OS の観点から調査してみましたヽ(•̀ω•́ )ゝ✧

結局、 Portrait なの? Landscape なの?

最近の小型 PC には避けて通ることができない Portrait 問題。

これは小型の液晶が基本的にスマホタブレットで使う前提で作られていることから Portrait (縦長) のものが多く、コストや性能の関係でスマホ用の液晶を止む無く横倒しにして使ってる事が多いことに起因する諸問題です。

GPD MicroPC が出る前後もこの問題で話題になり、 Landscape 液晶を使ってるのではという情報も飛び交った事がありました。

結論から言うと、少なくともデフォルトでは Portrait です。

通常起動すると GOP は解像度 720 x 1280 として報告し、実際 VRAM の構造もそのようになっています。最近のよくある小型 PC と変わらないですね。

問題は似非 Landscape モードがある点です。

内蔵の UEFI シェルから起動すると通常とは異なるドライバが読み込まれるようで、 GOP の報告する解像度が 1280 x 720 になり、 gop->Blt を呼び出した場合もそのように振舞います。

しかし、実際の VRAM 構造は Portrait のままなので、その状態で Windows を起動するとなんと横倒しのおかしな画面で起動します。(ディスプレイドライバが起動する前は)

これは本来

HorizontalResolution = 720
VerticalResolution = 1280
PixelsPerScanLine = 720

の画面モードを、単純に縦横入れ替えて

HorizontalResolution = 1280
VerticalResolution = 720
PixelsPerScanLine = 720

として報告しているようです。矛盾してます。

通常 PC の画面は HorizontalResolution <= PixelsPerScanLine が成立するので*1、拙作の OS では HorizontalResolution > PixelsPerScanLine だった場合に縦横を入れ替える処理を入れて対処しました。

USB とキーボードあれこれ

xHCI が特殊でうまく通信できないという前情報があったのでその調査がメインのつもりでしたが、拙作の OS では外部 USB キーボードが普通に動いたので、特に変わった挙動はなかったように思います。

内蔵キーボードが曲者で、ルートハブに繋がっている VID:PID = 6080:8061 という奇妙な ID *2 でプロダクト名も 「USB KEYBOARD」 と名乗っているデバイスのインターフェース #0 クラス 3.1.1 (ブートプロトコル、キーボード) からはデータがうまく読み出せず、インタフェース #1 クラス 3.0.0 (汎用 HID) から読み出せるデータはタッチパッドのデータのようでした。お前は一体何者なんだ。

一方 LPC バスに PS/2 コントローラーも存在し、ほとんどの PS/2 コマンドに対して意味のある応答があり、キーボードが存在するかのような応答はあるのですが、実際のキー信号がなぜかうまく取れません。
2回だけ、なぜかちゃんとキーボードが入力できた事があったのですが、再現性が不明で再起動後は同じコードでも何も起きず、ポーリングしてもキーコードを返してくれませんでした。

また、 UEFI シェルから ACPI ENABLE するコマンドを実行してみると USB 関連が正常に動作しなくなりますが、内蔵キーボードは相変わらず動作していました。

以上の経緯から内蔵キーボードは PS/2 接続してる可能性が濃厚ですが、 USB ・ PS/2 ともにキーボードとみられるデバイスにうまくアクセスできず、拙作の OS からはキー入力がちゃんとできていない状態です。

ACPI とレガシーモード

HW reduced ACPI ではありません!!(超重要)
先述のように ACPI ENABLE コマンドを実行すると UEFI の USB ドライバの挙動が変わります。

BIOS 設定画面で UEFI モードかレガシーモードか選択できるのですが、レガシーモードで起動すると起動デバイスが見つからないので結局使い道がありませんでした。ドライバが足りないだけで CSM は入ってるんでしょうか?

全体的にハードウェアは近年のよくある小型デバイスよりもレガシーな雰囲気があります。

TL; DR

GPD って変なデバイス好きですね。

*1:稀にパディングを入れて HorizontalResolution < PixelsPerScanLine になる機種があるが、ほとんどの機種では WindowsUEFI 要件によって HorizontalResolution = PixelsPerScanLine になる

*2:検索してもベンダーIDがまったく見つからない

BitTest 命令

x86 には BitTest というマイナーな命令があります。

具体的には BT BTS BTR BTC の4種類あって、それぞれ特定のビットをテスト(1かどうか返す)、セット(1)する、リセット(0)する、反転する(0⇔1)命令になります。

BitTest 命令をレジスタに使った場合はちょっとだけビット操作が楽になりますが、別に AND OR XOR でもいいよね感がありました。

実は BitTest 命令はレジスタに対して実行した場合とメモリに対して実行した場合重要な違いがあります。

x86 CPU はシフト命令などでビットを指定するオペランドは 32bit (64bitオペランドの場合は64bit) に制限され、例えばシフト命令に 100 を指定しても実際には 100回もシフトしませんでした。
BitTest 命令をレジスタに対して実行した場合はシフト命令などと同様に 32bit の制限がかかっていますが、メモリに対して実行した場合は 32bit の制限がありません。
つまり、何百何万もある巨大な配列の先頭を指定して BitTest 命令を実行するとそこから何百何万番目の遥か遠くにあるビットに対してビット操作する事ができます。

また、マルチスレッドプログラミングにおいても重要な機能があり、 BitTest 命令は実行前の対象ビットの値を CF レジスタに返します。
つまり、 LOCK プレフィックスとセットで使うと Mutex などのメモリ上の特定のビット操作をアトミックに扱う事ができます。

マイナーだけど、すごい命令です。

exFAT について

exFAT というファイルシステムがあります。
FAT ファイルシステムの次世代ファイルシステムとしてリムーバブルメディアなどに採用実績がありますが、仕様が公開されておらず特許が絡むために Microsoft 製以外の OS からはあまり使い勝手がよくありませんでした。

しかし、仕様が公開され、特許の条件も緩和されようとしているようです。

docs.microsoft.com

exFAT は名前の通り FAT を改良したファイルシステムで、以下のような FAT によく似た特徴があります。

  • クラスタという単位でセクタを管理しており、最初のクラスタ番号は2である
  • FAT というテーブルによりクラスタチェインを管理している
  • 32バイト単位のディレクトリエントリでファイルを管理している

一方で以下のような相違点もあります。

  • 空き領域はビットマップで管理されており、 FAT 上で管理されていないファイルが存在する
  • 8.3 ファイル名が存在せず、通常のファイルはディレクトリエントリを複数必要とする
  • FAT には存在しなかったメタデータ拡張機能が存在する

FAT はフロッピーディスクの時代に設計されたもので小容量ではそれなりにうまく動いていましたが、メディアが GB 単位の時代になってくると色々と不都合が増えてきました。 exFAT はより大きなメディア、特にソリッドステートメモリーでうまく扱えることを前提に設計されています。

クラスタ

FAT におけるクラスタとは、生のセクタを直接扱うと量が多すぎて管理が大変になるので、複数のセクタをまとめてクラスタという名前の論理セクタとして扱うことでトータルの論理セクタ数を減らして管理を楽にするための概念です。
よく使われるクラスタサイズとして 4KiB や 32KiB などがあります。4KiBはページングのページサイズ、32KiBはよくあるフラッシュメモリーのページサイズと一致するため管理の都合が良い数値です。

クラスタ単位でディスクアクセスすることでセクタ単位でアクセスするよりも高速で効率的なディスクアクセスが可能になります。
一方で、クラスタサイズはファイルの最小単位でもあり、1バイトでも内容のあるファイルはディスク上で最低1クラスタを占有するため、クラスタサイズが大きすぎると必ずしもディスクを効率的に扱えるとは限りません。

クラスタ番号は歴史的な事情で2から開始します。*1 クラスタ番号から実際のセクタ番号に変換する場合はオフセットを考慮する必要があります。

FAT とクラスタチェーン

クラスタサイズを超えるファイルを格納するには2つ以上のクラスタが必要です。
1番目のクラスタ番号はディレクトリエントリから辿れますが、2番目以降のクラスタを探す方法が別途必要になります。
この時 FAT 系ファイルシステムは FAT というテーブルを参照する事で2番目以降のクラスタを探します。

例えばクラスタ番号123のクラスタの次のクラスタを探す場合、 FAT テーブルの123番目の要素を調べ、そこに456とあった場合は次のクラスタは456と決めることができます。

FAT では FAT の要素のサイズによって FAT12FAT16FAT32 というバリエーションがありました。*2
exFAT は稀に FAT64 という俗称で呼ばれることがありますが FAT の要素サイズは 32bit となっています。

FAT ファイルシステムでは FAT テーブルはクラスタの利用状況を管理するためにも使い、 FAT テーブル上に 0 と記述されたクラスタは空き領域、 0xF...F7 と記述されたクラスタ不良セクタや予約セクタを含んでいるのでデータとして使用禁止、という風にクラスタを管理していました。

一方、exFAT では利用状況の管理には FAT テーブルを利用せずビットマップで管理しており、クラスタが連続していることが保証されている属性のファイルは FAT 上にクラスタチェーンを形成しません。
つまり、exFAT には FAT によるクラスタチェーンで管理しているファイルと FAT を利用しないファイルの2種類のファイルがあります。

これによるメリットは2つあります。
ビットマップは FAT テーブルに比べてサイズが小さいので空き領域を割り当てる検索処理が素早くできます。空き領域をビットマップで管理するのは他のファイルシステムでも割とよく使われる方法です。
また、ファイルアクセス時に毎回 FAT 上のクラスタチェーンを辿るのはコストがかかるため、クラスタチェーンを辿らなくても連続したクラスタにアクセスできることで高速化に寄与します。

exFAT におけるファイル管理は基本的には FAT 上にクラスタチェーンを生成しないように連続した領域を割り当て、どうしても断片化してしまった時に改めてクラスタチェーンを生成するのが効率的な管理になります。 exFAT という名前でありながら基本的には FAT を利用しないのが exFAT です。

ディレクトリエントリ

FAT も exFAT も32バイト単位のディレクトリエントリという構造体でファイルの情報を管理します。

FAT のディレクトリエントリは FAT12 の時代に設計されたもので、 FAT32 の時代になるとかなり無理のある使い方をしてすでに限界に近い状態でした。
当初、ファイル名は8.3形式の短いファイル名のみサポートしていましたが、その後特殊な属性のディレクトリエントリを繋げることで長いファイル名をサポートし、互換性のために短いファイル名と長いファイル名の2つでファイルを管理していました。

exFAT でもディレクトリエントリの基本サイズは32バイトですが、ディレクトリエントリの構造を大幅に変更することで FAT よりも多くの情報を含めることができるようになり、タイムスタンプの2秒問題などが改善しています。
通常のファイルは1つのディレクトリエントリに情報が収まりきらないため3つ以上のディレクトリエントリを繋げて使用します。
8.3形式のファイル名には対応しておらず、 FAT の長いファイル名と同等の制限の緩いファイル名を使うことができます。

また、細かい違いとして従来サブディレクトリに存在した「.」「..」という存在理由のよくわからないディレクトリエントリは exFAT のボリューム上には存在しません。

メタデータ、大文字テーブル

exFAT では従来の FAT ファイルシステムにはなかったメタデータが増えています。
例えば、先述の空き領域ビットマップはメタデータとして専用のディレクトリエントリがルートディレクトリにあります。

また、 exFAT の特徴的なメタデータとして大文字テーブルというものがあります。
FAT ファイルシステムはもともとファイル名を大文字で管理し、ファイルの検索は大文字小文字を区別しません。
exFAT では大文字に変換するためのテーブルがボリューム上のメタデータとして存在します。*3
大文字テーブルで変換したファイル名からハッシュ値を計算し、ディレクトリエントリのハッシュ値と比較することで大小文字を無視したファイルの検索が高速にできるのが他のファイルシステムにはないユニークな特徴となっています。この部分が特許に絡んでいた気がしますが、ソースを見つけることができませんでした。

ソリッドステートメモリーとの親和性

exFAT は先述のようにクラスタが連続しているファイルは FAT によるクラスタチェーンを経由せず高速にアクセスできます。
また、クラスタサイズやオフセットを調整してソリッドステートメモリーのページサイズと一致させることで効率の良いアクセスが可能になっています。
そのためにソリッドステートメモリーの特性を記述できる拡張属性が定義されています。

*1:なぜ2から始まるのかはよくわかりません

*2:実は FAT32 は要素サイズは 32bit ですが 32bit 全て使っている訳ではありません

*3:しかし仕様書によると内容は固定のようです...

MOE は MEG-OS MOE になりました。

MOE は MEG-OS MOE になりました。

f:id:neriring16:20190901000042p:plain

MEG-OS Dawn

もともと MOE は内部コードネーム「MEG-OS Dawn」として企画された MEG-OS ファミリーの最も新しい OS でした。

MEG-OS Dawn の主な要件は以下の通りです。

  • 新しい 64bit OS
  • UEFI で起動し、 PIC や PIT に依存しないモダンな OS を目指す
  • マルチコアに対応する
  • 当初は PS/2 キーボード・マウスに対応し、将来は USB 対応を目指す

企画自体は数年前から存在していましたが、新しい試みが多く開発するための余裕や決意が弱くなかなか進みませんでした。
そして、とある契機で今から約一年ほど前に moe として始まることになりました。

コードネーム warbler

moe は開発が順調に進んで v0.5 になり、そろそろ USB 対応しようとしたところで細かい問題にぶつかって開発がうまく進まなくなりました。
そこで少し休眠した後 v0.6 からリフレッシュして開発再開し、ついに当初の目標を達成することができました。

古代の meg-os は v0.5 で開発が止まって v0.6 になることはできませんでした。
moe は v0.6 になった現在も精力的に開発が進んでおり、現在は v0.7 以降へ向けてロードマップの整備中です。

そこで moe を v0.6 から正式な meg-os の後継にし、今後は独立したコードネームを付ける事にします。

新しい MEG-OS MOE をこれからもよろしくお願いします。

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