お手軽Linuxカーネル開発/自動テスト

はじめに

これはLinux Advent Calendar 2016 14日目の記事です。

本記事では、ワンコマンドでお手軽にlinuxカーネル開発/自動テスト環境を構築する方法を紹介します。その後開発、自動テストの流れについてもチュートリアル形式で紹介します。本記事ではelkdat(以下、本ツール)というツールを使用します。本記事の対象読者はバリバリのカーネル開発者だけでなく、自分のカーネルカーネルモジュールを一度作ってみたかった人、開発には興味がないけれどカーネルのテスト(たとえばカーネルのバグの原因となったコミットを突き止めたい)には興味のある人も含みます。

本記事に記載されているコードを実際に試すためには、仮想化機能を持つCPUを搭載したPCにインストールされたUbuntu16.04が必要です。CPUの仮想化機能を持っているどうかは、shellから以下のプログラムを実行すればわかります。1以上の値を出力すればCPUは仮想化機能を持っています。

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

それに加えてBIOSにおいて仮想化機能が有効になっている必要があります。下記コマンドが成功すればOKです。

IntelのCPU

$ sudo modprobe kvm-intel

AMDのCPU

$ sudo modprobe kvm-amd

失敗する場合はBIOSの設定を見直してください。

本記事に書かれているソースが対応しているのはlinux v4.14までです。

linuxに限らず、カーネルの開発/テストは通常のソフトウェアのそれに比べて遥かに面倒です。理由は次のようなものです。

  • 設定、ビルド、インストール、ビルドしたカーネルを使ってのブートに、独特のお作法を覚える必要がある
  • ビルド時間が長い。とくにdistroのデフォルト設定の場合、数時間を要することもある
  • テスト前後にシステムのリブートが必要
  • バグによる被害が大きい。ブートしない、ブートしてもシステム全体が停止する、データが破壊される、など
  • VMなどの別マシンでテストしようとしても、そのための開発環境を作るのが面倒

本ツールはこれらの問題を解決しワンコマンドで

  1. linuxカーネルのソースを入手(必要なら)
  2. テスト用VMを作成
  3. テスト用の各種設定

などの開発/テスト環境の準備を全て済ませます。環境設定後のビルド、インストール、各種テストなどの処理もワンコマンドでできます。

環境

筆者は Ubuntu 16.04/x86_64 を使用しました。他のdistroでは未検証です。他のdistroをお使いのかたは、必要に応じて適宜読み替えて下さい。

本ツールの使用前に、以下のように必要なパッケージをインストールしておく必要があります。

$ sudo apt-get install git vagrant libvirt-bin libvirt-dev kernel-package qemu-kvm libssl-dev libncurses5-dev
$ sudo usermod -aG libvirtd <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

初期化

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

たったこれでカーネルソース、テスト用VMイメージのダウンロード、各種設定などの初期化がすべて完了し、開発/テストの準備が整います。./initの実行中にVMイメージやlinuxカーネルのgitリポジトリをダウンロードしますので、回線速度が遅ければ長時間を要します。

既にカーネルソースをお持ちの方は、./initの実行前にソースをelkdatのソースディレクトリの直下にlinuxという名前で配置してください(シンボリックリンクを張ってもよいです)。本ツールを使用する際には、カーネルソースに対してmake mrproperを実行して既存のビルド済カーネルや.configを削除してしまうのでご注意下さい。

開発環境が不要になったら

以下のコマンドを実行してください。

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

チュートリアル

自分でビルドしたカーネルを動かす(ソース改変なし)

本記事執筆時点でリリースされたばかりのlinux v4.9を動かしてみましょう。次のコマンドでビルド、インストール、それを使ってのブートまですべてを実行できます。

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

    1 of 1 tests were successful

$ 

成功したようです。ではVMにログインして本当に成功しているか確かめてみましょう。

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

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

最後に元のカーネルでブートしなおしましょう。

vagrant@packer-qemu:~$ exit
$ vagrant halt
...
$ vagrant reload
...
$ cd ../
$ 

これでおしまいです。再度自前カーネルで起動したい場合は次のようにします。

$ cd elkdat
$ vagrant ssh -c sudo grub-reboot ktest
...
$ vagrant reload 
...
$ 

上記 git checkout の v4.9 の箇所をお好きなtagやcommit IDに変更することによって、好きなカーネルを使えます。

自分でビルドしたカーネルを動かす(ソース改変あり)

