借り初めのひみつきち

仮ブログです。

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

BIOS で消耗するな、 UEFI で消耗せよ。

この記事は 自作OS Advent Calendar 2018 - Adventar の記事です。

未来ある若者よ、 BIOS で消耗するな UEFI で消耗せよ。

Intel は 2020 年までに CSM (BIOS 互換機能) を撤廃して UEFI に完全移行する方針を発表しました。つまり、もう BIOS に未来はありません。
すでに UEFI Class 3 と呼ばれる BIOS 互換機能をサポートしていない PC も増えてきているので決して楽観視できる出来事ではありません。
近い将来 BIOS に依存した OS は実機で起動しなくなり、起動する PC を求めて秋葉原の裏路地を彷徨う必要があるかもしれません。

彼の者の名は UEFI

BIOS は40年近く前からあって設計が古く問題も多いです。 BIOS にあった根本的な問題を解決するために UEFI は生まれました。

BIOS モードで起動すると CPU は歴史ある 16bit モードで起動します。
このモードでは CPU 本来の機能のいくつかが実質的に使えない状態になっていて、メモリもたったの 640KB しか通常の方法ではアクセスできません。 640KB がどれくらい少ないかというといまどきの CPU キャッシュよりも少ないです。
この状態から 64bit モードに移行するには PC の歴史を辿るかのようにいくつかのステップが必要です。
しかし、 BIOS は 16bit モードを前提としているのでモード遷移すると多くの BIOS にアクセスできなくなる問題もあります。
一方 UEFI なら最初から 64bit で最低限の設定された状態で起動し、 64bit モードから API にアクセスできます。

BIOS モードは16色で解像度の荒いテキストしか表示できない画面モードで起動します。
これには合理的な理由も存在していて、 たったの 640KB のメモリしか扱えない 16bit モードではそもそもまともなグラフィックス処理をする余裕がありません。
一方 UEFI は最初から美しいグラフィックスモードで起動し、多くの場合はディスプレイ本来の解像度がそのまま使えます。
テキストモードは存在しませんが、テキスト BIOS 相当の機能を提供するグラフィカルコンソール API があります。

BIOS モードで OS を起動するにはたったの 512 バイトしかないローダーを通常のファイルシステムからアクセスできない特殊な場所に書き込む必要があります。
BIOSファイルシステムに関与しないので自力でファイルシステムを解釈してシステムに必要なファイルをロードする必要があります。
ほとんどの OS はこの処理をたったの 512 バイトでは書ききれないので、一旦小さなローダーをロードし、そこから改めてシステムファイルをロードしたり別のチェインローダーをロードしたり複雑な起動処理が必要になります。
また、複数の OS を切り替えて起動する用途は想定していない構造になっていたり、最近の大容量ストレージ技術には適合しないなどの問題もあります。
一方 UEFI は少なくとも FAT ファイルシステムをサポートしており、通常の OS でファイルアクセスを行うのと同じような感覚で API を使ってシステムに必要なファイルをロードすることができます。
また、起動する際のファイルパスの指定の仕方なども仕様として決まっているので複数の OS を切り替えて起動するのが容易にできます。

BIOSx86 CPU と IBM PC アーキテクチャに強く依存していましたが、 UEFI はそれらに依存しないようになっていて ARM などにも移植されています。
拙作のブートローダーもコンパイルオプションを変えるだけで ARM などでも動く事が確認できています。

このように、 BIOS は設計が古いのでモダンな OS を起動するにはたくさんのステップを踏む必要がありますが、 UEFI なら最初から考えなくていい問題が多いです。未来のない BIOS で消耗するより UEFI で解決して OS 本体を作る時間に費やした方がお得ですよね。

UEFI 最初のステップ

さて、 UEFI でハローワールドを調べるとこんな感じのソースがよく出てくるかと思います。

#include "efi.h"

EFI_STATUS EFIAPI efi_main(IN EFI_HANDLE image, IN EFI_SYSTEM_TABLE *st)
{
  st->ConOut->OutputString(st->ConOut, L"Hello, world!\r\n");
  for (;;)
    ;
}

