Linuxカーネルがブートに失敗しUARTに何も出ないときのprintデバッグ

皆様こんにちは。ミラクシアエッジテクノロジーの川井です。 ミラクシアでは組込みLinuxの起動高速化ソリューションやLinux BSPの受託開発サービス等を提供しています。カスタムSoCをターゲットとしたブートローダ・Linuxカーネルの移植にも対応しています。 カーネルのデバッグはJTAGデバッガを用意することが多いと思いますが、JTAGデバッガは取り回しが悪いし、特にMMU有効化処理の周辺はデバッグが難しいため、効率化のためにも早期にprintデバッグを実現したいところです。 そこで本稿ではカーネル起動の初期段階にprintデバッグコードを埋め込む方法を紹介します。

課題

Linuxカーネルの初期化は概ね以下のような処理が行われます。
  • 特権モードの設定などCPU初期設定
  • MMU初期化
  • (ここまでアセンブラ、以後C言語)
  • セカンダリCPU起動などCPUの初期設定
  • (setup_archでearlyconが起動、以降シリアルドライバが対応していればprintk使用可)
  • 仮想メモリ初期化
  • サブシステム初期化
  • 初期スレッドの起動
    • デバイスドライバ起動
    • (ここでシリアルドライバが起動、以降通常のシステムコンソールでprintkが使用可)
    • ファイルシステム初期化
    • ユーザー空間起動
シリアルドライバがearlyconに対応している場合はsetup_arch以降でprintkが利用可能ですが、そうでない場合は初期スレッド起動後のinitcallのレベル5まで待たなければならず、初期化処理のかなり後ろまでprintデバッグができません。 シリコンベンダーが提供しているBSPを使う立場としてはearlyconがあれば困ることはまずありませんが、BSPを作る側としては、earlyconよりもっと早いタイミングでprintデバッグできるようにしておきたい事があります。

QEMU Virtマシンモデル

