Top

第1章 ACPI

HPETを使用するに当たり、この章で、ACPIからHPETの情報の引き出し方を説明します。

なお、この章は前著*1より前の著作*2で扱ったブートローダーに関する説明も含みます。ブートローダーに関する項は「boot:」を、カーネルに関する項は「kernel:」を、共通の項には「共通:」を項のタイトルに付けていますので、「ブートローダーは道具として使うだけ」等の場合は適宜読み飛ばしてください。ブートローダーとカーネルはバイナリレベルで分かれているため、カーネルを作る上でブートローダーの中身まで知らなくても大丈夫です。

[*1] フルスクラッチで作る!x86_64自作OS (パート1)

[*2] フルスクラッチで作る!UEFIベアメタルプログラミング パート1/パート2

1.1 共通: この章でやること

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の通りです。

HPETレジスタのアドレスを知るまでの流れ

図1.1: HPETレジスタのアドレスを知るまでの流れ

本書では「UEFIの各種テーブルからRSDPを見つけてカーネルへ渡す」事をブートローダーの役割とし、「RSDPからHPETレジスタの先頭アドレスを見つけ、HPETを制御する」のをカーネルの役割とします。

次の項からは、それらの役割をブートローダーとカーネルへ実装していきます。

1.2 boot: EFI Configuration Tableを見てみる

まずはEFI System TableからEFI Configuration Tableを見つけ、EFI Configuration Tableの内容を見てみるようにブートローダーへ実装してみます。

この項のサンプルディレクトリは「010_dump_config」です。

1.2.1 やること

EFI System Tableは、PC起動時にUEFIファームウェアがブートローダーを呼び出す際、「struct EFI_SYSTEM_TABLE」という構造体のポインタとしてエントリ関数の引数に渡してくれます*4

[*4] 詳しくは拙著「フルスクラッチで作る!UEFIベアメタルプログラミング」をご覧ください。

そして、EFI System TableとEFI Configuration Tableの関係は図1.2の通りです。

EFI System TableとEFI Configuration Tableの関係

図1.2: EFI System TableとEFI Configuration Tableの関係

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: EFI_ACPI_TABLE_GUID

//
// ACPI 2.0 or newer tables should use EFI_ACPI_TABLE_GUID
//
#define EFI_ACPI_TABLE_GUID \
  {0x8868e871,0xe4f1,0x11d3,\
   {0xbc,0x22,0x00,0x80,0xc7,0x3c,0x88,0x81}}

リスト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を取得できます。

  1. EFI_SYSTEM_TABLEのConfigurationTableからEFI_CONFIGURATION_TABLEの配列の先頭アドレスを取得
  2. EFI_CONFIGURATION_TABLEの配列の中からVendorGuidがEFI_ACPI_TABLE_GUIDと一致するものを探す

1.2.2 実装

それでは、実装していきます。

まずは、efi.hのstruct EFI_SYSTEM_TABLE定義箇所へNumberOfTableEntriesとConfigurationTableメンバを追加します(リスト1.2)。

また、これから実装するEFI Configuration Tableをダンプする関数「dump_efi_configuration_table」のプロトタイプ宣言も追加しておきます(リスト1.2)。

リスト1.2: 010_dump_config/include/efi.h(310行目辺り)

/* ・・・省略・・・ */

struct __attribute__((packed)) EFI_SYSTEM_TABLE {
        char _buf1[44];
        /* ・・・省略・・・ */
        } *BootServices;
        /* 追加(ここから) */
        unsigned long long NumberOfTableEntries;
        struct EFI_CONFIGURATION_TABLE {
                struct EFI_GUID VendorGuid;
                void *VendorTable;
        } *ConfigurationTable;
        /* 追加(ここまで) */
};

/* ・・・省略・・・ */

void efi_init(struct EFI_SYSTEM_TABLE *SystemTable);
void dump_efi_configuration_table(void);        /* 追加 */

#endif

次に、EFI Configuration Tableをダンプする関数「dump_efi_configuration_table」をlibuefi/efi.cへ追加します(リスト1.3)。

