Ubuntu Weekly Recipe

第793回自作のカーネルモジュールをRustで作る

第791回では基本的なカーネルモジュールの作り方とそれをDKMSに対応させる方法を紹介しました。今回はカーネルの新しい機能のひとつであるRustでカーネルモジュールを作る方法を紹介しましょう。

UbuntuカーネルにおけるRustの対応

Linuxカーネルでは、Kernel 6.1からプログラミング言語であるRustの機能が取り込まれました。これはRust for Linuxの成果で、カーネルの機能をC言語やアセンブラだけでなく、Rustでも書けるようにするというものです。メモリー安全性や強い静的型付けなどの特徴を取り込むことで、カーネルのセキュリティ問題の原因の多くを占めるメモリー関連の不具合に対して、一種の対策になることが期待されます。

あらゆるケースにおいてRustに置き換えられるというわけではありませんが、今後カーネルのコードを読み書きする上でC言語やアセンブラだけでなくRustに対する理解が必要となる機会はきっと増えていくことでしょう。今回は第791回で作成したC言語版の自作カーネルモジュールのサンプルを、Rustに置き換える手順を紹介します。

Rustをカーネルで使う上でポイントなのが使用しているカーネルがRust機能を有効化しているのか?Rustのバージョンはどれを使うのが良いのか?という2点です。まずはこれについてUbuntuの状況を説明しましょう。

かんたんにまとめると現時点でのUbuntuは、⁠Ubuntu 23.04以降のamd64向けのgenericフレーバーといくつかのフレーバーであればRustをサポート」しています。具体的には次のカーネルコンフィグが有効化されていればOKです。

$ grep _RUST /boot/config-$(uname -r)
CONFIG_RUST_IS_AVAILABLE=y
CONFIG_RUST=y
CONFIG_RUSTC_VERSION_TEXT="rustc 1.68.2 (9eb3afe9e 2023-03-27) (built from a source tarball)"
CONFIG_HAVE_RUST=y
# CONFIG_SAMPLES_RUST is not set
# CONFIG_RUST_DEBUG_ASSERTIONS is not set
CONFIG_RUST_OVERFLOW_CHECKS=y
# CONFIG_RUST_BUILD_ASSERT_ALLOW is not set

上記でもっとも重要なのがCONFIG_RUSTで、これがyとなっていないカーネルではRustは使えません。異なるカーネルフレーバーに切り替えてください。ちなみに現時点ではUbuntuとしては「amd64」に限定しているものの、将来的にはriscv64やarm64などでも使えるようになる見込みです。

Rustは従来サポートしていたGCCに比べるとリリース周期が短いツールです。カーネルのRust対応においては、後方互換性の維持が保証されない「不安定」な機能をまだいくつか利用しています。そこで各種ディストリビューションがカーネルをメンテナンスしやすいように、カーネルのリリースごとに特定のバージョンのRustのみをサポートし、次のリリースに向けての開発中に対応バージョンを見直すようにしています。それが上記で言うところのCONFIG_RUSTC_VERSION_TEXTです。厳密に言うとこれは、そのカーネルがビルドされる時にインストールされていたrustcコマンドのバージョンになります。他にもサポートしている最低バージョンは、次のコマンドでも確認できます。

$ /usr/src/linux-headers-$(uname -r)/scripts/min-tool-version.sh rustc
1.68.2

Rust版のモジュールをビルドするには、ターゲットとなるカーネルに合わせたバージョンのRustツールチェインが必要です。ちなみにカーネルの6.4まではRust 1.62.0、6.5は1.68.2、6.6は1.72.1、そして来る6.7は1.73.0になる見込みのようです

注意しなければいけないのはUbuntu 22.04 LTS以前のHWEカーネルではRustはサポートしていないことです。たとえばデスクトップ版のUbuntu 22.04.3 LTSだと、23.04で使われた6.2カーネルが採用されていますが、こちらは次のようにCONFIG_RUSTは無効化されています。

$ lsb_release -d
Description:    Ubuntu 22.04.3 LTS

$ uname -r
6.2.0-26-generic

$ grep _RUST /boot/config-$(uname -r)
CONFIG_RUST_IS_AVAILABLE=y
CONFIG_HAVE_RUST=y

22.04.4以降でどうなるかは不明ですが、サポートしない可能性は高いでしょう。よってもし試したいのであれば、最新リリースであるUbuntu 23.10か、2024年4月リリースに向けて開発中のnobleを使用してください。今回はUbuntu 23.10とカーネル6.5、Rust 1.68の組み合わせで紹介します。

