長い人生において誰もが一度は遭遇するであろう経験のひとつが
Ubuntuのカーネルについて
Ubuntuのカーネルは、アップストリームであるLinuxカーネルの特定のバージョンに対して、Ubuntu独自のパッチを加えて構築されています。その仕組みにはUbuntu固有の手順も多く、本連載ではこれまでにもさまざまな方法でカーネルをビルド・
- 第278回
「Ubuntuカーネルとの付き合い方」 - Ubuntuカーネルの基本的な使い方。
- 第333回
「カーネルパッケージをビルドしよう」 - Ubuntuカーネルのgitリポジトリもしくはカーネルソースパッケージを利用した、Ubuntuのカーネルパッケージのビルド方法。
- 第524回
「Hades Canyon/ Kaby Lake GのdGPUを有効化する 」 - Ubuntuのメインラインビルドを利用したビルド済み最新カーネルのインストール方法。
- 第526回
「Ubuntuで最新のカーネルをお手軽にビルドする方法」 - バニラカーネルからDebianパッケージをビルドする方法。
- 第578回
「メインラインカーネルにパッチをあてる」 - Ubuntuパッチとセットでアップストリームのカーネルをデイリーでビルドしている、メインラインカーネルをカスタマイズする方法。
しかしながら、たとえば新しいハードウェアの対応等を目的として
- セキュアブートを有効化した環境では、モジュールの署名が必要
- カーネルのバージョンが更新された場合、再ビルドが必要になることがある
前者については、未署名のモジュールをロードしようとすると次のようなエラーになります。
モジュールのロード時にエラーになる $ sudo insmod hello.ko insmod: ERROR: could not insert module hello.ko: Operation not permitted カーネル側にもエラーログが残る [22505.175003] Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
これに対応する方法は
さらにやっかいなのがCONFIG_
を有効化しているため、ABI[2]が変わらない程度の更新であれば、同じバイナリをそのままロードできます。しかしながらABIが変わると再ビルドが必要になってくるのです。
これらの懸念事項を解決できる仕組みが
前述の第782回のOFEDドライバーだけでなく、VirtualBoxやNVIDAなどの何らかの理由でカーネルにソースコードを取り込めないもの・
今回は、まずシンプルなサードパーティのカーネルドライバーを作成した上で、そのドライバーをDKMSに対応させてみましょう。
ゼロからカーネルドライバーを作ろう
実はカーネルドライバーを作ること自体はそこまで難しくありません。とりあえずC言語を使える環境とカーネルのビルドシステムとヘッダーファイルがあればなんとかなります。余計な依存関係もなく、言語固有のパッケージ管理システムに悩む必要もありませんし、コード自体もシンプルなC言語で済みますので、ただただコーディングに専念すれば良いことになります[3]。
最低限用意しなければいけないのは、Makefileとソースコードの2ファイルです。まずはソースコードは次のように記述します。これをhello.
」
// SPDX-License-Identifier: GPL-2.0-only
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void)
{
pr_info("%s: Hello world!\n", __func__);
return 0;
}
static void __exit hello_exit(void)
{
pr_info("%s: exit hello module\n", __func__);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_DESCRIPTION("hello");
MODULE_AUTHOR("Mitsuya Shibata");
MODULE_LICENSE("GPL v2");
内容自体は定番のコードで、カーネルモジュールをロードしたらカーネルのログバッファーにHello world!
」exit hello module
」
次に
# SPDX-License-Identifier: CC0-1.0
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KVER ?= $(shell uname -r)
KDIR ?= /usr/lib/modules/$(KVER)/build
default:
$(MAKE) -C $(KDIR) M=$$PWD
install:
kmodsign sha512 \
/var/lib/shim-signed/mok/MOK.priv \
/var/lib/shim-signed/mok/MOK.der \
hello.ko
$(MAKE) -C $(KDIR) M=$$PWD modules_install
depmod -A
clean:
$(MAKE) -C $(KDIR) M=$$PWD clean
endif
こちらは普通のMakefileと異なる部分があります。Linuxカーネルはobj-m
」hello.
をコンパイルしたhello.
を使う」hello.
」
Kbuild用のMakefileとしてはobj-m
」ifneq...
」
もうひとつはKVER
」KDIR
」/usr/
」
ここでは/usr/
」/usr/
」
$ ls -l /usr/lib/modules/5.15.0-1047-kvm/build lrwxrwxrwx 1 root root 38 Nov 2 14:25 /usr/lib/modules/5.15.0-1047-kvm/build -> /usr/src/linux-headers-5.15.0-1047-kvm
さて、これをKVER
」uname -r
」uname -r
」make KVER=6.
」
話をもとに戻して、次はdefaultターゲットです。これは単にカーネルモジュールをビルドしているだけです。実際にビルドしてみましょう。まずはカーネルモジュールをビルドするためにコンパイラとmake
コマンドをインストールします。これは次のコマンドが一番かんたんです。
$ sudo apt install build-essential
あとはmake
コマンドをっこうすればビルド完了です。
$ make make -C /usr/lib/modules/5.15.0-1047-kvm/build M=$PWD make[1]: Entering directory '/usr/src/linux-headers-5.15.0-1047-kvm' CC [M] /home/ubuntu/recipe-0791/hello.o MODPOST /home/ubuntu/recipe-0791/Module.symvers CC [M] /home/ubuntu/recipe-0791/hello.mod.o LD [M] /home/ubuntu/recipe-0791/hello.ko BTF [M] /home/ubuntu/recipe-0791/hello.ko Skipping BTF generation for /home/ubuntu/recipe-0791/hello.ko due to unavailability of vmlinux make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-1047-kvm'
この時点ではカーネルモジュールは署名されていません。セキュアブート環境でロードしようとすると冒頭のエラーメッセージのように失敗してしまいます。署名はインストール環境で実施していますがこれには先に事前準備が必要です。第782回の途中にある
ちなみにUbuntuインストール時に
$ ls /var/lib/shim-signed/mok/ MOK.der MOK.priv
もしファイルが存在しない場合は次のように生成しましょう。
$ sudo update-secureboot-policy --new-key Generating a new Secure Boot signing key: Can't load /var/lib/shim-signed/mok/.rnd into RNG 405701BE5D7F0000:error:12000079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:106:Filename=/var/lib/shim-signed/mok/.rnd (中略)
生成した鍵をUEFIの変数領域にあるデータベースにインポートします。これはセキュリティの都合で、Linuxから直接はインポートできません。次のコマンドで一旦ブートローダーに鍵登録のUEFIアプリケーション
mokutil
コマンドでMOKの証明書をインポートします。
$ sudo mokutil --import /var/lib/shim-signed/mok/MOK.der [sudo] shibata のパスワード: input password: input password again:
sudoのパスワードを入力したあとは、MOK登録用のワンタイムパスワードを入力します。これは再起動後にMokManagerが動く際に必要になるので覚えておいてください。一回だけなのでずっと覚えておく必要はありません。シンプルな文字列でも問題ありません。 このあと再起動したら画面の指示に従ってください。手順は前述の第782回に書いていますのでそちらも参照すると良いでしょう。
MOKの準備ができたら、作成したカーネルモジュールを署名しましょう。これは前述のMakefileのinstallターゲットで実施しています。実施していることは次のとおりです。
kmodsign
でカーネルモジュール(koファイル) を署名 - Kbuildのmodules_
installターゲットを実行 depmod
コマンドでモジュールデータベースを更新
セキュアブート環境で独自のカーネルを使いたいのであればカーネルの署名が必要です。先ほど生成したMOKの秘密鍵と証明書を使って、kmodsign
で署名を行います。セキュアブートが無効化された環境では署名は不要ですが、署名されているモジュールがロードできないわけではないため、常に署名するようにしておいても問題ないでしょう。
modules_
ターゲットを実行すると、生成したカーネルモジュールを適切な場所にインストールしてくれます。Ubuntuの場合は/usr/
」depmod
コマンドでカーネルモジュールの依存関係等が書かれたファイルを更新します。ちなみにKbuildでもdepmod相当のコマンドを実行してくれるのですが、今回の作り方だと次のようにスキップした旨の警告が表示されます。
Warning: modules_install: missing 'System.map' file. Skipping depmod.
これを回避するためにMakefileの中でdepmodを明示的に実行しています。さて実際にモジュールをインストールしてみましょう。installターゲットの各コマンドは管理者権限が必要であるため、まとめてsudoで実行してしまいます。
$ sudo make install kmodsign sha512 \ /var/lib/shim-signed/mok/MOK.priv \ /var/lib/shim-signed/mok/MOK.der \ hello.ko make -C /usr/lib/modules/5.15.0-1047-kvm/build M=$PWD modules_install make[1]: Entering directory '/usr/src/linux-headers-5.15.0-1047-kvm' INSTALL /lib/modules/5.15.0-1047-kvm/extra/hello.ko SIGN /lib/modules/5.15.0-1047-kvm/extra/hello.ko DEPMOD /lib/modules/5.15.0-1047-kvm Warning: modules_install: missing 'System.map' file. Skipping depmod. make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-1047-kvm' depmod -A
これでモジュールのインストールは完了です。実際にモジュールの情報を表示してみると、きちんと署名も付加されていることがわかります。
$ modinfo hello filename: /lib/modules/5.15.0-1047-kvm/extra/hello.ko license: GPL v2 author: Mitsuya Shibata description: hello depends: retpoline: Y name: hello vermagic: 5.15.0-1047-kvm SMP mod_unload modversions sig_id: PKCS#7 signer: dkms Secure Boot Module Signature key sig_key: 52:DA:67:99:83:7A:8C:4D:F0:F2:50:35:BF:45:48:78:4C:96:7D:4A sig_hashalgo: sha512 signature: 05:89:7F:F5:C8:C0:12:84:4A:F4:89:07:94:5A:26:E0:8A:67:D7:4C: F0:6F:6F:CF:FD:C8:2C:23:80:4A:7D:47:80:B2:02:14:E4:29:CB:C2: D2:A1:E9:1E:C9:70:E6:B1:33:31:4A:FB:82:B7:71:44:73:E7:C1:B4: EB:F8:88:2D:6B:20:75:9F:36:E2:69:ED:64:22:04:11:83:B4:15:23: C2:7B:96:95:91:D1:6A:DD:83:17:FE:48:3B:04:33:B5:0C:AD:07:77: 4A:D6:DB:B5:F3:23:10:8F:59:81:6E:D9:8A:7A:E8:78:A1:17:25:68: C0:16:F6:55:D7:A6:86:FA:0B:A4:BD:AF:EC:FB:65:85:7E:CD:A7:11: F9:75:97:30:3F:46:DC:54:F4:9D:E5:98:A4:86:46:C5:69:71:D4:99: DB:C0:D3:1C:49:99:35:F7:74:F4:23:EC:78:59:9B:8D:06:4E:E9:81: 4B:5E:F6:A0:6A:9D:24:DD:11:FE:BE:C8:A9:30:C1:7C:E1:F3:3E:9B: 3B:3B:97:A8:E7:B7:06:D3:59:BE:28:9E:AE:8B:79:D7:EE:03:9F:B4: 56:AB:11:54:BF:DB:E7:24:F4:E9:C2:7B:1D:EB:5F:60:E6:5A:9B:C6: 56:10:DF:C1:4D:D9:C6:23:CD:BC:1B:1D:26:BE:42:7A
あとは作ったモジュールをロードするだけです。インストール前のモジュールならinsmod
コマンドでロードできますし、インストールしdepmod
すればmodprobe
コマンドでロードできます。今回はロード時とアンロード時にメッセージを残すようにしているので、それを一度に試してみましょう。
$ sudo modprobe hello $ sudo modprobe -r hello $ journalctl -r -k | head -n 2 Dec 04 14:19:41 dkms kernel: hello_exit: exit hello module Dec 04 14:19:35 dkms kernel: hello_init: Hello world!
上記のようにロード時とアンロード時のメッセージが残っていたら成功です。あとはこれを改造していくだけで、任意のカーネルドライバーを作成できます。
もしhelloモジュールが不要になったなら削除しておきましょう。
$ sudo rm /lib/modules/カーネルリリース名/extra/hello.ko $ sudo depmod -A
これでカーネルドライバーの最低限の作り方がわかりました。
カーネルドライバーをDKMSに対応させる
ここまで作ると次のような改善ポイントが浮かび上がってきます。
- 複数のカーネルリリースに対してビルドする際に
「 make KVER=XXX
」を何度も実行しなくてはならない - インストールするターゲットごとにMOKの作成が必要
- インストールするターゲットごとにソースファイルを配布しないといけない
これをある程度解決するのが今回紹介する
- DKMSに対応するとカーネルが更新される度に、自動的に
「インストールされているすべてのカーネル」 に対してモジュールの再ビルドと署名を実行します - DKMSパッケージをインストールすると、MOKの生成とUEFIへのインポートを行ってくれます。ただし初回の再起動時のMokManagerの操作は必要です
- DKMSに対応したカーネルモジュールのパッケージはソースコードを
/usr/
以下に保存しますので、ユーザー側がソースコードを意識することはありませんsrc
またDKMSにはDebian/
さっそく先ほど作ったカーネルドライバーをDKMSに対応してみましょう。まず、DKMSのパッケージとDebianパッケージ化するためのdebhelperパッケージをインストールします。
$ sudo apt install dkms debhelper
次に/usr/
にパッケージ名-バージョン
」hello.
」/usr/
」
$ sudo mkdir /usr/src/hello-1.0 $ sudo cp hello.c /usr/src/hello-1.0/
ただしMakefile
」
# SPDX-License-Identifier: CC0-1.0
obj-m := hello.o
ifneq ($(KERNELRELEASE),)
KVER ?= $(shell uname -r)
endif
KDIR ?= /usr/lib/modules/$(KVER)/build
default:
$(MAKE) -C $(KDIR) M=$$PWD
clean:
$(MAKE) -C $(KDIR) M=$$PWD clean
DKMSのmake実行時は自動的にKERNELREASE
が設定されているために、ifneq...
の範囲を絞りました。またinstallはDKMSがやってくれるので削除しています。ただしターゲットがぶつかる可能性やDKMSを経由せずに手動でビルドすることも考えると、本来はトップディレクトリのMakefileとカーネルモジュール用のMakefileは分けるべきなのでしょう。
最後にDKMS用の設定ファイルであるdkms.
」
PACKAGE_NAME="hello"
PACKAGE_VERSION="1.0"
CLEAN="make clean"
MAKE[0]="make KVER=$kernelver"
BUILT_MODULE_NAME[0]="hello"
DEST_MODULE_LOCATION[0]="/updates"
AUTOINSTALL="yes"
書式と変数についてはPACKAGE_
」PACKAGE_
」MAKE[0]
」BUILT_
」MAKE[0]
」KVER
変数を渡すのを忘れないようにしてください。$kernelver
はDKMSが勝手に設定してくれる変数です。ちなみにMakefileをわけると、このKVER
の設定も不要になります。またUbuntuの場合、DEST_
」
これをDKMSシステムに登録しましょう。
$ sudo dkms add hello/1.0 Creating symlink /var/lib/dkms/hello/1.0/source -> /usr/src/hello-1.0 $ ls -l /var/lib/dkms/hello/1.0/source lrwxrwxrwx 1 root root 18 Dec 3 05:18 /var/lib/dkms/hello/1.0/source -> /usr/src/hello-1.0
これは任意のディレクトリにあるソースツリーに対するシンボリックリンクを/var/
」add
コマンドの代わりにremove
コマンドを実行してください。
DKMSでカーネルモジュールを手動ビルドしてみましょう。これはdkms build
」
$ sudo dkms build hello/1.0 (中略) Building module: cleaning build area...(bad exit status: 2) make -j2 KERNELRELEASE=5.15.0-1047-kvm KVER=5.15.0-1047-kvm... Signing module: - /var/lib/dkms/hello/1.0/5.15.0-1047-kvm/x86_64/module/hello.ko Nothing to do. cleaning build area...(bad exit status: 2)
再ビルドしたい場合はsudo dkms unbuild hello/
」sudo dkms build hello/
」
ビルドしたカーネルモジュールをシステムにインストールするにはdkms install
」
$ sudo dkms install hello/1.0 hello.ko: Running module version sanity check. - Original module - No original module exists within this kernel - Installation - Installing to /lib/modules/5.15.0-1047-kvm/updates/dkms/ depmod...
ここまでできたら実際にsudo modprobe hello
」sudo dkms uninstall hello/
」
カーネルアップデート時
/etc/kernel/header_postinst.d/dkms /etc/kernel/install.d/dkms /etc/kernel/postinst.d/dkms /etc/kernel/prerm.d/dkms
最後にここまでの成果物をDebianパッケージ化してみましょう。これはdkms mkdeb
」
$ sudo dkms mkdeb hello/1.0 Using /etc/dkms/template-dkms-mkdeb copying template... modifying debian/README.Debian... modifying debian/changelog... modifying debian/compat... modifying debian/control... modifying debian/copyright... modifying debian/dirs... modifying debian/postinst... modifying debian/prerm... modifying debian/rules... copying legacy postinstall template... Copying source tree... Building binary package...dpkg-buildpackage: warning: using a gain-root-command while being root dpkg-source --before-build . fakeroot debian/rules clean dh_clean: warning: Compatibility levels before 10 are deprecated (level 7 in use) debian/rules build fakeroot debian/rules binary dh_installdirs: warning: Compatibility levels before 10 are deprecated (level 7 in use) dh_strip: warning: Compatibility levels before 10 are deprecated (level 7 in use) dh_compress: warning: Compatibility levels before 10 are deprecated (level 7 in use) dh_installdeb: warning: Compatibility levels before 10 are deprecated (level 7 in use) dh_shlibdeps: warning: Compatibility levels before 10 are deprecated (level 7 in use) dpkg-genbuildinfo --build=binary -O../hello-dkms_1.0_amd64.buildinfo dpkg-genchanges --build=binary -O../hello-dkms_1.0_amd64.changes dpkg-source --after-build . Moving built files to /var/lib/dkms/hello/1.0/deb... Cleaning up temporary files...
これで/var/
」
$ dpkg-deb --info /var/lib/dkms/hello/1.0/deb/hello-dkms_1.0_amd64.deb new Debian package, version 2.0. size 5050 bytes: control archive=1306 bytes. 292 bytes, 10 lines control 247 bytes, 4 lines md5sums 1228 bytes, 49 lines * postinst #!/bin/sh 332 bytes, 28 lines * prerm #!/bin/sh Package: hello-dkms Version: 1.0 Architecture: amd64 Maintainer: Dynamic Kernel Modules Support Team <pkg-dkms-maint@lists.alioth.debian.org> Installed-Size: 21 Depends: dkms (>= 1.95) Provides: hello-modules (= 1.0) Section: misc Priority: optional Description: hello driver in DKMS format.
パッケージ名等は自動的に決定されます。Depends
」
$ dpkg-deb --contents /var/lib/dkms/hello/1.0/deb/hello-dkms_1.0_amd64.deb drwxr-xr-x root/root 0 2023-12-03 06:22 ./ drwxr-xr-x root/root 0 2023-12-03 06:22 ./usr/ drwxr-xr-x root/root 0 2023-12-03 06:22 ./usr/share/ drwxr-xr-x root/root 0 2023-12-03 06:22 ./usr/share/hello-dkms/ -rwxr-xr-x root/root 8137 2023-12-03 06:22 ./usr/share/hello-dkms/postinst drwxr-xr-x root/root 0 2023-12-03 06:22 ./usr/src/ drw-r-xr-x root/root 0 2023-12-03 06:04 ./usr/src/hello-1.0/ -rw-r--r-- root/root 442 2023-12-03 06:18 ./usr/src/hello-1.0/Makefile -rw-r--r-- root/root 175 2023-12-03 06:18 ./usr/src/hello-1.0/dkms.conf -rw-r--r-- root/root 413 2023-12-03 06:18 ./usr/src/hello-1.0/hello.c
中身はとてもシンプルですね。./
」dkms add/
」
$ sudo apt install ./hello-dkms_1.0_amd64.deb
Linuxカーネルの中身を知る上で、ソースコードやドキュメント・
まずはシンプルなカーネルモジュールを作ってみて、少しずつ知識の範囲を広げてみるのはいかがでしょうか。何かターゲットデバイスを決めてそのドライバーを作ってみるのも良いかもしれません。ぜひ冬休みの課題のひとつとして、カーネルドライバーの作成を候補に入れてみてください。