借り初めのひみつきち

仮ブログです。

x86の割り込みが遅いワケ

CPUが現在実行中のプログラムを中断して処理しなければならない事象が発生した時、一般に「割り込み」というメカニズムを使ってその事象を処理します。

広義の割り込みは実際には以下の3種類に分類できます。

  • 例外

CPUが命令の実行を継続できない事象が発生したときにOSに判断を委ねるために例外が発生します。
多くの場合はプログラムのミスによるエラーや悪意あるプログラムによる不正な特権操作を行おうとした場合に例外が発生してアプリケーションが終了しますが、全ての例外が必ずしもエラー終了となるわけではなく、ページフォールトのようにOSの処理によって命令実行が続行可能な場合もある例外もあります。

  • 外部割り込み

外部デバイスのデータ転送が終了したり、データ転送要求があったり、タイマーの状態が変化したときなどにCPUに通知するために割り込みが発生します。狭義の割り込みはこれを指します。
x86は歴史的事情により通常のマスク可能割り込み(IRQ)とマスク不可能割り込み(NMI)の2種類に大別されます。マスク可能割り込みはフラグレジスターのIFフラグで割り込みをマスク(禁止)することができることからこの名前で呼ばれます。IRQはデバイスから直接通知される場合とPICやAPICを介して割り込みが通知される場合があります。Local APICを使ってIPIを送信する場合もマスク可能割り込みの一種として通知されます。

  • ソフトウェア割り込み

専用の命令を使って任意のタイミングでソフトウェア的に割り込みを発生させる場合に使います。
多くの場合はOSを呼び出すために使われるのでシステムコールと同一視されることもあります。

それぞれの詳細については今回の記事では重要ではないので省略します。

x86の場合、ほとんどの割り込みは割り込みベクターと呼ばれる0番〜255番までの256種類の番号で割り込みの要因を区別します。*1割り込みベクターの0番〜31番まではIntelによって例外のために予約されているため、通常のOSは外部割り込みやソフトウェア割り込みをこの範囲外のものを割り当てます。なお、レガシーBIOSが使用する割り込みは例外が多数定義されて予約のルールが徹底される前に設計されたので定義済みの例外と被っているものがたくさんあります。

さて、割り込みが発生するとCPUはまずメモリ上のIDTというテーブルからゲートディスクリプターというものを探します。
ゲートディスクリプターには割り込み先のCS,xIPレジスタの値やディスクリプターの属性などの情報が記述されています。
ゲートディスクリプターには割り込みで使えない種類のものも存在するので、CPUはディスクリプターが本当に存在するか、割り込みに適合したものかなどをチェックします。このチェックで不適切なディスクリプターと判断されると一般保護例外が発生します。一般保護例外も処理できないとダブルフォールトやトリプルフォールトという擬似的な例外が発生してCPUが停止し、通常はトリプルフォールトが発生するとリセットがかかるようにハードウェア設計されています。開発途中のOSでは例外がうまく処理できないとよくリセット地獄に陥りデバッグが困難です。

ゲートディスクリプターのチェックが終わると、そこに記述されているコードセグメントをロードしようとします。
セグメントのロードはメモリにあるGDTやLDTというテーブルに記述されたセグメントディスクリプターというものを読み込みます。
ここでもIDTのときのようにディスクリプターが適切なものかどうかチェックされ、不適切だった場合は一般保護例外などの例外が発生します。

多くの場合、割り込みが発生した時のコンテキストはユーザーモードのアプリケーションプログラムであるのに対し、割り込みを処理するプログラムはカーネルモードのシステムプログラムであるケースがほとんどです。
カーネルモードとユーザーモードを切り替えるときはお互いのスタックに不適切にアクセスされないようにスタックを自動的に切り替える機能がCPUにあります。このときTSSという構造体から新しいスタックセグメントとスタックポインターを読み込みます。
スタックセグメントはコードセグメントと同様にGDTやLDTからセグメントディスクリプターをロードしてチェックが行われ、不適切だった場合はスタック例外が発生します。

ここまで準備が終わると、割り込み処理のためのCS,xIPの内容がゲートディスクリプターからロードされ、割り込み発生前のCS,xIP,フラグレジスターなどがスタックにPUSHされ、やっと割り込み処理プログラムに制御が移ります。
割り込みを終了するときはIRET命令を実行するとスタックからCS,xIP,フラグレジスターを復元し、必要な場合は追加でSS,xSPも復元します。このときもセグメントレジスターは前述のようにセグメントディスクリプターから読み込んでチェックが行われます。

とても複雑ですね。

x86IDTには割り込みゲート、トラップゲート、タスクゲートを置くことができ、それぞれに16bitと32bitのバージョンが存在しました。また、大昔はメモリ空間の保護にセグメンテーション機構が使われており、これらのディスクリプターの内容が非常に重要だったため厳密にチェックする必要がありました。

x64になるとIDTに配置できるディスクリプターの種類が減り、64ビット固定の割り込みゲートとトラップゲートのみになりました。この2つの違いは自動的に割り込みを無効に設定するかどうかだけなので実質的にはほとんど同じものです。また、セグメンテーションも大幅に機能縮小され、通常の状態ではほぼ意識する必要がなくなりました。
にも関わらず、x64の割り込みのメカニズムはx86の時代とほとんど変わっていません。
拡張を繰り返した結果ゲートディスクリプターの構造は複雑奇怪になっています。
本来割り込みゲートに必要な情報は割り込み先のRIPとわずかな追加情報、セグメントの切り替えもカーネルモードかユーザーモードかのフラグを1ビットを切り替えるだけで必要十分なはずですが、互換性のためかx86の複雑な機構をほぼそのまま継承して引きずっています。

