第1章 はじめに

本書を手にとって頂いてありがとうございます。この本はLinuxカーネルモジュールのビルド手順を説明する本です。Hello Worldを画面に表示するだけのモジュールを紹介します。さらに、カーネルにブレークポイントを仕掛けるkprobesやカーネル内の変数を公開できるsysfsというLinuxの機能を自作モジュールから使ってみます。

読者は基本的なLinuxの操作ができ、C言語の読み書きができることを前提としています。Linuxカーネルのソースコードは読んだことがなくても大丈夫です。

1.1 Linuxカーネルモジュールとは

Linuxカーネルモジュールとは、Linuxカーネルの本体に機能を追加するためのオブジェクトファイルのことです。モジュールはカーネルに機能を追加する方法の1つで、もう1つはカーネルのビルド時に機能を組み込む方法です。前者はデバイスドライバなどでよく採用される方法で、後者はメモリ管理やプロセス管理などのコア機能に多いです。

ビルド時に組み込む方法は実行時のオーバーヘッドが少ないですが、その機能を使わくてもメモリを消費してしまいます。一方、モジュールは動的にカーネルに読み込むことができますから、その機能を使わない限りメモリを消費しません。Linuxがサポートすべきデバイスは多種多様ですが、あるシステムが扱うデバイスは限られています。そのため、多くのデバイスドライバはモジュールの形で提供されています。

1.2 動作確認環境

この本で出てくるサンプルコードはUbuntu 16.04で動作を確認しています。カーネルバージョンは4.4.0-59-genericです。

LinuxカーネルモジュールやkprobesはUbuntu固有というわけではないため、他のディストリビューションを使っても動くはずです。その際、パスなどを若干調整する必要があるかもしれません。

1.3 著者について

サイボウズ株式会社で、ソフトウェアエンジニアとして自社インフラ用のシステムを作っています。障害発生を検知してメールやSlackに通知するためのモニタリングシステムの構築、アクセスログを集約して高速に解析する基盤の構築、リアルタイムにブロックデバイスをバックアップするWalBというシステムの開発支援、導入支援などをやってきました。

職業柄、日常的にLinuxに触っています。新人の頃、チームメンバーから報告された「なぜかソフトウェアRAIDの処理でエラーが出る」という不可解なバグを調査するためにmdadmのソースコードを読みました。あるチームメンバーがぼそっと言った「LVMのボリューム名が32文字。ハハハ、まさかね。」という、フラグにも聞こえる言葉を確認するために。mdadmの処理を辿ってみるとボリューム名の文字列を32バイトの配列にstrcpyしている*1箇所がありました(見事なフラグ回収!)。こうして、32文字以上を指定するとバッファオーバーフローを起こすことが確定したのでした。

ソフトウェアエンジニアの傍ら、会社の図書委員も兼務しており、最近はバーコードリーダーとNFC機能付き社員証を使って本の貸出管理を行うシステムを作ったりもしました。Raspberry Piってたーのしー!

[*1] strcpyはC言語を知っている人なら誰でも知っている有名な標準関数ですが、バッファサイズを指定できないために容易にバッファオーバーフローを引き起こします。代わりにstrncpyを使いましょう。

1.4 本書の取り扱い方法

本書の著作権は著者である内田公太に帰属します。法律の範囲内でご使用ください。

本書の電子データは個人的な範囲でのみご使用ください。例えば、誰でもアクセスできる場所にアップロードしてはいけませんし、USBメモリなどに入れて複数人で共有するのもダメです。また、本書のダウンロードURLは秘密ですので、(ネット、リアル含め)公開の場所に書かないでください。

第2章 Hello Worldモジュール

Linuxカーネルモジュールは一定の構造を持つオブジェクトファイルです。作成はとても簡単なので、本章では事前の説明なく、いきなり最初のモジュール作成をやってみます。

2.1 プログラム本体のソースコード