何の変更もしていないものを動かすだけではあまり面白くないので、自分でソースを改変したカーネルを動かしてみましょう。ここでは例として用意したパッチをlinux v4.9に適用したカーネルを作ってみます。このパッチは、起動時に簡単なメッセージをカーネルのログに出力するだけのものです。

まずパッチの中身を見てみましょう。

$ cat example/kernel-patch/first/0001-Print-a-message-on-boot.patch 
From 93cc6bf35ed2850634cb1bcfe621b38d81c6ab25 Mon Sep 17 00:00:00 2001
From: Satoru Takeuchi <satoru.takeuchi@gmail.com>
Date: Wed, 14 Dec 2016 20:42:17 +0900
Subject: [PATCH] Print a message on boot

Signed-off-by: Satoru Takeuchi <satoru.takeuchi@gmail.com>
---
 init/main.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/init/main.c b/init/main.c
index 2858be7..9736dac 100644
--- a/init/main.c
+++ b/init/main.c
@@ -657,6 +657,7 @@ asmlinkage __visible void __init start_kernel(void)
        }

        ftrace_init();
+       printk("my patch is applied!\n");

        /* Do the rest non-__init'ed, we're now alive */
        rest_init();
--
2.10.2

カーネルを作って起動しましょう。

$ cd linux
$ git checkout -b test v4.9
Switched to a new branch 'test'
$ git am ../example/kernel-patch/first/0001-Print-a-message-on-boot.patch 
Applying: Print a message on boot
$ cd ../
$ ./test boot
...
*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

成功したようです。起動したカーネルバージョンを確認しておきましょう。

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

ちゃんと自前カーネルが動いているようです。

では、カーネルのログに期待したメッセージは出ているか、確認してみましょう。

vagrant@packer-qemu:~$ dmesg | grep "my patch"
[    0.167288] my patch is applied!
$ 

期待したとおり、カーネル起動時に "my patch is applied!" というメッセージが出力されました。

このパッチを起点に、メッセージを変更したり、メッセージを挿入する場所を変えたり(そして挿入する場所を間違えてブートしなくなったり…)、別の関数を使ってみたり、と、いろんなことに挑戦してみてください。どうせ環境を復旧するのは簡単です。

この後、またカーネルをもとに戻しておいて下さい。

カーネルの設定を変更

カーネルの設定を変更するには(たとえばデフォルトではビルドされないファイルシステムを有効にするなど)、./testを実行する前に次のコマンドを実行してください。

$ cp ktest/minconfig{,.bak}
$ cp ktest/minconfig linux/.config
$ cd linux
$ make menuconfig
...                                      # ここで設定を好きに変更
$ mv .config ../ktest/minconfig
$ make mrproper
$ cd ../

変更した設定を使ってビルドしたカーネルがブートしない場合は、ktest/minconfig{.bak,}によって設定ファイルを正しく動作するものに復元してください。カーネルビルドに慣れていない人がカーネルの設定を変更する際のコツは、一気にたくさん変更しないことです。

カーネルの設定についての詳細は後述の参考資料「linux kernelのmakeターゲットについてのあれこれ」をごらんくださ

さらに高度なことをする

上記の独自カーネルのビルド、インストール、ブート以外にも、elkdatにはいくつかのさらに高度な機能があります。

ユーザ定義テストを流す

次のコマンドを実行してください。

$ ./test test <the path of your own test>

以下は独自カーネルをブート後にexample/test/helloというテストを流した例です。

$ ./test test example/test/hello
...
** Monitor flushed **
run test /home/sat/src/elkdat/example/test/hello
/home/sat/src/elkdat/example/test/hello ... [0 seconds] SUCCESS
kill child process 18446
closing!

Build time:   6 minutes 53 seconds
Install time: 8 seconds
Reboot time:  17 seconds
Test time:    1 second



*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

example/test/hello's のログはktest/ktest.logにあります。

以下に、テストを必ず失敗するexample/test/failに置き換えた結果も載せておきます。

$ ./test test example/test/fail
...
** Monitor flushed **
run test /home/sat/src/elkdat/example/test/fail
/home/sat/src/elkdat/example/test/fail ... [0 seconds] FAILED!
CRITICAL FAILURE... test failed
REBOOTING
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 sync ... [1 second] SUCCESS
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 reboot; ... Connection to 192.168.121.181 closed by remote host.
[0 seconds] SUCCESS
 See /home/sat/src/elkdat/ktest/ktest.log for more info.
test failed
$ 

パッチセットのテスト

