【超入門】デバイスドライバ


概要

デバイスドライバについて調べた事のまとめ。
とりあえずinsmodしてカーネルメッセージを出力するまでを目標とする。

デバイスドライバとは

デバイスドライバとは、パソコンに接続されている周辺装置をカーネルが制御するためのプログラム。
デバイスドライバは普通のプログラムと違いカーネル空間で動作する。
デバイスドライバをビルドするためには、カーネルバージョンと同一バージョンの、
kernel-develとkernel-headersパッケージが必要。

環境

今回は下記の環境で検証してみる。

$ uname -r
4.18.0-13-generic

$ cat /etc/lsb-release | head -n2
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.10

$ gcc --version
gcc (Ubuntu 8.2.0-7ubuntu1) 8.2.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

書いてみる

以下が簡単なデバイスドライバ。
ただただhello worldを出力するだけのものだ。

#include <linux/init.h>
#include <linux/module.h>

static int m_init(void)
{
    printk(KERN_ALERT "Hello World\n");
    return 0;
}

static void m_exit(void)
{
    printk(KERN_ALERT "driver unloaded\n");
}

// モジュール開始と終了のマクロ
module_init(m_init);
module_exit(m_exit);
MODULE_LICENSE("GPL2");

上記のポイントはまずmain()関数が無いこと。
ユーザ空間でのプログラムはmain関数がエントリポイントといった決まりがあるが、
デバイスドライバではmodule_initに渡した関数がエントリとなる。

module_initとmodule_exitは kernel/include/linux/module.hで定義されている。

module.hを覗いてみると以下のようにさらに実装は別箇所
本題と逸れるためこれ以降は割愛。
initcall()とexitcall()はそれぞれ kernel/include/linux/init.hを追うと読めるようだ。

printk() は、カーネル内で使える printf()といった程度の理解。
※printkをするときに\nがないと、dmesgのログに出力されないらしい。\nでdmesg用のバッファに出力している模様

module.h
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 *
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)  __initcall(x);

/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 *
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)  __exitcall(x);
init.h
/*
 * initcalls are now grouped by functionality into separate
 * subsections. Ordering inside the subsections is determined
 * by link order. 
 * For backwards compatibility, initcall() puts the call in 
 * the device init subsection.
 *
 * The `id' arg to __define_initcall() is needed so that multiple initcalls
 * can point at the same handler without causing duplicate-symbol build errors.
 *
 * Initcalls are run by placing pointers in initcall sections that the
 * kernel iterates at runtime. The linker can do dead code / data elimination
 * and remove that completely, so the initcall sections have to be marked
 * as KEEP() in the linker script.
 */

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn;

/*
 * Early initcalls run before initializing SMP.
 *
 * Only for built-in code, not modules.
 */
#define early_initcall(fn)      __define_initcall(fn, early)

/*
 * A "pure" initcall has no dependencies on anything else, and purely
 * initializes variables that couldn't be statically initialized.
 *
 * This only exists for built-in code, not for modules.
 * Keep main.c:initcall_level_names[] in sync.
 */
#define pure_initcall(fn)       __define_initcall(fn, 0)

#define core_initcall(fn)       __define_initcall(fn, 1)
#define core_initcall_sync(fn)      __define_initcall(fn, 1s)
#define postcore_initcall(fn)       __define_initcall(fn, 2)
#define postcore_initcall_sync(fn)  __define_initcall(fn, 2s)
#define arch_initcall(fn)       __define_initcall(fn, 3)
#define arch_initcall_sync(fn)      __define_initcall(fn, 3s)
#define subsys_initcall(fn)     __define_initcall(fn, 4)
#define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)
#define fs_initcall(fn)         __define_initcall(fn, 5)
#define fs_initcall_sync(fn)        __define_initcall(fn, 5s)
#define rootfs_initcall(fn)     __define_initcall(fn, rootfs)
#define device_initcall(fn)     __define_initcall(fn, 6)
#define device_initcall_sync(fn)    __define_initcall(fn, 6s)
#define late_initcall(fn)       __define_initcall(fn, 7)
#define late_initcall_sync(fn)      __define_initcall(fn, 7s)

