📚

Rust for Linuxを手元で試す

2022/06/26に公開

RustをLinuxカーネルに組込みプロジェクト、Rust for Linuxが進行中です。

https://github.com/Rust-for-Linux

このプロジェクトはLinuxカーネル全体をRustで置き換えるわけではなく、第二言語としてRustを採用してデバイスドライバなどのモジュールを書くことができるようにしようというものです。
RustはOSのような低レイヤーソフトウェアを実装する言語として、C言語に代わる選択肢として注目されてきたわけですが、Linuxのような広く使われているシステムに採用されるとなればかなり熱いですね。

実際にLinuxのメインラインに取り入れられるにはまだまだ課題は多いものの、Linus氏を含むLinuxの開発者からのフィードバックも比較的ポジティブでこれからが注目されています。

そんなRust for Linuxを手元でビルドして動かしてみました。
一応、基本的な手順はレポジトリ内のドキュメントにまとまっているのですが、いくつかつまずいた点も含めて紹介します。

https://github.com/Rust-for-Linux/linux/blob/rust/Documentation/rust/quick-start.rst

筆者はLinuxカーネルの開発経験がほとんどないため、誤っている点もあるかもしれませんがご了承ください。

カーネルをビルドする

Rust for Linuxの変更はLinuxをフォークしたレポジトリ内で管理されています。

https://github.com/Rust-for-Linux/linux.git

定期的にメーリングリストにパッチとしても投稿されているので、こちらを取り込むことでも試すことができるかと思います。
この記事の執筆時点で最新のものは2022/05/23に投稿されたPatch v7です。

https://lore.kernel.org/rust-for-linux/20220523020209.11810-1-ojeda@kernel.org/T/#m3f95c64ad43390c4237df111d147367548940b32

基本的には先程出したQuick Startのガイドに沿ってRustと関連ツール、またLinuxビルドに必要なものをもインストールする必要があります。
Linuxのビルドに必要なものは、使用しているOSがUbuntuの場合こちらを参考にするといいと思います。

https://wiki.ubuntu.com/Kernel/BuildYourOwnKernel

Rustは基本的にLLVMバックエンドに依存しているため、カーネルのビルドにもLLVMのツールが必要です。Ubuntu 20.04でデフォルトで入るllvmのツール群はバージョンが古く、このままではビルドできないので、llvmの公式から新しいバージョンを落としてくる必要があります。筆者はllvm-12を手元でビルドしてインストールしました。

https://releases.llvm.org/download.html

LinuxをビルドするにはKconfigという仕組みで.configという設定ファイルを編集して、必要なオプションを切り替えてあげる必要があります。
RustのサポートもKconfigで設定する必要があり、CONFIG_RUSTという変数が有効になっている必要があります。
make menuconfigとするとTUIを使って.configを編集することができます。
ドキュメントによるとGeneral setupRust supportというのがあると書かれています。が、デフォルトの状態では現れない場合があります。
これはRust supportが依存しているconfigが正しく設定されていない場合に起こります。
menuconfig実行中に/を入力することで探したい設定を検索できます。rustで検索すると以下のようにRUSTシンボルが何に依存しているかDepends onのところに書いてあります。

設定の検索結果画面

RUST_IS_AVAILABLEなどが有効でGCC_PLUGINなどは逆に無効になっている必要があります。これで満たしていないシンボルについて/で検索をかけて設定を変更しましょう。

make menuconfigの他にscripts/configというスクリプトで個別のシンボルの編集もできます。
scripts/config --enable RUSTとすれば依存関係が満たされていればRust supportを有効にすることができます。

configについて他に気をつける部分としては、CONFIG_SYSTEM_TRUSTED_KEYSという項目があり、デフォルト値のままだと自分で署名用のキーを用意しないといけません。
自分で署名用のキーを用意する、またはこの設定を無効化してしまうことで対処しましょう(参考)。

最も手っ取り早い方法としては、すでに用意されているconfigファイルをコピーしてしまうことです。
Rust for LinuxはGithub Actionsを利用してCIをおこなっていて、それのための.configファイルが.github/workflowsディレクトリ以下に存在しています。
.github/workflows/kernel-x86_64-debug.configがx86_64系アーキテクチャのものでは一番利用しやすいのではないでしょうか。
これを.configにコピーしてあげればとりあえずビルドはできます。

make LLVM=1などでコンパイルができます。ただし、デフォルトだとビルドが並列化されないので-jオプションで並列実行しましょう。
Linuxカーネルのビルドには時間がかかるため、できるだけパフォーマンスの高いPCで実行することをおすすめします。

ビルドしたカーネルを試す

