Linux Kernel Modules
Linux カーネルモジュール 完全ガイド
最終更新: 2026-04-10 対象カーネルバージョン: Linux 5.x / 6.x
目次
- はじめに
- カーネルモジュールのアーキテクチャとライフサイクル
- モジュールのロードとアンロード
- モジュール依存関係と depmod
- 基本的なカーネルモジュールの作成
- モジュールパラメータ
- シンボルのエクスポート
- モジュールライセンス
- セキュアブートとモジュール署名
- Proc および sysfs インターフェースの作成
- キャラクタデバイスドライバモジュール(完全な例)
- ネットワークモジュールの例
- Makefile と Kbuild システム
- アウトオブツリーモジュール
- DKMS(Dynamic Kernel Module Support)
- モジュールバージョニング(modversions)
- モジュールスタッキング
- モジュールのデバッグ
- /etc/modprobe.d/ 設定
- /etc/modules-load.d/ による自動ロード
- モジュールのブラックリスト
- ツール一覧と詳細
- トラブルシューティング
- ベストプラクティスとセキュリティ
- まとめ
はじめに
Linux カーネルモジュールは、カーネルの機能を動的に拡張するためのメカニズムである。カーネルモジュールを使用することで、カーネル全体を再コンパイルすることなく、デバイスドライバ、ファイルシステム、ネットワークプロトコルなどの機能を追加・削除できる。
カーネルモジュールとは何か
カーネルモジュールは、実行時にカーネルにロードおよびアンロードできるコードの断片である。モジュールは .ko(Kernel Object)ファイルとしてコンパイルされ、通常は /lib/modules/$(uname -r)/ ディレクトリ配下に配置される。
# 現在のカーネルバージョンを確認
$ uname -r
6.8.0-generic
# モジュールの格納場所を確認
$ ls /lib/modules/$(uname -r)/
build kernel modules.alias modules.builtin modules.dep
extra updates modules.alias.bin modules.builtin.bin modules.dep.bin
モジュールを使う理由
カーネルモジュールの主な利点は以下の通りである。
- 動的な機能追加: システムの再起動なしに新しいハードウェアドライバやファイルシステムを追加できる
- メモリ効率: 必要な機能のみをロードし、不要になったらアンロードすることでメモリを節約できる
- 開発効率: カーネル全体を再コンパイルせずにモジュールのみを再コンパイルしてテストできる
- 配布の柔軟性: サードパーティがカーネル本体とは別にドライバを配布できる
- 保守性: 特定の機能に関するコードを独立したモジュールとして管理できる
モノリシックカーネルとモジュラーカーネル
Linux カーネルは「モノリシックカーネル」に分類されるが、カーネルモジュールによりモジュラー性を実現している。
┌─────────────────────────────────────────────────┐
│ ユーザー空間 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ アプリA │ │ アプリB │ │ アプリC │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────┤
│ システムコールインターフェース │
├─────────────────────────────────────────────────┤
│ カーネル空間 │
│ ┌─────────────────────────────────────────┐ │
│ │ カーネルコア (vmlinuz) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ VFS │ │ スケ │ │ メモリ │ │ │
│ │ │ │ │ ジュー │ │ 管理 │ │ │
│ │ │ │ │ ラ │ │ │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ ext4 │ │ NIC │ │ USB │ │ GPU │ ← モジュール│
│ │ .ko │ │ .ko │ │ .ko │ │ .ko │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────────────────┤
│ ハードウェア │
└─────────────────────────────────────────────────┘
カーネルモジュールのアーキテクチャとライフサイクル
モジュールの内部構造
カーネルモジュール(.ko ファイル)は ELF(Executable and Linkable Format)形式のオブジェクトファイルである。通常のオブジェクトファイルとは異なり、カーネルモジュール固有のセクションを含んでいる。
# モジュールのELFセクションを確認
$ readelf -S /lib/modules/$(uname -r)/kernel/drivers/net/ethernet/intel/e1000/e1000.ko
Section Headers:
[Nr] Name Type Address Off Size
[ 0] NULL 0000000000000000 000000 000000
[ 1] .note.gnu.build-i NOTE 0000000000000000 000040 000024
[ 2] .note.Linux NOTE 0000000000000000 000064 000018
[ 3] .text PROGBITS 0000000000000000 000080 01a000
[ 4] .rodata PROGBITS 0000000000000000 01a080 004000
[ 5] .modinfo PROGBITS 0000000000000000 01e080 000800
[ 6] __param PROGBITS 0000000000000000 01e880 000100
[ 7] __versions PROGBITS 0000000000000000 01e980 000400
...
各セクションの役割は以下の通りである。
| セクション名 | 説明 |
|---|---|
.text | モジュールの実行可能コード |
.rodata | 読み取り専用データ |
.data | 初期化済み書き込み可能データ |
.bss | 未初期化データ |
.modinfo | モジュールのメタ情報(ライセンス、作者、説明等) |
__param | モジュールパラメータの定義 |
__versions | シンボルバージョン情報(CRC値) |
.gnu.linkonce.this_module | struct module のインスタンス |
.init.text | 初期化関数のコード(ロード後に解放される) |
.exit.text | クリーンアップ関数のコード |
struct module 構造体
カーネル内部では、各ロード済みモジュールは struct module 構造体で管理されている。この構造体はカーネルソースの include/linux/module.h で定義されている。
/* include/linux/module.h の主要フィールド(簡略化) */
struct module {
enum module_state state; /* モジュールの状態 */
/* メンバーリスト(ロード済みモジュールのリンクリスト) */
struct list_head list;
/* モジュール名 */
char name[MODULE_NAME_LEN];
/* シンボルテーブル */
const struct kernel_symbol *syms;
const s32 *crcs;
unsigned int num_syms;
/* GPL エクスポートシンボル */
const struct kernel_symbol *gpl_syms;
const s32 *gpl_crcs;
unsigned int num_gpl_syms;
/* パラメータ */
struct kernel_param *kp;
unsigned int num_kp;
/* 初期化・クリーンアップ関数 */
int (*init)(void);
void (*exit)(void);
/* モジュールの使用カウント */
struct module_ref __percpu *refcnt;
/* 依存するモジュールのリスト */
struct list_head source_list;
struct list_head target_list;
/* モジュールのメモリレイアウト */
struct module_memory mem[MOD_MEM_NUM_TYPES];
/* モジュール署名情報 */
bool sig_ok;
/* モジュール情報 */
struct module_attribute *modinfo_attrs;
const char *version;
const char *srcversion;
...
};
モジュールの状態遷移
モジュールは以下の状態を遷移する。
┌─────────────────┐
│ MODULE_STATE │
│ UNFORMED │
│ (ELF解析中) │
└────────┬────────┘
│
┌────────▼────────┐
│ MODULE_STATE │
│ COMING │
│ (初期化中) │
└────────┬────────┘
│
init() 成功?
┌────────┴────────┐
│ │
┌────▼────┐ ┌─────▼─────┐
│ LIVE │ │ GOING │
│ (稼働中) │ │ (削除中) │
└────┬────┘ └─────┬─────┘
│ │
rmmod │
│ │
┌────▼────┐ │
│ GOING │ │
│ (削除中) │ │
└────┬────┘ │
│ │
└────────┬────────┘
│
┌────────▼────────┐
│ (解放済み) │
└─────────────────┘
モジュールのロードプロセス(詳細)
モジュールがロードされる際の詳細なプロセスは以下の通りである。
ユーザー空間 カーネル空間
┌──────────┐ ┌────────────────────────┐
│ insmod/ │ ── syscall ──────→ │ sys_init_module() │
│ modprobe │ init_module() │ │
└──────────┘ │ 1. ELFヘッダ検証 │
│ 2. セクション解析 │
│ 3. メモリ確保 │
│ 4. セクションコピー │
│ 5. シンボル解決 │
│ 6. リロケーション処理 │
│ 7. パラメータ設定 │
│ 8. ライセンスチェック │
│ 9. モジュール署名検証 │
│ 10. sysfs エントリ作成 │
│ 11. init() 関数呼び出し │
│ 12. .init セクション解放 │
└────────────────────────┘
各ステップの詳細は以下の通り。
- ELFヘッダ検証: ファイルが有効なELFオブジェクトであることを確認
- セクション解析:
.modinfo、.text、.data等のセクションを解析 - メモリ確保: カーネルメモリにモジュール用の領域を確保(
module_alloc()) - セクションコピー: ユーザー空間からカーネル空間にセクションデータをコピー
- シンボル解決: モジュールが参照する外部シンボルのアドレスを解決
- リロケーション処理: アドレスの再配置を実行
- パラメータ設定: コマンドラインから渡されたパラメータを設定
- ライセンスチェック: GPL互換ライセンスかどうかを確認
- モジュール署名検証: 署名が有効かどうかを検証(Secure Boot環境)
- sysfs エントリ作成:
/sys/module/<name>/配下にエントリを作成 - init() 関数呼び出し: モジュールの初期化関数を実行
- .init セクション解放: 初期化専用コードのメモリを解放
モジュールのロードとアンロード
insmod コマンド
insmod は最も基本的なモジュールロードコマンドである。指定された .ko ファイルを直接カーネルにロードする。依存関係の自動解決は行わない。
# 基本的な使い方
$ sudo insmod /path/to/module.ko
# パラメータを指定してロード
$ sudo insmod /path/to/module.ko param1=value1 param2=value2
# 例: hello モジュールをロード
$ sudo insmod ./hello.ko
# 例: パラメータ付きでロード
$ sudo insmod ./hello.ko debug_level=3 device_name="mydevice"
insmod の内部動作:
# strace で insmod のシステムコールを確認
$ strace insmod ./hello.ko 2>&1 | grep -E "(open|read|init_module)"
openat(AT_FDCWD, "./hello.ko", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0"..., 261632) = 261632
init_module(0x7f8a12345000, 261632, "") = 0
rmmod コマンド
rmmod はロード済みのモジュールをカーネルからアンロードする。
# 基本的な使い方
$ sudo rmmod module_name
# 強制アンロード(危険 - カーネルパニックの可能性あり)
$ sudo rmmod -f module_name
# 例: hello モジュールをアンロード
$ sudo rmmod hello
# アンロード前に使用状況を確認
$ lsmod | grep hello
hello 16384 0
# ^ 使用カウントが 0 であることを確認
rmmod が失敗するケース:
# 使用中のモジュールはアンロードできない
$ sudo rmmod ext4
rmmod: ERROR: Module ext4 is in use
# 他のモジュールから依存されている場合
$ sudo rmmod usbcore
rmmod: ERROR: Module usbcore is in use by: xhci_hcd uhci_hcd ehci_hcd
modprobe コマンド
modprobe は最も推奨されるモジュール管理コマンドである。依存関係を自動的に解決し、必要なモジュールを正しい順序でロード・アンロードする。
# モジュールをロード(依存関係を自動解決)
$ sudo modprobe module_name
# パラメータを指定してロード
$ sudo modprobe module_name param1=value1
# モジュールをアンロード(依存モジュールも自動アンロード)
$ sudo modprobe -r module_name
# ドライランモード(実際にはロードしない)
$ sudo modprobe -n -v module_name
# 詳細出力
$ sudo modprobe -v module_name
# 設定ファイルを指定
$ sudo modprobe -C /path/to/modprobe.conf module_name
# すべてのモジュールをロード(modules.conf の設定に基づく)
$ sudo modprobe -a module1 module2 module3
modprobe の動作フロー:
modprobe e1000e
│
├─ /etc/modprobe.d/*.conf を読み込み
│ ├─ alias 定義を確認
│ ├─ options 定義を確認
│ └─ blacklist 定義を確認
│
├─ /lib/modules/$(uname -r)/modules.dep を参照
│ └─ 依存モジュールの一覧を取得
│
├─ 依存モジュールを先にロード
│ └─ (例) ptp.ko → e1000e.ko
│
└─ 対象モジュールをロード
└─ init_module() システムコール
modprobe と insmod の比較
┌─────────────┬───────────────────────┬───────────────────────┐
│ 機能 │ insmod │ modprobe │
├─────────────┼───────────────────────┼───────────────────────┤
│ 依存関係解決 │ なし │ 自動解決 │
│ ファイル指定 │ フルパスが必要 │ モジュール名のみ │
│ 設定ファイル │ 使用しない │ /etc/modprobe.d/ 参照 │
│ blacklist │ 無視 │ 尊重 │
│ alias │ 非対応 │ 対応 │
│ アンロード │ rmmod が必要 │ -r オプションで対応 │
│ 用途 │ 開発・デバッグ │ 運用環境 │
└─────────────┴───────────────────────┴───────────────────────┘
モジュール依存関係と depmod
depmod の概要
depmod は /lib/modules/$(uname -r)/ 配下のモジュールを走査し、依存関係データベースを生成するツールである。
# 現在のカーネルの依存関係を再構築
$ sudo depmod -a
# 特定のカーネルバージョンの依存関係を構築
$ sudo depmod -a 6.8.0-generic
# 詳細出力
$ sudo depmod -v
# ドライランモード
$ sudo depmod -n
# 特定のモジュールの依存関係を表示
$ sudo depmod -n | grep e1000e
depmod が生成するファイル
$ ls /lib/modules/$(uname -r)/modules.*
modules.alias # デバイスIDとモジュール名のマッピング
modules.alias.bin # modules.alias のバイナリ版(高速検索用)
modules.builtin # カーネルに組み込まれたモジュールの一覧
modules.builtin.bin # modules.builtin のバイナリ版
modules.builtin.alias.bin # 組み込みモジュールのエイリアス
modules.builtin.modinfo # 組み込みモジュールの情報
modules.dep # モジュール依存関係(テキスト形式)
modules.dep.bin # modules.dep のバイナリ版
modules.devname # デバイスノード名のマッピング
modules.order # モジュールの推奨ロード順序
modules.softdep # ソフト依存関係
modules.symbols # エクスポートシンボルの一覧
modules.symbols.bin # modules.symbols のバイナリ版
modules.dep の中身
# modules.dep の内容例
$ cat /lib/modules/$(uname -r)/modules.dep | head -20
kernel/arch/x86/crypto/aesni-intel.ko: kernel/crypto/aes_generic.ko \
kernel/crypto/cryptd.ko kernel/crypto/crypto_simd.ko
kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko: kernel/drivers/ptp/ptp.ko
kernel/drivers/usb/storage/usb-storage.ko: kernel/drivers/scsi/scsi_mod.ko
kernel/fs/ext4/ext4.ko: kernel/fs/jbd2/jbd2.ko kernel/lib/crc16.ko \
kernel/crypto/mbcache.ko
上記の形式は モジュール: 依存モジュール1 依存モジュール2 ... である。
modules.alias の中身
# modules.alias はデバイスIDからモジュール名へのマッピングを定義
$ grep "e1000e" /lib/modules/$(uname -r)/modules.alias
alias pci:v00008086d00001502sv*sd*bc*sc*i* e1000e
alias pci:v00008086d00001503sv*sd*bc*sc*i* e1000e
alias pci:v00008086d0000150Csv*sd*bc*sc*i* e1000e
ソフト依存関係
ソフト依存関係は、モジュールが動作する上で必須ではないが、あると便利な依存関係を示す。
# modules.softdep の例
$ cat /lib/modules/$(uname -r)/modules.softdep
softdep e1000e pre: ptp
softdep xfs pre: crc32c
softdep bluetooth pre: rfkill
pre: はメインモジュールの前にロードすべきモジュールを、post: はメインモジュールの後にロードすべきモジュールを示す。
# modprobe.d で softdep を定義する例
$ cat /etc/modprobe.d/softdep.conf
softdep my_module pre: dep_module1 dep_module2 post: helper_module
基本的なカーネルモジュールの作成
最小限のカーネルモジュール
以下は、最も基本的なカーネルモジュールの例である。
/* hello.c - 最小限のカーネルモジュール */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
/* モジュール情報 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");
MODULE_VERSION("1.0");
/* モジュール初期化関数 */
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World! Module loaded.\n");
return 0; /* 0 = 成功, 負の値 = エラー */
}
/* モジュールクリーンアップ関数 */
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, World! Module unloaded.\n");
}
/* 初期化・クリーンアップ関数の登録 */
module_init(hello_init);
module_exit(hello_exit);
Makefile
# Makefile for hello module
obj-m += hello.o
# カーネルソースのディレクトリ
KDIR := /lib/modules/$(shell uname -r)/build
# デフォルトターゲット
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
# クリーンターゲット
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# インストールターゲット
install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
ビルドと実行
# ビルドに必要なパッケージをインストール
$ sudo apt-get install build-essential linux-headers-$(uname -r)
# RHEL/CentOS の場合
$ sudo yum install kernel-devel kernel-headers gcc make
# ビルド
$ make
make -C /lib/modules/6.8.0-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-6.8.0-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
CC [M] /home/user/hello/hello.mod.o
LD [M] /home/user/hello/hello.ko
BTF [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-generic'
# モジュール情報を確認
$ modinfo ./hello.ko
filename: /home/user/hello/hello.ko
version: 1.0
description: A simple Hello World kernel module
author: Your Name
license: GPL
srcversion: ABC1234567890DEF1234567
depends:
retpoline: Y
name: hello
vermagic: 6.8.0-generic SMP preempt mod_unload modversions
# モジュールをロード
$ sudo insmod ./hello.ko
# カーネルログで確認
$ dmesg | tail -1
[12345.678901] Hello, World! Module loaded.
# ロード済みモジュールの確認
$ lsmod | grep hello
hello 16384 0
# モジュールをアンロード
$ sudo rmmod hello
# カーネルログで確認
$ dmesg | tail -1
[12345.789012] Goodbye, World! Module unloaded.
__init と __exit マクロ
__init と __exit はメモリ最適化のためのマクロである。
/* __init: 初期化後にメモリが解放されるセクションに配置 */
static int __init my_init(void)
{
/* このコードは初期化時のみ実行される */
/* 初期化完了後、このコードが使用していたメモリは解放される */
return 0;
}
/* __exit: モジュールが組み込みの場合はコンパイルから除外 */
static void __exit my_exit(void)
{
/* アンロード時のクリーンアップコード */
}
/* __initdata: 初期化時のみ使用するデータ */
static int __initdata initial_value = 42;
/* __initconst: 初期化時のみ使用する定数データ */
static const char __initconst banner[] = "Module initializing...\n";
エラーハンドリングパターン
カーネルモジュールの初期化では、適切なエラーハンドリングが極めて重要である。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
static void *buffer1;
static void *buffer2;
static void *buffer3;
static int __init robust_init(void)
{
int ret = 0;
/* リソース1の確保 */
buffer1 = kmalloc(4096, GFP_KERNEL);
if (!buffer1) {
pr_err("Failed to allocate buffer1\n");
ret = -ENOMEM;
goto fail_buf1;
}
/* リソース2の確保 */
buffer2 = kmalloc(4096, GFP_KERNEL);
if (!buffer2) {
pr_err("Failed to allocate buffer2\n");
ret = -ENOMEM;
goto fail_buf2;
}
/* リソース3の確保 */
buffer3 = kmalloc(4096, GFP_KERNEL);
if (!buffer3) {
pr_err("Failed to allocate buffer3\n");
ret = -ENOMEM;
goto fail_buf3;
}
pr_info("All resources allocated successfully\n");
return 0;
/* エラーハンドリング - 逆順にリソースを解放 (goto チェーン) */
fail_buf3:
kfree(buffer2);
fail_buf2:
kfree(buffer1);
fail_buf1:
return ret;
}
static void __exit robust_exit(void)
{
kfree(buffer3);
kfree(buffer2);
kfree(buffer1);
pr_info("All resources freed\n");
}
module_init(robust_init);
module_exit(robust_exit);
printk ログレベル
/* printk のログレベル(優先度順) */
printk(KERN_EMERG "Emergency: System is unusable\n"); /* 0 */
printk(KERN_ALERT "Alert: Action must be taken\n"); /* 1 */
printk(KERN_CRIT "Critical: Critical conditions\n"); /* 2 */
printk(KERN_ERR "Error: Error conditions\n"); /* 3 */
printk(KERN_WARNING "Warning: Warning conditions\n"); /* 4 */
printk(KERN_NOTICE "Notice: Normal but significant\n"); /* 5 */
printk(KERN_INFO "Info: Informational\n"); /* 6 */
printk(KERN_DEBUG "Debug: Debug-level messages\n"); /* 7 */
/* 推奨: pr_* マクロを使用(モジュール名が自動付加される) */
pr_emerg("Emergency message\n");
pr_alert("Alert message\n");
pr_crit("Critical message\n");
pr_err("Error message\n");
pr_warn("Warning message\n");
pr_notice("Notice message\n");
pr_info("Info message\n");
pr_debug("Debug message\n"); /* CONFIG_DYNAMIC_DEBUG 有効時のみ */
/* dev_* マクロ(デバイスドライバ向け - デバイス名が自動付加) */
dev_err(&pdev->dev, "Device error\n");
dev_warn(&pdev->dev, "Device warning\n");
dev_info(&pdev->dev, "Device info\n");
dev_dbg(&pdev->dev, "Device debug\n");
モジュールパラメータ
module_param マクロ
モジュールパラメータを使用すると、モジュールロード時に動的に値を設定できる。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Module parameters example");
/* 整数パラメータ */
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=basic, 2=verbose)");
/* 文字列パラメータ */
static char *device_name = "mydevice";
module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Name of the device");
/* ブールパラメータ */
static bool enable_feature = false;
module_param(enable_feature, bool, 0644);
MODULE_PARM_DESC(enable_feature, "Enable special feature");
/* unsigned int パラメータ */
static unsigned int buffer_size = 4096;
module_param(buffer_size, uint, 0444);
MODULE_PARM_DESC(buffer_size, "Size of the internal buffer");
/* 配列パラメータ */
static int ports[4] = { 0x3f8, 0x2f8, 0x3e8, 0x2e8 };
static int num_ports;
module_param_array(ports, int, &num_ports, 0444);
MODULE_PARM_DESC(ports, "I/O port addresses (max 4)");
static int __init params_init(void)
{
int i;
pr_info("Module loaded with parameters:\n");
pr_info(" debug_level = %d\n", debug_level);
pr_info(" device_name = %s\n", device_name);
pr_info(" enable_feature = %d\n", enable_feature);
pr_info(" buffer_size = %u\n", buffer_size);
pr_info(" ports (%d specified):", num_ports);
for (i = 0; i < 4; i++)
pr_cont(" 0x%x", ports[i]);
pr_cont("\n");
return 0;
}
static void __exit params_exit(void)
{
pr_info("Module unloaded\n");
}
module_init(params_init);
module_exit(params_exit);
パラメータの使用方法
# パラメータ付きでロード
$ sudo insmod ./params.ko debug_level=2 device_name="eth0" enable_feature=Y
# modprobe でパラメータ指定
$ sudo modprobe params debug_level=2 device_name="eth0"
# 配列パラメータ
$ sudo insmod ./params.ko ports=0x3f8,0x2f8
# sysfs 経由でパラメータを確認
$ ls /sys/module/params/parameters/
buffer_size debug_level device_name enable_feature ports
$ cat /sys/module/params/parameters/debug_level
2
# 書き込み可能なパラメータの動的変更(パーミッション 0644 の場合)
$ echo 1 | sudo tee /sys/module/params/parameters/debug_level
1
パラメータのパーミッション
/* module_param の第3引数はパーミッションを指定する */
/*
* 0 : sysfs にエントリを作成しない
* 0444 : 読み取り専用(所有者・グループ・その他すべて読み取り可能)
* 0644 : 所有者が読み書き可能、その他は読み取り専用
* 0600 : 所有者のみ読み書き可能
* S_IRUGO : 全員読み取り可能 (= 0444)
* S_IRUGO | S_IWUSR : 所有者が読み書き、その他は読み取り (= 0644)
*/
module_param(readonly_param, int, 0444);
module_param(readwrite_param, int, 0644);
module_param(hidden_param, int, 0); /* sysfs に表示されない */
コールバック付きパラメータ
パラメータの値が変更された際にコールバック関数を呼び出すことができる。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("GPL");
static int threshold = 50;
/* パラメータ変更時のコールバック */
static int threshold_set(const char *val, const struct kernel_param *kp)
{
int new_val;
int ret;
ret = kstrtoint(val, 10, &new_val);
if (ret)
return ret;
/* 値のバリデーション */
if (new_val < 0 || new_val > 100) {
pr_err("threshold must be between 0 and 100\n");
return -EINVAL;
}
pr_info("threshold changed from %d to %d\n", threshold, new_val);
threshold = new_val;
/* ここで新しい値に基づいた処理を実行できる */
return 0;
}
static const struct kernel_param_ops threshold_ops = {
.set = threshold_set,
.get = param_get_int,
};
module_param_cb(threshold, &threshold_ops, &threshold, 0644);
MODULE_PARM_DESC(threshold, "Threshold value (0-100)");
static int __init callback_init(void)
{
pr_info("Module loaded, threshold = %d\n", threshold);
return 0;
}
static void __exit callback_exit(void)
{
pr_info("Module unloaded\n");
}
module_init(callback_init);
module_exit(callback_exit);
シンボルのエクスポート
EXPORT_SYMBOL と EXPORT_SYMBOL_GPL
カーネルモジュールは他のモジュールから使用できるように関数やデータをエクスポートできる。
/* provider.c - シンボルを提供するモジュール */
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Symbol provider module");
/* すべてのモジュールからアクセス可能 */
int shared_counter = 0;
EXPORT_SYMBOL(shared_counter);
/* すべてのモジュールからアクセス可能な関数 */
int add_values(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add_values);
/* GPL ライセンスのモジュールのみアクセス可能 */
int multiply_values(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(multiply_values);
/* 名前空間付きエクスポート(Linux 5.4+) */
int namespaced_func(int x)
{
return x * 2;
}
EXPORT_SYMBOL_NS(namespaced_func, MY_NAMESPACE);
int namespaced_gpl_func(int x)
{
return x * 3;
}
EXPORT_SYMBOL_NS_GPL(namespaced_gpl_func, MY_NAMESPACE);
static int __init provider_init(void)
{
pr_info("Provider module loaded\n");
return 0;
}
static void __exit provider_exit(void)
{
pr_info("Provider module unloaded\n");
}
module_init(provider_init);
module_exit(provider_exit);
/* consumer.c - シンボルを使用するモジュール */
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Symbol consumer module");
/* 名前空間のインポート(Linux 5.4+) */
MODULE_IMPORT_NS(MY_NAMESPACE);
/* 外部シンボルの宣言 */
extern int shared_counter;
extern int add_values(int a, int b);
extern int multiply_values(int a, int b);
extern int namespaced_func(int x);
static int __init consumer_init(void)
{
int result;
shared_counter++;
pr_info("shared_counter = %d\n", shared_counter);
result = add_values(3, 4);
pr_info("add_values(3, 4) = %d\n", result);
result = multiply_values(3, 4);
pr_info("multiply_values(3, 4) = %d\n", result);
result = namespaced_func(5);
pr_info("namespaced_func(5) = %d\n", result);
return 0;
}
static void __exit consumer_exit(void)
{
pr_info("Consumer module unloaded\n");
}
module_init(consumer_init);
module_exit(consumer_exit);
エクスポートされたシンボルの確認
# カーネルのシンボルテーブルを確認
$ cat /proc/kallsyms | grep -E "T (add_values|multiply_values)"
ffffffffc0001000 T add_values [provider]
ffffffffc0001020 T multiply_values [provider]
# Module.symvers ファイル
$ cat Module.symvers
0x12345678 add_values /path/to/provider EXPORT_SYMBOL
0x9abcdef0 multiply_values /path/to/provider EXPORT_SYMBOL_GPL
0x11223344 namespaced_func /path/to/provider EXPORT_SYMBOL_NS MY_NAMESPACE
モジュールライセンス
MODULE_LICENSE マクロ
カーネルモジュールのライセンスは MODULE_LICENSE マクロで宣言する。これはカーネルの法的・技術的要件を満たすために重要である。
/* 有効なライセンス文字列 */
MODULE_LICENSE("GPL"); /* GNU General Public License v2 */
MODULE_LICENSE("GPL v2"); /* GNU General Public License v2 */
MODULE_LICENSE("GPL and additional rights"); /* GPL v2 + 追加権利 */
MODULE_LICENSE("Dual BSD/GPL"); /* BSD/GPL デュアルライセンス */
MODULE_LICENSE("Dual MIT/GPL"); /* MIT/GPL デュアルライセンス */
MODULE_LICENSE("Dual MPL/GPL"); /* MPL/GPL デュアルライセンス */
MODULE_LICENSE("Proprietary"); /* プロプライエタリ(非GPL) */
ライセンスの影響
┌──────────────────┬───────────────┬─────────────────┐
│ ライセンス │ GPL シンボル │ カーネル汚染 │
├──────────────────┼───────────────┼─────────────────┤
│ GPL │ 使用可能 │ なし │
│ GPL v2 │ 使用可能 │ なし │
│ Dual BSD/GPL │ 使用可能 │ なし │
│ Dual MIT/GPL │ 使用可能 │ なし │
│ Proprietary │ 使用不可 │ あり (tainted) │
│ (指定なし) │ 使用不可 │ あり (tainted) │
└──────────────────┴───────────────┴─────────────────┘
カーネル汚染フラグ
# カーネルの汚染状態を確認
$ cat /proc/sys/kernel/tainted
0 # 0 = 汚染なし
# 汚染フラグの意味
# ビット 0 (P): プロプライエタリモジュールがロードされた
# ビット 1 (F): 強制ロードされたモジュールがある
# ビット 2 (S): SMP非対応モジュールが SMP カーネルにロードされた
# ビット 3 (R): 強制アンロードされたモジュールがある
# ビット 4 (M): マシンチェック例外が発生した
# ビット 5 (B): bad page reference が検出された
# ビット 6 (U): ユーザーが手動で汚染を設定した
# ビット 7 (D): DIE notification が発生した
# ビット 8 (A): ACPI テーブルがオーバーライドされた
# ビット 9 (W): 警告が発生した
# ビット 10 (C): ステージングドライバーがロードされた
# ビット 11 (I): ファームウェアの回避策が適用された
# ビット 12 (O): アウトオブツリーモジュールがロードされた
# ビット 13 (E): 未署名のモジュールがロードされた
# ビット 14 (L): ソフトロックアップが発生した
# ビット 15 (K): カーネルがライブパッチされた
# ビット 16 (X): 補助的な汚染(ディストリビューション定義)
# ビット 17 (T): テストに汚染されたカーネルが使用された
# 汚染状態の詳細確認
$ dmesg | grep -i taint
セキュアブートとモジュール署名
署名の仕組み
Linux カーネルはモジュールにデジタル署名を付与し、ロード時に検証する機能を持つ。UEFI Secure Boot 環境では、これが必須となる場合がある。
┌─────────────────────────────────────────────────────┐
│ モジュール署名の流れ │
│ │
│ ビルド時: │
│ ┌──────┐ ┌──────────┐ ┌──────────────┐ │
│ │ .ko │ ──→│ sign-file │ ──→│ 署名付き .ko │ │
│ │ファイル│ │ ツール │ │ ファイル │ │
│ └──────┘ └──────────┘ └──────────────┘ │
│ ↑ │
│ ┌────┴────┐ │
│ │ 秘密鍵 │ │
│ │ X.509証明書│ │
│ └─────────┘ │
│ │
│ ロード時: │
│ ┌──────────────┐ ┌──────────┐ ┌────────┐ │
│ │ 署名付き .ko │ ──→│ カーネル │ ──→│ 検証結果 │ │
│ │ ファイル │ │ 署名検証 │ │ OK/NG │ │
│ └──────────────┘ └──────────┘ └────────┘ │
│ ↑ │
│ ┌────┴────┐ │
│ │ 信頼された │ │
│ │ 公開鍵 │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────┘
カーネルコンフィグオプション
# モジュール署名に関連するカーネル設定
CONFIG_MODULE_SIG=y # モジュール署名機能を有効化
CONFIG_MODULE_SIG_FORCE=y # 署名なしモジュールのロードを拒否
CONFIG_MODULE_SIG_ALL=y # ビルド時にすべてのモジュールに署名
CONFIG_MODULE_SIG_SHA256=y # SHA-256 ハッシュアルゴリズムを使用
CONFIG_MODULE_SIG_HASH="sha256" # 使用するハッシュ
CONFIG_MODULE_SIG_KEY="certs/signing_key.pem" # 署名鍵のパス
CONFIG_SYSTEM_TRUSTED_KEYRING=y # システム信頼キーリング
CONFIG_SECONDARY_TRUSTED_KEYRING=y # セカンダリ信頼キーリング
自己署名証明書の作成と使用
# 1. 設定ファイルの作成
cat > x509.genkey << 'EOF'
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
prompt = no
x509_extensions = myexts
[ req_distinguished_name ]
O = My Organization
CN = Module Signing Key
emailAddress = admin@example.com
[ myexts ]
basicConstraints=critical,CA:FALSE
keyUsage=digitalSignature
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid
EOF
# 2. 秘密鍵と証明書の生成
$ openssl req -new -nodes -utf8 -sha256 -days 36500 \
-batch -x509 -config x509.genkey \
-outform PEM -out signing_key.pem \
-keyout signing_key.pem
# 3. 公開鍵(DER形式)の抽出
$ openssl x509 -in signing_key.pem -outform der -out signing_key.x509
# 4. モジュールへの署名
$ /usr/src/linux-headers-$(uname -r)/scripts/sign-file \
sha256 signing_key.pem signing_key.x509 my_module.ko
# 5. 署名の確認
$ modinfo my_module.ko | grep sig
sig_id: PKCS#7
signer: Module Signing Key
sig_key: AB:CD:EF:12:34:56:78:90
sig_hashalgo: sha256
# 6. 公開鍵をカーネルのキーリングに追加
# MOK (Machine Owner Key) として追加
$ sudo mokutil --import signing_key.x509
# 再起動後に MOK Manager で承認が必要
DKMS でのモジュール署名
# /etc/dkms/framework.conf に署名設定を追加
$ cat /etc/dkms/framework.conf
## Sign modules on install
sign_tool="/usr/lib/linux-kbuild-$(uname -r | cut -d. -f1-2)/scripts/sign-file"
mok_signing_key="/var/lib/dkms/mok.key"
mok_certificate="/var/lib/dkms/mok.pub"
Proc および sysfs インターフェースの作成
/proc インターフェース
/proc ファイルシステムはカーネルとユーザー空間の間の情報交換に使用される。
/* proc_example.c - /proc インターフェースの作成 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Proc filesystem interface example");
#define PROC_NAME "my_module_info"
#define PROC_DIR "my_module"
#define BUFFER_SIZE 256
static struct proc_dir_entry *proc_dir;
static struct proc_dir_entry *proc_file;
static char proc_buffer[BUFFER_SIZE];
static unsigned long proc_buffer_size = 0;
static int access_count = 0;
/* /proc ファイルの読み取り */
static int my_proc_show(struct seq_file *m, void *v)
{
access_count++;
seq_printf(m, "=== My Module Info ===\n");
seq_printf(m, "Access count: %d\n", access_count);
seq_printf(m, "Buffer content: %s\n", proc_buffer);
seq_printf(m, "Buffer size: %lu bytes\n", proc_buffer_size);
seq_printf(m, "Kernel version: %s\n", UTS_RELEASE);
return 0;
}
static int my_proc_open(struct inode *inode, struct file *file)
{
return single_open(file, my_proc_show, NULL);
}
/* /proc ファイルへの書き込み */
static ssize_t my_proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *pos)
{
if (count > BUFFER_SIZE - 1)
count = BUFFER_SIZE - 1;
if (copy_from_user(proc_buffer, buffer, count))
return -EFAULT;
proc_buffer[count] = '\0';
proc_buffer_size = count;
pr_info("Received %zu bytes from user space\n", count);
return count;
}
static const struct proc_ops my_proc_ops = {
.proc_open = my_proc_open,
.proc_read = seq_read,
.proc_write = my_proc_write,
.proc_lseek = seq_lseek,
.proc_release = single_release,
};
static int __init proc_example_init(void)
{
/* /proc/my_module/ ディレクトリの作成 */
proc_dir = proc_mkdir(PROC_DIR, NULL);
if (!proc_dir) {
pr_err("Failed to create /proc/%s\n", PROC_DIR);
return -ENOMEM;
}
/* /proc/my_module/my_module_info ファイルの作成 */
proc_file = proc_create(PROC_NAME, 0666, proc_dir, &my_proc_ops);
if (!proc_file) {
pr_err("Failed to create /proc/%s/%s\n", PROC_DIR, PROC_NAME);
proc_remove(proc_dir);
return -ENOMEM;
}
strcpy(proc_buffer, "(empty)");
proc_buffer_size = 7;
pr_info("Proc interface created: /proc/%s/%s\n", PROC_DIR, PROC_NAME);
return 0;
}
static void __exit proc_example_exit(void)
{
proc_remove(proc_file);
proc_remove(proc_dir);
pr_info("Proc interface removed\n");
}
module_init(proc_example_init);
module_exit(proc_example_exit);
使用例:
# モジュールをロード
$ sudo insmod ./proc_example.ko
# 読み取り
$ cat /proc/my_module/my_module_info
=== My Module Info ===
Access count: 1
Buffer content: (empty)
Buffer size: 7 bytes
Kernel version: 6.8.0-generic
# 書き込み
$ echo "Hello from userspace" | sudo tee /proc/my_module/my_module_info
# 再度読み取り
$ cat /proc/my_module/my_module_info
=== My Module Info ===
Access count: 2
Buffer content: Hello from userspace
Buffer size: 21 bytes
Kernel version: 6.8.0-generic
sysfs インターフェース
sysfs はカーネルオブジェクトの属性をユーザー空間に公開するためのファイルシステムである。
/* sysfs_example.c - sysfs インターフェースの作成 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Sysfs interface example");
static struct kobject *my_kobj;
static int my_value = 42;
static char my_string[256] = "default";
/* 属性の読み取り */
static ssize_t my_value_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
return sysfs_emit(buf, "%d\n", my_value);
}
/* 属性の書き込み */
static ssize_t my_value_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
int ret;
ret = kstrtoint(buf, 10, &my_value);
if (ret < 0)
return ret;
return count;
}
static ssize_t my_string_show(struct kobject *kobj,
struct kobj_attribute *attr, char *buf)
{
return sysfs_emit(buf, "%s\n", my_string);
}
static ssize_t my_string_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
if (count >= sizeof(my_string))
return -EINVAL;
strscpy(my_string, buf, sizeof(my_string));
/* 末尾の改行を除去 */
my_string[strcspn(my_string, "\n")] = '\0';
return count;
}
/* 属性の定義 */
static struct kobj_attribute my_value_attr =
__ATTR(my_value, 0664, my_value_show, my_value_store);
static struct kobj_attribute my_string_attr =
__ATTR(my_string, 0664, my_string_show, my_string_store);
/* 属性グループ */
static struct attribute *attrs[] = {
&my_value_attr.attr,
&my_string_attr.attr,
NULL,
};
static struct attribute_group attr_group = {
.attrs = attrs,
};
static int __init sysfs_example_init(void)
{
int ret;
/* /sys/kernel/my_module に kobject を作成 */
my_kobj = kobject_create_and_add("my_module", kernel_kobj);
if (!my_kobj)
return -ENOMEM;
/* 属性グループを登録 */
ret = sysfs_create_group(my_kobj, &attr_group);
if (ret) {
kobject_put(my_kobj);
return ret;
}
pr_info("Sysfs interface created: /sys/kernel/my_module/\n");
return 0;
}
static void __exit sysfs_example_exit(void)
{
kobject_put(my_kobj);
pr_info("Sysfs interface removed\n");
}
module_init(sysfs_example_init);
module_exit(sysfs_example_exit);
使用例:
# モジュールをロード
$ sudo insmod ./sysfs_example.ko
# sysfs エントリの確認
$ ls /sys/kernel/my_module/
my_string my_value
# 読み取り
$ cat /sys/kernel/my_module/my_value
42
$ cat /sys/kernel/my_module/my_string
default
# 書き込み
$ echo 100 | sudo tee /sys/kernel/my_module/my_value
100
$ echo "new_value" | sudo tee /sys/kernel/my_module/my_string
new_value
# 確認
$ cat /sys/kernel/my_module/my_value
100
$ cat /sys/kernel/my_module/my_string
new_value
キャラクタデバイスドライバモジュール
完全なキャラクタデバイスドライバの例
以下は、読み書き可能な仮想キャラクタデバイスの完全な実装例である。
/* chardev.c - 完全なキャラクタデバイスドライバ */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A complete character device driver example");
MODULE_VERSION("1.0");
#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mychar"
#define BUFFER_SIZE 4096
#define NUM_DEVICES 1
/* デバイス構造体 */
struct mychardev_data {
struct cdev cdev;
char *buffer;
size_t size;
struct mutex lock;
};
static dev_t dev_num; /* デバイス番号 */
static struct class *dev_class; /* デバイスクラス */
static struct mychardev_data *dev_data; /* デバイスデータ */
/* open 操作 */
static int mychardev_open(struct inode *inode, struct file *file)
{
struct mychardev_data *data;
data = container_of(inode->i_cdev, struct mychardev_data, cdev);
file->private_data = data;
pr_info("mychardev: device opened\n");
return 0;
}
/* release 操作 */
static int mychardev_release(struct inode *inode, struct file *file)
{
pr_info("mychardev: device closed\n");
return 0;
}
/* read 操作 */
static ssize_t mychardev_read(struct file *file, char __user *buf,
size_t count, loff_t *offset)
{
struct mychardev_data *data = file->private_data;
ssize_t ret;
if (mutex_lock_interruptible(&data->lock))
return -ERESTARTSYS;
/* オフセットがバッファサイズを超えている場合 */
if (*offset >= data->size) {
ret = 0;
goto out;
}
/* 読み取りサイズの調整 */
if (*offset + count > data->size)
count = data->size - *offset;
/* ユーザー空間にデータをコピー */
if (copy_to_user(buf, data->buffer + *offset, count)) {
ret = -EFAULT;
goto out;
}
*offset += count;
ret = count;
pr_info("mychardev: read %zu bytes from offset %lld\n", count, *offset);
out:
mutex_unlock(&data->lock);
return ret;
}
/* write 操作 */
static ssize_t mychardev_write(struct file *file, const char __user *buf,
size_t count, loff_t *offset)
{
struct mychardev_data *data = file->private_data;
ssize_t ret;
if (mutex_lock_interruptible(&data->lock))
return -ERESTARTSYS;
/* バッファサイズを超える書き込みを制限 */
if (*offset + count > BUFFER_SIZE)
count = BUFFER_SIZE - *offset;
if (count <= 0) {
ret = -ENOSPC;
goto out;
}
/* ユーザー空間からデータをコピー */
if (copy_from_user(data->buffer + *offset, buf, count)) {
ret = -EFAULT;
goto out;
}
*offset += count;
if (*offset > data->size)
data->size = *offset;
ret = count;
pr_info("mychardev: wrote %zu bytes at offset %lld\n", count, *offset);
out:
mutex_unlock(&data->lock);
return ret;
}
/* ioctl 操作 */
#define MYCHARDEV_IOC_MAGIC 'k'
#define MYCHARDEV_IOCRESET _IO(MYCHARDEV_IOC_MAGIC, 0)
#define MYCHARDEV_IOCGSIZE _IOR(MYCHARDEV_IOC_MAGIC, 1, int)
#define MYCHARDEV_IOCSSIZE _IOW(MYCHARDEV_IOC_MAGIC, 2, int)
static long mychardev_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
struct mychardev_data *data = file->private_data;
int ret = 0;
int size;
/* コマンドの検証 */
if (_IOC_TYPE(cmd) != MYCHARDEV_IOC_MAGIC)
return -ENOTTY;
if (mutex_lock_interruptible(&data->lock))
return -ERESTARTSYS;
switch (cmd) {
case MYCHARDEV_IOCRESET:
/* バッファをリセット */
memset(data->buffer, 0, BUFFER_SIZE);
data->size = 0;
pr_info("mychardev: buffer reset\n");
break;
case MYCHARDEV_IOCGSIZE:
/* 現在のデータサイズを取得 */
size = data->size;
if (copy_to_user((int __user *)arg, &size, sizeof(int)))
ret = -EFAULT;
break;
case MYCHARDEV_IOCSSIZE:
/* データサイズを設定 */
if (copy_from_user(&size, (int __user *)arg, sizeof(int))) {
ret = -EFAULT;
break;
}
if (size < 0 || size > BUFFER_SIZE) {
ret = -EINVAL;
break;
}
data->size = size;
break;
default:
ret = -ENOTTY;
break;
}
mutex_unlock(&data->lock);
return ret;
}
/* lseek 操作 */
static loff_t mychardev_llseek(struct file *file, loff_t offset, int whence)
{
struct mychardev_data *data = file->private_data;
loff_t new_pos;
switch (whence) {
case SEEK_SET:
new_pos = offset;
break;
case SEEK_CUR:
new_pos = file->f_pos + offset;
break;
case SEEK_END:
new_pos = data->size + offset;
break;
default:
return -EINVAL;
}
if (new_pos < 0 || new_pos > BUFFER_SIZE)
return -EINVAL;
file->f_pos = new_pos;
return new_pos;
}
/* ファイルオペレーション構造体 */
static const struct file_operations mychardev_fops = {
.owner = THIS_MODULE,
.open = mychardev_open,
.release = mychardev_release,
.read = mychardev_read,
.write = mychardev_write,
.unlocked_ioctl = mychardev_ioctl,
.llseek = mychardev_llseek,
};
/* モジュール初期化 */
static int __init mychardev_init(void)
{
int ret;
struct device *device;
/* 1. デバイス番号の動的割り当て */
ret = alloc_chrdev_region(&dev_num, 0, NUM_DEVICES, DEVICE_NAME);
if (ret < 0) {
pr_err("mychardev: failed to allocate device number\n");
return ret;
}
pr_info("mychardev: registered with major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
/* 2. デバイスクラスの作成 */
dev_class = class_create(CLASS_NAME);
if (IS_ERR(dev_class)) {
pr_err("mychardev: failed to create device class\n");
ret = PTR_ERR(dev_class);
goto fail_class;
}
/* 3. デバイスデータの確保 */
dev_data = kzalloc(sizeof(*dev_data), GFP_KERNEL);
if (!dev_data) {
ret = -ENOMEM;
goto fail_data;
}
/* 4. バッファの確保 */
dev_data->buffer = kzalloc(BUFFER_SIZE, GFP_KERNEL);
if (!dev_data->buffer) {
ret = -ENOMEM;
goto fail_buffer;
}
/* 5. Mutex の初期化 */
mutex_init(&dev_data->lock);
/* 6. cdev の初期化と登録 */
cdev_init(&dev_data->cdev, &mychardev_fops);
dev_data->cdev.owner = THIS_MODULE;
ret = cdev_add(&dev_data->cdev, dev_num, 1);
if (ret < 0) {
pr_err("mychardev: failed to add cdev\n");
goto fail_cdev;
}
/* 7. デバイスファイルの作成 (/dev/mychardev) */
device = device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(device)) {
pr_err("mychardev: failed to create device\n");
ret = PTR_ERR(device);
goto fail_device;
}
pr_info("mychardev: device created successfully at /dev/%s\n",
DEVICE_NAME);
return 0;
fail_device:
cdev_del(&dev_data->cdev);
fail_cdev:
kfree(dev_data->buffer);
fail_buffer:
kfree(dev_data);
fail_data:
class_destroy(dev_class);
fail_class:
unregister_chrdev_region(dev_num, NUM_DEVICES);
return ret;
}
/* モジュールクリーンアップ */
static void __exit mychardev_exit(void)
{
device_destroy(dev_class, dev_num);
cdev_del(&dev_data->cdev);
kfree(dev_data->buffer);
kfree(dev_data);
class_destroy(dev_class);
unregister_chrdev_region(dev_num, NUM_DEVICES);
pr_info("mychardev: device removed\n");
}
module_init(mychardev_init);
module_exit(mychardev_exit);
キャラクタデバイスの使用方法
# モジュールをビルドしてロード
$ make && sudo insmod ./chardev.ko
# デバイスファイルの確認
$ ls -la /dev/mychardev
crw------- 1 root root 234, 0 Apr 10 12:00 /dev/mychardev
# パーミッションの変更(テスト用)
$ sudo chmod 666 /dev/mychardev
# データの書き込み
$ echo "Hello, Kernel!" > /dev/mychardev
# データの読み取り
$ cat /dev/mychardev
Hello, Kernel!
# dd コマンドでの操作
$ dd if=/dev/zero of=/dev/mychardev bs=1024 count=4
$ dd if=/dev/mychardev of=output.bin bs=1024 count=4
# カーネルログの確認
$ dmesg | tail -10
[12345.678901] mychardev: device opened
[12345.678902] mychardev: wrote 15 bytes at offset 15
[12345.678903] mychardev: device closed
[12345.678904] mychardev: device opened
[12345.678905] mychardev: read 15 bytes from offset 15
[12345.678906] mychardev: device closed
ioctl を使用するユーザー空間プログラム
/* test_chardev.c - キャラクタデバイスのテストプログラム */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#define DEVICE_PATH "/dev/mychardev"
#define MYCHARDEV_IOC_MAGIC 'k'
#define MYCHARDEV_IOCRESET _IO(MYCHARDEV_IOC_MAGIC, 0)
#define MYCHARDEV_IOCGSIZE _IOR(MYCHARDEV_IOC_MAGIC, 1, int)
#define MYCHARDEV_IOCSSIZE _IOW(MYCHARDEV_IOC_MAGIC, 2, int)
int main(void)
{
int fd;
char buffer[256];
int size;
/* デバイスを開く */
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
/* データを書き込む */
const char *msg = "Test data for ioctl";
write(fd, msg, strlen(msg));
/* データサイズを取得 */
if (ioctl(fd, MYCHARDEV_IOCGSIZE, &size) == 0)
printf("Current data size: %d\n", size);
/* データを読み取る */
lseek(fd, 0, SEEK_SET);
int n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("Read: %s\n", buffer);
}
/* バッファをリセット */
ioctl(fd, MYCHARDEV_IOCRESET);
printf("Buffer reset\n");
/* サイズを確認 */
ioctl(fd, MYCHARDEV_IOCGSIZE, &size);
printf("Data size after reset: %d\n", size);
close(fd);
return 0;
}
# テストプログラムのコンパイルと実行
$ gcc -o test_chardev test_chardev.c
$ ./test_chardev
Current data size: 19
Read: Test data for ioctl
Buffer reset
Data size after reset: 0
ネットワークモジュールの例
仮想ネットワークインターフェースモジュール
以下は、仮想ネットワークインターフェースを作成するカーネルモジュールの例である。
/* vnet.c - 仮想ネットワークインターフェースモジュール */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Virtual network interface module");
#define VNET_NAME "vnet0"
struct vnet_priv {
struct net_device_stats stats;
struct sk_buff *skb;
spinlock_t lock;
};
/* パケット送信 */
static netdev_tx_t vnet_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct vnet_priv *priv = netdev_priv(dev);
spin_lock(&priv->lock);
/* 統計情報の更新 */
priv->stats.tx_packets++;
priv->stats.tx_bytes += skb->len;
spin_unlock(&priv->lock);
/* ここでは単にパケットを破棄する(ループバック等の処理を追加可能) */
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
/* インターフェースを有効化 */
static int vnet_open(struct net_device *dev)
{
pr_info("vnet: interface %s opened\n", dev->name);
netif_start_queue(dev);
return 0;
}
/* インターフェースを無効化 */
static int vnet_stop(struct net_device *dev)
{
pr_info("vnet: interface %s stopped\n", dev->name);
netif_stop_queue(dev);
return 0;
}
/* 統計情報の取得 */
static struct net_device_stats *vnet_get_stats(struct net_device *dev)
{
struct vnet_priv *priv = netdev_priv(dev);
return &priv->stats;
}
/* ネットワークデバイスオペレーション */
static const struct net_device_ops vnet_netdev_ops = {
.ndo_open = vnet_open,
.ndo_stop = vnet_stop,
.ndo_start_xmit = vnet_xmit,
.ndo_get_stats = vnet_get_stats,
};
static struct net_device *vnet_dev;
/* デバイスの初期設定 */
static void vnet_setup(struct net_device *dev)
{
struct vnet_priv *priv;
ether_setup(dev);
dev->netdev_ops = &vnet_netdev_ops;
dev->flags |= IFF_NOARP;
dev->features |= NETIF_F_HW_CSUM;
/* ランダムなMACアドレスを設定 */
eth_hw_addr_random(dev);
/* プライベートデータの初期化 */
priv = netdev_priv(dev);
memset(priv, 0, sizeof(*priv));
spin_lock_init(&priv->lock);
}
static int __init vnet_init(void)
{
int ret;
/* ネットワークデバイスの割り当て */
vnet_dev = alloc_netdev(sizeof(struct vnet_priv),
VNET_NAME, NET_NAME_UNKNOWN, vnet_setup);
if (!vnet_dev) {
pr_err("vnet: failed to allocate net device\n");
return -ENOMEM;
}
/* デバイスの登録 */
ret = register_netdev(vnet_dev);
if (ret) {
pr_err("vnet: failed to register net device\n");
free_netdev(vnet_dev);
return ret;
}
pr_info("vnet: virtual network interface %s created\n", vnet_dev->name);
return 0;
}
static void __exit vnet_exit(void)
{
unregister_netdev(vnet_dev);
free_netdev(vnet_dev);
pr_info("vnet: virtual network interface removed\n");
}
module_init(vnet_init);
module_exit(vnet_exit);
使用例:
# モジュールをロード
$ sudo insmod ./vnet.ko
# インターフェースの確認
$ ip link show vnet0
3: vnet0: <BROADCAST,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN
link/ether 12:34:56:78:9a:bc brd ff:ff:ff:ff:ff:ff
# インターフェースの有効化
$ sudo ip link set vnet0 up
$ sudo ip addr add 10.0.0.1/24 dev vnet0
# 状態確認
$ ip addr show vnet0
3: vnet0: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN
link/ether 12:34:56:78:9a:bc brd ff:ff:ff:ff:ff:ff
inet 10.0.0.1/24 scope global vnet0
# 統計情報
$ cat /sys/class/net/vnet0/statistics/tx_packets
0
# アンロード
$ sudo ip link set vnet0 down
$ sudo rmmod vnet
Makefile と Kbuild システム
基本的な Makefile
# 単一ソースファイルモジュール
obj-m += hello.o
# 複数ソースファイルモジュール
obj-m += mydriver.o
mydriver-objs := mydriver_core.o mydriver_io.o mydriver_util.o
# コンパイルフラグ
ccflags-y += -DDEBUG -I$(src)/include
ldflags-y += -T$(src)/my_linker_script.lds
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
depmod -a
.PHONY: all clean install
高度な Kbuild Makefile
# Kbuild ファイル(Makefile とは別に配置可能)
# 条件付きコンパイル
obj-$(CONFIG_MY_MODULE) += my_module.o
# デバッグビルド
ifdef CONFIG_MY_MODULE_DEBUG
ccflags-y += -DMY_MODULE_DEBUG
my_module-objs += debug_helper.o
endif
# 複数モジュールのビルド
obj-m += module_a.o
obj-m += module_b.o
module_a-objs := mod_a_main.o mod_a_utils.o
module_b-objs := mod_b_main.o mod_b_net.o
# アーキテクチャ固有のソース
module_a-$(CONFIG_X86) += mod_a_x86.o
module_a-$(CONFIG_ARM) += mod_a_arm.o
# ヘッダーファイルの依存関係
ccflags-y += -I$(src)/include -I$(src)/../common
# アセンブリソース
module_a-objs += mod_a_asm.o # .S ファイルもサポート
完全なプロジェクト構成例
my_driver/
├── Kbuild # Kbuild ルール
├── Makefile # トップレベル Makefile
├── dkms.conf # DKMS 設定
├── include/
│ └── my_driver.h # ヘッダーファイル
├── src/
│ ├── main.c # メインソース
│ ├── device.c # デバイス操作
│ ├── ioctl.c # ioctl ハンドラ
│ └── utils.c # ユーティリティ関数
├── tests/
│ ├── test_basic.sh # 基本テスト
│ └── test_stress.sh # ストレステスト
└── README.md
# トップレベル Makefile
KDIR ?= /lib/modules/$(shell uname -r)/build
MODULE_NAME := my_driver
# Kbuild に処理を委譲
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install: all
$(MAKE) -C $(KDIR) M=$(PWD) modules_install
depmod -a
@echo "Module installed. Load with: modprobe $(MODULE_NAME)"
uninstall:
rm -f /lib/modules/$(shell uname -r)/extra/$(MODULE_NAME).ko
depmod -a
test: all
sudo insmod $(MODULE_NAME).ko
bash tests/test_basic.sh
sudo rmmod $(MODULE_NAME)
@echo "All tests passed"
.PHONY: all clean install uninstall test
# Kbuild ファイル
obj-m += my_driver.o
my_driver-objs := src/main.o src/device.o src/ioctl.o src/utils.o
ccflags-y += -I$(src)/include -Wall -Werror
クロスコンパイル
# ARM 向けクロスコンパイル
$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
KDIR=/path/to/arm/kernel/build
# ARM64 (AArch64) 向け
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
KDIR=/path/to/arm64/kernel/build
# RISC-V 向け
$ make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- \
KDIR=/path/to/riscv/kernel/build
アウトオブツリーモジュール
アウトオブツリーモジュールとは
アウトオブツリー(out-of-tree)モジュールは、Linuxカーネルソースツリーの外部で管理・ビルドされるモジュールである。
インツリーモジュール:
/usr/src/linux/drivers/net/ethernet/intel/e1000e/
アウトオブツリーモジュール:
/home/user/my_driver/
/opt/vendor/gpu_driver/
アウトオブツリーモジュールの特徴
┌─────────────────┬───────────────────┬────────────────────┐
│ 項目 │ インツリー │ アウトオブツリー │
├─────────────────┼───────────────────┼────────────────────┤
│ ビルドシステム │ カーネルの Kconfig │ 独自の Makefile │
│ 配布方法 │ カーネルに同梱 │ 別途配布 │
│ 品質保証 │ カーネルメンテナ │ 自己責任 │
│ API 互換性 │ 保証される │ 保証されない │
│ 署名 │ カーネル署名 │ 自己署名が必要 │
│ カーネル汚染 │ なし │ フラグ O が設定 │
│ 更新 │ カーネル更新に追従 │ 手動更新が必要 │
└─────────────────┴───────────────────┴────────────────────┘
vermagic の一致
アウトオブツリーモジュールは、ロード先カーネルの vermagic と一致する必要がある。
# カーネルの vermagic を確認
$ cat /proc/version
Linux version 6.8.0-generic (builder@host) (gcc version 13.2.0)
# SMP preempt mod_unload modversions
# モジュールの vermagic を確認
$ modinfo ./my_module.ko | grep vermagic
vermagic: 6.8.0-generic SMP preempt mod_unload modversions
# vermagic が一致しない場合のエラー
$ sudo insmod ./my_module.ko
insmod: ERROR: could not insert module ./my_module.ko: Invalid module format
$ dmesg | tail -1
[12345.678901] my_module: version magic '6.7.0-generic SMP preempt mod_unload'
should be '6.8.0-generic SMP preempt mod_unload modversions'
DKMS
DKMS(Dynamic Kernel Module Support)
DKMS はカーネル更新時にアウトオブツリーモジュールを自動的に再ビルドするフレームワークである。
# DKMS のインストール
$ sudo apt-get install dkms # Debian/Ubuntu
$ sudo yum install dkms # RHEL/CentOS
dkms.conf の設定
# /usr/src/my_driver-1.0/dkms.conf
PACKAGE_NAME="my_driver"
PACKAGE_VERSION="1.0"
# ビルド対象のモジュール
BUILT_MODULE_NAME[0]="my_driver"
BUILT_MODULE_LOCATION[0]="src/"
DEST_MODULE_LOCATION[0]="/updates/dkms/"
# ビルドコマンド
MAKE[0]="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build modules"
CLEAN="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build clean"
# 自動インストール設定
AUTOINSTALL="yes"
# POST_INSTALL スクリプト
POST_INSTALL="post_install.sh"
DKMS のワークフロー
# 1. ソースコードを DKMS ツリーにコピー
$ sudo cp -r my_driver /usr/src/my_driver-1.0
# 2. DKMS に登録
$ sudo dkms add -m my_driver -v 1.0
Creating symlink /var/lib/dkms/my_driver/1.0/source ->
/usr/src/my_driver-1.0
DKMS: add completed.
# 3. 現在のカーネル向けにビルド
$ sudo dkms build -m my_driver -v 1.0
Building module:
cleaning build area...
make -C /lib/modules/6.8.0-generic/build M=/var/lib/dkms/my_driver/1.0/build...
DKMS: build completed.
# 4. インストール
$ sudo dkms install -m my_driver -v 1.0
my_driver.ko:
Running module version sanity check.
- Original module
- Installation
- Installing to /lib/modules/6.8.0-generic/updates/dkms/
depmod...
DKMS: install completed.
# 5. 状態確認
$ dkms status
my_driver, 1.0, 6.8.0-generic, x86_64: installed
# 6. アンインストール
$ sudo dkms uninstall -m my_driver -v 1.0
# 7. DKMS から削除
$ sudo dkms remove -m my_driver -v 1.0 --all
DKMS の自動再ビルド
# カーネル更新時に自動再ビルドされることを確認
$ sudo apt-get upgrade linux-image-*
# DKMS が自動的に新しいカーネル向けにモジュールを再ビルド
# 手動で全カーネル向けにビルド
$ sudo dkms autoinstall
# 特定のカーネルバージョン向けにビルド
$ sudo dkms build -m my_driver -v 1.0 -k 6.9.0-generic
$ sudo dkms install -m my_driver -v 1.0 -k 6.9.0-generic
モジュールバージョニング
modversions の概要
modversions(モジュールバージョニング)は、カーネルシンボルの CRC チェックサムを使用して、モジュールとカーネル間の ABI 互換性を検証するメカニズムである。
# カーネル設定で modversions が有効か確認
$ grep CONFIG_MODVERSIONS /boot/config-$(uname -r)
CONFIG_MODVERSIONS=y
# Module.symvers の内容(CRCチェックサム付き)
$ head -5 /lib/modules/$(uname -r)/build/Module.symvers
0x12345678 printk vmlinux EXPORT_SYMBOL
0x9abcdef0 __kmalloc vmlinux EXPORT_SYMBOL
0x11223344 kfree vmlinux EXPORT_SYMBOL
0x55667788 register_chrdev_region vmlinux EXPORT_SYMBOL
0x99aabbcc class_create vmlinux EXPORT_SYMBOL_GPL
CRC の不一致エラー
# CRC 不一致時のエラーメッセージ
$ sudo insmod ./my_module.ko
insmod: ERROR: could not insert module ./my_module.ko: Invalid module format
$ dmesg | tail -3
[12345.678901] my_module: disagrees about version of symbol printk
[12345.678902] my_module: Unknown symbol printk (err -22)
__versions セクション
# モジュールの CRC 情報を確認
$ modprobe --dump-modversions my_module.ko
0x12345678 printk
0x9abcdef0 __kmalloc
0x11223344 kfree
0x55667788 module_layout
モジュールスタッキング
モジュールスタッキングとは
モジュールスタッキングは、あるモジュールが別のモジュールの提供する機能(エクスポートされたシンボル)を使用する構造のことである。
┌──────────────────────────────────────────┐
│ 上位モジュール │
│ (例: e1000e ネットワークドライバ) │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 中間モジュール │ │
│ │ (例: ptp - Precision Time Protocol) │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ カーネルコア │ │ │
│ │ │ (組み込み機能 - vmlinux) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└──────────────────────────────────────────┘
スタッキングの例
/* base_module.c - ベースモジュール */
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
static int base_value = 100;
int base_get_value(void)
{
return base_value;
}
EXPORT_SYMBOL_GPL(base_get_value);
void base_set_value(int val)
{
base_value = val;
pr_info("base: value set to %d\n", val);
}
EXPORT_SYMBOL_GPL(base_set_value);
static int __init base_init(void)
{
pr_info("base: module loaded (value=%d)\n", base_value);
return 0;
}
static void __exit base_exit(void)
{
pr_info("base: module unloaded\n");
}
module_init(base_init);
module_exit(base_exit);
/* upper_module.c - 上位モジュール */
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
/* ベースモジュールの関数を使用 */
extern int base_get_value(void);
extern void base_set_value(int val);
static int __init upper_init(void)
{
int val = base_get_value();
pr_info("upper: current base value = %d\n", val);
base_set_value(val * 2);
pr_info("upper: doubled base value to %d\n", base_get_value());
return 0;
}
static void __exit upper_exit(void)
{
base_set_value(100); /* デフォルト値に戻す */
pr_info("upper: module unloaded\n");
}
module_init(upper_init);
module_exit(upper_exit);
# ロード順序
$ sudo insmod ./base_module.ko # 先にベースをロード
$ sudo insmod ./upper_module.ko # 上位モジュールをロード
# 依存関係の確認
$ lsmod | grep -E "(base|upper)"
upper_module 16384 0
base_module 16384 1 upper_module
# アンロード順序(依存関係の逆順)
$ sudo rmmod upper_module # 先に上位をアンロード
$ sudo rmmod base_module # 次にベースをアンロード
# 依存モジュールがある状態でベースをアンロードしようとするとエラー
$ sudo rmmod base_module
rmmod: ERROR: Module base_module is in use by: upper_module
モジュールのデバッグ
printk によるデバッグ
/* デバッグ用マクロの定義 */
#ifdef MY_MODULE_DEBUG
#define DBG(fmt, ...) pr_debug("%s:%d: " fmt, __func__, __LINE__, ##__VA_ARGS__)
#else
#define DBG(fmt, ...) /* do nothing */
#endif
/* 使用例 */
static int my_function(int param)
{
DBG("entering with param=%d\n", param);
/* 処理 */
DBG("returning %d\n", result);
return result;
}
Dynamic Debug(動的デバッグ)
# Dynamic Debug の制御ファイル
$ cat /sys/kernel/debug/dynamic_debug/control | head -20
# 特定モジュールのデバッグ出力を有効化
$ echo 'module my_module +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# 特定ファイルのデバッグ出力を有効化
$ echo 'file my_source.c +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# 特定関数のデバッグ出力を有効化
$ echo 'func my_function +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# 特定行のデバッグ出力を有効化
$ echo 'file my_source.c line 100 +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# フラグの説明:
# +p : デバッグ出力を有効化
# -p : デバッグ出力を無効化
# +f : 関数名をメッセージに付加
# +l : 行番号をメッセージに付加
# +m : モジュール名をメッセージに付加
# +t : スレッドIDをメッセージに付加
# 全フラグ有効化の例
$ echo 'module my_module +pflmt' | sudo tee /sys/kernel/debug/dynamic_debug/control
# 現在の設定を確認
$ grep my_module /sys/kernel/debug/dynamic_debug/control
GDB によるカーネルモジュールのデバッグ
# 1. QEMU でカーネルを起動(GDB サーバー付き)
$ qemu-system-x86_64 \
-kernel /path/to/bzImage \
-initrd /path/to/initramfs.img \
-append "console=ttyS0 nokaslr" \
-nographic \
-s -S # -s: GDB サーバーを 1234 ポートで起動, -S: 起動を一時停止
# 2. 別ターミナルで GDB を起動
$ gdb vmlinux
(gdb) target remote :1234
(gdb) continue
# 3. モジュールをロード後、モジュールのシンボルを読み込み
(gdb) add-symbol-file /path/to/my_module.ko 0xffffffffc0000000
# アドレスは /sys/module/my_module/sections/.text から取得
# 4. ブレークポイントの設定
(gdb) break my_module_init
(gdb) break my_source.c:42
# 5. モジュールのセクションアドレス取得
$ cat /sys/module/my_module/sections/.text
0xffffffffc0001000
$ cat /sys/module/my_module/sections/.data
0xffffffffc0002000
$ cat /sys/module/my_module/sections/.bss
0xffffffffc0003000
ftrace によるトレース
# ftrace の基本設定
$ cd /sys/kernel/debug/tracing
# 利用可能なトレーサーの確認
$ cat available_tracers
function function_graph nop
# function トレーサーを有効化
$ echo function > current_tracer
# 特定のモジュール関数のみトレース
$ echo ':mod:my_module' > set_ftrace_filter
# トレースを有効化
$ echo 1 > tracing_on
# ログの確認
$ cat trace
# トレースの無効化
$ echo 0 > tracing_on
$ echo nop > current_tracer
kprobe によるデバッグ
/* kprobe_example.c - kprobe を使用したデバッグ */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kprobes.h>
MODULE_LICENSE("GPL");
static struct kprobe kp = {
.symbol_name = "do_sys_openat2",
};
/* プリハンドラー(関数呼び出し前に実行) */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
pr_info("kprobe: do_sys_openat2 called\n");
return 0;
}
/* ポストハンドラー(関数呼び出し後に実行) */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
pr_info("kprobe: do_sys_openat2 returned\n");
}
static int __init kprobe_init(void)
{
int ret;
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("kprobe: registration failed, error %d\n", ret);
return ret;
}
pr_info("kprobe: registered at %pS\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
pr_info("kprobe: unregistered\n");
}
module_init(kprobe_init);
module_exit(kprobe_exit);
/etc/modprobe.d/ 設定
設定ファイルの基本
/etc/modprobe.d/ ディレクトリには、modprobe の動作を制御する設定ファイルを配置する。ファイル名は .conf で終わる必要がある。
# 設定ファイルの一覧
$ ls /etc/modprobe.d/
aliases.conf blacklist.conf intel-microcode-blacklist.conf
amd64-microcode-blacklist.conf dkms.conf options.conf
alias(エイリアス)
# /etc/modprobe.d/aliases.conf
# デバイスIDとモジュール名のマッピング
alias pci:v00008086d00001502sv*sd*bc*sc*i* e1000e
# ネットワークインターフェースのエイリアス
alias net-pf-2 ipv4
alias net-pf-10 ipv6
# カスタムエイリアス
alias my_network_driver e1000e
alias my_storage_driver ahci
options(オプション)
# /etc/modprobe.d/options.conf
# モジュールのデフォルトパラメータを設定
options e1000e InterruptThrottleRate=3000 RxIntDelay=0
options snd-hda-intel power_save=1 power_save_controller=Y
options iwlwifi power_save=1 11n_disable=0
options bonding mode=4 miimon=100
# NVIDIA ドライバのオプション例
options nvidia NVreg_UsePageAttributeTable=1
options nvidia-drm modeset=1
install / remove コマンド
# /etc/modprobe.d/custom_install.conf
# モジュールロード時にカスタムコマンドを実行
install my_module /sbin/modprobe --ignore-install my_module && /usr/local/bin/setup_device.sh
# モジュールアンロード時にカスタムコマンドを実行
remove my_module /usr/local/bin/cleanup_device.sh && /sbin/modprobe -r --ignore-remove my_module
# 特定モジュールのロードを完全にブロック
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
softdep(ソフト依存関係)
# /etc/modprobe.d/softdep.conf
# ソフト依存関係の定義
softdep e1000e pre: ptp
softdep xfs pre: crc32c
softdep bluetooth pre: rfkill post: btusb
# pre: メインモジュールの前にロードされるべきモジュール
# post: メインモジュールの後にロードされるべきモジュール
/etc/modules-load.d/ による自動ロード
基本的な設定
/etc/modules-load.d/ ディレクトリに配置された設定ファイルに記載されたモジュールは、システム起動時に自動的にロードされる。
# /etc/modules-load.d/my_modules.conf
# 起動時に自動ロードするモジュール(1行に1モジュール)
# '#' で始まる行はコメント
# 空行は無視される
# 仮想化関連
vhost_net
vhost_vsock
# ネットワーク関連
bonding
8021q
# ストレージ関連
nbd
loop
# セキュリティ関連
br_netfilter
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
systemd との連携
# systemd-modules-load サービスの確認
$ systemctl status systemd-modules-load
● systemd-modules-load.service - Load Kernel Modules
Loaded: loaded (/lib/systemd/system/systemd-modules-load.service; static)
Active: active (exited) since Mon 2026-04-10 10:00:00 JST
# モジュールロードの失敗を確認
$ journalctl -u systemd-modules-load
# 手動で再実行
$ sudo systemctl restart systemd-modules-load
udev ルールによる自動ロード
# /etc/udev/rules.d/99-my-module.rules
# 特定のデバイスが接続された際にモジュールをロード
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1234", \
ATTR{idProduct}=="5678", RUN+="/sbin/modprobe my_usb_driver"
# PCI デバイスの自動ロード
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x8086", \
ATTR{device}=="0x1502", RUN+="/sbin/modprobe e1000e"
モジュールのブラックリスト
ブラックリストの設定
# /etc/modprobe.d/blacklist.conf
# ブラックリストに追加されたモジュールは自動ロードされない
# ただし、手動での insmod/modprobe は可能
# 不要なモジュールのブラックリスト
blacklist pcspkr # PC スピーカー
blacklist snd_pcsp # PC スピーカーサウンド
blacklist nouveau # オープンソース NVIDIA ドライバ
blacklist rivafb # NVIDIA Riva フレームバッファ
blacklist nvidiafb # NVIDIA フレームバッファ
blacklist rivatv # NVIDIA TV
blacklist i2c_nvidia_gpu # NVIDIA I2C
# セキュリティ目的のブラックリスト
blacklist cramfs # 不要なファイルシステム
blacklist freevxfs
blacklist jffs2
blacklist hfs
blacklist hfsplus
blacklist squashfs
blacklist udf
# ネットワークプロトコルのブラックリスト
blacklist dccp # DCCP プロトコル
blacklist sctp # SCTP プロトコル
blacklist rds # RDS プロトコル
blacklist tipc # TIPC プロトコル
ブラックリストの確認と適用
# ブラックリストが適用されているか確認
$ modprobe --showconfig | grep blacklist
# ブラックリストされたモジュールを確認
$ grep -r "blacklist" /etc/modprobe.d/
# ブートパラメータでもブラックリスト可能
# GRUB の設定: /etc/default/grub
GRUB_CMDLINE_LINUX="modprobe.blacklist=nouveau,pcspkr"
# GRUB の更新
$ sudo update-grub
# 注意: blacklist はautoprobeのみをブロックする
# 完全にブロックするには install を使用
# /etc/modprobe.d/blacklist-complete.conf
install nouveau /bin/false
install pcspkr /bin/false
ツール一覧と詳細
lsmod
# ロード済みモジュールの一覧
$ lsmod
Module Size Used by
nvidia_drm 73728 3
nvidia_modeset 1249280 5 nvidia_drm
nvidia 56397824 122 nvidia_modeset
ext4 978944 2
mbcache 16384 1 ext4
jbd2 155648 1 ext4
# 特定モジュールの検索
$ lsmod | grep -i nvidia
# lsmod は /proc/modules の内容を整形して表示する
$ cat /proc/modules | head -5
nvidia_drm 73728 3 - Live 0xffffffffc0a00000
nvidia_modeset 1249280 5 nvidia_drm, Live 0xffffffffc0800000
nvidia 56397824 122 nvidia_modeset, Live 0xffffffffc0000000
modinfo
# モジュールの詳細情報を表示
$ modinfo e1000e
filename: /lib/modules/6.8.0-generic/kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko
version: 3.8.7-k
license: GPL v2
description: Intel(R) PRO/1000 Network Driver
author: Intel Corporation, <linux.nics@intel.com>
srcversion: A1B2C3D4E5F6G7H8I9J0K
alias: pci:v00008086d00001502sv*sd*bc*sc*i*
depends: ptp
retpoline: Y
intree: Y
name: e1000e
vermagic: 6.8.0-generic SMP preempt mod_unload modversions
sig_id: PKCS#7
signer: Build time autogenerated kernel key
sig_key: AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90
sig_hashalgo: sha512
signature: ...
parm: InterruptThrottleRate:Interrupt Throttling Rate (array of int)
parm: RxIntDelay:Receive Interrupt Delay (array of int)
parm: TxIntDelay:Transmit Interrupt Delay (array of int)
# 特定のフィールドのみ表示
$ modinfo -F filename e1000e
/lib/modules/6.8.0-generic/kernel/drivers/net/ethernet/intel/e1000e/e1000e.ko
$ modinfo -F depends e1000e
ptp
$ modinfo -F parm e1000e
InterruptThrottleRate:Interrupt Throttling Rate (array of int)
RxIntDelay:Receive Interrupt Delay (array of int)
TxIntDelay:Transmit Interrupt Delay (array of int)
# パラメータの確認
$ modinfo -p e1000e
depmod の詳細
# すべてのモジュールの依存関係を再構築
$ sudo depmod -a
# 特定カーネルバージョンの依存関係を構築
$ sudo depmod -a 6.8.0-generic
# 依存関係をテキストで標準出力に表示
$ sudo depmod -n
# エラーのみ表示
$ sudo depmod -e
# シンボルの重複を警告
$ sudo depmod -w
# カスタムモジュールディレクトリを指定
$ sudo depmod -b /path/to/alternate/root -a 6.8.0-generic
dkms の詳細コマンド
# DKMS の状態確認
$ dkms status
my_driver, 1.0, 6.8.0-generic, x86_64: installed
nvidia, 535.183.01, 6.8.0-generic, x86_64: installed
# モジュールの追加
$ sudo dkms add -m my_driver -v 1.0
# ビルド
$ sudo dkms build -m my_driver -v 1.0
# インストール
$ sudo dkms install -m my_driver -v 1.0
# アンインストール
$ sudo dkms uninstall -m my_driver -v 1.0
# 完全削除
$ sudo dkms remove -m my_driver -v 1.0 --all
# ソースからの追加とビルドを一括実行
$ sudo dkms add -m my_driver -v 1.0 && \
sudo dkms build -m my_driver -v 1.0 && \
sudo dkms install -m my_driver -v 1.0
# DKMS のログ確認
$ cat /var/lib/dkms/my_driver/1.0/build/make.log
トラブルシューティング
よくある問題と解決方法
1. モジュールがロードできない
# 症状: insmod が Invalid module format を返す
$ sudo insmod ./my_module.ko
insmod: ERROR: could not insert module ./my_module.ko: Invalid module format
# 診断: dmesg でエラー詳細を確認
$ dmesg | tail -5
# 解決策1: vermagic の確認
$ modinfo ./my_module.ko | grep vermagic
$ uname -r
# 一致しない場合は、正しいカーネルヘッダーでリビルド
# 解決策2: カーネルヘッダーの再インストール
$ sudo apt-get install --reinstall linux-headers-$(uname -r)
$ make clean && make
2. シンボルが見つからない
# 症状: Unknown symbol エラー
$ dmesg | grep "Unknown symbol"
[12345.678901] my_module: Unknown symbol some_function (err -2)
# 診断: シンボルの存在を確認
$ grep some_function /proc/kallsyms
$ grep some_function /lib/modules/$(uname -r)/build/Module.symvers
# 解決策: 依存モジュールを先にロード
$ sudo modprobe dependency_module
$ sudo insmod ./my_module.ko
3. パーミッションエラー
# 症状: Operation not permitted
$ insmod ./my_module.ko
insmod: ERROR: could not insert module ./my_module.ko: Operation not permitted
# 解決策1: root 権限で実行
$ sudo insmod ./my_module.ko
# 解決策2: Secure Boot 環境での署名確認
$ mokutil --sb-state
SecureBoot enabled
# モジュールに署名が必要
4. モジュールのメモリリーク検出
# kmemleak を使用(CONFIG_DEBUG_KMEMLEAK=y が必要)
$ echo scan | sudo tee /proc/kmemleak
$ cat /proc/kmemleak
# KASAN を使用(CONFIG_KASAN=y が必要)
# dmesg に KASAN のレポートが出力される
$ dmesg | grep -A 20 "BUG: KASAN"
ベストプラクティスとセキュリティ
開発のベストプラクティス
- goto チェーンによるエラーハンドリング: リソース解放を確実に行う
- ロック戦略の明確化: mutex、spinlock の使い分けを明確にする
- エラーコードの適切な使用: 適切な errno 値を返す
- メモリ管理: kmalloc/kfree のペアを確実に管理
- ユーザー空間とのインターフェース: copy_to_user/copy_from_user を必ず使用
- printk の適切な使用: 本番環境では不要なログを出力しない
- コーディングスタイル: Linux カーネルのコーディングスタイルに従う
# Linux カーネルコーディングスタイルチェック
$ /path/to/linux/scripts/checkpatch.pl --file my_module.c
$ /path/to/linux/scripts/checkpatch.pl --strict --file my_module.c
# sparse による静的解析
$ make C=1 M=$(PWD) modules
# coccinelle による自動パッチ
$ make coccicheck MODE=report M=$(PWD)
セキュリティのベストプラクティス
# 1. 不要なモジュールを無効化
$ cat /etc/modprobe.d/security-hardening.conf
# 不要なネットワークプロトコル
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
# 不要なファイルシステム
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install squashfs /bin/false
install udf /bin/false
# USB ストレージ(必要に応じて)
install usb-storage /bin/false
# Firewire(必要に応じて)
install firewire-core /bin/false
# 2. モジュール署名の強制
# カーネルパラメータに追加
module.sig_enforce=1
# 3. モジュールのロード制限(sysctl)
$ echo 1 | sudo tee /proc/sys/kernel/modules_disabled
# 注意: これを設定すると、再起動するまでモジュールのロードが完全に無効化される
# 4. 現在ロードされているモジュールの監査
$ lsmod | awk '{print $1}' | sort > /tmp/current_modules.txt
# 既知の安全なモジュールリストと比較
まとめ
Linux カーネルモジュールは、カーネルの機能を動的に拡張するための強力なメカニズムである。本ガイドでは以下の主要なトピックを網羅した。
主要コマンド早見表
┌───────────────┬───────────────────────────────────────────────┐
│ コマンド │ 用途 │
├───────────────┼───────────────────────────────────────────────┤
│ lsmod │ ロード済みモジュールの一覧表示 │
│ modinfo │ モジュールの詳細情報表示 │
│ insmod │ モジュールのロード(依存関係なし) │
│ rmmod │ モジュールのアンロード │
│ modprobe │ モジュールの管理(依存関係自動解決) │
│ modprobe -r │ モジュールのアンロード(依存関係含む) │
│ depmod │ モジュール依存関係データベースの再構築 │
│ dkms │ 動的カーネルモジュールサポート管理 │
└───────────────┴───────────────────────────────────────────────┘
重要なディレクトリとファイル
┌───────────────────────────────────┬──────────────────────────┐
│ パス │ 説明 │
├───────────────────────────────────┼──────────────────────────┤
│ /lib/modules/$(uname -r)/ │ モジュール格納ディレクトリ │
│ /lib/modules/$(uname -r)/build/ │ カーネルビルドディレクトリ │
│ /etc/modprobe.d/ │ modprobe 設定ファイル │
│ /etc/modules-load.d/ │ 自動ロード設定 │
│ /sys/module/ │ ロード済みモジュール情報 │
│ /proc/modules │ モジュール一覧 │
│ /proc/kallsyms │ カーネルシンボルテーブル │
│ /usr/src/ │ カーネルヘッダー/ソース │
│ /var/lib/dkms/ │ DKMS データ │
└───────────────────────────────────┴──────────────────────────┘
カーネルモジュールの開発には、カーネルの内部構造に対する深い理解が必要であるが、適切なツールとベストプラクティスに従うことで、安全で効率的なモジュールを開発できる。
本ドキュメントは AI によって生成されました。 Generated by: Claude (Anthropic) Date: 2026-04-10