リスト1.3: 010_dump_config/libuefi/efi.c(60行目辺り)

/* ・・・省略・・・ */

void efi_init(struct EFI_SYSTEM_TABLE *SystemTable)
{
        /* ・・・省略・・・ */
        ST = SystemTable;
        /* ・・・省略・・・ */
}

/* 追加(ここから) */
void dump_efi_configuration_table(void)
{
        unsigned long long i;
        for (i = 0; i < ST->NumberOfTableEntries; i++) {
                puth(i, 1);
                putc(L':');
                puth((unsigned long long)&ST->ConfigurationTable[i], 16);
                putc(L':');
                puth(ST->ConfigurationTable[i].VendorGuid.Data1, 8);
                putc(L' ');
                puth(ST->ConfigurationTable[i].VendorGuid.Data2, 4);
                putc(L' ');
                puth(ST->ConfigurationTable[i].VendorGuid.Data3, 4);
                putc(L' ');
                unsigned char j;
                for (j = 0; j < 8; j++)
                        puth(ST->ConfigurationTable[i].VendorGuid.Data4[j], 2);
                putc(L':');
                puth((unsigned long long)ST->ConfigurationTable[i].VendorTable,
                     16);
                puts(L"\r\n");
        }
}
/* 追加(ここまで) */

poibootは起動時に実行するefi_init関数でEFI_SYSTEM_TABLEのポインタをグローバル変数STへ格納しているので、dump_efi_configuration_table関数ではこれを使っています。

やっていることは単に、NumberOfTableEntriesの数だけループを回してConfigurationTableのVendorGuidとVendorTable(ポインタ)をダンプしているだけです。

最後に、poiboot.cへdump_efi_configuration_table関数を呼び出す処理を追加します(リスト1.4)。

リスト1.4: 010_dump_config/poiboot.c(35行目辺り)

/* ・・・省略・・・ */

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        efi_init(SystemTable);

        puts(L"Starting poiboot ...\r\n");

        /* 追加(ここから) */
        /* ConfigurationTableの内容を表示して停止する */
        dump_efi_configuration_table();
        while (TRUE);
        /* 追加(ここまで) */

        /* ・・・省略・・・ */
}

/* ・・・省略・・・ */

1.2.3 動作確認

poiboot.confのロードなどの直前に処理を追加しているので、ビルドして生成されたpoiboot.efi単体で実行してみることができます。

実行してみると、EFI_ACPI_TABLE_GUIDと一致するものがあることがわかります(図1.3)。

010_dump_configの実行結果

図1.3: 010_dump_configの実行結果

struct定義時の「__attribute__((packed))」について

リスト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))」を付けるようにしています。

1.3 boot: ACPI Tableを見つける処理を実装する

前項では、EFI Configuration TableをダンプしてACPI Tableが存在することを確認しました。この項ではACPI Tableを見つける処理を実装します。

この項のサンプルディレクトリは「011_find_acpi」です。

1.3.1 実装

新たに説明することは特に無いので、さっそく実装してみます。

まず、libuefi/efi.cへACPI Tableを見つけてその先頭アドレスを返す「find_efi_acpi_table」関数を追加します(リスト1.5)。

リスト1.5: 011_find_acpi/libuefi/efi.c(80行目辺り)

/* ・・・省略・・・ */

void dump_efi_configuration_table(void)
{
        /* ・・・省略・・・ */
}

/* 追加(ここから) */
void *find_efi_acpi_table(void)
{
        const struct EFI_GUID efi_acpi_table = {
                0x8868e871, 0xe4f1,0x11d3,
                {0xbc, 0x22, 0x00, 0x80, 0xc7, 0x3c, 0x88, 0x81}};

        unsigned long long i;
        for (i = 0; i < ST->NumberOfTableEntries; i++) {
                struct EFI_GUID *guid = &ST->ConfigurationTable[i].VendorGuid;
                if ((guid->Data1 == efi_acpi_table.Data1)
                    && (guid->Data2 == efi_acpi_table.Data2)
                    && (guid->Data3 == efi_acpi_table.Data3)) {
                        unsigned char is_equal = TRUE;
                        unsigned char j;
                        for (j = 0; j < 8; j++) {
                                if (guid->Data4[j] != efi_acpi_table.Data4[j])
                                        is_equal = FALSE;
                        }
                        if (is_equal == TRUE)
                                return ST->ConfigurationTable[i].VendorTable;
                }
        }
        return NULL;
}
/* 追加(ここまで) */

