現代における自作OSの難しさ 〜自作OSのいまと昔 [第2回]
前回の記事では、自作OSとは何か、そしてこれまでの自作OSの歴史を紹介しました。しかし、近年になって自作OSを取り巻く状況は大きく変化してきています。そこで今回は、現代における自作OSでは、これまでと比べてどのような点が変化してきたのか、どのような難しい点があるのかについて紹介し、さらにそれらに対する解決策を、筆者の経験をもとに提案してゆきます。
自作OSをとりまく環境の変化
川合秀実氏著「30日でできる!OS自作入門」が出版された2006年頃と比べて、コンピュータをとりまく環境は大きく変化してきました。その変化には、CPUのコア数やビット幅からファームウェアの変遷、そしてデバイスの進化も含まれています。まず最初に、CPUのコア数について着目してみましょう。
CPUコア数の増加
上記の図は、1999年から2019年までにIntelから発売されたCPUのコア数の変化を示したグラフです。ご覧の通り、2004年まではシングルコアのプロセッサしかありませんでしたが、2005年以降、出荷された製品の平均コア数は年々増加の傾向にあります。並列計算向けのXeon Phiプロセッサまで含めれば、2017年には1プロセッサあたり72コアに至るものまで登場しています。(縦軸が対数であることに注意してください)
さらに特筆すべきこととして、2016年以降にはシングルコアのプロセッサが発表されていないことが挙げられます。このことは、私たちが日常的に使うほとんどのコンピュータが、今やすべて2つ以上のCPUコアを持っているということを意味します。
自作OSの楽しみ方は人によって様々ですが、ハードウェアの能力を最大限に引き出したいという欲求から自作OSをする人も多くいます。そう考えると、今後の自作OSとしてはマルチコア・マルチプロセッサへの対応も求められてくるのかもしれません。
とはいえ、マルチコアプロセッサの上で既存のシングルコア向け自作OSが動かないのかというと、そうではありません。というのも、少なくともx86_64のCPUであれば、後方互換性のため、マルチコアのプロセッサでも、電源投入直後はシングルコアのプロセッサとして動作するようになっているためです。
「なあんだ、それじゃあ別に既存の自作OSでいいじゃないか!」と思われる方もいらっしゃるかもしれませんが、残念ながら自作OSをとりまく環境の変化は他にもあります。
Legacy BIOSからUEFI BIOSへの移行
数ある環境の変化の中でも最も自作OSへの影響が大きいと思われるもの、それはLegacy BIOSからUEFI BIOSへの移行です。(ここでは、AT互換機に搭載されていた、UEFI以前のBIOSのことをLegacy BIOSと呼ぶことにします)
コンピュータの電源が入った際にCPUが最初に実行するプログラムは、OSではなくBIOS(Basic Input / Output System)です。BIOSは、マザーボード上の不揮発性メモリに書き込まれているプログラムで、一般のユーザが書き換えることはほとんどありません。
BIOSがコンピュータに接続されているハードウェアの初期化を行い、OSもしくはそのローダをメモリにロードして実行を引き渡すことによってはじめて、OSは起動することができます。また、BIOSはその名の通り、OSやそのローダに対して、基本的な入出力などの機能を提供するという面も併せ持っています。
これらの特徴は、Legacy BIOSとUEFI BIOSに共通のものです。では、Legacy BIOSとUEFI BIOSの異なる点は一体どこにあるのでしょうか?
MBRを読むか、ファイルシステムを読むか
一つ目の大きな違いは、ブートプロセスにあります。Legacy BIOSでは、各記憶装置の最初の512バイト(MBR)を読み出して、その領域の末尾に、起動可能かどうかを示すマジックナンバー(0x55, 0xAA)があるかを確認して、それが存在すればそのデータを0x7c00番地に配置して、そこに実行を引き渡します。
この方式は、固定長の領域を読み出すだけの単純なロード方式なので、BIOSのコードを単純にできるという利点があります。しかし、たった512バイトのコードでOSの残りの部分をロードしなければならず、OS開発者に負担を強いることになっていました。
一方UEFI BIOSでは、各記憶装置上のファイルシステムを読み、仕様書で規程されているパス(x86_64アーキテクチャにおいては/EFI/BOOT/BOOTx64.EFI)に置かれている実行ファイルをロードするようになっています。これにより、最初にロードされるプログラムのサイズの制限が実質なくなり、自作OS開発者にとっては多段階のブートローダを書く必要がないという利点があります。また、Legacy BIOS向けのローダを書き込む領域は、ファイルシステム外のディスク領域の先頭であったため、書き込むには低レベルなコマンド操作や、普段使わない専用のプログラムを使う必要がありましたが、UEFI BIOS向けのローダはファイルとしてファイルシステム上に存在するため、通常のファイル操作(cpコマンドやGUIファイルマネージャでの操作)でブートローダを書き換えることができるようになりました。
16bitから64bitへの移行
Legacy BIOSとUEFI BIOSの違いはこれだけではありません。Legacy BIOSでは、ブートローダがロードされた直後のCPUの実行モードはリアルモードであり、プロテクトモードやロングモードへの移行やページングの有効化はOS側で行う必要がありました。一方UEFI BIOSでは、ロードされた直後からすでに64ビットモードで動きはじめます。
現代のCコンパイラでは、16bitコードを正しく出力することはもはや難しくなってしまっているので、Legacy BIOSを使用する場合は、どうしてもアセンブラを利用してブートローダや32bitモードへの移行までを記述する必要があります。しかし、UEFI BIOSを利用すれば、最初から64bitモードで動作するため、CやC++、Rustなどの高級言語を利用してOSをすべて記述することも可能です。
BIOSサービスの呼び出し方法が割り込みから関数呼び出しに
前述した通り、BIOSはOSのロードを行うだけでなく、OSやローダに対して入出力などのサービスを提供するライブラリとしての役割も持っています。
Legacy BIOSでは、これらのサービスを呼び出す場合はソフトウェア割り込み(INT命令)を用いていました。また、BIOSが動作するのは基本的に16bitリアルモードのみだったため、プロテクテッドモード移行後は利用することができませんでした。
一方UEFI BIOSでは、入出力などのサービスはすべて通常の関数と同様に呼び出されるようになりました。関数へのアドレスは、プログラムへのロード時に構造体(EFI_SYSTEM_TABLE)を経由して、もしくはそこから呼び出せる関数経由で取得されます。これにより、高級言語からUEFIの提供するサービスを利用することが簡単になりました。
UEFI BIOSのレガシーエミュレーションが終わる日
Legacy BIOSとUEFI BIOSには、これまで見てきたように、互換性のない違いが数多くあります。しかし、これまではLegacy BIOS向けに書かれたソフトウェアが動かないというケースはあまり多くありませんでした。これは、UEFI BIOSのCompatibility Support Module (CSM)という機能によって、Legacy BIOSがエミュレーションされていたためでした。しかし、Intelは2020年までに、このCSMを搭載しない、UEFI Class 3 への移行をすると発表しています。
これにより、2020年以降に出荷されるマシンでは、Legacy BIOSを前提とした自作OSが動かなくなるため、今後の自作OSではUEFIへの対応が必須になると考えられます。
PS/2キーボード・マウスの終焉
Legacy BIOSからUEFI BIOSへの移行だけにとどまらず、いくつかのレガシーデバイスは新しい規格にとって代わられ、そのエミュレーション機能も削除されようとしています。その代表例のひとつが、キーボードとマウスのレガシー規格、PS/2です。
PS/2は、1987年に発売された IBM Personal System/2 で採用された接続規格で、この端子を通じてキーボードとマウスがキーボードコントローラに接続され、CPUからはI/Oバス経由でキーボードコントローラとやり取りをします。
しかし、PS/2キーボードやマウスはホットプラグに対応しておらず、またキーボードやマウス以外のデバイスを接続することができないため、より汎用性の高いUniversal Serial Bus(USB)接続のキーボードやマウスが近年では主流になってきています。
とはいえ、PS/2キーボードにしか対応していないデバイスも当初は存在したため、BIOSレベルでUSBキーボードをPS/2キーボードに見せかけるエミュレーション機能が存在していました。しかし、近年はほとんどのソフトウェアがUSBデバイスをサポートするようになったため、PS/2キーボードのエミュレーション機能を提供しないBIOSも増えてきています。
このようなマシンで自作OSをする場合は、USBドライバを自力で書くか、キーボード以外の入出力の手段を確保する必要があります。
そのほかのレガシーデバイスとその代替
キーボード・マウス以外にも、新たなものにとって代わられつつあるレガシーデバイスは数多くあります。たとえば、OSにとって必要不可欠な、タイマーと割り込みコントローラについても、より新しいものへの置き換えが進んでいます。
8254 PITからHPET、そしてその先へ
レガシータイマーとして知られる8254 Programmable Interval Timer (PIT)は、はりぼてOS(「30日でできる!OS自作入門」の中で作るOS)でも利用されている古典的な、周期割り込みを発生することができるタイマーです。
OSは、タイマー割り込みを契機としてコンテキストスイッチを発生させたり、アプリケーションへのタイマー機能を提供します。そのため、実時間でのスケールがわかり、かつ一定時間ごとに割り込みを発生させることのできるタイマーの存在は極めて重要です。しかし、CPUクロックが数GHzになった現在、最高でも約1.2MHzの周波数でしか動作できないPITではタイマーの分解能が不足してきました。
そこで、近年の多くのコンピュータにはHPET(High Precision Event Timers)という、最低でも10MHzの周波数が保証された高精度タイマーが搭載されています。このタイマーは、8254 PITとは異なり、I/Oバス経由ではなくメモリマップドI/O経由で制御します。従来のPITとの互換モードも用意されているため、割り込みまわりの実装を変更することなく、PITの置き換えとして利用できるため、まずPITから脱却したい!という場合には使いやすいタイマーです。
またHPETの他にも、新しい割り込みコントローラであるAPICに内蔵されたLocal APICタイマーや、電源管理を担当するACPIに搭載されたACPI PMタイマーなど、コンピューター上には数多くのタイマーが存在します。しかし、これらは「割り込みを発生させることはできるが周波数は取得できない」「周波数は既知だが割り込みを発生させることができない」など、単一のタイマーでは役割を果たせない場合が多く、いくつかを組み合わせて使う必要があります。発展的な自作OSでは、これら数多くあるタイマーを適切に組み合わせて、アプリケーションに抽象化して提供するという挑戦をしてみてもよいでしょう。
8259 PICからマルチコア時代のAPICへ
レガシーな割り込みコントローラである8259 Programmable Interrupt Controller (PIC)は、元々は単一の割り込みピンしかもたないCPUで、複数の機器からの割り込みをサポートするために誕生した外部チップでした。しかし最初にも述べたように、時はもはやマルチコア全盛時代。割り込みを取りまとめて一つのCPUに通知するだけではなく、複数のCPUに対して割り込みを分配したり、CPU間での割り込みのサポート、さらには外部機器も増えたことから、より多数の外部割り込みをサポートする必要が出てきました。
そこで、CPUの各コアごとにLocal APICを持たせ、外部割り込みを受け付けるI/O APICと、これらをつなぐバスによって割り込みを処理する新しい仕組みがつくられました。これをAdvanced PIC、つまりAPICと呼びます。 LocalAPICがCPUに内蔵された機能であるため、APICの仕様はCPUの仕様書に載っています。(Intel SDM Vol.3 CHAPTER 10 ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (APIC))
マルチコアやLocal APIC タイマー、PCIeデバイスのMSI割り込み等をサポートしたい場合には、自作OSでAPICをサポートする必要があるでしょう。
2020年に向けた自作OSのつくりかた
これらの状況をふまえて、2020年以降の最新マシンでも通用するような自作OSの書き方を筆者が考えてみました。
ブートはUEFI、64bitコードで
2020年にCSMサポートが打ち切られることを考えると、Legacy BIOSからUEFI BIOSへの移行は必須になるでしょう。とはいえ、開発自体はむしろLegacy BIOS時代よりも容易になってきています。たとえばC/C++で開発をする場合、clangとlldを使うことで、開発を行うマシンのOSやアーキテクチャに左右されることなく、高級言語から直接UEFIアプリケーションを生成することができます。
UEFIの提供するサービスは、構造体と関数ポインタを扱えれば自分で記述することが可能なので、ライブラリを使わずに開発を行うことも可能です。実際、私の開発しているliumOSでは、必要となるUEFIの構造体を自前で定義して利用しています。UEFIを使って複雑なことを行わないのであれば、必要な定義はそこまで多くないため、現実的です。
一方で「定型文的な記述はしたくない!」「手っ取り早くライブラリを使って楽をしたい!」という方には、EDK2やgnu-efiという既存ライブラリがおすすめです。利用例は公式ドキュメントの他、下記のようなものがあります。
UEFIに移行すると、自ずと64bit対応のOSを書く必要がありますが、基本的には高級言語を使っている限りコンパイラが面倒をみてくれるので、根本的な変更が必要になるわけではありません。しかし、CPUの機能に依存する部分(割り込み処理・ページング等)についてはデータ構造の変更や機能追加があるので注意が必要です。
また、x86_64における32bitに対する64bitでの大きな変更点としては、セグメンテーション機能の大部分が無効化されているという点が挙げられます。そのため、はりぼてOSのように仮想メモリとメモリ保護をセグメンテーションで実現するのではなく、ページングを利用する必要があります。木構造の練習になりますね!
キー入力はがんばってUSB対応する。デバッグはシリアル入出力を活用する。
キー入力は難関です。QEMUなどで動くだけでもよいのであれば、ひとまずはこれまで通りPS/2キーボードに対応するのがよいでしょう。
しかし、PS/2キーボードに対応しない実機で動作させたい場合はそういうわけにはいきません。
それはさすがに大変だという場合、代替案として、シリアルポートに対応するという方法があります。というのも、PS/2ポートやエミュレーション機能をもたないマシンでも、リモートでの操作用にシリアル入出力をサポートしているマシンは、サーバーマシンを中心として比較的多く存在するためです。
また、GPD Micro PCという小型PCは、UEFIブートをサポートしている上に物理シリアルポートを装備しています。この機材のシリアルポートはなぜかCOM1ではなくCOM2なので注意が必要ですが、シリアルポートはI/Oポートをいくつか叩くだけで操作できるため、とても便利です。
シリアルポートもないしPS/2ポートもないよ!という場合は、あきらめてUSBホストコントローラのドライバとUSBキーボードのHIDクラスドライバを書きましょう!
最近では、著者の開発している自作OSも含め、いくつかのOSがUSBキーボードドライバの独自実装に成功しており、日本語での情報も増えつつあります。特に、uchan_nos氏による「USB 3.0 ホストドライバ自作入門」は非常に有用ですので、一読の価値があります。
タイマーはまずPITからHPETへ
タイマーについては、まずレガシーなPITをサポートし、割り込み等の環境を一度整えてから、それをHPETで置き換える、という方法がスムーズでよいでしょう。
HPETタイマーの実装については、Intelによる仕様書のほか、OSDev.orgのWikiにコード例を交えた解説があります。また、日本語の情報では大神祐真氏による「フルスクラッチで作る!x86_64自作OS パート2 ACPIでHPET取得してスケジューラを作る本」がHTMLとして公開されていますので、一読するとよいでしょう。
HPETの先、Local APICタイマーなどへ進みたい方には、uchan_nos氏による「Local APICタイマー入門」が役立ちます。
APICとマルチコアサポート
私の試した環境では、Local APICに関しては、起動時にUEFIが有効にしてくれている場合がほとんどのようでした。したがって、Local APICごとに割り当てられた固有のLocal APIC IDを取得し、それをターゲットとするようにI/O APICの割り込みリダイレクトテーブルを設定してあげれば、Legacy PICからの脱却が可能です。ただし、Local APICにはxAPICとx2APICの2つの動作モードが存在し、xAPICの場合はメモリマップドI/O、x2APICの場合はrdmsr / wrmsr命令で読み書きできるCPUのModel Specific Register(MSR)経由でLocal APICとやりとりをする必要があるので注意が必要です。
マルチコアについては、冒頭でも説明した通り、マルチコアのCPUであっても最初はシングルコアのCPUと同等の動作をし、ブートを担当するコア(Boot Service Processor, BSP)以外のコア(Application Processor, AP)については停止した状態になっています。そこで、マルチコアCPUとして動作させるためには、他のCPUを起こしてあげる必要があるわけです。そのためにはAPICを介してコア間割り込みを発生させる必要があるのですが、なんと自分でコードを書かなくても、UEFI PI(Platform Initialization)のプロトコルにそのための関数が用意されています。したがって、UEFI経由で他のプロセッサを起動させることで、容易にマルチコアサポートを実現することができます。詳細は大神祐真氏の「フルスクラッチで作る!x86_64自作OS パート5 てっとりばやくマルチコア」を参考にしてください。
日本語情報も増えつつある
前回の記事でも触れましたが、ここ数年で自作OS界隈は再び活気を取り戻してきています。特に、osdev-jp結成以降、自作OSもくもく会参加者だけでなくそれ以外の人々も、自作OSに関する情報発信を行うことに積極的になってきています。また、技術系の同人誌即売会が活発になってきたこともあり、日本語で自作OS関連の情報を発信している人が、数多く現れるようになってきました。筆者もそのうちの一人ですが、ここで有用な自作OS関連の同人誌等の情報をご紹介しておきたいと思います。
大神祐真氏のサークル「へにゃぺんて」では、UEFIをライブラリなしで叩く、UEFIベアメタルプログラミングから、HPETを制御してスケジューラを開発したり、ネットワークカードを制御してイーサネットフレームの送受信を行ったり、UEFI経由でマルチプロセッサの初期化を行うなどに至るまで、最新の自作OS向けに役立つ数多くの同人誌が出版されています。HTML版およびPDF版については無料で公開されていますので、興味のある項目を一読してみることをおすすめします。
記事中でも何度か紹介したuchan_nos氏による「USB 3.0 ホストドライバ自作入門」は、USB3.0ホストコントローラ(xHCI)のドライバを自作することに関して、日本語ではっきりとまとまっている現状唯一の冊子になります。xHCIドライバを実装される方は必読と言っても過言ではないでしょう。また、「Local APICタイマー入門」は、現代のコンピュータに搭載されている各種タイマーの比較やLocal APICタイマーの制御について詳しく解説されています。
もぷり氏のサークル「moppris」では、OSの中では少し高レイヤとなる、メモリアロケータ・プロセススケジューラ・ファイルシステムについて、それぞれいくつかのアルゴリズムを取り上げてコード例も交えつつ解説する冊子を頒布しています。残念ながら電子版やオンラインでの販売はないようですが、技術書典などの即売会で見かけた際には要チェックです。
retrage氏によるサークル「海洋軟件」では、UEFIを中心にしてLinuxやGRUBの実装に迫る冊子が頒布されています。最新の「UEFI読本ごった煮編」では、バイナリレベルでUEFIアプリケーションをつくってみる方法や、UEFIアプリケーションをqemu上でデバッグする方法、プログラミング言語RustをUEFIターゲットで利用する方法などが解説されており、UEFIアプリケーションをより深く理解したい場合におすすめです。
bosuke氏による「ほんのり詳しいUEFI BIOS」では、UEFIはそもそもどのようなものなのか、UEFIがOS起動前にどのようにしてDIMMやPCIeを初期化しているか、ACPIやSMBIOSからどのような情報が取れるかなど、UEFIを使ってOSを書くだけであれば知る必要はなくとも、知っておいて損はない裏側の様子が解説されており、教養としてためになります。
最後に、私hikaliumによる「OS Girls」では、自作OS入門者向けに、簡単なUEFIアプリケーションのつくりかたや、コンピューターのブートシーケンスの裏側、キーボード制御の基本などを小説形式で軽くまとめています。今後も続編を出す予定ですので、お楽しみに!
まとめ
これまで見てきたように、自作OSはコンピューターの進化から逃れることはできません。これまではレガシーハードウェアのエミュレーション機能に救われ、10年以上前の知識でも実機で動作するOSをつくることができていたかもしれませんが、それも近いうちにタイムリミットを迎えます。ですが幸いなことに、新しいハードウェアをサポートするための情報は少しずつ世の中に出回ってきています。むしろ、新しい時代のハードウェアだからこそ、過去の遺産を引きずっている既存のOSと比べてシンプルな実装で、同等以上の機能を実現することができるかもしれません。
今はまだ、現代的なコンピューター向けの自作OS入門本として最適なものは誕生していませんが、このように情報も集まりつつあることですし、近いうちに問題は解決されることでしょう。ですが、そのような本の登場を待たなくても、簡単なOSもどき程度であれば、ここにある情報をたどることで作り始めることができるはずです。そして、そこで得られた知見を広く共有することで、自作OSの世界をより一層盛り上げることができます。どうですか?私たちと一緒に、自作OSをしませんか?
次回は、これらの困難を乗り越えた先にどんな未来があるかについて見ていくことにしましょう。