Top

第1章 ブートローダーを変更し各APをカーネルへジャンプさせる

マシンの電源を入れ、最初から動作している1つ目のプロセッサを「BSP(BootStrap Processor)」と呼びます。対して、電源を入れた直後は動作していない、2つ目以降のプロセッサを「AP(Application Processor)」と呼びます。これまで、この自作OSシリーズでは、BSPしか扱ってきませんでした。

この章では、ブートローダーの段階でUEFI経由でAPをセットアップし、AP側もBSPと同様にカーネルへジャンプさせる所までを順を追って紹介します。

1.1 マシンで有効なプロセッサ数を取得する

久々のUEFIです。この項では、UEFIの機能の呼び出し方の復習も兼ねて、UEFI経由で使用可能プロセッサ数を取得してみます。

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

1.1.1 EFI_MP_SERVICES_PROTOCOLの準備

UEFIが提供する色々な機能は「プロトコル」という単位でまとめられています。マルチコアの制御も同様で、「EFI_MP_SERVICES_PROTOCOL」にまとめられています。

プロトコルの実体は関数ポインタをメンバーに持つ構造体で、LocateProtocol()などの関数を使用すると構造体の先頭アドレスを取得できます。

それでは、まず、EFI_MP_SERVICES_PROTOCOLを、UEFI関連の定義を記載しているinclude/efi.hへ追加します(リスト1.1)。

リスト1.1: 011_get_nproc/include/efi.h(本書で使用する箇所のみ抜粋)

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

struct EFI_DEVICE_PATH_UTILITIES_PROTOCOL {
        /* ・・・ 省略 ・・・ */
};

/* 追加(ここから) */
struct EFI_CPU_PHYSICAL_LOCATION {
        /* ・・・ 省略 ・・・ */
};

struct EFI_PROCESSOR_INFORMATION {
        /* ・・・ 省略 ・・・ */
};

struct EFI_MP_SERVICES_PROTOCOL {
        unsigned long long (*GetNumberOfProcessors)(
                struct EFI_MP_SERVICES_PROTOCOL *This,
                unsigned long long *NumberOfProcessors,
                unsigned long long *NumberOfEnabledProcessors);
        /* ・・・ 省略 ・・・ */
        unsigned long long (*StartupAllAPs)(
                struct EFI_MP_SERVICES_PROTOCOL *This,
                void (*Procedure)(void *ProcedureArgument),
                unsigned char SingleThread,
                void *WaitEvent,
                unsigned long long TimeoutInMicroSeconds,
                void *ProcedureArgument,
                unsigned long long **FailedCpuList);
        /* ・・・ 省略 ・・・ */
        unsigned long long (*WhoAmI)(
                struct EFI_MP_SERVICES_PROTOCOL *This,
                unsigned long long *ProcessorNumber);
};
/* 追加(ここまで) */

extern struct EFI_SYSTEM_TABLE *ST;
/* ・・・ 省略 ・・・ */
extern struct EFI_DEVICE_PATH_UTILITIES_PROTOCOL *DPUP;
extern struct EFI_MP_SERVICES_PROTOCOL *MSP;            /* 追加 */

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

EFI_MP_SERVICES_PROTOCOLと、そこから参照される新たな構造体も2つ追加しました。なお、構造体のメンバーは、本書で使用しないものは省略しています。構造体定義の全体像はサンプルディレクトリ内のファイルを参照してください。

EFI_MP_SERVICES_PROTOCOL構造体定義に並ぶ各関数がマルチコア向けにUEFIが提供している関数です。プロセッサ数は「GetNumberOfProcessors」で取得します(使い方は後述)。

efi.hの最後に、EFI_MP_SERVICES_PROTOCOL構造体のポインタ変数「MSP」をexternしています。ここへ、LocateProtocol関数を使用して取得したEFI_MP_SERVICES_PROTOCOL構造体の先頭アドレスを格納します。この行自体はextern宣言で、変数の実体はefi周りの初期化処理を行うlibuefi/efi.cで定義します。