みなさんがカーネル開発者ならば、機能拡張やバグ修正をする際にパッチセットをLKMLなどのカーネル開発MLに投稿するでしょう。(当然ながら)パッチセットは投稿前にテストする必要があります。このとき、パッチセット内の全パッチを適用したカーネルをテストするだけでは不十分です。パッチセット内の個々のパッチを全てテストする必要があります。これは、もしいずれかのパッチにバグがあった場合、その後のgit bisectが動作しなくなるからです(二分探索できない)。

elkdatは指定したパッチセット内の全パッチを自動テストできます。

4つのパッチから成るパッチセットのうち3つ目のパッチだけに、ブートしないというバグがある場合の例を示します。パッチセットはここにあります

$ git log --oneline -5 
f80a34f377c1 4/4: fine again
227ef171c7f5 3/4: BUG
d662eff22070 2/4: fine
925417fc1d36 1/4: fine
69973b830859 Linux 4.9
$ 

パッチセットをテストするには次のコマンドを実行します。

$ ./test patchcheck 925417fc1d36 f80a34f377c1
...
Going to test the following commits:
925417fc1d3670f994c26bb09369b5f6c02c60bb 1/4: fine
d662eff220707c43c7bce87cf0343e27e67ce848 2/4: fine
227ef171c7f59c570fb821a81581ef78eed5be89 3/4: BUG
f80a34f377c1832d450dc0cc402288ee86ae2836 4/4: fine again

Processing commit "925417fc1d3670f994c26bb09369b5f6c02c60bb 1/4: fine"
...
Build time:   6 minutes 58 seconds
Install time: 8 seconds
Reboot time:  21 seconds

Processing commit "d662eff220707c43c7bce87cf0343e27e67ce848 2/4: fine"
...
** Monitor flushed **
kill child process 30367
closing!

Build time:   1 minute 15 seconds
Install time: 9 seconds
Reboot time:  19 seconds

Processing commit "227ef171c7f59c570fb821a81581ef78eed5be89 3/4: BUG"