const定義しているefi_acpi_tableのGUIDに一致するものをforループで一つずつチェックしながら見つけているだけです。

次に、この関数をpoiboot.cから呼び出せるようにinclude/efi.hへプロトタイプ宣言を追加します(リスト1.6)。

リスト1.6: 011_find_acpi/include/efi.h(510行目辺り)

/* ・・・省略・・・ */

void efi_init(struct EFI_SYSTEM_TABLE *SystemTable);
void dump_efi_configuration_table(void);
void *find_efi_acpi_table(void);        /* 追加 */

#endif

最後にfind_efi_acpi_table関数をpoiboot.cから呼び出してみます(リスト1.7)。

リスト1.7: 011_find_acpi/poiboot.c(40行目辺り)

/* ・・・省略・・・ */

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        efi_init(SystemTable);

        puts(L"Starting poiboot ...\r\n");

        /* 変更(ここから) */
        /* RSDPのシグネチャを表示して停止する */
        char *s = find_efi_acpi_table();
        putc(*s++);     /* 'R' */
        putc(*s++);     /* 'S' */
        putc(*s++);     /* 'D' */
        putc(*s++);     /* ' ' */
        putc(*s++);     /* 'P' */
        putc(*s++);     /* 'T' */
        putc(*s++);     /* 'R' */
        putc(*s);       /* ' ' */
        while (TRUE);
        /* 変更(ここまで) */

        /* ・・・省略・・・ */

ACPI Tableの構成について詳しくは後述しますが、ACPI Tableの先頭にはASCIIのシグネチャが並んでいます。RSDPには8バイト(8文字)のシグネチャがあり、find_efi_acpi_table関数でとってきたACPI Table(RSDP)先頭アドレスを利用してシグネチャを表示してみています。コメントにも書いてある通りですが、XSDTのシグネチャは"RSD PTR "の8文字(最後にスペース有)なので、それが表示されるはずです。

1.3.2 動作確認

試しに実行してみると、確かに"RSD PTR "というシグネチャが表示されることを確認できます(図1.4)。

011_find_acpiの実行結果

図1.4: 011_find_acpiの実行結果

1.4 boot: RSDPをカーネルへ渡すようにする

ブートローダー側の変更の最後として、見つけたACPI Table先頭アドレス(RSDP)をカーネルへ渡すように変更します。

この項のサンプルディレクトリは「012_pass_rsdp」です。

1.4.1 やること

これまでEFI System Tableの先頭アドレスや、フレームバッファ情報の先頭アドレス等を渡してきたように、RSDPもカーネル側のエントリ関数の引数として渡します。

ただし、カーネルへ渡したい情報は今後も増えると思われ、その都度引数を増やすのも面倒です。そのため、フレームバッファとRSDPを「プラットフォーム情報(struct platform_info)」という一つの構造体にまとめ、その先頭アドレスをカーネルへ渡すことにします。

この項で行う変更を図示すると図1.5の通りです。

struct platform_infoを追加し、カーネル側エントリ関数の第2引数で渡す

図1.5: struct platform_infoを追加し、カーネル側エントリ関数の第2引数で渡す

1.4.2 実装

変更を行うのはpoiboot.cのみです。

まず、struct platform_infoを定義し、グローバル変数piを作成します(リスト1.8)。

リスト1.8: 012_pass_rsdp/poiboot.c(15行目辺り)

/* ・・・省略・・・ */

#define MB              1048576 /* 1024 * 1024 */

/* 追加(ここから) */
struct __attribute__((packed)) platform_info {
        struct fb fb;
        void *rsdp;
} pi;
/* 追加(ここまで) */