それでは、libuefi/efi.cの変更点を示します(リスト1.2)。

リスト1.2: 011_get_nproc/libuefi/efi.c

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

struct EFI_SYSTEM_TABLE *ST;
/* ・・・ 省略 ・・・ */
struct EFI_DEVICE_PATH_UTILITIES_PROTOCOL *DPUP;
struct EFI_MP_SERVICES_PROTOCOL *MSP;   /* 追加 */
struct EFI_GUID lip_guid = {0x5b1b31a1, 0x9562, 0x11d2,
/* ・・・ 省略 ・・・ */

void efi_init(struct EFI_SYSTEM_TABLE *SystemTable)
{
        /* ・・・ 省略 ・・・ */
        /* 追加(ここから) */
        struct EFI_GUID msp_guid = {0x3fdda605, 0xa76e, 0x4f46,
                                    {0xad, 0x29, 0x12, 0xf4,
                                     0x53, 0x1b, 0x3d, 0x08}};
        /* 追加(ここまで) */

        ST = SystemTable;
        ST->BootServices->SetWatchdogTimer(0, 0, 0, NULL);
        /* ・・・ 省略 ・・・ */
        ST->BootServices->LocateProtocol(&dpup_guid, NULL, (void **)&DPUP);
        ST->BootServices->LocateProtocol(&msp_guid, NULL, (void **)&MSP); //追加
}
/* ・・・ 省略 ・・・ */

EFI_MP_SERVICES_PROTOCOLの先頭アドレスを指すポインタ変数MSPをグローバル変数として定義し、efi_init()の中でLocateProtocol()を使用してEFI_MP_SERVICES_PROTOCOLの先頭アドレスを取得しています。

これで、EFI_MP_SERVICES_PROTOCOLの機能をポインタ変数MSPを通して呼び出せるようになりました。

1.1.2 GetNumberOfProcessors()でプロセッサ数を取得する

EFI_MP_SERVICES_PROTOCOLが持つGetNumberOfProcessors()でマシンのプロセッサ数を取得できます。

GetNumberOfProcessors()のプロトタイプ宣言部分を再掲します(リスト1.3)。

リスト1.3: GetNumberOfProcessors()

unsigned long long (*GetNumberOfProcessors)(
        struct EFI_MP_SERVICES_PROTOCOL *This,
        unsigned long long *NumberOfProcessors,
        unsigned long long *NumberOfEnabledProcessors);

引数と戻り値は以下の通りです。

プロセッサの有効/無効はEFI_MP_SERVICES_PROTOCOLのEnableDisableAP()で変更できます。本書では特に使用しないため、詳しくは後述のコラム記載の仕様書を参照してみてください。なお、include/efi.hのEFI_MP_SERVICES_PROTOCOLの定義には、この関数の宣言も含めていますので、呼び出すことは可能です。

それではさっそく、この関数を使ってみます。poiboot.cへ処理を追加してみました(リスト1.4)。

リスト1.4: 011_get_nproc/poiboot.c

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

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        /* ・・・ 省略 ・・・ */
        put_param(L"kernel_arg3", kernel_arg3);

        /* 追加(ここから) */
        /* プロセッサ数を表示してみる */
        unsigned long long nproc, nproc_en;
        status = MSP->GetNumberOfProcessors(MSP, &nproc, &nproc_en);
        assert(status, L"MSP->GetNumberOfProcessors");

        puts(L"nproc: ");
        puth(nproc, 1);
        puts(L"\r\n");
        puts(L"nproc_en: ");
        puth(nproc_en, 1);
        puts(L"\r\n");
        while (TRUE);
        /* 追加(ここまで) */