システムコールに関しては現代のOSでは割り込み命令を使わず専用の命令(sysenter,syscallなど)を使ってディスクリプターのロードやチェックなどを省略できるものが主流になっています。

通常の割り込みでもこの辺りが改良されると高速に割り込み処理できるようになると思うのですが…

*1:SMIやLocal APICのStartup IPIなどのようにベクター番号の存在しない特殊な割り込みも一部存在します。

ACPI BGRT

昔同じタイトルの記事を書いたところ結構googleしてる人がいるみたいなので、ちゃんとした記事のせときますね:;(∩´﹏`∩);:

win8くらいの時からPCが起動する時にメーカーロゴが表示された状態のままwindowsが起動してることにお気づきでしょうか?

この仕組みは ACPI にある BGRT (Boot Graphics Resource Table) というテーブルで実現されています。

このテーブルは以下のような構造になっています。

typedef struct {
    acpi_header_t   Header;
    uint16_t    Version;
    uint8_t     Status, Image_Type;
    uint64_t    Image_Address;
    uint32_t    Image_Offset_X, Image_Offset_Y;
} __attribute__((packed)) acpi_bgrt_t;

この中で重要なのは Image_Address, Image_Offset_X, Image_Offset_Y の3つです。
Image_Address の指しているアドレスには BMP ファイルがそのまま格納されています。
Image_Offset_X/Y は実際にイメージを表示する左上の座標になります。解像度を変更した場合は不正な座標になります。
残りの項目は ACPI 共通のお作法だったり、将来拡張があった時のためだったりします。(たぶん使われないでしょう)

実際にこれを使う場合、 EFI_SYSTEM_TABLE の ConfigurationTable から ACPI テーブル(RSD Ptr)を検索し、 XSDT から BGRT を探し、 BGRT から画像を取り出すという手順になります。
ということで表示するプログラムを作ってみました。

github.com

f:id:neriring16:20181220212425p:plain

最小のEXEファイル?

EXEファイルにはいろいろ形式がありますが、現在主流なのはPEという形式です。

これはもともとUN*X方面で使われていたCOFFという実行ファイルの形式にWindowsのために必要な機能を拡張したもので、PE-COFFなどの名称で呼ばれることもあります。
本家UN*X系OSではCOFFでは機能が不十分になったために現在ではELF形式が主流となっていますが、Windowsの世界ではCOFFを独自拡張したPE形式をそのまま使い続けています。
そもそもEXE形式はMS-DOS用の実行ファイル形式だったMZ形式から始まり、いくつかの追加ヘッダをつけた亜種があり、最終的に追加でPEヘッダを付けたPE形式が現在の主流となっています。

そういう経緯で生まれた形式のため、MZからPEに辿るまでの互換性のための項目があったり、元々のCOFFにあった今では使われていない機能があったり、PEで拡張された項目にもあまり使われていない機能があったり・・・よく見るとけっこうスカスカです。

そして、スカスカのヘッダーの使われていない部分を切り詰めて小さい EXE ファイルを作る遊びが一時期流行りました。
世界レコードが何バイトかは知らないですが、ハローワールドが256バイト切れた時代もありました。

Windows XP SP2 がでたとき、実は密かにヘッダーのチェックが厳しくなっていて、それまで使えたテクニックのいくつかは修正が必要でした。その後、遊びも下火になってぼく自身もほとんど忘れたまま何年も経ちました。

EXE ファイルはどこまで小さくできるか限界を探すという懐かしい話題を見かけ、昔を懐かしんで古い最小バイナリたちを見てみると Windows 10 ではどれも動かなくなっていました。

そこで現在の最小らしいバイナリを改良していったところ、ある程度小さくすることには成功しました。
しかし、いじっていくと突然バイナリを生成した瞬間マルウェアに感染しましたと出てきてバイナリが消滅しました。

どうもマルウェアの中にはヘッダの値がでたらめなものがあるため、最近はチェックが厳しくなっているのだそうな・・・

ぼくはただゴルフを遊んでいただけなのに、突然警察が来てこの場所は違法になったってボールを没収して行くのです。
ぼくの目にはこないだまで普通に遊べたただの空き地にしか見えないのに。

世界一有名な人。

世界で最も有名な人は誰だろう?

候補をあげるなら一人目は Phil Katz だと思う。
彼は PKZIP を開発し、彼のイニシャルはたくさんの ZIP ファイルの先頭に刻まれている。
また、 ZIP ファイルはしばしば名前を隠して至る所に存在している。
何億台だかの PC で走るのに客先の PC ではさっぱり走らないあのソフトウェアとかね。

もう一人候補を挙げるなら Mark Zbikowski かな。
MS-DOS version 2 に数々の革新的機能を実装した彼のイニシャルはたくさんの EXE ファイルの先頭に刻まれている。
アンチ M$ 教徒のあなたも安心してほしい。
ROM-BIOS やブートパーティションにある EFI ファイルには確かに彼のイニシャルが刻まれている。

他に有名な人は誰がいるだろう。

フォントのお話

さいきん進行中のUEFIプロジェクトをはじめるにあたってちょうど良いフォントがなくてフリーで使えるフォントを探していたのですが、FONTX2フォントってだいぶ配布してるところが減ってきて、ライセンスも不明瞭だったり作者も行方不明で配布元も閉鎖したり・・・

全盛期に比べるとだいぶ廃れてしまった感じのあるビットマップフォントですが、

そういえばmegosのフォントも現在は入手不可能だったなと気付きました。

megosは当時の他の自作OSに比べてフォントにも拘っていましたが、現在となってはサンプルも見れないし、どんなフォントがあるかすら明らかになっていない。
手元には全てあるんですけどね。

これらについて近日正式に公開する予定です。