ちなみに2023年12月現在では、nobleのカーネルは23.10と同じ6.5のままです。今後、開発のどこかでカーネルバージョンを決める予定ですが、これまでの慣例を踏まえるとリリース済みの6.6か、まもなくリリースされるであろう6.7になるでしょう。

Rustモジュールのビルドに必要なもの

Rustモジュールのビルドには、Rustのツールチェインの他にLLVMやclangも必要です。そこで次のようにインストールしてください。

$ sudo apt install rustc-1.68 rust-1.68-src rustfmt-1.68 bindgen-0.56 llvm clang build-essential linux-lib-rust-$(uname -r)

上記はUbuntu 23.10の場合の値です。Ubuntu 23.04なら「1.68」「1.62」に置き換えてください。nobleは2023年12月時点では上記と同じですが、カーネルが更新されたら「1.73」に置き換えることになるでしょう。その場合は、bindgenも「0.66」に更新する必要があるかもしれません。これらは次の方法で確認できます。

$ uname -r
6.5.0-9-generic
$ /usr/src/linux-headers-$(uname -r)/scripts/min-tool-version.sh rustc
1.68.2
$ /usr/src/linux-headers-$(uname -r)/scripts/min-tool-version.sh bindgen
0.56.0

実はgccやbinutils、llvmなども「最低バージョン」が上記で確認できるのですが、Ubuntuの場合は基本的にすべて満たしているはずです。これだけで準備は完了です。

Rustのカーネルモジュールを作ってみる

では、実際にRust版のカーネルモジュールを作ってみましょう。やろうとしていることは第791回と同じで、カーネルモジュールをロードする時とアンロードする時にメッセージを記録するだけです。

第791回では、ひとつのMakefileでKbuildと通常のmakeの両方に対応しましたが、今回はMakefileを次のように分けることにします。

Makefile
src/
  Makefile
  hello.rs

これによりsrc/MakefileはKbuild専用の記述にできます[1]

まず最初にトップディレクトリのMakefileは次のように記述してください。

# SPDX-License-Identifier: CC0-1.0

KVER ?= $(shell uname -r)
KDIR ?= /usr/lib/modules/$(KVER)/build

RUSTFMT = rustfmt-1.68
RUST_FLAGS = CROSS_COMPILE=x86_64-linux-gnu-
RUST_FLAGS += HOSTRUSTC=rustc-1.68
RUST_FLAGS += RUSTC=rustc-1.68
RUST_FLAGS += BINDGEN=bindgen-0.56
RUST_FLAGS += RUSTFMT=$(RUSTFMT)
RUST_FLAGS += RUST_LIB_SRC=/usr/src/rustc-1.68.2/library

default:
        $(MAKE) $(RUST_FLAGS) -C $(KDIR) M=$$PWD/src

install: default
        kmodsign sha512 \
                /var/lib/shim-signed/mok/MOK.priv \
                /var/lib/shim-signed/mok/MOK.der \
                src/hello.ko
        $(MAKE) -C $(KDIR) M=$$PWD/src modules_install
        depmod -A

fmt:
        find . -type f -name '*.rs' | xargs $(RUSTFMT)

clean:
        $(MAKE) $(RUNST_FLAGS) -C $(KDIR) M=$$PWD/src clean

基本的には第791回で説明したとおりの内容です。異なるのは次の点となります。

  • Rust関連の変数が追加された
  • fmtターゲットが追加された
  • Kbuild用の行が削除された
  • ソースコードが別ディレクトリになったため、M=オプションのパスが一段深くなった

Rust関連の変数については、カーネル側に明示的に特定バージョンのRustツールチェインを使わせるために指定しています。Ubuntuの場合、リリースによって複数バージョンのRustツールチェインを公式リポジトリからインストールできます。その場合、⁠rustc-バージョン」のような名前で区別され、⁠rustc」コマンドそのものは特定のバージョンのみを示します。よって確実に「1.68」を使いたいのであれば、上記のように明示的に指定する必要があるのです。