void load_config(
        struct EFI_FILE_PROTOCOL *root, unsigned short *conf_file_name,
        unsigned long long *kernel_start, unsigned long long *stack_base,
        unsigned long long *fs_start);
/* ・・・省略・・・ */

また、前項で追加した処理は削除しておきます(リスト1.9)。

リスト1.9: 012_pass_rsdp/poiboot.c(30行目辺り)

/* ・・・省略・・・ */

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        efi_init(SystemTable);

        puts(L"Starting poiboot ...\r\n");

        /* 削除(ここから) */
        /* RSDPのシグネチャを表示して停止する */
        char *s = find_efi_acpi_table();
        putc(*s++);     /* 'R' */
        putc(*s++);     /* 'S' */
        putc(*s++);     /* 'D' */
        putc(*s++);     /* ' ' */
        putc(*s++);     /* 'P' */
        putc(*s++);     /* 'T' */
        putc(*s++);     /* 'R' */
        putc(*s);       /* ' ' */
        while (TRUE);
        /* 削除(ここまで) */

        /* ・・・省略・・・ */

そして、piへパラメータを設定します。そして、カーネルの第2引数へfbに代わりpiのポインタを指定するようにします(リスト1.10)。

リスト1.10: 012_pass_rsdp/poiboot.c(60行目辺り)

/* ・・・省略・・・ */

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        /* ・・・省略・・・ */

        /* カーネルへ引数として渡す内容を変数に準備する */
        unsigned long long kernel_arg1 = (unsigned long long)ST;
        put_param(L"kernel_arg1", kernel_arg1);
        init_fb();
        /* unsigned long long kernel_arg2 = (unsigned long long)&fb;
         * ↑変更前 */
        /* 変更後(ここから) */
        pi.fb.base = fb.base;
        pi.fb.size = fb.size;
        pi.fb.hr = fb.hr;
        pi.fb.vr = fb.vr;
        pi.rsdp = find_efi_acpi_table();
        unsigned long long kernel_arg2 = (unsigned long long)&pi;
        /* 変更後(ここまで) */
        put_param(L"kernel_arg2", kernel_arg2);
        unsigned long long kernel_arg3;
        if (has_fs == TRUE)
                kernel_arg3 = fs_start;
        else
                kernel_arg3 = 0;
        put_param(L"kernel_arg3", kernel_arg3);

        /* ・・・省略・・・ */

動作確認のためにはカーネル側でRSDPを受け取る処理が必要です。そのため、動作確認は次の項で行います。

1.5 kernel: RSDPをブートローダーから受け取る

ブートローダーからRSDPを受けとるようにカーネル側を変更します。

この項のサンプルディレクトリは「013_dump_rsdp」です。

1.5.1 実装: RSDPを受け取り、シグネチャを表示

さっそく実装します。

まず、platform_infoの定義を追加します(リスト1.11)。

リスト1.11: 013_dump_rsdp/main.c

/* ・・・省略・・・ */
#include <common.h>

/* 追加(ここから) */
struct __attribute__((packed)) platform_info {
        struct framebuffer fb;
        void *rsdp;
};
/* 追加(ここまで) */

/* ・・・省略・・・ */

次に、この定義を使用してブートローダーからplatform_infoを受け取るようにカーネル側のエントリ関数(start_kernel関数)を変更します(リスト1.12)。

リスト1.12: 013_dump_rsdp/main.c

void start_kernel(void *_t __attribute__ ((unused)), struct platform_info *pi,
                  void *_fs_start)                             /* ↑変更 */
{
        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);        /* 変更 */
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();

        /* ・・・省略・・・ */

start_kernel関数の第2引数をplatform_info構造体へ変更し、fb_init関数へ渡すframebuffer構造体のポインタもplatform_info内のものを参照するように変更しています。

そして、platform_infoの中のrsdpを使って、ブートローダー側で試した時と同様にRSDPのシグネチャを表示させてみます(リスト1.13)。