[    0.135879] ftrace: allocating 32412 entries in 127 pages
[    0.163806] 1/4 patch is applied!
[    0.164408] 2/4 patch is applied!
[    0.164933] 3/4 patch is applied!
[    0.165469] ------------[ cut here ]------------
[    0.166151] kernel BUG at /home/sat/src/elkdat/linux/init/main.c:663!
[    0.167041] invalid opcode: 0000 [#1] SMP
[    0.167647] Modules linked in:
[    0.168216] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 4.9.0-ktest+ #3
[    0.169076] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.9.3-20161025_171302-gandalf 04/01/2014
[    0.170451] task: ffffffff8ee0e540 task.stack: ffffffff8ee00000
[    0.171254] RIP: 0010:[<ffffffff8ef81fd9>]  [<ffffffff8ef81fd9>] start_kernel+0x460/0x462
[    0.172501] RSP: 0000:ffffffff8ee03f50  EFLAGS: 00010282
[    0.173241] RAX: 0000000000000015 RBX: ffffffffffffffff RCX: ffffffff8ee54108
[    0.174175] RDX: 0000000000000000 RSI: 0000000000000246 RDI: 0000000000000246
[    0.175109] RBP: ffffffff8ee03f80 R08: 0000000000000000 R09: 0000000000000000
[    0.176043] R10: ffff9b179ffd7000 R11: 0000000000000098 R12: ffff9b179ffd06c0
[    0.176987] R13: ffffffff8f030840 R14: ffffffff8f03d2e0 R15: 000000000008a000
[    0.177924] FS:  0000000000000000(0000) GS:ffff9b179fc00000(0000) knlGS:0000000000000000
[    0.179070] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    0.179853] CR2: 00000000ffffffff CR3: 0000000013e07000 CR4: 00000000000406f0
[    0.180831] Stack:
[    0.181216]  ffffffff8f03d2e0 0000000000000000 000000000000008e 0000ffffffff8ef8
[    0.182537]  0000000000000020 ffffffff8ef81120 ffffffff8ee03f90 ffffffff8ef812ca
[    0.183854]  ffffffff8ee03fe8 ffffffff8ef81419 00000000ffffffff 8ef88e0000101117
[    0.185206] Call Trace:
[    0.185639]  [<ffffffff8ef81120>] ? early_idt_handler_array+0x120/0x120
[    0.186521]  [<ffffffff8ef812ca>] x86_64_start_reservations+0x24/0x26
[    0.187383]  [<ffffffff8ef81419>] x86_64_start_kernel+0x14d/0x170
[    0.188228] Code: 02 00 e8 b7 b1 02 00 48 c7 c7 5d 13 c5 8e e8 e6 a2 21 ff 48 c7 c7 76 13 c5 8e e8 da a2 21 ff 48 c7 c7 8f 13 c5 8e e8 ce a2 21 ff <0f> 0b 31 c0 80 3f 00 55 48 89 e5 75 0f c7 05 4c 80 17 00 01 00 
[    0.194669] RIP  [<ffffffff8ef81fd9>] start_kernel+0x460/0x462
[    0.195523]  RSP <ffffffff8ee03f50>
[    0.196097] ---[ end trace f68728a0d3053b52 ]---
[    0.196776] Kernel panic - not syncing: Attempted to kill the idle task!
[    0.197707] ---[ end Kernel panic - not syncing: Attempted to kill the idle task!
bug timed out after 1 seconds
Test forced to stop after 60 seconds after failure
CRITICAL FAILURE... failed - got a bug report
REBOOTING
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 sync ... [18 seconds] FAILED!
ssh -i /home/sat/src/elkdat/private_key root@192.168.121.181 reboot; ... ssh: connect to host 192.168.121.181 port 22: No route to host
[3 seconds] SUCCESS
 See /home/sat/src/elkdat/ktest/ktest.log for more info.
failed - got a bug report
$ 

3つめのパッチにバグがあることを正しく検出できました。

Find which commit introduce a bug by bysect

もし独自カーネルにバグが見つかり、それより古いとあるバージョンでは当該バグが存在しない場合、バグを仕込んだコミットを見つけるためにtest bisectコマンドが使えます。このコマンドはgit bisect1のように働きます。

git bisectを直接カーネル開発に使うのは難しいです。理由は、ユーザプログラムとは異なり、一回のテストごとにシステムをリブートする必要があるからです。

ここでは

  • v4.9に対して10個のパッチを作成した
  • 上記すべてを適用したら起動時パニック障害が発生した
  • 1つ目のパッチにバグが無いことは明らか

という場合を考えます。パッチセットはこちら

実際には次のように6番目のパッチでバグが仕込まれました。

$ git log --oneline -11
e617cb9e8cc0 10/10: BUG
d5159dda90f5 9/10: BUG
ddd7cdeacf47 8/10: BUG
9f6c5fbcd327 7/10: BUG
966f935e572c 6/10: BUG
f4504cce28bc 5/10: fine
cacbea15ec6a 4/10: fine
ee916bd4a2a8 3/10: fine
b61a82b33071 2/10: fine
5b762eff2275 1/10: fine
69973b830859 Linux 4.9

バグを仕込んだコミット、つまり966f935e572cを見つけるために、以下のコマンドを実行します。

$ ./test bisect 5b762eff2275 e617cb9e8cc0 boot
...
RUNNING TEST 1 of 1 with option bisect boot

git rev-list --max-count=1 5b762eff2275 ... SUCCESS
git rev-list --max-count=1 e617cb9e8cc0 ... SUCCESS
git bisect start ... [0 seconds] SUCCESS
git bisect good 5b762eff2275a414938275c00ccae7d2847f10b4 ... [0 seconds] SUCCESS
git bisect bad e617cb9e8cc0b49a507bc2fd2840fb803da00436 ... SUCCESS
Bisecting: 4 revisions left to test after this (roughly 2 steps) [f4504cce28bcb56b15df0c936e1598cb733f1658]
...
git bisect good ... SUCCESS
Bisecting: 2 revisions left to test after this (roughly 1 step) [9f6c5fbcd3276216291f60f41504bab6003c95e6]
...
git bisect bad ... SUCCESS
Bisecting: 0 revisions left to test after this (roughly 0 steps) [966f935e572c728f17877ab8a8fac454e04deda6]
...
...
git bisect bad ... SUCCESS
Found bad commit... 966f935e572c728f17877ab8a8fac454e04deda6
...
Bad commit was [966f935e572c728f17877ab8a8fac454e04deda6]



*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!!         **
*******************************************
*******************************************

    1 of 1 tests were successful

$ 

バグを仕込んだ修正は6/10であると、正しく見つけられました。

おわりに

この記事、このツールをきっかけに、カーネルに親しみを持ってくれるかたが増えることを願っています。

参考資料

linux kernel auto test by using ktest ... ktestのチュートリアル linux kernelのmakeターゲットについてのあれこれ ... カーネルの設定についての情報を記載しています カーネルモジュール作成によるlinuxカーネル開発入門 - 第一回 hello world


  1. このコマンドについてご存知ではないかたはman 1 git-bisectをごらんください。