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特設採用サイト <–
参考文献