借り初めのひみつきち

仮ブログです。

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

前回は最小ステップで UEFI からカーネルを起動するところまでできたので、今回はその続きを作ってみようと思います。

github.com

なお、前回は起動までの最短ステップを紹介しましたが、 UEFI Aware な OS を作る場合には RuntimeServices API を使うためにもう少し手順を踏む必要があります。今はまだ使わないのでそのまま放置します。

前回からの大きな変更点として、元々他で作っていたブートローダーを追加しました。簡単なメニュー画面を表示することができます。

極論を言うとカーネル読み込んで ExitBootServices を呼び出してしまえば UEFI の世界は終わります。
ここから先はカーネルの世界になるので一気にやることが増えます。

1. メモリ管理サブシステムの初期化

uintptr_t mm_init(void* efi_mmap, uintptr_t efi_mmap_size, uintptr_t efi_mmap_desc_size);

ExitBootServices API の呼び出し手続き中に GetMemoryMap API で取得したメモリマップを元にメモリ管理サブシステムを初期化します。
GetMemoryMap から取得できるメモリマップは UEFI 動作中のメモリ管理に使われているので ExitBootServices API の実行前後で意味が変わって空きメモリ扱いになる領域が存在します。しかし、この時点で UEFI の管理していたメモリを空きメモリとして解放してしまうと、ページテーブルなどの重要なデータが破壊されてしまうのでこの時点では解放しません。

現時点では静的に確保したヒープを割り当てる機能のみ提供します。

2. ACPI の初期化

void acpi_init(acpi_rsd_ptr_t* _rsdp);

UEFI から渡された ACPI テーブルを保存します。
この時点ではまだ使いませんが、ディスクリプタテーブル検索 API だけ提供しています。

3. CPU 固有機能の初期化

void arch_init();

CPU 固有の機能を初期化します。

3.1. ページングの初期化?

32bit モードではページングに頼らずセグメンテーションを活用するという選択肢も少なからず存在しますが、 64bit モードではページングは避けて通れない問題です。
UEFI は 32bit/64bit にかかわらず Identity Mapped Paging と呼ばれる状態のページングモードで起動します。これはページング機構は有効になっているけど、そのまま仮想メモリアドレス=物理メモアドレスとして扱えるようにページテーブルが設定された状態になっています。

本来はこのあたりでページテーブルの初期化が必要かと思いましたが、前述の通り UEFI が Identity Mapped Paging の状態で設定してくれていて UEFI の管理していたメモリ領域はまだ空き領域として解放していないので、本格的なページングの初期化をするまでこのまま使います。

3.2. GDT の初期化

uint16_t gdtr_init(void);

64bit モードにはセグメンテーションがないと言われることがありますが、実際には存在しています。
64bit モードではベースアドレスやセグメントリミットは一部の例外をのぞいて存在しないものとして扱われますが、現在動作しているのが 32bit なのか 64bit なのか、カーネルモードなのかユーザーモードなのか、等は 32bit モードの時代と同様に GDT にあるセグメントディスクリプタをロードするときに決定します。
そのため、割り込み処理やユーザーモードカーネルモードの間で遷移する際にセグメント切り替えが発生するので GDT の設定が必要になります。
GDT に必要なディスクリプタの種類は少ないし単純なのであらかじめ固定値で用意しておきます。また、 GDTR のロードや新しいセグメントの読み込みはC言語で書けない処理なのでこの処理はほぼアセンブリ言語の関数で書きます。

3.3. IDT の初期化

void idtr_init(uint16_t cs_sel);

64bit モードの割り込みメカニズムはプロテクトモードによく似ています。
各種ディスクリプタが 64bit 対応になってサイズ2倍になったこと、スタックが動作リングに関わらず常時切り替わる事をのぞけば基本的な動作はかなりよく似ています。
IDTの設定はC言語の関数で行いますが、IDTRのロードはアセンブリ関数を使います。
また、割り込みハンドラはレジスタやスタックなどの処理が必要なので、アセンブリ言語の関数で先に処理した後でC言語の関数を呼び出すようにしています。

ここまで初期化が終わると割り込みが使えるようになりますが、 IRQ はまだ処理できません。

f:id:neriring16:20181007040533p:plain

割り込みが使えるようになるといろんなドライバーが作れるようになりますが、ドライバーを作る時間が間に合わなかったので今回はこの辺で終わりです。
とりあえず例外のダンプはできるようになりました。
また、対応している機種では ACPI の BGRT テーブルを参照してロゴ画像を表示します。