まあ、ハローワールドといえばハローワールドなんですが、ここからどうやって OS に繋げていけばいいのかよくわからないですよね。ぼくはわかりません。

efi_main の引数のひとつめはプログラム自身のハンドルで、これは UEFI は他にもデバイスドライバーなどのサービスがロードされているのでいくつかの API を呼び出す際に区別するために必要になります。
ふたつめの引数は UEFI API を呼び出すための重要なテーブルで、この中に API を呼び出すポインタなど UEFI のすべての情報が入っています。

システムテーブルの定義はこんな感じになります。

typedef struct {
	EFI_TABLE_HEADER Hdr;
	CHAR16 *FirmwareVendor;
	UINT32 FirmwareRevision;
	EFI_HANDLE ConsoleInHandle;
	EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn;
	EFI_HANDLE ConsoleOutHandle;
	EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
	EFI_HANDLE StandardErrorHandle;
	EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
	EFI_RUNTIME_SERVICES *RuntimeServices;
	EFI_BOOT_SERVICES *BootServices;
	UINTN NumberOfTableEntries;
	EFI_CONFIGURATION_TABLE *ConfigurationTable;
} EFI_SYSTEM_TABLE;

この中にある BootServices と RuntimeServices が UEFI のメイン API に当たりますが、2つあるのは UEFI の動作環境が関係しています。

BIOSMS-DOSデバイスドライバを起源としているので OS が起動した後も動き続けていて明確な終了の概念がありませんでしたが、 UEFI では OS 起動前と起動後が明確に区別され、 BootServices という環境と RuntimeServices という大きな2つの環境があります。*1
BootServices 環境は OS が起動する前に UEFI が簡易的な OS となってさまざまなサービスを提供する環境で、一般に UEFI という言葉でイメージされる環境が BootServices になります。
一方、 RuntimeServices 環境は UEFI から OS に完全に制御を受け渡した環境で、この状態では UEFI のほとんどのサービスが終了していて使える UEFI API はあまり多くありません。
BootServices 環境から RuntimeServices 環境へは OS の起動時に一度だけ移行でき、その逆の移行は再起動するしかありません。大昔のリアルモードとプロテクトモードの関係に少し似ています。

なお、以後 EFI_SYSTEM_TABLE は ST、EFI_BOOT_SERVICES は BS、EFI_RUNTIME_SERVICES は RT と略する事があります。

ExitBootServices

BootService 環境と RuntimeServices 環境を切り替えるための UEFI で最も重要な APIBootServices.ExitBootServices です。

BootServices.ExitBootServices は MapKey という引数をとり、実行に失敗する可能性のある API です。
MapKey は BootServices.GetMemoryMap を呼び出すと取得できますが、メモリマップ情報が更新されるたびにこの値は変化します。
OS が最新のメモリマップ情報を正しく取得していないとデタラメなメモリマップ情報を元に起動してしまう危険性があります。
そこで、最新のメモリマップ情報を取得しているか確認するために ExitBootServices の実行時に GetMemoryMap の戻り値の MapKey をチェックしています。
なお、 ExitBootServices は一度実行したら失敗しても元の UEFI BootServices 環境に戻ることはできず BootServices 環境と RuntimeServices 環境の中途半端な状態になってしまいます。実は正しい手順で ExitBootServices を呼び出しても初回にメモリマップが更新されて必ず失敗する環境もあるので適切なリトライ処理が必要です。
また、 GetMemoryMap に必要なバッファーのサイズも実行前にわからないのでリトライ処理が必要になります。これらを考慮するとこんな感じのコードになります。

EFI_MEMORY_DESCRIPTOR* mmap = NULL;
UINTN mmapsize = 0;
UINTN mapkey, descriptorsize;
UINT32 descriptorversion;

