- Linux Kernel Module Programming Guide は、Linux v5.10 以降でロード可能なカーネルモジュールを作成するための無料ガイドで、開発環境からビルド・ロード・デバッグ・主要カーネルインターフェースまでを一連の流れで扱う
- 序盤の例では
hello-*.c を使って module_init(), module_exit(), kbuild, insmod, rmmod, dmesg を学べるようになっており、QEMUベースの devtools によってホストシステムを破損するリスクを減らしている
- カーネルモジュールはカーネルのアドレス空間で実行されるため、誤ったポインタ、アンロード順序、並行性、ユーザーメモリコピーのミスが カーネルメモリ破損 やシステム不安定化につながる可能性がある
- 文字デバイス、
/proc, seq_file, threaded IRQ, input, PCI, USB, ブロック、ネットワーク、Device Model、Device Tree、static key まで拡張しながら、登録・解除の順序 と lifetime 管理を繰り返し扱う
- カーネル内部 API はバージョンごとに変わるため、
LINUX_VERSION_CODE, KERNEL_VERSION, CONFIG_MODVERSIONS, SecureBoot 署名、version magic などの条件を確認しつつ、例でも条件付きコンパイルを含んでいる
ガイドの構成と基本フロー
- このガイドは GitHub リポジトリ と PDF ドキュメント を提供するカーネルモジュール学習資料で、Open Software License 3.0 の条件で複製・改変・配布できる
- 現在のガイドは Linux v5.10 を最小サポート基準としており、長期サポートカーネル全般で例とガイダンスの互換性を維持することを目標としている
- 学習者には C 言語と一般的なプロセス向けプログラム作成経験が必要で、カーネルモジュールは動的にロード・アンロードされ、再起動なしでカーネル機能を拡張する
- 基本的な開発フローは、カーネルヘッダーのインストール、
make による .ko のビルド、modinfo による確認、insmod によるロード、dmesg または journalctl -k によるログ確認、rmmod によるアンロードへと続く
devtools/ はカーネルソースと BusyBox ルートファイルシステムをビルドして QEMU で起動し、examples/ を 9p virtfs で共有したうえで、ゲスト内でモジュールをテストできるようにする
- モジュールの初期化とクリーンアップには
module_init()・module_exit() の使用が推奨され、従来の init_module()・cleanup_module() 方式は x86 IBT が有効なカーネル 6.15 以降で一部条件下においてビルド失敗を起こす可能性がある
- カーネルモジュールは
printf() や libc を使わず、カーネルがエクスポートしたシンボルのみを利用でき、出力先はターミナルではなく カーネルログリングバッファ となる
- ユーザー空間とカーネル空間の間でデータを移動するには、
put_user, get_user, copy_to_user, copy_from_user のような専用関数が必要になる
- 文字デバイスの例では、
register_chrdev, file_operations, dynamic major number, /dev ノード作成、exclusive open、put_user ベースの read、未サポート write の処理を示している
/proc の例では、proc_create, proc_ops, read/write コールバック、seq_file API を扱い、Linux v5.6 以降で /proc ハンドラに file_operations の代わりに proc_ops が導入された変更を反映している
- threaded IRQ は
request_threaded_irq() により top-half と bottom-half を分離し、top-half は interrupt context で最小限の処理だけを行い、IRQ_WAKE_THREAD でスレッドベースの bottom-half を起こす
- その後の章では input, PCI, USB, ブロック, ネットワーク, Device Model, Device Tree といった実際のドライバ領域へ拡張し、各 subsystem の登録方式と userspace ABI の選択を中心に説明が続く
- 最適化と安全性の章では、
likely・unlikely, static key, 小さなカーネルスタック、FPU 使用禁止、初期化されていない padding の漏えい、double-underscore な内部 API 使用時の注意を扱う
ビルド・ロードで最初にぶつかる制約
- あるカーネル向けにコンパイルされたモジュールは別のカーネルではロードできない場合があり、version magic や
CONFIG_MODVERSIONS が一致しないと Invalid module format やシンボルバージョン不一致が発生する
- 多くの一般的な Linux ディストリビューションカーネルでは modversioning が有効になっている可能性があるため、例がそのまま動かない場合は modversioning を無効にしたカーネル や QEMU 環境を検討する必要がある
- SecureBoot が有効なシステムでは署名されていないモジュールのロードが制限される場合があり、
Lockdown: insmod: unsigned module loading is restricted が表示されたら SecureBoot の無効化またはモジュール署名手順が必要になる
QEMU ベースの実習環境
devtools/setup.sh はカーネル tarball と BusyBox をダウンロード・ビルドし、initramfs をパッケージ化する
devtools/build-modules.sh は QEMU カーネル対象のモジュールをビルドし、devtools/boot.sh はゲストシェルを提供、devtools/test-modules.sh はモジュールごとの insmod・rmmod 自動テストを実行する
- GDB デバッグは
LKMPG_NO_PREBUILT=1 devtools/setup.sh で vmlinux をビルドしたあと、devtools/boot.sh --gdb とリモート GDB 接続で進める
カーネルコード作成のルール
- init 関数では登録・割り当てが失敗する可能性があるため、取得したリソースは
goto ベースのエラーパスで 逆順に解放 しなければならない
- カーネルにコールバック構造を登録すると、ユーザー空間が init の戻り前でもコールバックを呼び出せる可能性があるため、内部初期化を完了してから最後に登録し、解除は先に行う register last, unregister first ルールが重要になる
- process context, softirq/tasklet context, hardirq context では、sleep、ユーザーメモリアクセス、
GFP_KERNEL, mutex の使用可否が異なり、この区別を誤解すると典型的なカーネルバグにつながる
デバイスと subsystem ごとの注意点
- 文字デバイスは major number でドライバを識別し、minor number でドライバ内部の複数デバイスを区別する。現代的な方法では
register_chrdev() より cdev インターフェース が推奨される
- PCI ドライバは固定アドレスを前提にせず、PCI core が列挙した BAR resource をマッピングする。Linux 5.10 以降のコードでは
pcim_enable_device() と device-managed resource API が teardown バグ低減に有用である
- USB ドライバでは hotplug と disconnect を通常イベントとして扱う必要があり、URB completion と disconnect・timeout・suspend・userspace shutdown が競合しうることを前提に設計しなければならない
- ブロックドライバは
blk-mq, request, gendisk, queue limit, flush/FUA semantics を中心に動作し、単純な read/write コールバックではなく 非同期 request completion モデルに参加する
- ネットワークドライバは
struct net_device, net_device_ops, sk_buff, NAPI, offload feature flag, link-state レポートと結び付いており、誤った offload 宣言はトラフィック破損につながる可能性がある
カーネルバージョン変化への対応
- 例では、Linux 6.4 の
class_create() シグネチャ変更、Linux v5.6 の proc_ops, Linux 6.11 の remove() 戻り値型変更、Linux 5.15〜6.9 間の blk-mq helper 変化を条件付きコンパイルで処理している
- カーネル内部インターフェースはシステムコールより頻繁に変わるため、複数カーネルをサポートするモジュールでは
LINUX_VERSION_CODE と KERNEL_VERSION の比較を避けにくい
安全性チェックポイント
- カーネルスタックはユーザー空間スタックよりはるかに小さく、多くのシステムで 8 KiB または 16 KiB 程度であるため、大きな配列には
kmalloc()・kzalloc() を使うべきである
copy_to_user() でユーザー空間へデータを送る際には、padding を含むすべてのバイトが初期化されていなければならず、そうでないと カーネルメモリ情報漏えい が発生しうる
__kmalloc(), __list_add() のように double underscore で始まる API は内部前提条件を仮定している場合があるため、ドキュメントで要求されていない限り public wrapper を優先して使うべきである
省略された範囲
- 入力処理の過程で一部の原文チャンクが長さ・コスト制限により省略されたと明記されているため、この要約はガイド全体のすべての章・例・コードパスを漏れなく扱うものではない
1件のコメント
Hacker Newsのコメント
QEMU は、カーネルハックを体験してみるのに良い方法
誰かが LDD(Linux Device Drivers) や Linux カーネル本を更新してくれるといいし、こういう技術書は収益化しにくいので、Linux Foundation が支援してもよさそう
今週も、v6.8 で arm64 のカーネルコマンドラインパラメータが 146 文字を超えると、カーネルが即座に静かに停止する問題を QEMU + GDB で再現し、Debian 12 Bookworm amd64 ホスト上で arm64 カーネルビルドをエミュレートして、問題のコードを 1 行ずつ追いながら原因を突き止めた
手順としては、ビルド依存関係とクロスコンパイルツールがそろった環境で arm64 カーネルイメージと GDB 用スクリプトをビルドし、ホストに
gdb、必要ならgdb-multiarch、それにqemu-system-armをインストールしたうえで、qemu-system-aarch64を-S -gdb tcp::1234で停止状態のまま起動し、別ターミナルからgdb-multiarch ./vmlinuxで接続する形その後 GDB で
target remote :1234、break __parse_cmdline、continueを実行すれば、メモリ・変数・スタックの確認やシングルステップ実行など、通常の GDB 機能が使えるカーネルの GDB デバッグと
lx-*スクリプトについては https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-... を参照GDB に
lx-*Python スクリプトを使わせるには、通常echo "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinitのようにパス許可も必要関連 HN スレッド: https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030
The Linux Memory Manager も参考になりそう: https://linuxmemory.org/chapters
著者が 7 月初めに送った最後の更新によると、草稿は書き終えており、現在は出版社とともに編集段階に入っているとのこと
いくつかの例は、実際に自分で動かしてみるのが難しそう
たとえば「Detecting button presses」は RPi 向けモジュールをビルドできることを前提にしているが、それ自体がクロスコンパイルのような作業を要するので、簡単ではないかもしれない
詳細で実践的かつ、すぐに カーネルモジュールをビルド してみるチュートリアルなので素晴らしい
あわせて見るとよさそうな資料: https://0xax.gitbooks.io/linux-insides/content/index.html
ファイルシステムやメモリ管理のような Linux カーネルプログラミング全般 は、どこで学ぶのがよいのだろう
昔は Robert Love の「Linux Kernel Development」があったが、もう更新されていないようだ
これを初めて読んだのは約 22 年前 だった :)