練習用のディレクトリを作ります。どこでもいいのですが、以下では$HOME/workspace/kmod/helloにしたとして話を進めます。この中に必要なファイルを作っていきます。

まず、モジュールを構成するファイルの中で中核となるソースコードを書きましょう。

リスト2.1: hello.c

#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
    printk(KERN_INFO "Hello, World!\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Goodbye...\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSEはモジュールのライセンスを宣言するマクロです。ここではBSDとGPLのデュアルライセンスとしています。モジュールのライセンスはそのモジュールのロード時に検査され、GPLと非互換の場合は警告が出てしまいます。

hello_inithello_exitは、それぞれモジュールの初期化と後処理を担当する関数です。2つのマクロmodule_initmodule_exitを使って「登録」します。登録について詳しくは第3章「モジュール解剖」を参照してください。

2.2 Makefile

次に、ソースコードから.koファイルを生成するためのMakefileを準備します。

リスト2.2: Makefile

obj-m := hello.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

今回はソースファイルが1つなのでobj-mだけを指定しています。このように書くことでhello.cファイルからカーネルモジュールhello.koが生成されます。ソースファイル名とターゲットファイル名の拡張子を除いた部分が一致します。

一方、複数のファイルからなるモジュールをビルドする場合は次のように書きます。

リスト2.3: Makefile(複数ファイル)

obj-m := hello.o
hello-objs := src1.o src2.o ...

obj-mに設定するファイル名はhello-objsのいずれとも異なる名前にしなければなりません。単一ファイルの場合とはルールが変わりますのでご注意ください。

2.3 ビルド

ファイルが準備できたので、いよいよビルドしてみましょう*1

[*1] ビルドに必要なプログラム(makeとかCコンパイラとか)は、UbuntuやDebianであればbuild-essentialというパッケージをインストールすれば手に入ります。

$ make
make -C /lib/modules/4.4.0-59-generic/build M=/home/uchan/workspace/kmod/hello modules
make[1]: ディレクトリ '/usr/src/linux-headers-4.4.0-59-generic' に入ります
  CC [M]  /home/uchan/workspace/kmod/hello/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/uchan/workspace/kmod/hello/hello.mod.o
  LD [M]  /home/uchan/workspace/kmod/hello/hello.ko
make[1]: ディレクトリ '/usr/src/linux-headers-4.4.0-59-generic' から出ます

ビルドが完了するとモジュールの本体となるhello.koファイルが生成されているはずです。これは単なるELFファイルなのでreadelfなどのコマンドで中身を調べることができます。詳しくは第3章「モジュール解剖」を参照してください。

2.4 実行

カーネルモジュールの実行には危険が伴います。カーネルモジュールは当然ながらカーネル空間で動きますので、モジュール内のバグはPC全体のダウンなどを引き起こします。データ破損が発生する可能性もあります。筆者はこの本の執筆のためにカーネルモジュールの実験をしていてバグを発生させ、OSを再インストールするはめになりました。ですから、壊れてもよいPC上、または仮想環境上で実験すると良いと思います。

ここまで読み、なお実行する勇気のある方は実行させてみましょう。まあ、Hello Worldだけなら固定の文字列を表示しているだけですから、そうそうバグは出ないでしょう。

$ sudo insmod ./hello.ko
$ dmesg
...
[19946.768924] Hello, World!

insmodコマンドはモジュールファイルを引数にとり、モジュールをカーネルに追加するコマンドです。dmesgコマンドを実行し、表示されたメッセージの最終行付近を見てみましょう。Hello, Worldと表示されていることが分かると思います。

最後に後片付けをしておきましょう。

$ sudo rmmod hello
$ dmesg
...
[19946.768924] Hello, World!
[19977.033060] Goodbye...

rmmodコマンドは指定した名前のモジュールをカーネルから削除するコマンドです。dmesgで確認すると、hello_exit関数が実行されていることが分かると思います。

試し読みはここまでです。
この続きは、製品版でお楽しみください。