do {
    status = bs->GetMemoryMap(&mmapsize, mmap, &mapkey, &descriptorsize, &descriptorversion);
    while (status == EFI_BUFFER_TOO_SMALL) {
        if (mmap) {
            bs->FreePool(mmap);
        }
        status = bs->AllocatePool(EfiLoaderData, mmapsize, (void**)&mmap);
        status = bs->GetMemoryMap(&mmapsize, mmap, &mapkey, &descriptorsize, &descriptorversion);
    }
    status = bs->ExitBootServices(image, mapkey);
} while (EFI_ERROR(status));

UEFI で OS を実行する最低限必要な処理は以上になりますが、 ExitBootServices を実行してしまうとシステムテーブルの内容はクリアされてしまってアクセスできなくなります。
対象になるハードウェアを完全に把握していればこれだけでも OS を作れそうな感じもしますが、 普通の PC を対象とするならハードウェアの情報を集める必要があります。

EFI_GRAPHICS_OUTPUT_PROTOCOL

まずは画面表示のためにグラフィックスの情報を取得します。
最低限必要なコードは以下のようになります。

    EFI_GRAPHICS_OUTPUT_PROTOCOL* gop = NULL;
    status = bs->LocateProtocol(&EfiGraphicsOutputProtocolGuid, NULL, (void**)&gop);

UEFI ではシステムテーブルにない API はこのサンプルのように BootServices.LocateProtocol などの API を使って GUID を指定して取得する事が多いです。似たような API は何種類かあります。
また取得した構造体を UEFI では Protocol と呼びますが中身は大抵関数ポインタの集まった構造体で、GUID と Protocol の定義は UEFI 仕様書にたくさんのっています。
実は SDK やバージョンによって同じ Procotol でも微妙に名前が違う事がありますが、重要なのは GUID の値と Protocol 構造体の定義です。

EFI_GRAPHICS_OUTPUT_PROTOCOL の内容は以下のような感じになります。
EFI_GRAPHICS_OUTPUT_PROTOCOL は GOP と略して呼ぶ事が多いです。

typedef struct {
	EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE QueryMode;
	EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE SetMode;
	EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT Blt;
	EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
} EFI_GRAPHICS_OUTPUT_PROTOCOL;

最初の3つは関数ポインタになっていて BootServices 環境で呼び出すことができる API ですが、 RuntimeServices 環境で必要なのは最後の Mode の中身で、以下のような構造体になっています。

typedef struct {
	UINT32 MaxMode;
	UINT32 Mode;
	EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;
	UINTN SizeOfInfo;
	EFI_PHYSICAL_ADDRESS FrameBufferBase;
	UINTN FrameBufferSize;
} EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE;

typedef struct {
	UINT32 Version;
	UINT32 HorizontalResolution;
	UINT32 VerticalResolution;
	EFI_GRAPHICS_PIXEL_FORMAT PixelFormat;
	EFI_PIXEL_BITMASK PixelInformation;
	UINT32 PixelsPerScanLine;
} EFI_GRAPHICS_OUTPUT_MODE_INFORMATION;

2つの構造体に分かれているのでちょっと複雑になりますが、画面サイズは GOP->Mode->Info->HorizontalResolution と GOP->Mode->Info->VerticalResolution 、実際の VRAM の開始アドレスは GOP->Mode->FrameBufferBase、座標から VRAM アドレスを計算する時に必要になるのが GOP->Mode->Info->PixelsPerScanLine、VRAMのサイズは GOP->Mode->FrameBufferSize などが主な情報です。
PixelsPerScanLine は HorizontalResolution と同じ値になることが多いですが、稀に異なる場合があるので考慮する必要があります。
PixelFormat は通常の PC では PixelBlueGreenRedReserved8BitPerColor という値になり、これは 32bit の RGB (ARGB) 値を VRAM に書き込めばそのとうりの色が表示されます。
つまり UEFI では 32bit フルカラーグラフィックスが事実上の標準となっていて、他のモードはほぼ存在しません。

BIOS の世界では VGA 互換グラフィックスが標準となっていますが、 UEFI の世界では VGA 互換モードは利用できません。

