C LANGUAGE TECHNOLOGY

【C言語】Linuxカーネルのデバッグ方法【printk,BUG_ON,WARN_ON,QEMU,GDB】

2022年9月6日

悩んでいる人

C言語でLinuxカーネルのデバッグ方法を教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOS(Linuxカーネル)の授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
  • プログラミング歴15年以上,習得している言語: C/C++PythonSolidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開
  • 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
  • 最近は自然言語処理AIイーサリアムに関する有益な情報発信に従事.
    • (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
    • (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.

こういった私から学べます.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

本記事は以下の記事を理解していることを前提とします.

【C言語】Linuxカーネルのデバッグ方法

C言語でLinuxカーネルのデバッグ方法を紹介します.

Linuxカーネルの開発サイクルは以下の繰り返しになります.

  • コードを書く
  • カーネルやモジュールをビルドする
  • デプロイする
  • テストやデバッグする

カーネルのデバッグの制約により,プロのカーネル開発者でもデバッグが真のボトルネックとなります.

なので,カーネルのデバッグ方法に慣れておくことは,時間と労力を節約するために重要です.

カーネルのデバッグ方法は以下になりますので,それぞれ解説していきます.

  • printk関数でデバッグメッセージを表示
  • BUG_ONマクロ,WARN_ONマクロでコードのアサーションとカーネルパニックのメッセージを解析
  • QEMUとGDBを利用したデバッグ

printk関数

printk関数は,printf関数のカーネルレベル版です.

printk関数を利用すると,カーネルレベルで変数の値を表示できるので,デバッグしやすくなります.

printk関数のログレベルの定義

printk関数では,ログレベルを指定する必要があります(デフォルトのレベルはKERN_ERRまたはKERN_WARNING).

linux/include/linux/kern_levels.hにのログレベル0~7の定義があります.

※ログレベルの数値が小さいほど優先度が高くなります.

  • ログレベル0:KERN_EMERG
  • ログレベル1:KERN_ALERT
  • ログレベル2:KERN_CRIT
  • ログレベル3:KERN_ERR
  • ログレベル4:KERN_WARNING
  • ログレベル5:KERN_NOTICE
  • ログレベル6:KERN_INFO
  • ログレベル7:KERN_DEBUG

また,linux/include/linux/printk.hに上記のエイリアスのマクロがあります.

printk関数のフォーマット指定子は「How to get printk format specifiers right」に記載してあります.

printk関数はprintf関数とは異なり,浮動小数点数の表示はサポートしていないことに注意して下さい.

printk関数は以下のように利用します.

printk関数では,現在のログレベルより高い優先度(小さい数値)のログレベルのメッセージのみを出力します.

私のLinuxカーネルの.configでは,「CONFIG_MESSAGE_LOGLEVEL_DEFAULT=4」と設定しています.

なので,ログレベル4のKERN_WARNINGがデフォルトになっていて,ログレベル3以下のprintk関数が表示されることがわかります.

printk関数のログレベルの取得と変更

printk関数のログレベルの情報は/proc/sys/kernel/printkにあります.

以下のコマンドでログレベルの情報を表示できます.

左から右の数値の意味は以下になります.

  • 4:現在のログレベル
  • 4:デフォルトのログレベル
  • 1:最小値のログレベル(ログレベル0は除外)
  • 7:ブート時のログレベル

現在のコンソールのログレベルを8に変更したい(ログレベル0~7を表示したい)場合はrootで以下のコマンドを実行します.

また,dmesgコマンドsysctlコマンドでも同様に変更できます.

ログレベルを確認すると現在のログレベルが8に変更されていることがわかります.

上記の方法は一時的に変更するので,Linuxを再起動するとデフォルトの値に戻ってしまいます.

デフォルト値を変更するためには,Linuxカーネルを再ビルドすればできなくはないですが,時間がかかります.

なので,デフォルト値を変更したい場合は,/etc/default/grubのGRUB_CMDLINE_LINUXにログレベルの引数を設定します.

ログレベル8に設定する方法は以下になります.

変更後に,以下のコマンドでgrubを更新します.

printk関数でログレベルを表示するコード

printk関数でログレベルを表示するコード一式をこちらからダウンロードして下さい.

以下のコマンドで解凍して下さい.

中身は以下になります.

  • Makefile:カーネルモジュール用の構成ファイル
  • printk_kernel_module.c:printk関数で全ログレベルを表示するコード

printk関数のコンソールの表示はCtrl + Alt + F1~F6の仮想コンソールでしか表示されないことに注意して下さい.

※GUIで起動する場合,仮想コンソールはCtrl + Alt + F3~F6になります.

GUIのコンソールやSSHで接続したネットワークのコンソールでは表示されません.

もしGUIのコンソールやSSHで接続したネットワークのコンソールで表示したい場合は,dmesgコマンドで以下のようにオプションをつけて実行します.

  • -wオプション:新しいメッセージが出力されるのを待つ.
  • -Hオプション:人間に読みやすい出力にする.
  • -lオプション:ログレベルを指定する.(以下の例ではログレベル4~7を表示する.)

※これまでのdmesgのログが表示されますが,その後にprintk関数を実行すると指定したログレベルで表示されます.

本記事ではこちらの方法でprintk関数の動作を確認します.

makeでビルドします.

dmesgコマンドでログレベル4~7を表示する設定におけるカーネルモジュールのロードとアンロードの結果は以下になります.

ログレベル4~7のprintk関数の結果が表示されていることがわかります.

dmesgの表示を終了する場合は,pkillコマンドを利用します.

次に,ログレベル0~7を表示してみましょう.

以下のコマンドで設定します.

dmesgコマンドでログレベル0~7を表示する設定におけるカーネルモジュールのロードとアンロードの結果は以下になります.

ログレベル0~7のprintk関数の結果が表示されていることがわかります.

dmesgの表示を終了するために,pkillコマンドを利用します.

BUG_ONマクロ,WARN_ONマクロ,カーネルパニック

BUG_ONマクロ,WARN_ONマクロはアサーションのカーネルレベル版です.

C言語でアサーションを知りたいあなたはこちらからどうぞ.

BUG_ONマクロは,引数cが真の場合,カーネルパニックを発生させ,コールスタックを表示した後に強制終了します.

これに対して,WARN_ONマクロは,引数cが真の場合,カーネルはコールスタックを表示し,実行を継続します.

BUG_ONマクロ,WARN_マクロの定義

linux/include/asm-generic/bug.hにBUG_ONマクロ,WARN_ONマクロが定義されています.

BUG_ONマクロとWARN_ONマクロはアーキテクチャ共通の定義とアーキテクチャ固有の定義の2種類があります.

x86-64とARM64はHAVE_ARCH_BUG_ONとHAVE_ARCH_WARN_ONは未定義なので,こちらを利用します.

BUG_ONマクロとWARN_ONマクロを利用するコード

BUG_ONマクロとWARN_ONマクロを利用するコード一式はこちらからダウンロードして下さい.

中身は以下になります.

  • Makefile:カーネルモジュール用の構成ファイル
  • bug_on_and_warn_on_kernel_module.c:BUG_ONマクロとWARN_ONマクロを利用するコード

ログレベル0~7を表示するために,以下のコマンドで設定します.

カーネルモジュールをロードすると,WARN_ONマクロによるコールスタックを表示します.

カーネルモジュールをアンロードすると,カーネルパニックを表示します.

以下の3行目で「kernel BUG at /home/chishiro/c-language/bug_on_and_warn_on_kernel_module/bug_on_and_warn_on_kernel_module.c:13!」と表示されています.

なので,bug_on_and_warn_on_kernel_module.cの13行目でバグが発生したことがわかります.

カーネルモジュールを再ロードできなくなったりフリーズしたりする等の不具合が発生するので,再起動しましょう!

QEMUとGDBを利用したデバッグ方法

上記の簡単なコードでカーネルパニックの場所を見つけることは簡単ですが,複雑なコードでは難しいです.

なので,複雑なコードでカーネルパニックの場所を効率的に見つける方法を知りたいですよね.

そんなあなたにQEMUとGDBを利用したデバッグ方法を紹介します.

QEMUとGDB

QEMUは,仮想マシン全体をエミュレートすることができるフルシステムエミュレータです.

QEMUは,CPU,メモリ,デバイスのソフトウェアモデルを利用していますが,エミュレーションに時間がかかります.

また,QEMUはハードウェア仮想化支援機構(Hardware Virtualization Extensions)との併用により,高性能な仮想化を実現することも可能です.

参考までに,KVMは,インカーネルでの仮想化サポートとQEMUの支援機構を組み合わせています.

GDBはプログラムを効率的にデバッグするためのツールです.

まずはGDBを知りたいあなたはこちらからどうぞ.

GDBサーバは,元々はリモートマシン上で実行されているプログラムをデバッグするために利用します.

下図のように低性能な組込みシステムのようにリモートマシン上でGDBが利用できない場合にGDBクライアント・サーバとして通信することでGDBを実行できます.

GDBクライアント・サーバ間の通信には,TCP/IP,Serial(RS-232C等),JTAG等が利用されます.

GDBクライアント・サーバ

QEMUとGDBを利用したカーネルのデバッグ方法

それでは,QEMUとGDBを利用したカーネルのデバッグ方法を紹介します.

Kernel Debugging with QEMU and GDB

上図のように,本記事ではGDBクライアント・サーバは同一のx86-64のホストPCであると仮定します.

※実機がARM64のボードの場合はアーキテクチャ依存やボード依存の設定に変更する必要があります.

Linuxカーネルは仮想マシン(QEMUまたはKVMでエミュレート)で実行されます.

ハードウェアデバイスはQEMUでエミュレートされます.

GDBサーバは,エミュレートされた仮想マシンであるQEMUで動作するため,QEMU上で動作するLinuxカーネルを完全に制御できます.

実機で動作する場合と比較して,QEMUを利用することでデバッグやコード探索が簡単にできます.

QEMUとGDBを利用したカーネルのデバッグは主に2つの方法がありますので,それぞれ解説していきます.

  • 最小限のLinuxディストリビューションを実行する
    • debootstrapディストリビューションを利用する
    • ルートファイルシステムはホストシステムのディレクトリである
    • ユーザ空間アプリケーションでの機能は限定的になる
  • 完全なLinuxディストリビューションを実行する
    • QEMUディスクイメージ(qcow2またはraw disk)を利用する
    • ルートファイルシステムはディスクイメージ上にある
    • ユーザ空間アプリケーションをフルに動作させることができる

以下のコマンドで実行環境をセットアップします.

最小限のLinuxディストリビューション(debootstrap)を実行

QEMUとGDBによるデバッグ用カーネルの構築方法を紹介します.

GDBスクリプト,9p,virtioを有効にしてカーネルを再構築します.

.configにある以下の設定をしてLinuxカーネルをビルドして下さい.

  • CONFIG_GDB_SCRIPTSを有効:「Provide GDB scripts for kernel debugging」をチェックする
  • CONFIG_DEBUG_INFO_REDUCEDを無効:「Reduce debugging information」を未チェック状態にする
  • CONFIG_FRAME_POINTERをサポートしている場合は有効の状態にする

CONFIG_GDB_SCRIPTSを有効,CONFIG_DEBUG_INFO_REDUCEDを無効にします.

また,.configファイルで以下の設定になっているか確認して下さい.

もしそうでない場合は.configファイルを直接編集しましょう!

後は,ビルドを成功するために,.configファイルを直接編集して以下の設定に変更しましょう.

※CONFIG_DEBUG_INFOは有効の状態にします.

「CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"」を「CONFIG_SYSTEM_TRUSTED_KEYS=""」に変更

「CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"」を「CONFIG_SYSTEM_REVOCATION_KEYS=""」に変更

「CONFIG_LOCALVERSION=""」を「CONFIG_LOCALVERSION="-mykernel"」のように好きな文字列に変更

https://hiroyukichishiro.com/build-linux-kernel-and-implement-new-system-call-in-c-language/

makeでビルドします.

デプロイを容易にするために必要な機能はビルトインされるので,「make modules_install」や「make install」は必要ないことに注意して下さい.

以下を作成します.

  • linux-chroot:debootstrapでベーシックなDebianシステムのブートストラップファイルシステム
  • .gdbinit:GDBの設定ファイル

QEMUでビルドしたLinuxカーネルを起動します.

QEMUの主なオプションは以下になります.

  • -kernel:デバッグするLinuxカーネルのファイルを指定する.
  • -s:GDBサーバを有効にし,ポート1234を開きます.
  • -S:(オプション)最初のカーネル命令でGDBクライアント接続の継続を待って一時停止する.(以下の例では未使用)

rootの端末が起動することを確認しましょう.

QEMUの実行を終了したい時は,「Ctrl-a + x」を入力します.

-Sオプションを利用する場合は,GDB側からLinuxカーネルを操作する必要があります.

QEMUを実行すると何も表示されません.

他の端末でGDBを「sudo gdb vmlinux」で起動し,「target remote :1234」でLinuxカーネルに接続します.

そして,c(continue)を入力するとLinuxカーネルが起動するので確認しましょう.

GDBを利用したLinuxカーネルのデバッグ方法の詳細を知りたいあなたは「Debugging kernel and modules via gdb」を読みましょう!

完全なLinuxディストリビューションを実行

完全なLinuxディストリビューションを実行する方法を紹介します.

.configにある以下の設定をしてLinuxカーネルをビルドして下さい.

  • CONFIG_GDB_SCRIPTSを有効:「Provide GDB scripts for kernel debugging」をチェックする
  • CONFIG_DEBUG_INFO_REDUCEDを無効:「Reduce debugging information」を未チェック状態にする
  • CONFIG_FRAME_POINTERをサポートしている場合は有効の状態にする

CONFIG_GDB_SCRIPTSを有効,CONFIG_DEBUG_INFO_REDUCEDを無効にします.

後は,ビルドを成功するために,.configファイルを直接編集して以下の設定に変更しましょう.

※CONFIG_DEBUG_INFOは有効の状態にします.

「CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"」を「CONFIG_SYSTEM_TRUSTED_KEYS=""」に変更

「CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"」を「CONFIG_SYSTEM_REVOCATION_KEYS=""」に変更

「CONFIG_LOCALVERSION=""」を「CONFIG_LOCALVERSION="-mykernel"」のように好きな文字列に変更

https://hiroyukichishiro.com/build-linux-kernel-and-implement-new-system-call-in-c-language/

makeでビルドします.

Ubuntuの公式ページからUbuntu 22.04 LTSのVMDKファイルをダウンロードします.

※自分で作成したVMDKファイルでもOKです.

VMDKファイルをQCOW2ファイルに変換します.

以下のコマンドで起動します.

debootstrapより遅いですが,Linuxカーネルが起動することを確認しましょう.

まとめ

C言語でLinuxカーネルのデバッグ方法を紹介しました.

Linuxカーネルのデバッグ方法を習得して,開発を効率化しましょう!

Linuxカーネルに独自機能を追加する自作カーネルの開発手順をまとめると以下になります.

  1. カーネルの独自機能をテストするための簡単なユニットテストコードを書く.
  2. Linuxホストで開発する.
  3. 最小限のLinuxディストリビューション(debootstrap)のQEMU環境で,すべてのユニットテストケースがパスするまでテストとデバッグする.
  4. 完全なLinuxディストリビューションのQEMU環境でテストとデバッグする.
  5. 実機環境でテストとデバッグする.

Linuxカーネルのデバッグを深く理解したいあなたは,以下の記事を読みましょう!

カーネルのデバッグ方法は以下の動画が参考になります.

GDBサーバの使い方は以下の動画がわかりやすいです.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

-C LANGUAGE, TECHNOLOGY
-, , ,