#define __initcall(fn) device_initcall(fn)

#define __exitcall(fn)                      \
    static exitcall_t __exitcall_##fn __exit_call = fn

とりあえず大事なのは下記2点

・「module_init」でエントリーポイントを指定する
・「module_exit」でアンロード時の処理関数を指定する

ビルド

今回はビルド用に下記Makefileを用意。
先ほど用意したソースと同じ階層にファイルを置きmakeを打つだけでビルドされる。

Makefile
obj-m := hello.o

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

ビルド手順は以下の通り
拡張子 *.koが存在しているのが分かる。これがカーネルモジュールだ。

$ make
make -C /lib/modules/4.18.0-13-generic/build M=/root modules
make[1]: ディレクトリ '/usr/src/linux-headers-4.18.0-13-generic' に入ります
Makefile:970: "Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel"
  CC [M]  /root/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/hello.mod.o
  LD [M]  /root/hello.ko
make[1]: ディレクトリ '/usr/src/linux-headers-4.18.0-13-generic' から出ま

$ ls -l
合計 28
-rw-r--r-- 1 root root  155 Jan 11 07:14 Makefile
-rw-r--r-- 1 root root    0 Jan 11 07:16 Module.symvers
-rw-r--r-- 1 root root  350 Jan 11 07:14 hello.c
-rw-r--r-- 1 root root 3784 Jan 11 07:16 hello.ko
-rw-r--r-- 1 root root  596 Jan 11 07:16 hello.mod.c
-rw-r--r-- 1 root root 2568 Jan 11 07:16 hello.mod.o
-rw-r--r-- 1 root root 2104 Jan 11 07:16 hello.o
-rw-r--r-- 1 root root   22 Jan 11 07:16 modules.order

カーネルモジュールを読みこむ基本コマンドは「insmod」で、引数として *.ko ファイルを指定する。
自動ロード(プラグ&プレイ)機能を使うならmodprobeコマンドを使用する必要がある。
https://tech.nikkeibp.co.jp/it/article/COLUMN/20130909/503342/

ここではisnmodを使用しカーネルメッセージを確認してみる。
insmodコマンドはroot権限で実行する必要がある。
起動時のカーネルメッセージの確認などに使われるをdmesgを用いて確認する。

$ sudo insmod hello.ko

# メッセージの確認
$ sudo dmesg
[    0.000000] Linux version 4.18.0-13-generic (buildd@lgw01-amd64-048) (gcc version 8.2.0 (Ubuntu 8.2.0-7ubuntu1)) #14-Ubuntu SMP Wed Dec 5 09:04:24 UTC 2018 (Ubuntu 4.18.0-13.14-generic 4.18.17)
(中略)
[ 1572.805848] Hello World

# モジュールの確認
$ sudo lsmod | grep -E "hello|Mod"
Module                  Size  Used by
hello                  16384  0

確認の最後で使っているlsmodは以下の内容を確認できる。
主に見たいのは依存関係だが今回は特に依存なしである。当然といえば当然。

Module ドライバ名
Size メモリ上のドライバサイズ
Used 参照カウンタ
by 依存ドライバ

ちなみにドライバのアンロードはrmmodコマンドで行う。

$ sudo rmmod hello

$ dmesg | tail -n 3
[ 1572.805215] Disabling lock debugging due to kernel taint
[ 1572.805848] Hello World
[ 2577.303835] driver unloaded

感想

とりあえずドライバをちょっと触ってみる、といったことはできた気がするのでとりあえず満足。
実際ここを縄張りとして開発している方々は本当にすごいと思う。
ドライバの必要性だったりドライバの歴史だったりが知れたので良かったのかもしれない。

参考リンク

http://linux-dvr.biz/
http://public2016.hatenablog.com/entry/2016/09/01/231611
https://qiita.com/rarul/items/308d4eef138b511aa233