カーネルパニック

本章はエナガ本7.4節 (カーネルパニック)の内容に対応しています。

カーネルパニックは、カーネルが続行不能なエラーに遭遇したときに発生するもので、GoやRustでいうpanicに似た概念です。次のPANICマクロがカーネルパニックの実装です。

kernel.h
#define PANIC(fmt, ...)                                                        \
    do {                                                                       \
        printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__);  \
        while (1) {}                                                           \
    } while (0)

エラー内容を表示した後に無限ループに入って処理を停止します。ここではマクロとして定義しています。なぜかというと、ソースファイル名 (__FILE__) と行番号 (__LINE__) を正しく表示するためです。これを関数として定義すると、__FILE____LINE__ は、PANICの呼び出し元ではなく、PANICが定義されているファイル名と行番号になってしまいます。

このマクロでは、2つのイディオムが登場しています。

1つ目は do-while 文です。while (0)であることから、このループは必ず1回しか実行されません。これは、複数の文からなるマクロを定義したいときに頻出する書き方です。単に { ...} で括ってしまうと、if 文などと組み合わせたときに意図しない動作に繋がる (分かりやすい例) ことがあります。また、行末のバックスラッシュ (\) にも注意してください。マクロは複数行で定義されていますが、展開時には改行が無視されます。

2つ目のイディオムは ##__VA_ARGS__ です。これは可変長引数を受け取るマクロを定義するときに便利なコンパイラ拡張機能 (参考: GCCのドキュメント) です。## が付いていることで、可変長引数が空のときに直前の , を削除してくれます。これにより、PANIC("booted!") のように引数が1つのときにもコンパイルが通るようになります。

マクロの書き方を理解したところで、PANICを使ってみましょう。使い方は printf と同じです。

kernel.c
void kernel_main(void) {
    memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
   
    PANIC("booted!");
    printf("unreachable here!\n");
}

実際に動かしてみて、正しいファイル名と行番号、そしてPANIC以後の処理が実行されないこと ("unreachable here!"が表示されないこと) を確認してください。

$ ./run.sh
PANIC: kernel.c:46: booted!