        /* UEFIのブートローダー向け機能を終了させる */
        exit_boot_services(ImageHandle);

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

GetNumberOfProcessors()を呼び出して、引数でポインタを渡している「nproc」と「nproc_en」へ、それぞれ「マシンが持つプロセッサ数」と「マシンが持つ有効なプロセッサ数」を格納させ、画面へ表示しているだけです。

なお、nprocとnproc_enを表示する際、16進数でダンプするputh()へは、桁数を1で設定していますので、マシンのプロセッサ数が16以上の場合は*1良しなにこの桁数を増やしてください。

[*1] 自作OSでそんな環境を使えることはそうそう無いと思いますが。。

1.1.3 動作確認

ビルドし実行するとブートローダーのログの最後にnprocとnproc_enの値を表示して固まります(図1.1)。

011_get_nprocの実行結果

図1.1: 011_get_nprocの実行結果

筆者のマシンの場合、プロセッサの数(NumberOfProcessors)は4で、全てが使用可能(NumberOfEnabledProcessorsも4)でした*2

[*2] おそらく、一般的なPCにおいては、EFI_MP_SERVICES_PROTOCOLの関数で意図的に無効化しない限り、NumberOfProcessorsとNumberOfEnabledProcessorsは一致する(NumberOfProcessorsの全てがNumberOfEnabledProcessors)かと思います。

EFI_MP_SERVICES_PROTOCOLのドキュメントは?

EFI_MP_SERVICES_PROTOCOLについては、「フルスクラッチで作る!UEFIベアメタルプログラミング」で紹介したUEFIの仕様書には書かれていません。

マルチコア等、プラットフォームに依存するUEFIの仕様は仕様書が分かれていて、通常の仕様書と同じくUEFI公式の仕様書のページ*3にある「UEFI Platform Initialization Specification」に書かれています。2019年6月現在はバージョン1.7が公開されていて、EFI_MP_SERVICES_PROTOCOLの仕様は「13 DXE Boot Services Protocol(P.462-)」に書かれています。

[*3] https://uefi.org/specifications

GetNumberOfProcessors()が返すプロセッサ数は論理プロセッサ数

GetNumberOfProcessors()が返すプロセッサ数は、論理プロセッサ(実行ユニット)の数です。そのため、例えばハイパースレッディングにより、各物理プロセッサに2つの論理プロセッサが動作する場合、物理プロセッサ数の2倍の値が返ります。

1.2 プロセッサ数をカーネルへ渡す

今後、カーネル側で「任意のタスクを任意のプロセッサで実行させる」という事を行うにあたり、プロセッサ数の情報はカーネル側でも必要です。

そこで、前項で取得したプロセッサ数をカーネルへ渡すようにします。

この項では、カーネル側もセットで変更します。サンプルディレクトリは以下の通りです。

1.2.1 ブートローダー: カーネルへプロセッサ数を渡す

カーネルへのプラットフォーム情報の受け渡しにはplatform_infoという構造体を用意していました。そのため、プロセッサ数はこの構造体へメンバーとして追加することにします。

前項のpoiboot.cをリスト1.5のように変更します。

リスト1.5: 012_1_add_nproc_kern_param/poiboot.c

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

struct __attribute__((packed)) platform_info {
        struct fb fb;
        void *rsdp;
        unsigned int nproc;     /* 追加 */
} pi;

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

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();
        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 nproc, nproc_en;
        status = MSP->GetNumberOfProcessors(MSP, &nproc, &nproc_en);
        assert(status, L"MSP->GetNumberOfProcessors");
        pi.nproc = nproc_en;
        /* 追加(ここまで) */
        unsigned long long kernel_arg2 = (unsigned long long)π
        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);

        /* 「プロセッサ数を表示してみる」の処理は削除 */

        /* UEFIのブートローダー向け機能を終了させる */
        exit_boot_services(ImageHandle);

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

platform_info構造体にプロセッサ数(nproc)のメンバーを増やし、nproc_en(有効なプロセッサ数)を設定しています。

1.2.2 カーネル: ブートローダーから渡されたプロセッサ数を表示する