ビルドしたカーネルを手元で試すには仮想マシン上で動かすのが手っ取り早いです。
Rust for LinuxのCIではQEMUを仮想マシンとして利用して、BusyBoxというソフトウェアと組み合わせることで簡単なテストを実行しています。
BusyBoxは標準UNIXでよく使われるコマンドたちを単一の実行ファイルに詰め込んだもので、標準のLinuxディストリビューションを用意するよりかははるかにコンパクトにLinuxを利用することができます。

BusyBoxは自分でソースコードをダウンロードしてビルドすることになります。

https://busybox.net/source.html

Linux同様、make menuconfigなどで.configファイルを編集してビルドする必要があります。
このとき、カーネル内でも動くようにライブラリを静的リンクしてあげる必要があるので、STATICを有効にしてあげましょう。
Rust for LinuxのCIで使われているをコピーする手もありますが、必要最低限の機能しか有効化されていないので使えるコマンドが大きく制限されているので、こちらに関しては自分で設定を用意するほうがいいと思います。

Linuxカーネルを起動させるには多くの場合はinitramfsという、初期化処理用に使うファイルシステムをRAM上に構築するという方法を用います。
今回の起動テストでもinitramfsを用意することでQEMU上で動かします。Rust for LinuxのCI上でも同様の処理が行われています。

initramfsを簡単に構築する方法として、Linuxのカーネルツリーに存在するusr/gen_init_cpioというツールを利用する方法があります。
これはinitramfs上に含めるファイルを記述した設定ファイルからinitramfsを自動生成する便利なツールです。
自分がテスト用につくった設定ファイルは以下のようになります。

dir     /bin                                          0755 0 0
dir     /sys                                          0755 0 0
dir     /dev                                          0755 0 0
file    /bin/busybox  busybox/busybox                 0755 0 0
slink   /bin/sh       /bin/busybox                    0755 0 0
file    /init         qemu-init.sh                    0755 0 0

busybox/busyboxというのが自分がビルドしたbuxyboxの実行バイナリへの相対パスになっています。
/initというのがカーネルが起動して最初に実行されるファイルです。内容としては単なるシェルスクリプトになっています。

#!/bin/sh

busybox ls
busybox sh
busybox reboot -f

busybox shがbusybox内のシェルを呼び出すコマンドです。このシェルが終了するとrebootするようになっています。
あとはrust-for-linux/usr/gen_init_cpio initramfs.desc > initramfs.imgなどとして、initramfsを生成しましょう。

これで、QEMU実行のための準備は整いました。QEMU自体はUbuntuのaptで入るもので大丈夫です。

qemu-system-x86_64 \
    -kernel "rust-for-linux/arch/x86_64/boot/bzImage" \
    -initrd qemu-initramfs.img \
    -smp 2 \
    -nographic \
    -vga none \
    -no-reboot \
    -M pc \
    -append console=ttyS0

としてあげれば、busyboxのシェルが立ち上がるでしょう。-kernelで渡すカーネルのイメージや-initrdで渡すinitramfsのパスは適宜変えてください。
-no-rebootをつけて、最後のreboot処理が実行されるとqemuが終了するようになっています。また、カーネルパニックなどで再起動がかかった場合も終了します。

おまけ

ビルドプロセスについて

このRust for LinuxでRustをコンパイルしている部分なのですが、なんとcargoは使っていないで、rustcとリンクオプションの指定でコンパイルをがんばっています。
具体的にはscripts/Makefile.buildこのあたりにルールの指定があります。
--externallockernelという2つのクレートを組み込んでいます。これはrustディレクトリ以下に存在しています。
allocは標準ライブラリのVecBoxといったヒープ領域を使う構造体を提供するクレートの改変版です。
普通のallocだと例えばVec::newなどを呼び出した場合、アロケーションに失敗した場合プログラム全体がpanicとなり強制終了されてしまいます。
通常のプログラミングではアロケーションに失敗するケースは深く考えずにpanicしても大きな問題になりませんが、Linuxカーネルの場合、カーネルパニックとなりシステム全体が落ちることになってしまいます。
そのため、Vec::new相当で値がResult型であり、アロケーションに失敗した場合、panicではなくErrを返して全体が止まらないようなメソッドを代わりに用意するために独自の改変が必要なわけです。
kernelはLinuxカーネル内部のメソッドやデータを扱うためのRustラッパになるクレートです。

開発Tips

実はrust-analyzerとかにも対応しています。make rust-analyzerとしてあげるとrust-project.jsonというファイルができるので、これをrust-analyzerに読み込ませてあげるとコードジャンプなどができて便利です。
VS Codeの場合、.vscode/settings.jsonに以下のような設定を加えると読み込んでくれます。

{
    "rust-analyzer.linkedProjects": [
        "/path/to/rust-for-linux/rust-project.json"
    ]
}

Discussion