Linux Kernel Hack入門編

この記事はLinux Advent Calendar 2014の25日目ですヽ(=´▽`=)ノ

今回はLinux Kernel Hack入門編ということで入門的なことを書いてみたいと思います。
まず使用する環境ですけど最近出たばっかのFedora 21のWorkstationにしました。まあ、今回の内容的にはディストリビューションは問わないんですが、多くの人が馴染んでいるであろうfedora系というかパッケージマネージャがyumということでこれにしてみました。 自分は普段Arch Linuxなんですけど、こっちだとkernelのmake install時にちょっとしたスクリプトを書く必要があったりするのもあって、fedoraのほうが手軽かなというのもあります。

カーネルコード・リーディング

ブラウザベースでコード・リーディング

読めるソースは大概メインラインのカーネルということになりますが、通常はこれで事足りると思います。自分で何かを設定しなくても良いのでお手軽です。ブラウザで見る場合、free-electrons.comさんのlxrが便利です。

f:id:masami256:20141219222006p:plain

あと、忘れちゃいけないのがgithubですね。githubの場合、タグジャンプはできませんがblameはwebインターフェースでできるので

f:id:masami256:20141219222024p:plain

実装がどうなっているのかを見て行きたい場合はlxr、なんでこういう処理なのか?とか変数の由来等々を知りたい場合はblameで歴史を遡るのが良いかと思います。

エディタでコード・リーディング

エディタはお好みのものを使ってください。エディタで見る場合はctagsなどのツールが便利ですよね。Linuxの場合、makeのターゲットにタグを作るターゲットがあります。make helpの出力の抜粋ですが以下の3個が選べます。

  tags/TAGS       - Generate tags file for editors
  cscope          - Generate cscope index
  gtags           - Generate GNU GLOBAL index

自分でタグを作る利点はネットワークunreachableな環境でも使える、任意のバージョンのソースに対してタグをつけれるので例えばtipツリーとかメインライン以外のコード・リーディングもしやすくなるというのがありますね。 ソースをgitでcloneしていれば当然blameとかも使い放題です。

システムコールの探しかた

システムコールは基本的にはsys_foobarという感じでsys_が付くのですが、ソースコード上はこうなってません。大概はSYSCALL_DEFINE[0-6]というマクロが使われています。最後の数字は引数の数です。
例えばgetpid()はこうなります。

1084 SYSCALL_DEFINE0(getpid)
1085 {
1086         return task_tgid_vnr(current);
1087 }

なのでlxrでもgrepでも良いのですが、システムコールを探す場合はSYSCALL_DEFINEを使った形、例えばgetpid()なら、SYSCALL_DEFINE0(getpitで探すと見つけやすいです。引数の数はユーザーランド側での引数の数と一緒なのでmanを見ればわかります。あとはsocket関連のシステムコールはsocketcall(2)、ipc関連はipc(2)という風に別のシステムコールにまとめられる場合があるのでその辺りもお忘れなく。

カーネルの機能を知る

だいたい下記のどちらかだと思います。

  1. 特定の機能について知りたい場合
  2. 全般的に知りたい場合

前者の場合は目的が明確なのでそれらしい単語をググる等で読むべきコードが大体分かるんじゃないかと。

どこから手を付けたら良いのかわからないというのは後者の場合ですね。とりあえずお勧めしないのはbootから読んでいくパターンです。なんでかというと、boot処理というのは色々なものを初期化していくのでそこでやっていることを知ろうと思ったら、それらについてある程度は知らないと何をやってるのか理解しにくいからですね。ある機能があって、それが初期設定をどうやっているかを調べるのにbootプロセスを見るのは良いんですが、全体を知るには向いていないと思います。
他には、初期化と初期化後のデータを使うのは別のタイミングなのでbootだけみてもその機能はよくわからないというか、使っているところこそが大事ですよね、ってのもあります。これはカーネルじゃなくても一緒だと思います。

で、では全般的に知りたい場合はどうするかってなるとソースを読むよりも本を読んだほうが良いと思います。日本語で読めて全般的に解説しているとなると以下の2冊ですね。

詳解LINUXカーネル

詳解 Linuxカーネル 第3版

Linuxカーネル2.6解読室

Linuxカーネル2.6解読室

これらの本は全般的に解説しているのでLinuxカーネルが持っているオペレーティングシステムとしての基本的な機能については一通り学べると思います。ただ、発売から結構経っているため今のカーネルとあわない部分というのも当然ありますが。。それでも基本を知るという意味では良いかと思います。

webでの情報源としてはLinux Weekly Netですね。これは最新の状況を知るのに持って来いだと思います。

カーネルビルド

パッケージインストール

Fedora 21のWorkstationを入れるとgitなんかはすでに入っていたりしますが、gccなんかは入っていないのでまずは必要なパッケージをインストールします。この辺を入れておけば追加で依存パッケージも入ります。

[masami@fedora ~]$ sudo yum install ncurses-devel gcc patch ccache

gccとpatchは説明不要だと思いますがncurses-develはカーネルの設定をするときにmake menuconfigで設定したいのでncurses-develを入れます。他のディストリビューションの場合はncursesのヘッダファイルとかがあるパッケージを入れてください。ccacheはビルドしたobjectファイルをキャッシュしておいて次回移行のビルドを速くしたいというところです。

kernel sourceのダウンロード

カーネルソースコードgitでcloneするtar.xzで一式ダウンロードfedoraのsrc.rpmパッケージを使うという手がありますが、gitでcloneするのが一番手軽じゃないでしょうか。ということで、cloneします。

[masami@fedora ~]$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git linux-kernel

cloneが終わったらブランチ切ってそこでビルドしましょう。カーネルのバージョンは3.18を使ってみたいと思います。

[masami@fedora linux-kernel]$ git checkout -b  mykernel v3.18
カーネルのコンフィグ

次にカーネルの設定をしたいと思います。ここでどんな機能を有効にするか、設定値をどうするかってことをしていきますが、今回は今読み込まれているモジュールをベースとしたconfigをしたいと思います。
この場合のターゲットはlocalmodconfigです。

[masami@fedora linux-kernel]$ make localmodconfig

この後色々と機能をどうするか聞かれてきますが全部Enterキー押下で飛ばしてしまいましょう。 これで最小限の設定になっていると思いますが、さらに微調整をしたいので次はmenuconfigを行います。menuconfigの場合、機能menu項目の移動は上下、select、exitなどの下にあるメニューは左右キーで移動できます。決定はEnterキーです。

まずは、General setup -> Local Versionを選択します。

f:id:masami256:20141219225719p:plain

そうしたら適当な名前をつけましょう。OKを押してメニューに戻るとこうなるはずです。

f:id:masami256:20141219225748p:plain

そしたらExitを押してGeneral setupを抜け、もう一度Exitを押します。そうするとsaveするか聞かれるのでyesを選択して完了です。

makeのターゲットについてはLinux Advent Calendar 2014の7日目、satoru_takeuchiさんの「linux kernelのmakeターゲットについてのあれこれ」が参考になります。

make

configが終わったらビルドです。makeのターゲットはbzImageです。makeは並列に行いたいので-Jオプションをつけましょう。 使用可能なcore数が4なので-jに4を渡しています。ccacheを忘れずに。

[masami@fedora linux-kernel]$ nproc
4
[masami@fedora linux-kernel]$ ccache make -j4 bzImage

最後にこんな出力が出てれば無事にmake完了です。

  OBJCOPY arch/x86/boot/setup.bin
  BUILD   arch/x86/boot/bzImage
Setup is 16124 bytes (padded to 16384 bytes).
System is 5545 kB
CRC 318250a6
Kernel: arch/x86/boot/bzImage is ready  (#1)

bzImageが出来たら次はmoduleをビルドします。

[masami@fedora linux-kernel]$ ccache make -j4 modules

これも何事もなくシェルのプロンプトに戻っていれば完了です。

install

次にカーネルモジュールを/lib/にインストールします。これはroot権限が必要です。

[masami@fedora linux-kernel]$ sudo make modules_install

この後に、 /lib/modulesに今作った3.18mykenelがありますね。ここまできて思い出したんですが、Local Versionを設定するときに先頭の文字は"-"にしといたほうが綺麗に見えると思います。3.18.0-mykernelのほうが見やすいと思うので。

[masami@fedora linux-kernel]$ ls /lib/modules
3.17.4-301.fc21.x86_64  3.18.0mykernel

次に installターゲットで/bootにカーネルの配置、initramfsの作成をします。

[masami@fedora linux-kernel]$ sudo make install

ちなみに、installの実行時に/sbin/installkernelというコマンドが呼ばれ、これが/bootにファイルを置いたりするのですがディストリビューションによってはこのコマンドありません、例えばArch Linux。このような場合、自分で/sbin/installkernelを作ったほうが早いです。この辺の説明は以前のエントリ「φ(.. )メモシテオコウ /sbin/installkernelを適当に作っておく - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ」を参考にしてください。

make installも何事もなくシェルのプロンプトに戻っていれば完了です。

最後にgrubのconfigを更新して再起動します。

[masami@fedora linux-kernel]$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg

grubのメニューに作ったカーネルが入っています。

f:id:masami256:20141219233522p:plain

ログインしたらカーネルのバージョンを確認していましょう。

[masami@fedora ~]$ uname -a
Linux fedora 3.18.0mykernel #1 SMP Fri Dec 19 23:19:01 JST 2014 x86_64 x86_64 x86_64 GNU/Linux

新しいカーネルで起動していますね。これでカーネルのビルドは完了です。

カーネルモジュール

hello world位作るのがお約束な気がしたので・・・

module_init()でモジュールロード時のinit処理、module_exit()でモジュールアンロード時のクリーンアップ処理を登録します。先頭のKBUILD_MODNAMEはprintk()で自分のモジュール名を出したいときに使う技です。pr_infoはprintk(KERN_INFOシンタックスシュガーです。以下のコードをhelloworld.cとして保存します。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_DESCRIPTION("Hello, World");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

static int helloworld_init(void)
{
        pr_info("%s\n", __func__);
        return 0;
}

static void helloworld_cleanup(void)
{
        pr_info("%s\n", __func__);
}

module_init(helloworld_init);
module_exit(helloworld_cleanup);

Makefileはこちらです。

KERNDIR := /lib/modules/`uname -r`/build
BUILD_DIR := $(shell pwd)
VERBOSE = 0

obj-m := helloworld.o
smallmod-objs := helloworld.o

all:
        make -C $(KERNDIR) SUBDIRS=$(BUILD_DIR) KBUILD_VERBOSE=$(VERBOSE) modules

clean:
        rm -f *.o
        rm -f *.ko
        rm -f *.mod.c
        rm -f *~

これでmakeして作成されたhelloworld.koをinsmod、rmmodしてからjournalctl -r -bでカーネルのログを確認すると以下のようにログが出ているのが確認できます。

-- Logs begin at Fri 2014-12-19 21:36:25 JST, end at Fri 2014-12-19 23:43:47 JST. --
Dec 19 23:43:47 fedora kernel: helloworld: helloworld_cleanup
Dec 19 23:43:47 fedora sudo[2471]: masami : TTY=pts/0 ; PWD=/home/masami/helloworld ; USER=root ; COMMAND=/sbin/rmmod helloworld
Dec 19 23:43:23 fedora kernel: helloworld: helloworld_init
Dec 19 23:43:23 fedora kernel: helloworld: module verification failed: signature and/or  required key missing - tainting kernel

カーネルデバッグ

では、最後にデバッグを。まず最初にカーネルのconfigを変えます。make menuconfigでKernel hacking -> Memory debuggingを選択し、Kernel memory leak detectorを有効に、下のMaximum〜を4096にしたら保存してカーネル・モジュールをビルドしてインストールします。

f:id:masami256:20141219235208p:plain

再起動してdmesgでログを確認すると先ほど有効にしたkmemleakが初期化して開始したのがわかります。

[masami@fedora ~]$ dmesg | grep kmemleak
[    7.004780] kmemleak: Kernel memory leak detector initialized
[    7.004790] kmemleak: Automatic memory scanning thread started

次に、先ほど作成したhelloworld.cにバグを埋め込みます。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>

MODULE_DESCRIPTION("Hello, World");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

static char *ptr;

#define LEAK_DETECT_TEST() do { \
        ptr = kmalloc(4, GFP_KERNEL); \
        pr_info("ptr is 0x%p\n", ptr); \
} while(0)

static int helloworld_init(void)
{
        pr_info("%s\n", __func__);

        LEAK_DETECT_TEST();
        LEAK_DETECT_TEST();

        return 0;
}

static void helloworld_cleanup(void)
{
        pr_info("%s\n", __func__);
        kfree(ptr);
}

module_init(helloworld_init);
module_exit(helloworld_cleanup);

makeしてinsmod・rmmodするとこうのようにログがでます。

[  340.021392] helloworld: helloworld_init
[  340.021399] helloworld: ptr is 0xffff8802286da788
[  340.021403] helloworld: ptr is 0xffff8802286da790
[  349.762415] helloworld: helloworld_cleanup

kmemleakで即時scanで確認してみます。

[root@fedora helloworld]# echo scan > /sys/kernel/debug/kmemleak

そして、ログを見るとこのようにメモリーリークが検出されます。もし一回やってログが出なかった再度試してみてください。

[  479.691172] kmemleak: 1 new suspected memory leaks (see /sys/kernel/debug/kmemleak)

指示にしたがって/sys/kernel/debug/kmemleakを見てみます。memory leakしたであろうオブジェクトのアドレスは0xffff8802286da788と出ています。サイズが8なのはkmallocの最低サイズが切り上げられているためです。変数は初期化していないのでhex dumpの中身は今回は無意味ですね。commはプログラム名、pidはcommのpidです。メモリーリークの場合、メモリ確保のタイミングとリークのタイミングが違うのでバックとレースだけだと結構追うの大変ですがヒントとしてはでかいので重要です。

[root@fedora helloworld]# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff8802286da788 (size 8):
  comm "insmod", pid 1934, jiffies 4295007317 (age 212.238s)
  hex dump (first 8 bytes):
    90 a7 6d 28 02 88 ff ff                          ..m(....
  backtrace:
    [<ffffffff8173863e>] kmemleak_alloc+0x4e/0xc0
    [<ffffffff811f042c>] kmem_cache_alloc_trace+0x13c/0x250
    [<ffffffffa033d034>] 0xffffffffa033d034
    [<ffffffff81002148>] do_one_initcall+0xd8/0x210
    [<ffffffff81119b1d>] load_module+0x209d/0x27c0
    [<ffffffff8111a406>] SyS_finit_module+0xa6/0xe0
    [<ffffffff81746d69>] system_call_fastpath+0x12/0x17
    [<ffffffffffffffff>] 0xffffffffffffffff

とまあ、こんな感じでサクッとまとめてみました。入門の取っ掛かりになれば幸いですm(__)m

宣伝ですw 月一回ペースで秋葉原でテーマをLinuxカーネルに絞ったもくもく会をやってますので、興味のある方はどうぞ。

Linuxカーネルもくもく会 - connpass

最後に。これでLinux Advent Calendar 2014も終わりになります。
記事を投稿してくれたknokさん、satoru_takeuchiさん、mzyy94さん、hiraku_wfsさん、furandon_pigさん、tenforwardさん、xoxyuxuさん、akachochinさん、yunon_physさん、shigemk2さん, そして読んでくれた皆さん、ありがとうございましたm(__)m