カーネルモジュール作成によるlinuxカーネル開発入門 - 第一回 hello world

  • 104
    いいね
  • 0
    コメント

はじめに

本記事の目的は、linuxのカーネルモジュール(以下カーネルモジュール)というものの作成を通じてlinuxカーネル(以下カーネル)の開発に最低限必要な知識をつけることです。C言語のポインタがわかる程度の開発スキルがあれば多分読めると思います。

本記事は、過去にセキュリティ&プログラミングキャンプ2010というイベントの中のLinux開発者育成コースにおいて使用した資料を加筆、修正したものです。1つの記事に納めるのは無理がある分量なので、(不定期)連載という形式をとることにしました。

本記事に記載されているコードを実際に試すためには、仮想化機能が有効になっているPCにインストールされたUbuntuなどのLinuxディストリビューションが必要です。仮想化機能が有効になっているかどうかは、shellから以下のプログラムを実行すればわかります。マッチする行があれば仮想化機能は有効です。

$ egrep '^flags.*(vmx|svm)' /proc/cpuinfo

第一回では、そもそもカーネルモジュールとは何かというところから初めて、なぜカーネル開発を始めるためにカーネルモジュール作成という手段をとるかを説明した上で、hello worldを出力するだけの簡単なカーネルモジュールを作成します。

カーネルモジュールとは

カーネルモジュールとは、マシンの起動中にカーネルに機能を追加するための部品です。webブラウザに対するプラグインを思い浮かべてもらえればいいかと思います。カーネルの機能のうちの多くの部分は最初からカーネルに組み込んでおくこともできますし、モジュールとして独立したファイルにしておいて1、必要になった時にカーネルに組み込むこともできます。たとえばみなさんのPCに繋がっている各種デバイスを操作するデバイスドライバなどがそうです。モジュールはカーネル本体と同時にビルドできますし、後から別途個別にビルドもできます。本記事は後者のアプローチをとります。

ディストリビューションのカーネルは、カーネルが提供するほとんどのドライバをカーネルモジュールとして提供しています。起動時に読み込むカーネル本体は最小のサイズに抑え、その後でマシンに搭載されているデバイスに関するモジュールだけを必要に応じて読み込みます。これによって、

  • 高速な起動
  • カーネルによるメモリ使用量の最小化
  • なるべく多くのデバイスのサポート

を同時に達成しています。

なぜカーネルモジュールの作成なのか

カーネル開発は、カーネル本体の変更よりも、カーネルモジュールの作成から始めるほうが入門しやすいです。理由は次の通りです。

  • カーネルモジュールの開発に使用する言語(C言語。一部アセンブラ)もAPIもカーネル本体と同じ
  • 巨大なカーネル本体に手を入れるより単機能かつ少ないコード量で作れる
  • ビルド時間が短いので手軽に試せる
  • システム全体を意のままにできる万能性、バグがあればシステム全体がパニック/ハングするなどのスリルはカーネル本体とほぼ同じ

準備

開発に必要な環境を用意します。まずはシステムに必要パッケージをインストールします。Ubuntu 16.04の場合は次のようにします。

$ sudo apt-get install git vagrant libvirt-bin libvirt-dev kernel-package qemu-kvm libssl-dev libncurses5-dev
$ sudo usermod -aG libvirt <your user name>

ここでいったんログアウト&ログインします。

$ sudo sed -i'' "s/Specification.all = nil/Specification.reset/" /usr/lib/ruby/vendor_ruby/vagrant/bundler.rb         # See https://github.com/vagrant-libvirt/vagrant-libvirt/issues/575 for more details about this patching
$ vagrant plugin install vagrant-libvirt

他のディストリビューションをお使いのかたは、適宜読み替えて下さい2

次に開発環境を作成します。

$ git clone https://github.com/satoru-takeuchi/elkdat.git
Cloning into 'elkdat'...
...
$ cd elkdat
$ ./init
...
$ 

ここまでで、カーネル開発に必要な資材(ソフトウェア、ライブラリ、カーネルソース)が全て揃いました。自作カーネル、およびカーネルモジュールをテストするためのVMも作成済です。

もし開発環境が不要になったら以下のコマンドを実行してください。

$ ./fini                                                                        
...                                                                             
Finished to cleanup.                                                            
Now it's safe to delete the source directory.                                   
$ 

本記事ではカーネルv4.9を対象に開発をします。このため、このカーネルをビルドしてVM上でブートさせます。今回のトピックはカーネルモジュールの作成なので、カーネル自体は変更せず、upstreamのものをそのまま利用します。

$ cd linux
$ git checkout v4.9
$ cd ../
$ ./test boot
...
*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

VMにログインして、v4.9でブートできているか確認します。

$ cd elkdat
$ vagrant ssh
...
vagrant@packer-qemu:~$ uname -r
4.9.0-ktest
vagrant@packer-qemu:~$ exit
...
$ ../
$ 

uname -rとは現在のカーネルバージョンを確かめるためのコマンドです。ちゃんと4.9が動作していることがわかります3

では次の節で実際にカーネルモジュールを作ってみましょう。

hello world カーネルモジュールの作成

まずは開発用のディレクトリを作成して、そこに移動します。

$ mkdir -p dev/module/hello
$ cd dev/module/hello
$ 

以下のようなファイルを作成します。これがカーネルモジュールのソースコードです。

hello.c
#include <linux/module.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("Hello world kernel module");

static int mymodule_init(void) {
        printk(KERN_ALERT "Hello world!\n");
        return 0;
}