ACPI

現代の PC のハードウェア構成情報は基本的に ACPI から取得できます。
UEFI では以下のようにシステムテーブルの ConfigurationTable から GUID が一致するテーブルを検索する事で ACPI テーブルを取得することができます。
EFI_CONFIGURATION_TABLE の中には ACPI 以外のテーブルも入っているので、そのうち必要になったら取得しやすいように関数を分けています。

main {
    ...
    acpi = efi_find_config_table(st, &efi_acpi_20_table_guid);
    ...
}

static void* efi_find_config_table(EFI_SYSTEM_TABLE *st, CONST EFI_GUID* guid) {
    for (int i = 0; i < st->NumberOfTableEntries; i++) {
        EFI_CONFIGURATION_TABLE* tab = st->ConfigurationTable + i;
        if (IsEqualGUID(&tab->VendorGuid, guid)) {
            return tab->VendorTable;
        }
    }
    return NULL;
}

取得した ACPI テーブルの中身の扱い方は BIOS の時代と変わりません。
ACPI は今の PC にとってとても重要ですが、とても巨大な仕様なのでここでは深く触れません。

システムファイルの読み込み

本格的な OS はローダーとカーネルが分離している場合が多いかと思います。
その場合 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL などを使ってファイルを読み込んだりします。

BIOS 時代は他に選択肢がなかったのですが、 UEFI ではそもそもファイルが直接読み出せるので適切な場所に配置すれば UEFI が勝手に読み込んでくれます。
正確な上限はわからないですが、個人で作れる OS の規模なら十分ひとつのファイルにおさめることができます。
よってこの処理は省略できます。

適切な場所というのは具体的には PC の場合 /EFI/BOOT/BOOTX64.EFI というパスでファイルを配置すると、起動メニューから認識されます。
なお、例えば ARM64 の場合は BOOTX64 の部分が BOOTAA64 とかに変わります。

UEFI は不揮発メモリに特殊な変数を保存する機能があり、そこに起動ファイルのパスを設定すると上記の規則以外のファイルからでも起動する事ができます。おそらく変数を扱う機能自体はすべての機種にありますが、機種によって UI で設定できたりできなかったり、メニューから選択できたりできなかったり、若干差異があります。
よく Windows が勝手に起動するのはこの設定のためです。

また、 UEFI シェルを使えば UEFI からアクセスできるすべての場所から自由に起動する事ができます。
機種によっては BIOS ROM の中に UEFI シェルがインストール済みでメニューから起動できるものもあります。
簡単なファイル管理などもシェル上でできるようになるので、コンパイル済みバイナリを入手しておくと便利です。

最小ステップで作る UEFI OS ローダー

ここまでのステップで UEFI のモード切り替えと収集した情報をカーネルに渡すまでの最小限の OS ローダーを作ってみるとこんな感じになります。

https://github.com/neri/moe/blob/v0.5/src/kernel/efistub.c

拙作 OS プロジェクトで実際に使われているコードです。
なお、カーネル起動後は RuntimeServices に少しアクセスする以外は UEFI はもうほぼ登場しなくなります。

UEFI の光と闇

ここまで UEFI を紹介してきましたが、自作 OS で UEFI を導入する事で新しい問題が増えるのも事実です。

古き良き BIOS の時代は誰かが勝手に決めた謎のアドレスの一覧がハードウェアマニュアルに記載されていて、それを直接アクセスする事でハードウェアを操作する事ができました。
例えば 0x60 番のポートからデータを読み込むとキーコードが取れるような単純なものが多くありました。

今のコンピューターは多種多様な構成になっていて固定のアドレスに単純にアクセスするハードウェアはあまり多くありません。
複雑な決まりごとでアドレスが決まったり、アドレスが決まった後のアクセスも昔より多くの決まりごとが増えています。

UEFI Class 3 のコンピューターの中には IBM PC と互換性のあるハードウェアがほとんど残っていないものもあります。
BIOS の時代は MS-DOS が動く程度の互換性はずっと維持され続けてきましたが、今後は新しい規格に対応していないハードウェアはどんどん切り捨てられていくことになります。