platform_info構造体にnprocメンバーを増やしたので、カーネル側も併せて変更し、ブートローダーから受け取ったnprocを表示してみます。

main.cをリスト1.6のように変更します。

リスト1.6: 012_2_dump_nproc_at_kern/main.c

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

struct __attribute__((packed)) platform_info {
        struct framebuffer fb;
        void *rsdp;
        unsigned int nproc;     /* 追加 */
};

#define INIT_APP        "test"

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

        /* CPU周りの初期化 */
        gdt_init();
        intr_init();

        /* 追加(ここから) */
        /* ブートローダーから渡されたプロセッサ数を表示 */
        puts("NPROC ");
        puth(pi->nproc, 1);

        /* haltして待つ */
        while (1)
                cpu_halt();
        /* 追加(ここまで) */

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

特に解説することはありません。ブートローダーから受け取ったnprocを画面へ表示し、それ以降の処理へ進まないようwhile()で止めているだけです。

1.2.3 動作確認

実行すると、図1.2のように画面にプロセッサ数が表示されることを確認できます。

012のブートローダーとカーネルの実行結果

図1.2: 012のブートローダーとカーネルの実行結果

現状の自作カーネルは、起動の始めに画面を青で塗りつぶします。モノクロ印刷なので分かりにくいですが、背景が青になっているので、カーネルのレベルでプロセッサ数を表示できていることが分かります。

1.3 APを動作させてみる

前項まででEFI_MP_SERVICES_PROTOCOLの使い方とカーネルへのパラメータの受け渡しを紹介しました。APを動作させてみる事も、プロセッサ数の確認と同様に、EFI_MP_SERVICES_PROTOCOLの関数を呼び出すことで行えます。

この項では、またブートローダーに少し手を加え、EFI_MP_SERVICES_PROTOCOLのStartupAllAPs()を使用して全てのAPを動作させてみます。

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

1.3.1 StartupAllAPs()について

StartupAllAPs()は、その名の通り、全てのAPの動作を開始させる関数*4です。APに実行させたい関数を引数に渡すことで、全てのAPに指定した関数を実行させることができます。

[*4] 指定した特定のAPのみ動作を開始させる関数(StartupThisAP())もあります。本書では使用しないのですが、include/efi.hのEFI_MP_SERVICES_PROTOCOLの定義には含めていますので呼び出すことは可能です。詳しくは仕様書を見てみてください。

StartupAllAPs()をリスト1.7のように使用します。

リスト1.7: 013_start_ap/poiboot.c

/* ・・・ 省略 ・・・ */
void ap_main(void *_st);        /* 追加 */

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        /* ・・・省略  ・・・ */
        put_param(L"kernel_arg3", kernel_arg3);

        /* 追加(ここから) */
        /* BSP自身のプロセッサ番号を表示 */
        unsigned long long pnum;
        status = MSP->WhoAmI(MSP, &pnum);
        assert(status, L"MSP->WhoAmI");
        puth(pnum, 1);

        /* 全てのAPをスタートする */
        status = MSP->StartupAllAPs(MSP, ap_main, 0, NULL, 0, ST, NULL);
        assert(status, L"MSP->StartupAllAPs");

        /* BSP/APのプロセッサ番号を表示できたらそのまま止める */
        while (TRUE);
        /* 追加(ここまで) */

        /* UEFIのブートローダー向け機能を終了させる */
        exit_boot_services(ImageHandle);

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

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

/* 追加(ここから) */
void ap_main(void *_st)
{
        efi_init(_st);

        /* 自身のプロセッサ番号を表示 */
        unsigned long long pnum;
        unsigned long long status = MSP->WhoAmI(MSP, &pnum);
        assert(status, L"MSP->WhoAmI");
        puth(pnum, 1);
}
/* 追加(ここまで) */