static void mymodule_exit(void) {
        /* Do nothing */
}

module_init(mymodule_init);
module_exit(mymodule_exit);

20行程度の簡単なソースです。これだけでまがりなりにもカーネルの一機能を作成できます。見た目から、なんとなく何をしているのか想像できるかもしれません。

ここで覚えておいてほしいのは次のことです。

  • カーネルモジュールを作成するときは必ずlinux/module.hをincludeする必要がある
  • printk()は、おおよそprintfと同等に扱える。文字列先頭についているKERN_ALERTというのは、メッセージの重要度。今はあまり気にしなくてよい
  • mymodule_init()関数を上記のような引数、戻り値で作成した上でmodule_init()マクロに渡すことによって、このカーネルモジュールのロード時(insmod時)にこの関数が呼ばれる
  • mymodule_exit()関数を上記のような引数、戻り値で作成した上でmodule_exit()マクロに渡すことによって、このカーネルモジュールのロード時(rmmod時)にこの関数が呼ばれる
  • MODULE_LICENSE()マクロ内にライセンスを記載する。とくに理由がなければ"GPL v2"でよい。
  • MODULE_AUTHOR()マクロ内に作者の名前と連絡先となるメールアドレスを記載する。上記の例では筆者のものを使っているが、自分のものと書き換えてよい
  • MODULE_DESCRIPTION()マクロ内に、このモジュールが何をするものなのかという説明を記載する

ビルドするためには以下のようなMakefileの作成が必要です。

.PHONY: all clean

obj-m := hello.o

all:
        make -C ../../../output M=$(PWD) modules

clean:
        make -C ../../../linux M=$(PWD) clean

"obj-m :=" の後にカーネルモジュールのソースコード名の.cを.oに置換したものを記載すれば、当該ファイルをビルドした結果得られるオブジェクトファイル、hello.oをカーネルモジュール化できます。カーネルモジュールは.koという拡張子をもちます。

他の部分については仕組みが複雑な上に知ってもあまり幸せになれないので気にしなくていいです4

ではビルドしましょう。といってもmakeを実行するだけです。

$ make
...
$ 

成功したら、作成したモジュールをVM上にコピーします。

$ make install
...
$ 

カーネルモジュールをロードします。自作カーネルがブートしている状態ではないと失敗しますのでご注意ください。

$ make login
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# insmod /vagrant/hello.ko
root@packer-qemu:/home/vagrant# lsmod | grep hello
hello                  16384  0
root@packer-qemu:/home/vagrant# 

ロードは成功したようです。失敗した場合は一旦VMから抜けた上で5、次のコマンドを実行すればリカバリできます。その後でまたソースを書き換えて、再度ロードに挑戦してみてください。

$ cd ../../../elkdat
$ vagrant reload
...
$ vagrant ssh
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# grub-reboot ktest
root@packer-qemu:/home/vagrant# exit
exit
vagrant@packer-qemu:~$ exit
...
$ vagrant reload
...
$ 

さて、成功した場合の続きに戻ります。プログラミングしたとおりにカーネルモジュールのロード時にメッセージが出力されたか確認します。

root@packer-qemu:/home/vagrant# dmesg | tail -3
[  314.198886] random: crng init done
[  516.935519] hello: loading out-of-tree module taints kernel.
[  516.936950] Hello world!
root@packer-qemu:/home/vagrant# 

ちゃんとメッセージが出たようです。成功です。

カーネルモジュールのライセンスなどの情報が正しく設定できているかについても確認しておきましょう。

root@packer-qemu:/home/vagrant# modinfo /vagrant/hello.ko
filename:       /vagrant/hello.ko
description:    Hello world kernel module
author:         Satoru Takeuchi <satoru.takeuchi@gmail.com>
license:        GPL v2
srcversion:     9A88917F1C1411370998811
depends:        
vermagic:       4.9.0-ktest+ SMP mod_unload modversions 
root@packer-qemu:/home/vagrant# 

すべて指定したとおりになっています。成功です。

最後にモジュールのアンロードをしてVMから抜けましょう。

root@packer-qemu:/home/vagrant# rmmod hello
root@packer-qemu:/home/vagrant# exit
exit
vagrant@packer-qemu:~$ exit
logout
...
$ 

ここまでで最も単純なカーネルモジュールの作成は終わりです。

演習問題

  • カーネルモジュールの説明文書を変更する
  • ロード時に出力するメッセージを変更する
  • アンロード時にもメッセージを出力するように変更する
  • 関数名を変更する
  • モジュール名を変更する

どれも「そんなのやらなくてもわかるよ」というくらい簡単に見えますが、意外とやってみると間違えるものです。

おわりに

本書で作成したソースと同じものをexample/module/hello以下に配置しています。みなさんが作成したモジュールがどうしてもうまく動かなければ、みなさんが作成したソースとこれを比較してバグの箇所を特定してください。

次回は、カーネル内において所定の時間後に所定の処理をさせたいときに使う、カーネルタイマーを紹介します。


  1. linux/modules/<uname -rによって得られるカーネルバージョン>/以下に存在する、拡張子が".ko"であるファイルがそうです。 

  2. 他のディストリビューションでうまく動いたというかたは必要パッケージを教えてくださると助かります。本記事に追記します。 

  3. 末尾についている"-ktest"は気にしないでください)。 

  4. 興味のあるかたはlinux/Documentation/kbuild以下のファイルを見て下さい。 

  5. カーネルパニックが発生した場合は勝手にVMからホストに戻ります。