もうひとつ追加された項目が「fmt」ターゲットです。実はカーネルにはrustfmtやrustfmtcheckというターゲットがあり、コードをきれいに整形する仕組みが存在します。しかしながらサードパーティのモジュールがKbuildでビルドする場合(具体的にはmakeコマンドにM=オプションを指定する場合⁠⁠、このrustfmtターゲットは利用できません。そこで、今回は独自にfmtターゲットを追加してみました。

Kbuild用の行が削除されたのは「src/Makefile」側に移動したからです。⁠src/Makefile」は次のように記述してください。

# SPDX-License-Identifier: CC0-1.0

obj-m  := hello.o

そして本体となる「src/hello.rs」です。

// SPDX-License-Identifier: GPL-2.0-only

//! Rust module sample

use kernel::prelude::*;

module! {
    type: Hello,
    name: "hello",
    author: "Mitsuya Shibata",
    description: "hello",
    license: "GPL v2",
}

struct Hello {}

impl kernel::Module for Hello {
    fn init(_module: &'static ThisModule) -> Result<Self> {
        pr_info!("Hello world!\n");
        Ok(Hello {})
    }
}

impl Drop for Hello {
    fn drop(&mut self) {
        pr_info!("exit hello module\n");
    }
}

Rustの書式についてはRustの入門書に譲るとして、カーネルモジュール関連の説明をしていきましょう。

まず最初にmoduleマクロを用いて、モジュールを宣言しています。これはC言語版で言うところのMODULE_FOOマクロに近い扱いですが、必須のフィールドが増えています。最大のポイントがtype: Helloの行で、Helloの部分はModuleトレイトを実装する型を指定します。今回はHello構造体を作成しそれを指定することにしました。

impl kernel::Module for Helloにおいてトレイトの実装を行っています。カーネルモジュールの初期化時に呼ばれるinit()メソッドを定義し、その中でpr_info()マクロを呼んでいるだけです。第791回で言うところのmodule_init(hello_init)hello_init()の実装部分ですね。

Moduleトレイトにはmodule_exit()相当の機能はありません。しかしながらRustの標準機能であるDropトレイトを使えば、モジュールを終了する時(つまりスコープから抜けることで、モジュールのリソースを解放するとき)に、任意の処理を追加できます。今回は特にリソースの解放は不要であるため、単にpr_infoマクロでログを出力しているだけとなります。

コードが完成したらmakeしてみましょう。

$ make
make CROSS_COMPILE=x86_64-linux-gnu- HOSTRUSTC=rustc-1.68 RUSTC=rustc-1.68 BINDGEN=bindgen-0.56 RUSTFMT=rustfmt-1.68 RUST_LIB_SRC=/usr/src/rustc-1.68.2/library -C /usr/lib/modules/6.5.0-9-generic/build M=$PWD/src
make[1]: Entering directory '/usr/src/linux-headers-6.5.0-9-generic'
  RUSTC [M] /home/ubuntu/rust/src/hello.o
  MODPOST /home/ubuntu/rust/src/Module.symvers
  CC [M]  /home/ubuntu/rust/src/hello.mod.o
  LD [M]  /home/ubuntu/rust/src/hello.ko
  BTF [M] /home/ubuntu/rust/src/hello.ko
Skipping BTF generation for /home/ubuntu/rust/src/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-9-generic'

実行されるコマンド列は変わりますが、手順自体はC言語版と同じです。生成されたものも、普通のモジュールファイルです。

$ file src/hello.ko
src/hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=f87ad98bf2f25ac017602656c284b5b36a948a61, with debug_info, not stripped

$ modinfo src/hello.ko
filename:       /home/ubuntu/rust/src/hello.ko
author:         Mitsuya Shibata
description:    hello
license:        GPL v2
srcversion:     C487F60BD22E51D45FD0090
depends:
retpoline:      Y
name:           hello
vermagic:       6.5.0-9-generic SMP preempt mod_unload modversions

$ strings src/hello.ko | grep rustc
clang LLVM (rustc version 1.68.2 (9eb3afe9e 2023-03-27) (built from a source tarball))
/usr/src/rustc-1.68.2/library/core/src/ptr

外からはRust版であることはわからず、かろうじてファイルの中の文字列にコンパイラの情報が残っているぐらいですね。

実際にモジュールを署名してインストールしてみましょう。これを実行するためにはあらかじめ第791回でも説明した、MOK(Machine Owner Key)の生成と登録が必要です。ここでは実行済みとします。

$ sudo make install
make CROSS_COMPILE=x86_64-linux-gnu- HOSTRUSTC=rustc-1.68 RUSTC=rustc-1.68 BINDGEN=bindgen-0.56 RUSTFMT=rustfmt-1.68 RUST_LIB_SRC=/usr/src/rustc-1.68.2/library -C /usr/lib/modules/6.5.0-9-generic/build M=$PWD/src
make[1]: Entering directory '/usr/src/linux-headers-6.5.0-9-generic'
make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-9-generic'
kmodsign sha512 \
        /var/lib/shim-signed/mok/MOK.priv \
        /var/lib/shim-signed/mok/MOK.der \
        src/hello.ko
make -C /usr/lib/modules/6.5.0-14-generic/build M=$PWD/src modules_install
make[1]: Entering directory '/usr/src/linux-headers-6.5.0-14-generic'
  INSTALL /lib/modules/6.5.0-14-generic/updates/hello.ko
  SIGN    /lib/modules/6.5.0-14-generic/updates/hello.ko
  DEPMOD  /lib/modules/6.5.0-14-generic
Warning: modules_install: missing 'System.map' file. Skipping depmod.
make[1]: Leaving directory '/usr/src/linux-headers-6.5.0-14-generic'
depmod -A

$ modinfo hello
filename:       /lib/modules/6.5.0-14-generic/updates/hello.ko
author:         Mitsuya Shibata
description:    hello
license:        GPL v2
srcversion:     C487F60BD22E51D45FD0090
depends:
retpoline:      Y
name:           hello
vermagic:       6.5.0-9-generic SMP preempt mod_unload modversions
sig_id:         PKCS#7
signer:         rust2 Secure Boot Module Signature key
sig_key:        36:6E:89:CC:A8:24:44:14:F0:03:7A:0A:CA:DF:A9:C3:E9:29:C9:17
sig_hashalgo:   sha512
signature:      42:6B:5C:E3:3A:80:B2:E1:72:87:75:A3:B8:DF:16:38:84:67:89:77:
                6D:04:F3:41:BE:30:55:FB:6C:66:AD:C8:DE:F0:8F:74:99:54:CE:9A:
                A0:B9:A8:8B:4A:1B:38:D5:6F:BF:B9:A4:30:60:02:F8:ED:F5:45:C5:
                85:05:34:28:16:94:34:99:3A:14:E8:E4:23:7A:92:50:2F:55:12:C3:
                4A:F8:43:55:F2:79:20:FD:01:00:D7:19:8B:59:F0:2F:94:D7:A5:A5:
                F6:03:E8:39:E9:5D:04:DF:D0:BD:27:3D:2D:D8:C4:15:4A:83:08:7F:
                53:6B:7B:7E:25:A6:C9:CE:91:9F:4E:24:1D:F7:42:FC:F8:32:0C:9A:
                95:6A:C1:D9:F5:C6:E9:29:6B:68:31:00:9A:DC:39:50:FD:4F:A9:02:
                B7:EF:C1:6F:2E:BE:DA:EE:36:8A:20:F7:28:41:7E:74:87:8C:7A:6D:
                47:65:40:6D:35:84:44:7C:E2:2A:72:6D:82:F1:37:25:5C:86:0B:CC:
                71:E8:9D:CE:D0:F4:F2:50:6F:CE:36:D9:CD:33:80:0E:B7:3B:C1:81:
                E5:37:DD:AC:A7:D9:E2:81:2E:4A:1E:52:EC:85:D8:C4:DB:6D:E3:C0:
                3C:96:E0:CE:B7:7D:7B:B0:4D:D6:75:E9:2B:48:C2:D3

無事に署名とインストールができました。それでは実際にロード・アンロードして、カーネルログに残っているか確認してみましょう。

$ sudo modprobe hello
$ sudo modprobe -r hello
$ journalctl -r -k | head -n 2
Dec 18 17:50:10 rust2 kernel: hello: exit hello module
Dec 18 17:50:06 rust2 kernel: hello: Hello world!

問題なくRust版のカーネルモジュールが動作しました。

ここまで説明したように、Rustの基本的な知識があれば、Rustでシンプルなモジュールを作る程度であればそこまで難しくありません。もちろん本格的なデバイスドライバーを作るとなると、Rustとカーネルの両方においてそれなりの知識が必要ですしょう。現在はまだカーネルの開発者がRustを使ってみる準備が整ったという段階です。ただ、それでも新しいものを触ってみるのは常に楽しいものです。

年末年始に(自由に使える)休暇がある方は、Rustを使って何か作ってみてはいかがでしょうか。

おすすめ記事

記事・ニュース一覧