Linux Kernel Modules

Linux カーネルモジュール 完全ガイド

最終更新: 2026-04-10 対象カーネルバージョン: Linux 5.x / 6.x


目次

  1. はじめに
  2. カーネルモジュールのアーキテクチャとライフサイクル
  3. モジュールのロードとアンロード
  4. モジュール依存関係と depmod
  5. 基本的なカーネルモジュールの作成
  6. モジュールパラメータ
  7. シンボルのエクスポート
  8. モジュールライセンス
  9. セキュアブートとモジュール署名
  10. Proc および sysfs インターフェースの作成
  11. キャラクタデバイスドライバモジュール(完全な例)
  12. ネットワークモジュールの例
  13. Makefile と Kbuild システム
  14. アウトオブツリーモジュール
  15. DKMS(Dynamic Kernel Module Support)
  16. モジュールバージョニング(modversions)
  17. モジュールスタッキング
  18. モジュールのデバッグ
  19. /etc/modprobe.d/ 設定
  20. /etc/modules-load.d/ による自動ロード
  21. モジュールのブラックリスト
  22. ツール一覧と詳細
  23. トラブルシューティング
  24. ベストプラクティスとセキュリティ
  25. まとめ

はじめに

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

モジュールを使う理由

カーネルモジュールの主な利点は以下の通りである。

  1. 動的な機能追加: システムの再起動なしに新しいハードウェアドライバやファイルシステムを追加できる
  2. メモリ効率: 必要な機能のみをロードし、不要になったらアンロードすることでメモリを節約できる
  3. 開発効率: カーネル全体を再コンパイルせずにモジュールのみを再コンパイルしてテストできる
  4. 配布の柔軟性: サードパーティがカーネル本体とは別にドライバを配布できる
  5. 保守性: 特定の機能に関するコードを独立したモジュールとして管理できる

モノリシックカーネルとモジュラーカーネル

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_modulestruct 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 セクション解放   │
                                └────────────────────────┘

各ステップの詳細は以下の通り。

  1. ELFヘッダ検証: ファイルが有効なELFオブジェクトであることを確認
  2. セクション解析: .modinfo.text.data 等のセクションを解析
  3. メモリ確保: カーネルメモリにモジュール用の領域を確保(module_alloc()
  4. セクションコピー: ユーザー空間からカーネル空間にセクションデータをコピー
  5. シンボル解決: モジュールが参照する外部シンボルのアドレスを解決
  6. リロケーション処理: アドレスの再配置を実行
  7. パラメータ設定: コマンドラインから渡されたパラメータを設定
  8. ライセンスチェック: GPL互換ライセンスかどうかを確認
  9. モジュール署名検証: 署名が有効かどうかを検証(Secure Boot環境)
  10. sysfs エントリ作成: /sys/module/<name>/ 配下にエントリを作成
  11. init() 関数呼び出し: モジュールの初期化関数を実行
  12. .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"

ベストプラクティスとセキュリティ

開発のベストプラクティス

  1. goto チェーンによるエラーハンドリング: リソース解放を確実に行う
  2. ロック戦略の明確化: mutex、spinlock の使い分けを明確にする
  3. エラーコードの適切な使用: 適切な errno 値を返す
  4. メモリ管理: kmalloc/kfree のペアを確実に管理
  5. ユーザー空間とのインターフェース: copy_to_user/copy_from_user を必ず使用
  6. printk の適切な使用: 本番環境では不要なログを出力しない
  7. コーディングスタイル: 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