HPETを使用するに当たり、この章で、ACPIからHPETの情報の引き出し方を説明します。
なお、この章は前著*1より前の著作*2で扱ったブートローダーに関する説明も含みます。ブートローダーに関する項は「boot:」を、カーネルに関する項は「kernel:」を、共通の項には「共通:」を項のタイトルに付けていますので、「ブートローダーは道具として使うだけ」等の場合は適宜読み飛ばしてください。ブートローダーとカーネルはバイナリレベルで分かれているため、カーネルを作る上でブートローダーの中身まで知らなくても大丈夫です。
[*1] フルスクラッチで作る!x86_64自作OS (パート1)
[*2] フルスクラッチで作る!UEFIベアメタルプログラミング パート1/パート2
HPETはアドレス空間上に配置されたHPETのレジスタを使用して制御します。詳しくは後述しますが、タイマーの設定を行うレジスタや、タイマーのカウンタ値を取得したり設定したりするレジスタ等があり、これらのレジスタへ値をセットしたり、取得した値からタイマーの状態を確認しながらHPETを制御します。
そのため、HPETを制御するためにHPETのレジスタの先頭アドレスを取得する必要があるのですが、それには段階があります。
まず、PCに搭載されている各種デバイスの情報はACPI(Advanced Configuration and Power Interface)で管理されています。ACPIでは、個々のデバイス等の情報はそれぞれを管理するテーブルがあり、HPETにも「HPET Table」というテーブルがあります。このテーブルにHPETレジスタの先頭アドレスも格納されています。
そして、HPET Table含む個々のテーブルの先頭アドレス一覧を持つ「Extended System Description Table(XSDT)」*3というテーブルがあり、HPET Tableの先頭アドレスを知るにはXSDTの先頭アドレスを知る必要があります。
[*3] XSDTは64ビット用のテーブルです。名前の「Extended」というのは、元々32ビットの頃から「Root System Description Table(RSDT)」というテーブルがあり、それを64ビットへ拡張した、という意味です。
XSDTの先頭アドレスは、「Root System Description Pointer(RSDP)」というテーブルに持っていて、RSDPの先頭アドレスは、UEFI使用時はUEFIファームウェアが「EFI Configuration Table」というテーブルに持っています。
そしてEFI Configuration Tableのアドレスは、UEFIファームウェアがブートローダーを起動させるときに渡してくれる「EFI System Table」の中にあります。
EFI System TableからHPETのレジスタのアドレスを取得するまでの流れを図にまとめると図1.1の通りです。
本書では「UEFIの各種テーブルからRSDPを見つけてカーネルへ渡す」事をブートローダーの役割とし、「RSDPからHPETレジスタの先頭アドレスを見つけ、HPETを制御する」のをカーネルの役割とします。
次の項からは、それらの役割をブートローダーとカーネルへ実装していきます。
まずはEFI System TableからEFI Configuration Tableを見つけ、EFI Configuration Tableの内容を見てみるようにブートローダーへ実装してみます。
この項のサンプルディレクトリは「010_dump_config」です。
EFI System Tableは、PC起動時にUEFIファームウェアがブートローダーを呼び出す際、「struct EFI_SYSTEM_TABLE」という構造体のポインタとしてエントリ関数の引数に渡してくれます*4。
[*4] 詳しくは拙著「フルスクラッチで作る!UEFIベアメタルプログラミング」をご覧ください。
そして、EFI System TableとEFI Configuration Tableの関係は図1.2の通りです。
EFI Configuration Tableの実体はEFI_CONFIGURATION_TABLE構造体の配列です。EFI_CONFIGURATION_TABLEは、参照しているテーブルが何のテーブルであるのかを一意に決める「VendorGuid(struct EFI_GUID型)」というGUID*5と、そのテーブルへのポインタ「VendorTable(void *型)」のペアです。
[*5] Guaranteed Unique IDentifier。一意であることが保証されたID。
XSDTにもGUIDが決められており、その値はUEFI仕様に書かれています*6。なお、「XSDT」という呼び方はACPIの世界での呼び方で、UEFI仕様上は単に「ACPI Table」と呼んでいます*7。「EFI_ACPI_TABLE_GUID」という名前で値が書かれており、その内容はリスト1.1の通りです。
[*6] 4.6 EFI Configuration Table & Properties Table
[*7] XSDTさえ得られれば、そこから先はACPIの世界の話なので、UEFIから見ればACPIのテーブルはXSDTのみです。
リスト1.1のGUIDを持つEFI_CONFIGURATION_TABLEのVendorTableが、RSDPです。
なお、リスト1.1のコメントに「ACPI 2.0かそれ以降ではEFI_ACPI_TABLE_GUIDを使うべきである」旨が書かれている通り、EFI_ACPI_TABLE_GUIDはACPI 2.0以降向けのGUIDです。本書ではACPIのバージョンは2.0以降を想定して説明します。
そして、EFI_SYSTEM_TABLE構造体の最後のメンバである「ConfigurationTable」変数がEFI_CONFIGURATION_TABLEの配列の先頭を指していて、EFI_CONFIGURATION_TABLEが何個並んでいるかはNumberOfTableEntriesに格納されています。
まとめると、以下を行えばRSDPを取得できます。
それでは、実装していきます。
まずは、efi.hのstruct EFI_SYSTEM_TABLE定義箇所へNumberOfTableEntriesとConfigurationTableメンバを追加します(リスト1.2)。
また、これから実装するEFI Configuration Tableをダンプする関数「dump_efi_configuration_table」のプロトタイプ宣言も追加しておきます(リスト1.2)。
次に、EFI Configuration Tableをダンプする関数「dump_efi_configuration_table」をlibuefi/efi.cへ追加します(リスト1.3)。
poibootは起動時に実行するefi_init関数でEFI_SYSTEM_TABLEのポインタをグローバル変数STへ格納しているので、dump_efi_configuration_table関数ではこれを使っています。
やっていることは単に、NumberOfTableEntriesの数だけループを回してConfigurationTableのVendorGuidとVendorTable(ポインタ)をダンプしているだけです。
最後に、poiboot.cへdump_efi_configuration_table関数を呼び出す処理を追加します(リスト1.4)。
poiboot.confのロードなどの直前に処理を追加しているので、ビルドして生成されたpoiboot.efi単体で実行してみることができます。
実行してみると、EFI_ACPI_TABLE_GUIDと一致するものがあることがわかります(図1.3)。
リスト1.2でstruct EFI_SYSTEM_TABLEへstruct EFI_CONFIGURATION_TABLEの追加を説明するために引用していたEFI_SYSTEM_TABLEの定義箇所には「__attribute__((packed))」が付いていました。
前著までは付けていなかったので、このattributeについて簡単に補足しておくと、これは「構造体の各メンバーはメモリ空間上に隙間なく並べてくれ」というコンパイラへの指定です。
これが付いていないと、例えばchar型のメンバー変数Aとint型のメンバー変数Bがこの順に構造体のメンバーとして指定されていた時、「Bは4バイトアラインされた場所に配置しよう」としてAとBの間に3バイトの隙間を開ける可能性があります。
「単にchar型とint型の変数をセットで使えれば良い」という構造体ならば隙間は良しなに空けてもらっても良いのですが、EFI_SYSTEM_TABLEのような場合は「先頭からNバイト目には~が並ぶ」という仕様に基づくものなので、メンバー間で隙間が空いてしまうと目的の場所へ正しくアクセスできなくなってしまいます。
これまで筆者の手元で確認する限りメンバー間に隙間が空けられる事は無かったのですが、本書のサンプルを書く中で新たに定義した構造体でそのような事があったため、過去の構造体定義にも「__attribute__((packed))」を付けるようにしています。
前項では、EFI Configuration TableをダンプしてACPI Tableが存在することを確認しました。この項ではACPI Tableを見つける処理を実装します。
この項のサンプルディレクトリは「011_find_acpi」です。
新たに説明することは特に無いので、さっそく実装してみます。
まず、libuefi/efi.cへACPI Tableを見つけてその先頭アドレスを返す「find_efi_acpi_table」関数を追加します(リスト1.5)。
const定義しているefi_acpi_tableのGUIDに一致するものをforループで一つずつチェックしながら見つけているだけです。
次に、この関数をpoiboot.cから呼び出せるようにinclude/efi.hへプロトタイプ宣言を追加します(リスト1.6)。
最後にfind_efi_acpi_table関数をpoiboot.cから呼び出してみます(リスト1.7)。
ACPI Tableの構成について詳しくは後述しますが、ACPI Tableの先頭にはASCIIのシグネチャが並んでいます。RSDPには8バイト(8文字)のシグネチャがあり、find_efi_acpi_table関数でとってきたACPI Table(RSDP)先頭アドレスを利用してシグネチャを表示してみています。コメントにも書いてある通りですが、XSDTのシグネチャは"RSD PTR "の8文字(最後にスペース有)なので、それが表示されるはずです。
試しに実行してみると、確かに"RSD PTR "というシグネチャが表示されることを確認できます(図1.4)。
ブートローダー側の変更の最後として、見つけたACPI Table先頭アドレス(RSDP)をカーネルへ渡すように変更します。
この項のサンプルディレクトリは「012_pass_rsdp」です。
これまでEFI System Tableの先頭アドレスや、フレームバッファ情報の先頭アドレス等を渡してきたように、RSDPもカーネル側のエントリ関数の引数として渡します。
ただし、カーネルへ渡したい情報は今後も増えると思われ、その都度引数を増やすのも面倒です。そのため、フレームバッファとRSDPを「プラットフォーム情報(struct platform_info)」という一つの構造体にまとめ、その先頭アドレスをカーネルへ渡すことにします。
この項で行う変更を図示すると図1.5の通りです。
変更を行うのはpoiboot.cのみです。
まず、struct platform_infoを定義し、グローバル変数piを作成します(リスト1.8)。
また、前項で追加した処理は削除しておきます(リスト1.9)。
そして、piへパラメータを設定します。そして、カーネルの第2引数へfbに代わりpiのポインタを指定するようにします(リスト1.10)。
動作確認のためにはカーネル側でRSDPを受け取る処理が必要です。そのため、動作確認は次の項で行います。
ブートローダーからRSDPを受けとるようにカーネル側を変更します。
この項のサンプルディレクトリは「013_dump_rsdp」です。
さっそく実装します。
まず、platform_infoの定義を追加します(リスト1.11)。
次に、この定義を使用してブートローダーからplatform_infoを受け取るようにカーネル側のエントリ関数(start_kernel関数)を変更します(リスト1.12)。
start_kernel関数の第2引数をplatform_info構造体へ変更し、fb_init関数へ渡すframebuffer構造体のポインタもplatform_info内のものを参照するように変更しています。
そして、platform_infoの中のrsdpを使って、ブートローダー側で試した時と同様にRSDPのシグネチャを表示させてみます(リスト1.13)。
今の所、RSDP表示の後の処理は行わないように、whileの無限ループで止めています。
実行すると図1.6のようにRSDPのシグネチャが表示されます。
今回、「ブートローダーがカーネルへ渡す引数の内容を変える」という「ブートローダーとカーネルの間の仕様変更」を行いました。
ですが、実は、カーネル側がこの仕様変更を行う前のバージョンであっても(ブートローダー側だけ先にバージョンアップしてしまっても)動作できるように変更を行っています。
platform_info構造体の1番目のメンバーにframebuffer構造体を置いているのがその点です。構造体自身の先頭アドレスとその1つ目のメンバーの先頭アドレスは同じなので、カーネルがplatform_info構造体を知らなくても、framebuffer構造体だと思ってアクセスしてくれれば問題無いようになっています。
この項の最後にacpi.cというソースファイルを追加し、RSDPの定義とカーネル起動時に呼び出すACPI初期化関数を追加します。
まず、RSDPの定義を追加します(リスト1.14)。
RSDP構造体のメンバーで注目すべきはXsdtAddressです。その名の通り、XSDTを指すアドレスです。
その他のメンバーは使わないので気にしなくて良いです。
なお、acpi.cの外でRSDPの構造を気にする必要は無いため、acpi.cの中でRSDPを定義しています。
加えて、以降の処理のためにRSDPからXSDTのアドレスを取得してグローバル変数へ格納するようACPIの初期化関数(acpi_init)をacpi.cへ追加します(リスト1.15)。
start_kernel関数から呼び出せるようにacpi.hを作成してプロトタイプ宣言を追加します(リスト1.16)。
そして、start_kernel関数へacpi_init関数呼び出し処理を追加します(リスト1.17)。
最後に、Makefileへacpi.oをビルドターゲットに追加します(リスト1.18)。
これで、この項で実装する内容は終わりです。
RSDPからXSDTの先頭アドレスがわかりました。なので、次はXSDTの中身を表示してみます。
この項のサンプルディレクトリは「014_dump_xsdt」です。
この項ではXSDTから参照できるすべてのテーブルのシグネチャを表示させてみます。
まず、XSDTの構造は図1.7の通りです。
System Description Table Header(SDTH)は共通のヘッダ構造です。XSDTから参照できる各デバイス等の固有のテーブルにもACPIで管理するためのヘッダとしてSDTHが付いています。
SDTHに続いて各デバイス等の固有のテーブルの先頭アドレスが並びます。各個のテーブルにもヘッダとしてSDTHが付いていますので、先頭のシグネチャで何のテーブルであるかを識別できます。
また、XSDTがいくつのテーブルを参照しているかは自身のSDTHのLengthから知ることができます。LengthにはSDTHを含めたテーブルのサイズ(バイト)が入っているので、SDTHのサイズを引いてポインタサイズで割ればXSDTが参照するテーブル数になります。
まずSDTHとXSDTの定義を追加します。SDTHは今後acpi.cの外でも使うのでacpi.hで(リスト1.19)、対してXSDTはacpi.cの中でしか使わないのでacpi.cで定義します(リスト1.20)。
併せてグローバル変数xsdtの定義もXSDT構造体のポインタへ変更しておきました。
次に、XSDTが参照するテーブルのシグネチャを表示するdump_xsdt関数を実装します(リスト1.21)。
シグネチャの表示処理は何度か使いそうなのでdump_sdth_sigという関数へ分けました。
これらの関数のプロトタイプ宣言をacpi.hへ追加し(リスト1.22)、main.cのstart_kernel関数から呼び出すように変更します(リスト1.23)。
例えばQEMUで実行すると図1.8のようにXSDTが参照するテーブルの個数(NUM SDTS)と各テーブルのシグネチャが表示されます。
HPETのテーブルのシグネチャは"HPET"なのですが、、無いです。
実機で実行してみると図1.9のようにHPETのシグネチャも表示されます。
一旦この項はここまでにして、次項でこれを解決します。
古いバージョンのOVMF*8のACPIテーブルからはHPETが参照できない様で、OVMFのバージョンを上げるとHPETの項目が表示されるようになります。そのため、この項でOVMFのバージョンを上げます。
[*8] UEFIのオープンソースのファームウェア実装。QEMUでUEFIを使用する際に使っています。
なお、この項のサンプルディレクトリは前項に引き続き「014_dump_xsdt」です。
やることは単にOVMFの新しいバイナリをダウンロードしてきてQEMUコマンドの引数に与えるようにMakefileを修正するだけです。
これまで、OVMFのバイナリは"OVMF.fd"という単一のバイナリを使用していました。これはUEFIの実行コードのバイナリと、起動デバイスの優先順位などのパラメータを保存する先のバイナリとが1体となったものです。
最新のOVMFではこれが2つのバイナリに分かれており、今後はこの2つのバイナリの形式のものをqemuコマンドへ指定して使用するようにします。
まずは新しいOVMFバイナリを入手します。
ここでは、新しいOVMFバイナリを含むDebianパッケージ(debファイル)をダウンロードし、展開します。
Debian 9(Stretch)、あるいはsidを使っている場合、それぞれのバージョンのovmfパッケージに新しいOVMFが含まれています。その場合、APTでovmfパッケージのインストールを行えば良いです。手順は次節の「新しいOVMFを入手(APTで入手)」を参照してください。
それでは、手動でDebianパッケージをダウンロードし、展開します。ここではStretchのovmfパッケージのパッケージファイル(deb形式)を手動でダウンロードしてみます。
Debianパッケージは以下のウェブページから検索できます。
ページを下へスクロールして「パッケージディレクトリを検索」のフォームにキーワードを入力し[検索]ボタンをクリックすると関連するパッケージを検索できます。
各パッケージのバージョンごとのページのURLは固定化されています。ovmfパッケージのStretchバージョンのページは以下の通りです。
「ovmf のダウンロード」の項目の「アーキテクチャ」の箇所の「all」というリンクをクリックすると「ovmf_0~20161202.7bbe0b3e-1_all.deb のダウンロードページ」へ移動します。
実はダウンロードページもURLは固定なので、以下のURLです。
このページからDebianパッケージファイル(deb形式)をダウンロードできます。「ftp.jp.debian.org/debian」をクリックすると日本のミラーサイトからダウンロードできます。
debパッケージファイル名はバージョンアップにより変わるため、debパッケージのリンクまでは固定化できませんが、2018年6月26日現在は以下のURLです。
debファイルのダウンロードできたら、それを展開します。
debパッケージファイルはdpkgコマンドで展開できます。以下のコマンドで「ovmf_0~20161202.7bbe0b3e-1_all.deb」というdebパッケージファイルを「ovmf」ディレクトリへ展開します(コマンド実行時に展開先のディレクトリが無ければ作ってくれます)。
$ dpkg -x ovmf_0~20161202.7bbe0b3e-1_all.deb ovmf
Windowsの場合、dpkgコマンドを使った展開作業は、Microsoft StoreでDebian GNU/Linuxをインストールして使うと良いかと思います。
そして、ovmfディレクトリ内を見ると、OVMFのファームウェアファイルがあることを確認できます。
$ cd ovmf $ find . ./usr ./usr/share ./usr/share/OVMF ./usr/share/OVMF/OVMF_CODE.fd ./usr/share/OVMF/OVMF_VARS.fd ./usr/share/doc ./usr/share/doc/ovmf ./usr/share/doc/ovmf/changelog.Debian.gz ./usr/share/doc/ovmf/copyright ./usr/share/ovmf ./usr/share/ovmf/OVMF.fd ./usr/share/qemu ./usr/share/qemu/OVMF.fd
2018年6月現在最新の安定板であるDebian 9(コードネーム:Stretch)、あるいは開発版(コードネーム:sid)を使っている場合は、以下のコマンドでovmfパッケージをインストールすれば2つのバイナリの形式になったOVMFファームウェアが入手できます*9。
[*9] sidは日々新しいバージョンへ変わっているので、本書を読んだタイミングによってはパッケージの内容も変わっているかもしれません。
$ sudo apt install ovmf
dpkg -Lコマンドでパッケージでインストールされたファイルの一覧を確認すると以下の様に表示されます。
$ dpkg -L ovmf /. ./usr ./usr/share ./usr/share/OVMF ./usr/share/OVMF/OVMF_CODE.fd ./usr/share/OVMF/OVMF_VARS.fd ./usr/share/doc ./usr/share/doc/ovmf ./usr/share/doc/ovmf/changelog.Debian.gz ./usr/share/doc/ovmf/copyright ./usr/share/ovmf ./usr/share/ovmf/OVMF.fd ./usr/share/qemu ./usr/share/qemu/OVMF.fd
"OVMF_CODE.fd"と"OVMF_VARS.fd"が、2つのバイナリの形式になったOVMFファームウェアです。
最後に、MakefileのQEMUコマンドを修正します(リスト1.24)。
一つの"-bios"オプションだった箇所を、2つの"-drive"オプションへ変更しています。片方に"OVMF_CODE.fd"を指定し、もう片方に"OVMF_VARS.fd"を指定しているだけです。なお、本書ではOVMF_CODE.fdとOVMF_VARS.fdをホームディレクトリ直下のovmfディレクトリに配置している想定でMakefileを書いていますので、異なる場合は適宜修正してください。
新バージョンのOVMFを導入し、Makefileを書き換えた上で、改めて実行すると、図1.10の様にHPETのシグネチャが表示されることが確認できます。
XSDTがどのようになっているのかは確認できたので、ACPI編の最後としてHPETテーブルをXSDTの中から見つける処理を追加します。
この項のサンプルディレクトリは「015_find_hpet」です。
この項では指定されたシグネチャのテーブルをXSDTから探して先頭アドレスを返す処理を「get_sdt」という関数で実装します。
get_sdt関数を実装するにあたり、現在dump_xsdt関数内で行っている「XSDTから参照できるテーブルの数(num_sdts)を計算する処理」はget_sdt関数でも必要になるので、acpi_init関数で初期化時に行ってしまうようにします(リスト1.25)。
そして、get_sdt関数はリスト1.26の様に実装できます。
文字数指定で文字列比較を行うstrncmp関数は本書で新たにcommon.cへ追加していますが、特に説明するほどでもないためここでは説明しません。実装が気になる場合はサンプルディレクトリ内を見てみてください(何の変哲もない実装かと思います)。
get_sdt関数のプロトタイプ宣言をacpi.hへ追加し(コード省略)、main.cでget_sdt関数を呼び出してみます(リスト1.27)。
実行すると図1.11のようにHPETのシグネチャ("HPET")が表示されます。
ここまでで、ACPIに関する部分は終わりです。