まず「BSP自身のプロセッサ番号を表示」で、そのコードを実行しているプロセッサのプロセッサ番号を取得するために「WhoAmI()」を使用しています。この関数は、第2引数で与えるポインタの指す先へWhoAmI()を呼び出したプロセッサ番号を格納してくれます。そのため、このコードブロックでは、BSPのプロセッサ番号(=0)を取得しputh()で画面表示しているだけです。

次の「全てのAPをスタートする」のコードブロックでStartupAllAPs()を使用して全てのAPの実行を開始させています。各APに実行させる関数は「ap_main()」です(ここより下の方で定義を追加しています)。見てもらえば分かる通りですが、処理としてはWhoAmI()を使用して自身のプロセッサ番号を取得して画面表示しているだけです。AP側でもUEFIの機能を呼び出すため、ap_main()の引数(StartupAllAPs()の第6引数)では、EFI_SYSTEM_TABLEのポインタを渡しています。

StartupAllAPs()に関しては他に、第3引数(SingleThread)に0(各APは非同期)を、第4引数(WaitEvent)でNULL(BSPに対しAPは同期型)を指定しています。そのため、全てのAPで並列にap_main()が実行された後、StartupAllAPs()から戻ってくる挙動になります。

表示したプロセッサ番号を確認したいので、StartupAllAPs()から戻ってきた後は、無限ループで止めています。

1.3.2 動作確認

実行すると図1.3のように搭載しているプロセッサの数だけ番号が表示されます。

013_start_apの実行結果

図1.3: 013_start_apの実行結果

筆者のPCは論理プロセッサ4つなので、0から3までの番号が表示されています。

1.4 APもカーネルへジャンプさせる

APへ任意の関数を実行させることができました。それでは、この章の最後に、BSP同様にAPもカーネルへジャンプさせてみます。

この項のサンプルディレクトリは「014_jump_to_kern」です。これが本書で作成するブートローダーの最終版になります。

なお、APがカーネルへジャンプしてくる際は、カーネル側もそれに応じて変更する必要があるのですが、新しい内容も出てくるので次章でカーネル側の変更を説明します。

1.4.1 ap_main()でカーネルへジャンプするようにする

前項で、各APにap_main()を実行させるようにしました。基本的には、このap_main()でカーネルへジャンプするようにするだけです。

早速ですが、変更後のap_main()はリスト1.8の通りです。

リスト1.8: 014_jump_to_kern/poiboot.c

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

struct __attribute__((packed)) platform_info {
        struct fb fb;
        void *rsdp;
        unsigned int nproc;
} pi;

/* 追加(ここから) */
struct ap_info {
        unsigned long long kernel_start;
        unsigned long long stack_space_start;
        struct EFI_SYSTEM_TABLE *system_table;
} ai;
/* 追加(ここまで) */

/* ・・・ 省略 ・・・ */
void ap_main(void *_ai);        /* 変更 */

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

/* 変更(ここから) */
void ap_main(void *_ai)
{
        struct ap_info *ai = _ai;
        efi_init(ai->system_table);

        unsigned long long pnum;
        unsigned long long status = MSP->WhoAmI(MSP, &pnum);
        assert(status, L"MSP->WhoAmI");

        unsigned long long stack_base = ai->stack_space_start + (pnum * MB);
        unsigned long long kernel_arg1 = 0;
        unsigned long long kernel_arg2 = 0;
        unsigned long long kernel_arg3 = 0;

        /* カーネルへ渡す引数設定(引数に使うレジスタへセットする) */
        unsigned long long _sb = stack_base, _ks = ai->kernel_start;
        __asm__ (" mov     %0, %%rdx\n"
                 " mov     %1, %%rsi\n"
                 " mov     %2, %%rdi\n"
                 " mov     %3, %%rsp\n"
                 " jmp     *%4\n"
                 ::"m"(kernel_arg3), "m"(kernel_arg2), "m"(kernel_arg1),
                  "m"(_sb), "m"(_ks));

        while (TRUE);
}
/* 変更(ここまで) */