また、 BIOS の時代は明確な終了の概念がなかったので OS 起動後も BIOS はずっと生き続けていて必要であればデバイスドライバとして利用する事ができました。 UEFI の場合はほとんどのドライバが ExitBootService で使えなくなってしまうので別にドライバを用意する必要があります。

自作 OS にとって最大の壁はこの膨大なハードウェアへの対応だと思います。

とはいえ、これから低レイヤーを学ぶ人が未来のない BIOS を学ぶ意味はあまりないと思います。
「むかしは大変だったなー」と軽く済ませ、 BIOS で消耗するくらいなら UEFI を学びましょう。

*1:ファームウェアを作る人にとってはこれらのサービスが起動する前の環境もあったりしますが

最小ステップで作る UEFI OS v0.5.1

ウィンドウの基本的な部分はできましたが、本格的なウィンドウシステムとしては全然機能が足りてなかったので色々実装しました。

半透明のウィンドウもほぼチラつきなしで動くようになりました!

そして、自作OS界隈では元ネタより有名な noiz2bg なんかも移植してみました。
noiz2bg はビットマップ表示さえできれば簡単に動かせて見た目が一気に華やかになるのでオススメです。

実はシェルやコンソールは「どうせそのうち GUI 実装するから適当でいい」という考えでかなり手抜きでした。
スクロールできなかったりコマンドが1文字だったり・・・
今回の修正ではシェルもちゃんとコマンドを認識するようになりました。

まだファイルシステムもないのに普通に OS が動いてるみたいですね!

なお、ボタン類はまだ動作しません。

f:id:neriring16:20181204003859p:plain

f:id:neriring16:20181204003923p:plain

最小ステップで作る UEFI OS v0.5

今週は SMP 環境の安定化と格闘しておりましたがなんとか進捗出せました。。

github.com

ウィンドーシステム

スレッドが動くようになったのでウィンドウシステムの改良です。
今まではルートウィンドウとマウスカーソルだけの特別な処理だったので、他のウィンドウもちゃんと使えるように改良します。

とりあえずステータスバーとコンソールウィンドウを実装しました。
まだウィンドウの移動はできないし再描画のちらつきもすごいですが、大昔のウィンドウシステムも再描画のとき結構ちらつくものもあったので許容範囲でしょう。

f:id:neriring16:20181125200622p:plain

f:id:neriring16:20181125200638p:plain

ウィンドーが出てきて画面もだいぶそれっぽくなりました。今までのバージョンで一番見た目の変化が激しいです。
OS/2 だって ver1.0 から 1.1 になったとき DOS から Windows に変化したくらい変化しましたしね。

最小ステップで作る UEFI OS v0.4.1?

昔の CPU は世代が進むごとに動作周波数が向上していき、それに伴って動作も高速になっていました。

しかし、やがて動作周波数をあげるのが困難になり、別の方法で性能向上する必要が出てきました。
そこで偉い人は考えました。
超高速な CPU を1個作るより、そこそこ高速な CPU を2個並べた方が簡単で性能出るんじゃね?

そんなわけで、かつてはマルチプロセッサーというのはサーバーなど一部のハイエンドにしか存在しませんでしたが、現代の CPU はローエンドでも2コア以上のマルチコア構成が普通です。
シングルプロセッサー対応の OS は CPU の性能の半分も使い切れていません。SMP の対応が必要です。

APIC の SMP 対応

マルチプロセッサではローカル APIC の IPI (Inter Processor Interrupt) が特に重要になります。
SMP 構成のシステムでは CPU が起動時に BSP (BootStrap Processor) と AP (Application Processor) に自動的に役割が分かれます。
通常は BSP のみが動作し、 AP は起動時に AP に役割が決まった瞬間そのまま再び起こされるまで眠りにつきます。