リスト1.13: 013_dump_rsdp/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__ ((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();

        /* 追加(ここから) */
        /* RSDPのシグネチャを表示 */
        char *s = (char *)pi->rsdp;
        putc(*s++);     /* 'R' */
        putc(*s++);     /* 'S' */
        putc(*s++);     /* 'D' */
        putc(*s++);     /* ' ' */
        putc(*s++);     /* 'P' */
        putc(*s++);     /* 'T' */
        putc(*s++);     /* 'R' */
        putc(*s);       /* ' ' */
        while (1);
        /* 追加(ここまで) */

        /* ・・・省略・・・ */

今の所、RSDP表示の後の処理は行わないように、whileの無限ループで止めています。

1.5.2 動作確認

実行すると図1.6のようにRSDPのシグネチャが表示されます。

013_dump_rsdpの実行結果

図1.6: 013_dump_rsdpの実行結果

バイナリ間の後方互換性

今回、「ブートローダーがカーネルへ渡す引数の内容を変える」という「ブートローダーとカーネルの間の仕様変更」を行いました。

ですが、実は、カーネル側がこの仕様変更を行う前のバージョンであっても(ブートローダー側だけ先にバージョンアップしてしまっても)動作できるように変更を行っています。

platform_info構造体の1番目のメンバーにframebuffer構造体を置いているのがその点です。構造体自身の先頭アドレスとその1つ目のメンバーの先頭アドレスは同じなので、カーネルがplatform_info構造体を知らなくても、framebuffer構造体だと思ってアクセスしてくれれば問題無いようになっています。

1.5.3 実装: RSDPの定義とACPI初期化関数追加

この項の最後にacpi.cというソースファイルを追加し、RSDPの定義とカーネル起動時に呼び出すACPI初期化関数を追加します。

まず、RSDPの定義を追加します(リスト1.14)。

リスト1.14: 013_dump_rsdp/acpi.c

/* 追加(ここから) */
struct __attribute__((packed)) RSDP {
        char Signature[8];
        unsigned char Checksum;
        char OEMID[6];
        unsigned char Revision;
        unsigned int RsdtAddress;
        unsigned int Length;
        unsigned long long XsdtAddress;
        unsigned char Extended_Checksum;
        unsigned char Reserved[3];
};
/* 追加(ここまで) */

RSDP構造体のメンバーで注目すべきはXsdtAddressです。その名の通り、XSDTを指すアドレスです。

その他のメンバーは使わないので気にしなくて良いです。

なお、acpi.cの外でRSDPの構造を気にする必要は無いため、acpi.cの中でRSDPを定義しています。

加えて、以降の処理のためにRSDPからXSDTのアドレスを取得してグローバル変数へ格納するようACPIの初期化関数(acpi_init)をacpi.cへ追加します(リスト1.15)。

リスト1.15: 013_dump_rsdp/acpi.c

struct __attribute__((packed)) RSDP {
        /* ・・・省略・・・ */
};

/* 追加(ここから) */
unsigned long long xsdt;

void acpi_init(void *rsdp)
{
        xsdt = ((struct RSDP *)rsdp)->XsdtAddress;
}
/* 追加(ここまで) */

start_kernel関数から呼び出せるようにacpi.hを作成してプロトタイプ宣言を追加します(リスト1.16)。

リスト1.16: 013_dump_rsdp/include/acpi.h

/* 追加(ここから) */
#ifndef _ACPI_H_
#define _ACPI_H_

void acpi_init(void *rsdp);

#endif
/* 追加(ここまで) */

そして、start_kernel関数へacpi_init関数呼び出し処理を追加します(リスト1.17)。

リスト1.17: 013_dump_rsdp/main.c

#include <x86.h>
#include <intr.h>
#include <pic.h>
#include <acpi.h> /* 追加 */
#include <fb.h>
/* ・・・省略・・・ */

void start_kernel(void *_t __attribute__ ((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* フレームバッファ周りの初期化 */
        fb_init(&pi->fb);
        set_fg(255, 255, 255);
        set_bg(0, 70, 250);
        clear_screen();

        /* ACPIの初期化 */  /* 追加 */
        acpi_init(pi->rsdp); /* 追加 */

        /* RSDPのシグネチャを表示 */
        /* ・・・省略・・・ */

最後に、Makefileへacpi.oをビルドターゲットに追加します(リスト1.18)。

リスト1.18: 013_dump_rsdp/Makefile

TARGET = kernel.bin
CFLAGS = -Wall -Wextra -nostdinc -nostdlib -fno-builtin -fno-common -Iinclude
LDFLAGS = -Map kernel.map -s -x -T kernel.ld
OBJS = main.o iv.o fbcon.o fb.o font.o kbc.o x86.o intr.o pic.o \
        acpi.o handler.o fs.o common.o  # acpi.oを追加

# ・・・ 省略 ・・・

これで、この項で実装する内容は終わりです。

1.6 kernel: XSDTを見てみる

RSDPからXSDTの先頭アドレスがわかりました。なので、次はXSDTの中身を表示してみます。

この項のサンプルディレクトリは「014_dump_xsdt」です。

1.6.1 やること

この項ではXSDTから参照できるすべてのテーブルのシグネチャを表示させてみます。

まず、XSDTの構造は図1.7の通りです。

XSDTの構造

図1.7: XSDTの構造

System Description Table Header(SDTH)は共通のヘッダ構造です。XSDTから参照できる各デバイス等の固有のテーブルにもACPIで管理するためのヘッダとしてSDTHが付いています。

SDTHに続いて各デバイス等の固有のテーブルの先頭アドレスが並びます。各個のテーブルにもヘッダとしてSDTHが付いていますので、先頭のシグネチャで何のテーブルであるかを識別できます。

また、XSDTがいくつのテーブルを参照しているかは自身のSDTHのLengthから知ることができます。LengthにはSDTHを含めたテーブルのサイズ(バイト)が入っているので、SDTHのサイズを引いてポインタサイズで割ればXSDTが参照するテーブル数になります。

1.6.2 実装

まずSDTHとXSDTの定義を追加します。SDTHは今後acpi.cの外でも使うのでacpi.hで(リスト1.19)、対してXSDTはacpi.cの中でしか使わないのでacpi.cで定義します(リスト1.20)。

リスト1.19: 014_dump_xsdt/include/acpi.h

#ifndef _ACPI_H_
#define _ACPI_H_

/* 追加(ここから) */
struct __attribute__((packed)) SDTH {
        char Signature[4];
        unsigned int Length;
        unsigned char Revision;
        unsigned char Checksum;
        char OEMID[6];
        char OEM_Table_ID[8];
        unsigned int OEM_Revision;
        unsigned int Creator_ID;
        unsigned int Creator_Revision;
};
/* 追加(ここまで) */

void acpi_init(void *rsdp);

#endif

リスト1.20: 014_dump_xsdt/acpi.c

#include <acpi.h> /* 追加 */

struct __attribute__((packed)) RSDP {
        /* ・・・省略・・・ */
};

/* 追加(ここから) */
struct __attribute__((packed)) XSDT {
        struct SDTH Header;
        struct SDTH *Entry[0];
};
/* 追加(ここまで) */

struct XSDT *xsdt;      /* 変更 */

void acpi_init(void *rsdp)
{
        xsdt = (struct XSDT *)((struct RSDP *)rsdp)->XsdtAddress;    /* 変更 */
}

併せてグローバル変数xsdtの定義もXSDT構造体のポインタへ変更しておきました。

次に、XSDTが参照するテーブルのシグネチャを表示するdump_xsdt関数を実装します(リスト1.21)。

リスト1.21: 014_dump_xsdt/acpi.c

#include <acpi.h>
#include <fbcon.h>        /* 追加 */

/* ・・・ 省略 ・・・ */

void acpi_init(void *rsdp)
{
        xsdt = (struct XSDT *)((struct RSDP *)rsdp)->XsdtAddress;
}

/* 追加(ここから) */
void dump_sdth_sig(struct SDTH *h)
{
        unsigned char i;
        for (i = 0; i < 4; i++)
                putc(h->Signature[i]);
}

void dump_xsdt(void)
{
        dump_sdth_sig(&xsdt->Header);
        puts("\r\n");

        unsigned long long num_sdts =
                (xsdt->Header.Length - sizeof(struct SDTH))
                / sizeof(struct SDTH *);
        puts("NUM SDTS ");
        putd(num_sdts, 2);
        puts("\r\n");

        unsigned long long i;
        for (i = 0; i < num_sdts; i++) {
                dump_sdth_sig(xsdt->Entry[i]);
                putc(' ');
        }
        puts("\r\n");
}
/* 追加(ここまで) */

シグネチャの表示処理は何度か使いそうなのでdump_sdth_sigという関数へ分けました。

これらの関数のプロトタイプ宣言をacpi.hへ追加し(リスト1.22)、main.cのstart_kernel関数から呼び出すように変更します(リスト1.23)。

リスト1.22: 014_dump_xsdt/include/acpi.h

/* ・・・ 省略 ・・・ */

void acpi_init(void *rsdp);
void dump_sdth_sig(struct SDTH *h);     /* 追加 */
void dump_xsdt(void);                   /* 追加 */

#endif

リスト1.23: 014_dump_xsdt/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* ・・・ 省略 ・・・ */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* 変更(ここから) */
        /* XSDTをダンプ */
        dump_xsdt();
        while (1);
        /* 変更(ここまで) */

        /* ・・・ 省略 ・・・ */

1.6.3 動作確認

例えばQEMUで実行すると図1.8のようにXSDTが参照するテーブルの個数(NUM SDTS)と各テーブルのシグネチャが表示されます。

014_dump_xsdtのQEMU実行結果

図1.8: 014_dump_xsdtのQEMU実行結果

HPETのテーブルのシグネチャは"HPET"なのですが、、無いです。

実機で実行してみると図1.9のようにHPETのシグネチャも表示されます。

014_dump_xsdtの実機実行結果

図1.9: 014_dump_xsdtの実機実行結果

一旦この項はここまでにして、次項でこれを解決します。

1.7 kernel: OVMFのバージョンを上げる

古いバージョンのOVMF*8のACPIテーブルからはHPETが参照できない様で、OVMFのバージョンを上げるとHPETの項目が表示されるようになります。そのため、この項でOVMFのバージョンを上げます。

[*8] UEFIのオープンソースのファームウェア実装。QEMUでUEFIを使用する際に使っています。

なお、この項のサンプルディレクトリは前項に引き続き「014_dump_xsdt」です。

1.7.1 やること

やることは単にOVMFの新しいバイナリをダウンロードしてきてQEMUコマンドの引数に与えるようにMakefileを修正するだけです。

これまで、OVMFのバイナリは"OVMF.fd"という単一のバイナリを使用していました。これはUEFIの実行コードのバイナリと、起動デバイスの優先順位などのパラメータを保存する先のバイナリとが1体となったものです。

最新のOVMFではこれが2つのバイナリに分かれており、今後はこの2つのバイナリの形式のものをqemuコマンドへ指定して使用するようにします。

1.7.2 新しいOVMFを入手(debファイルをダウンロード)

まずは新しい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

1.7.3 新しいOVMFを入手(APTで入手)

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ファームウェアです。

1.7.4 実装: Makefileを修正

最後に、MakefileのQEMUコマンドを修正します(リスト1.24)。

リスト1.24: 014_dump_xsdt/Makefile

run: $(TARGET)
        cp $(TARGET) ../fs/
        # 変更(ここから)
        qemu-system-x86_64 -m 4G \
        -drive if=pflash,format=raw,readonly,file=$$HOME/ovmf/OVMF_CODE.fd \
        -drive if=pflash,format=raw,file=$$HOME/ovmf/OVMF_VARS.fd \
        -hda fat:../fs
        # 変更(ここまで)

一つの"-bios"オプションだった箇所を、2つの"-drive"オプションへ変更しています。片方に"OVMF_CODE.fd"を指定し、もう片方に"OVMF_VARS.fd"を指定しているだけです。なお、本書ではOVMF_CODE.fdとOVMF_VARS.fdをホームディレクトリ直下のovmfディレクトリに配置している想定でMakefileを書いていますので、異なる場合は適宜修正してください。

1.7.5 動作確認

新バージョンのOVMFを導入し、Makefileを書き換えた上で、改めて実行すると、図1.10の様にHPETのシグネチャが表示されることが確認できます。

014_dump_xsdtの実行結果(新バージョンのOVMF使用時)

図1.10: 014_dump_xsdtの実行結果(新バージョンのOVMF使用時)

1.8 kernel: HPETテーブルを見つける処理を追加する

XSDTがどのようになっているのかは確認できたので、ACPI編の最後としてHPETテーブルをXSDTの中から見つける処理を追加します。

この項のサンプルディレクトリは「015_find_hpet」です。

1.8.1 やること

この項では指定されたシグネチャのテーブルをXSDTから探して先頭アドレスを返す処理を「get_sdt」という関数で実装します。

1.8.2 実装

get_sdt関数を実装するにあたり、現在dump_xsdt関数内で行っている「XSDTから参照できるテーブルの数(num_sdts)を計算する処理」はget_sdt関数でも必要になるので、acpi_init関数で初期化時に行ってしまうようにします(リスト1.25)。

リスト1.25: 015_find_hpet/acpi.c

/* ・・・省略 ・・・ */

struct XSDT *xsdt;
unsigned long long num_sdts;    /* 追加 */

void acpi_init(void *rsdp)
{
        xsdt = (struct XSDT *)((struct RSDP *)rsdp)->XsdtAddress;

        num_sdts = (xsdt->Header.Length - sizeof(struct SDTH))       /* 追加 */
                / sizeof(struct SDTH *);                        /* 追加 */
}

/* ・・・省略 ・・・ */

void dump_xsdt(void)
{
        dump_sdth_sig(&xsdt->Header);
        puts("\r\n");

        /* 削除:num_sdts計算処理 */
        puts("NUM SDTS ");
        putd(num_sdts, 2);
        puts("\r\n");

        /* ・・・省略 ・・・ */

そして、get_sdt関数はリスト1.26の様に実装できます。

リスト1.26: 015_find_hpet/acpi.c

/* ・・・ 省略 ・・・ */
#include <fbcon.h>
#include <common.h>       /* 追加 */

/* ・・・ 省略 ・・・ */

void dump_xsdt(void)
{
        /* ・・・ 省略 ・・・ */
}

/* 追加(ここから) */
struct SDTH *get_sdt(char *sig)
{
        unsigned long long i;
        for (i = 0; i < num_sdts; i++) {
                if (!strncmp(sig, xsdt->Entry[i]->Signature, 4))
                        return xsdt->Entry[i];
        }

        return NULL;
}
/* 追加(ここまで) */

文字数指定で文字列比較を行うstrncmp関数は本書で新たにcommon.cへ追加していますが、特に説明するほどでもないためここでは説明しません。実装が気になる場合はサンプルディレクトリ内を見てみてください(何の変哲もない実装かと思います)。

get_sdt関数のプロトタイプ宣言をacpi.hへ追加し(コード省略)、main.cでget_sdt関数を呼び出してみます(リスト1.27)。

リスト1.27: 015_find_hpet/main.c

/* ・・・ 省略 ・・・ */

void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
                  void *_fs_start)
{
        /* ・・・ 省略 ・・・ */

        /* ACPIの初期化 */
        acpi_init(pi->rsdp);

        /* 変更(ここから) */
        /* HPETの情報を取得 */
        struct SDTH *hpet_table = get_sdt("HPET");
        dump_sdth_sig(hpet_table);
        while (1);
        /* 変更(ここまで) */

        /* ・・・ 省略 ・・・ */

1.8.3 動作確認

実行すると図1.11のようにHPETのシグネチャ("HPET")が表示されます。

015_find_hpetの実行結果

図1.11: 015_find_hpetの実行結果

ここまでで、ACPIに関する部分は終わりです。


Top