引数として「ap_info」という構造体を渡すようにしました。StartupAllAPs()で渡すことのできる引数は1つなので、AP側へ渡すべき情報はこの構造体にまとめます。そして、platform_infoと同様にグローバル変数を定義しています。

APがカーネルへジャンプするにあたって必要な情報は「カーネルの領域の先頭アドレス」と「AP毎のスタックポインタ」です。「カーネルの領域の先頭アドレス」はap_infoのkernel_startで渡します。「AP毎のスタックポインタ」は、AP毎にアドレスを変える必要があります。ここでは各APのスタックサイズを1MBとして、「AP用スタック領域の先頭アドレス + (プロセッサ番号 * 1MB)」をAP毎のスタックポインタとしています。ap_infoのstack_space_startが「AP毎のスタック領域の先頭アドレス」です。「プロセッサ番号」は、ap_main()内でWhoAmI()を実行して取得しています。

なお、カーネルへ渡す「kernel_arg1」から「kernel_arg3」の引数は、(少なくとも当面は、)AP側で使用しないため、全て0にしています。kernel_arg1はEFI_SYSTEM_TABLEへのポインタで、今の所、BSP側でカーネルが動作する際も使用していません。kernel_arg2はplatform_infoへのポインタで、kernel_arg3はファイルシステムの先頭アドレスです。これらに関しては、各種デバイスの初期化処理などは基本的にはBSP側で一度実施しておけば十分です。一部、AP側でも初期化が必要なものもありますが、そのために必要な情報はカーネルへジャンプした後、自ら取得可能です(少なくとも本書で扱う範囲では)。

ap_main()の最後で行っているカーネルへのジャンプのインラインアセンブラ自体は、BSP側で実行しているものと同じです。

1.4.2 ap_main()の呼び出し側の変更点

ap_main()の変更に合わせて、poiboot.cのリスト1.9の変更を行えば、この項の変更は完了です。

リスト1.9: 014_jump_to_kern/poiboot.c

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

#define MB              1048576 /* 1024 * 1024 */

/* 追加(ここから) */
/* AP側のUEFI処理完了までのBSPの待ち時間(単位: マイクロ秒) */
#define WAIT_FOR_AP_USECS       100000 /* 100ms */
/* 追加(ここまで) */

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

void efi_main(void *ImageHandle, struct EFI_SYSTEM_TABLE *SystemTable)
{
        /* ・・・ 省略 ・・・ */
                kernel_arg3 = 0;
        put_param(L"kernel_arg3", kernel_arg3);

        /* 変更(ここから) */
        /* 全てのAPをスタートする */
        ai.kernel_start = kernel_start;
        ai.stack_space_start = stack_base;
        ai.system_table = ST;
        status = MSP->StartupAllAPs(
                MSP, ap_main, 0, NULL, WAIT_FOR_AP_USECS, &ai, NULL);
        /* 変更(ここから) */

        /* UEFIのブートローダー向け機能を終了させる */
        exit_boot_services(ImageHandle);

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

グローバル変数として定義していたap_info型の変数aiを、ap_main()の引数として渡すようにしました。

また、StartupAllAPs()の第5引数のタイムアウト時間を100msに設定しています。今回のap_main()は、カーネルへジャンプした後、戻って来ることはありません。そのため、タイムアウト時間が0のままだとBSP側でのStartupAllAPs()の呼び出しが、AP側でap_main()から返ってくるまで無限に待ち続けてしまいます。そのため、タイムアウトを設定しています。なお、BSPはStartupAllAPs()から戻ってくると、UEFIの機能を終了させるexit_boot_services()を実行します。ap_main()でUEFIの機能としてWhoAmI()を実行するため、AP側でこれを終えるまではBSP側でUEFI終了の処理に進んでしまうことが無いようにタイムアウト時間を設定したいです。ここではざっくりと100msくらいでタイムアウトするようにしてみました。

APがカーネルへジャンプするためのブートローダー側の変更は以上です。カーネル側の変更点は次章で説明します。


Top