シングルプロセッサーシステムから SMP システムに移行するには AP に対して IPI を発行して起こす必要があります。
具体的なやり方は SDM Vol. 3 に書いてあって、各 AP に対して INIT IPI を発行して 10ms 待った後に(この間に AP 用の BIOS が起動するらしいです)Startup IPI を発行します。これで AP が起動します。

AP が起動した時どのアドレスが実行されるかは Startup IPI に指定するベクタ番号で決まります。実際のところこの値は割り込みベクタの番号ではなく実際の開始アドレスは 12bit シフトした値になります。
AP は起動直後リアルモードにいるので OS を実行するために速やかにロングモードに移行する必要があります。
処理を簡単にするため、ロングモードの動作に必要なほとんどのパラメーターは BSP からコピーした値を使います。

UEFI には Multi Processor に関する規格が存在しているので、この方法を使わなくても動作するかもしれません・・・

スケジューラの SMP 対応

前回までのコンテキストスイッチは SMP を一切考慮しておらず、スレッドリストを最初から順番に読み込んで切り替える実装でした。
シングルプロセッサ構成ならこの方法でもとりあえず動作しますが、マルチプロセッサ構成では現在実行中のスレッドがプロセッサコアごとに異なるために次に実行するべきスレッドの同期が取れません。下手をすると複数のプロセッサコアで同じスレッドの実行を取り合ってメモリ破壊する可能性もあります。

そこでいったん FIFO のキューにスレッドを全て入れて、スレッド切り替えの際にはキューから取り出して次に実行するスレッドを決めるように変更しました。この部分をもっと賢くする余地はまだたくさんあります。

実行するスレッドがなくなったときにはアイドルスレッドが必要ですが、複数のコアで同じアイドルスレッドを実行するのは色々まずいので、アイドルスレッドを特別扱いしてコアごとにアイドルスレッドを分離しました。
これ以外にもコアごとに分けて管理する必要があるものがあるので結構複雑になっています。なお、 SMP 起動前と起動後で結構変わる部分があって複雑になるためコア数が決定するまでスレッドサブシステムは動作しないように変更されました。

なお、 Lazy FPU Switch は現在の実装では SMP 環境と適合しないので OFF になっています。タイミングによっては SSE レジスタの値が大きく壊れます。

SMP 環境では予期せぬタイミングで他のプロセッサコアからメモリアクセスされる可能性があるので、シングルプロセッサーの時よりも排他制御に気を配る必要があります。

とりあえず実機ではそれなりに動作するようになりましたが、 qemu では謎のエラーが頻発していて原因不明です。

f:id:neriring16:20181118204258p:plain

最小ステップで作る UEFI OS v0.4

そろそろ GUI の実装に入りたい感じですね。

github.com

メモリの拡張

現在実装されているメモリ管理はコンパイル時に静的に割り当てたブロックを切り取って分配する手抜き実装でした。
今まではそこまで大量のメモリを必要としていなかったのでこれでよかったのですが、グラフィックス関係の処理を実装すると MB 単位のメモリが必要になるのでこの方法だとうまく動かなくなってきました。
かといって本格的なメモリ管理を実装するのも大変なので、 GetMemoryMap からもらったメモリマップから使えそうな大きさのメモリブロックを見つけてきて分配するように変更しました。
この変更によって実行時に確保するメモリ領域は EFI アプリケーション領域と完全に別の領域から割り当てられるようになったので、ポインタの値が直接見える場所では今までと大きく異なる値が表示されるようになります。

Window Managerの実装

まだちゃんとした Window までは実装できてませんが、今まで描画系の関数はすべて画面に直接描画していたのを「デスクトップウィンドウ」に描画するように変更しました。
マウスカーソルも独立したウィンドウに描画するように変更したので、マウスの移動で画面が崩れなくなりました!

また、今までコンソールのスクロールは直接画面を読み込んでコピーしていたので実機で動かすととても遅かったですが、スクロール処理と画面描画が完全に分離されたのでスクロールが早くなっています。

f:id:neriring16:20181111211210p: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 ではどれも動かなくなっていました。

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

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

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