本稿ではQEMUのVirtマシンモデルで動作するサンプルコードを紹介していきます。 VirtモデルはarmのPL011が0x09000000にありますので、PL011のTRMをみて文字を出力するプログラムを書いてみます。
movk x0, #0x0900, LSL #16  // x0 <- 0x09000000
mov x1, #0x101             // TXE, UARTEN
str x1, [x0, #0x30]        // UARTCR <- 0x101
mov x1, #0x41              // 'A'
str x1, [x0]               // UARTDR <- 0x30
このプログラムはVirtモデルのPL011にASCIIコード41h('A')を出力します。オフセット+30hにあるUARTCRレジスタのUARTEN, TXEビットを設定して、オフセット+00hのUARTDRに文字を書きだすだけです。簡単ですね。Trusted Firmware AのPL011ドライバ実装も参考になると思います。

Linuxカーネルのエントリーポイントで文字を出力する

arm64 Linuxカーネルのエントリーポイントはarm64/kernel/head.Sにあります。ブートローダーに問題がなければ、ここでarm64 CPUはEL2またはEL1hステートでMMUは未初期化なので、物理アドレスでペリフェラルデバイスにアクセスできます。ここにさっき書いたプログラムを追加します。
Patch license: GPL (same as linux kernel)
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/arch/arm64/kernel/head.S?h=v6.12.39#n56

diff --git a/arch/arm64/kernel/head.S b/arch/arm64/kernel/head.S
index cb68adcabe07..881672460125 100644
--- a/arch/arm64/kernel/head.S
+++ b/arch/arm64/kernel/head.S
@@ -57,6 +57,14 @@
        /*
         * DO NOT MODIFY. Image header expected by Linux boot-loaders.
         */
+
+       movk x0, #0x0900, LSL #16  // x0 <- 0x09000000
+       mov x1, #0x101             // TXE, UARTEN
+       str x1, [x0, #0x30]        // UARTCR <- 0x101
+       mov x1, #0x41              // 'A'
+       str x1, [x0]               // UARTDR <- 0x30
+1:     b 1b
+
        efi_signature_nop                       // special NOP to identity as PE/COFF executable
        b       primary_entry                   // branch to kernel start, magic
        .quad   0                               // Image load offset from start of RAM, little-endian
実行してみましょう。
git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/stable/linux.git -b v6.12.39
cd linux
# 上記パッチをあてる
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- allnoconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
# ビルドが成功すれば ./arch/arm64/boot/Image が生成されます
qemu-system-aarch64 -cpu cortex-a53 -M virt -nographic -serial mon:stdio -kernel ./arch/arm64/boot/Image
上手くいけば以下のようにターミナル上に'A'と表示されます。
  • 上記パッチは無限ループして止まるように作っているので、Ctrl+A → xでQEMUを終了してください
  • 通常のユースケースにおいてはallnoconfigのみでは機能が足りませんが、本稿の内容はカーネル初期化処理が動作すればよいのでallnoconfigとしています
このプログラムが動作しない場合、おそらくブートローダーに問題があります。ブートローダの設定が間違っていてエントリーポイントに到達していないか、そもそもカーネルを正しくロードできていないか、カーネルのブート要件を満たしていない等の原因を探っていくことになります。

C言語ゾーンのエントリーポイントで文字を出力する

Linuxカーネルのエントリーポイントまで動作することを確認できたら、ほとんどの場合はC言語ゾーンのエントリーポイントstart_kernelまでは問題なく動作します。start_kernelの先頭まで動作しているかどうか調べるため、UART出力するコードを埋め込んでみましょう。start_kernelではMMUが初期化済みなので、fixmapという仕組みでマップを作る必要があります。ioremapはまだ使えません。
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/init/main.c?h=v6.12.39#n903

Patch license: GPL (same as linux kernel)

diff --git a/init/main.c b/init/main.c
index c4778edae797..a8b5849f8fdc 100644
--- a/init/main.c
+++ b/init/main.c
@@ -905,6 +905,18 @@ void start_kernel(void)
        char *command_line;
        char *after_dashes;

+       {
+               void __iomem *uart;
+               early_fixmap_init();
+               set_fixmap_io(FIX_EARLYCON_MEM_BASE, 0x09000000);
+               uart = (void __iomem *)__fix_to_virt(FIX_EARLYCON_MEM_BASE);
+               writel(readl(uart + 0x30) | 0x101 /* TXE | UARTEN */, uart + 0x30);
+               while (readl(uart + 0x18) & 0x20 /* TXFF */) ;
+               writeb('@', uart);
+               while (readl(uart + 0x18) & 8 /* BUSY */) ;
+               while (1) ;
+       }
+
        set_task_stack_end_magic(&init_task);
        smp_setup_processor_id();
        debug_objects_early_init();
実行してみましょう。
qemu-system-aarch64 -cpu cortex-a53 -M virt -nographic -serial mon:stdio -kernel ./arch/arm64/boot/Image
上手くいけば以下のようにターミナル上に'@'と表示されます。
エントリーポイントまでは動作したがstart_kernelに来ないという場合は、どこまで動作するか二分探索してカーネルのブート要件の観点でブートローダの設定ミスを疑うことになります。例えばカーネル起動時の特権レベルが間違っている等です。 上記プログラム中で実行しているearly_fixmap_init(), set_fixmap_io(FIX_EARLYCON_MEM_BASE), __fix_to_virt(FIX_EARLYCON_MEM_BASE)は本来もっと後で実行される処理ですが、ローレベルな初期化処理なので、少なくとも本稿で検証したv6.12ではstart_kernelの先頭まで移動しても動作します。 本来の実行タイミング:

setup_arch以降

setup_arch以降ではかなりいろいろ動くようになります。earlyconも利用可能になるので、大抵の環境でprintkが動作するようになります。 UARTドライバがearlyconをサポートしていない場合は前節と同じようなコードを使うことになりますが、setup_arch以降ではearly_ioremapが、mm_core_init以降では通常のioremapが使用可能となります。
earlyconが使えない場合のsetup_archからmm_core_initまでのprintデバッグ。
earlyconが使える場合はprintkが動作する。
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/init/main.c?h=v6.12.39#n903

Patch license: GPL (same as linux kernel)

diff --git a/init/main.c b/init/main.c
index c4778edae797..91408bae018e 100644
--- a/init/main.c
+++ b/init/main.c
@@ -923,6 +923,17 @@ void start_kernel(void)
        page_address_init();
        pr_notice("%s", linux_banner);
        setup_arch(&command_line);
+
+       {
+               void __iomem *uart;
+               uart = (void __iomem *)early_ioremap(0x09000000, 0x1000);
+               writel(readl(uart + 0x30) | 0x101 /* CR_TXE | CR_UARTEN */, uart + 0x30);
+               while (readl(uart + 0x18) & 0x20 /* FR_TXFF */) ;
+               writeb('@', uart);
+               while (readl(uart + 0x18) & 8 /* FR_BUSY */) ;
+               while (1) ;
+       }
+
        /* Static keys and static calls are needed by LSMs */
        jump_label_init();
        static_call_init();
earlyconが使えない場合のmm_core_init以降のprintデバッグ。
earlyconが使える場合はprintkが動作する。
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/init/main.c?h=v6.12.39#n903

Patch license: GPL (same as linux kernel)

diff --git a/init/main.c b/init/main.c
index c4778edae797..916ec2c15b31 100644
--- a/init/main.c
+++ b/init/main.c
@@ -962,6 +962,17 @@ void start_kernel(void)
        sort_main_extable();
        trap_init();
        mm_core_init();
+
+       {
+               void __iomem *uart;
+               uart = (void __iomem *)ioremap(0x09000000, 0x1000);
+               writel(readl(uart + 0x30) | 0x101 /* CR_TXE | CR_UARTEN */, uart + 0x30);
+               while (readl(uart + 0x18) & 0x20 /* FR_TXFF */) ;
+               writeb('@', uart);
+               while (readl(uart + 0x18) & 8 /* FR_BUSY */) ;
+               while (1) ;
+       }
+
        poking_init();
        ftrace_init();

まとめ

本稿ではarm64ターゲットのLinux環境においてブートローダに問題があるようなケースでLinuxカーネル起動の初期段階のprintデバッグを行う手法を紹介しました。ブートローダを含めてLinux BSP全体を開発対象とするような場合にこのようなテクニックを知っていると課題解決の助けになることがあります。

採用情報

ミラクシアでは一緒に組込みソフト開発をする仲間を募集しています –> Miraxia特設採用サイト <–

参考文献