LoginSignup
30
11

More than 5 years have passed since last update.

Linux kernel moduleからRustを呼ぶkernel-rouletteを解析

Last updated at Posted at 2018-10-25

kernel-roulette

RustでLinux kernel moduleが書ける…。噂の真相を突き止めるため、GitHubへと向かった。
ネタばれ。噂は本当だった。


まず最初に見つかったのが、rust.koです。
内容は、kernel moduleをロードすると、"Hello from Rust!++"をprintkで出力する、というものです。
ただ、3年前からメンテナンスされていません。早々に見切りをつけて次を探しました。

次に発見したのが、kernel-rouletteでした。
character device driverを実装しており、デバイスファイルをreadすると、一定確率でkernel panicを起こす、というdriverでした。
なんて迷惑なdriverでしょうか
こういう高度な技術を使って、無駄なことするの、好きです。
最終更新日が7ヶ月前なので、トラブルがあっても、これからなんとかなりそうです。

というわけで、少々トラブルはあったのですが、ビルドして動かしすことができました。

感想から言うと、RustでLinux kernel moduleが書ける、というより、Linux kernel moduleからRustを呼び出せる、という感じでした。
RustでLinux driverを書けるからHappy!という世界ではないです。(おそらくそういう世界はずっと来ないと思います)

ただ、細かいことは置いておいて、面白いので見ていきましょう。

まず、何はともあれ、動かすとどうなるか、を紹介します。
その後、ソースコードを解析していきます。

Linux kernel driverやRustの基礎事項については解説しません。別途参照をお願いします。

ビルド&動作確認環境

項目 Versionなど
Linux kernel 4.4.0-137-generic
distribution Ubuntu 16.04 (Virtual Box)
rustc 1.31.0-nightly (77af31408 2018-10-11)

動かしてみよう

前準備

kernel-roulette READMEDependenciesにある通り、下記の前準備が必要です。

  • Linux kernel headers and build-essential (gcc, make, etc.)
  • Nightly Rust (install from https://rustup.rs)
  • Xargo and rust-src
    Download the Rust library source with rustup component add rust-src
    Install xargo with cargo install xargo

ビルド

レポジトリをcloneして、patchを当てます。

$ git clone https://github.com/souvik1997/kernel-roulette.git
$ cd kernel-roulette
$ wget https://raw.githubusercontent.com/tomoyuki-nakabayashi/blogs/master/Rust/kernel-roulette/0001-fix-build-error-in-the-latest-nightly-rust.patch
$ patch -p1 < 0001-fix-build-error-in-the-latest-nightly-rust.patch

READMEBuildingに従って手順を実行します。
以下、少し冗長に書きますので、手っ取り早く動かしたい方は、READMEの方をご覧下さい。

kernel moduleをビルドします。build/roullete.koが生成されることを確認します。

$ make
$ ls build/
...  roulette.ko  ...

いよいよ、緊張の瞬間です。kernel moduleをロードしてみましょう。

$ sudo insmod build/roullete.ko

ロードできちゃいます!
ちゃんとロードされているか、確認してみましょう。

$ lsmod
Module                  Size  Used by
roulette              106496  0

おるやん!
dmesgにroulette driverがログを出力するので、確認します。Panicする確率は1/100のようです!ほとんど死なないですね!

$ dmesg | tail -n 10
...
[16837.958611] Registered kernel-roulette with major device number 246
[16837.958613] Run /bin/mknod /dev/kernel-roulette c 246 0
[16838.468004] Panic probability: 1/100

roulette driverの指示通り、キャラクタデバイスファイルを作成します。

$ sudo /bin/mknod /dev/kernel-roulette c 246 0

作成したデバイスファイルをreadすると、1/100の確率で死にます!
catして、その時が来るのを待ちます。

$ cat /dev/kernel-roulette
Survived... sampled value is 79, which is >= 1
$ cat /dev/kernel-roulette
Survived... sampled value is 1, which is >= 1

死ねない!
死ぬ確率は、driverロード時にランダムで決定されるので、面倒なのでロードし直します。

$ sudo rmmod build/roulette.ko
$ sudo insmod build/roulette.ko

さて。

$ dmseg | tail -n 10
...
[17574.935478] Registered kernel-roulette with major device number 246
[17574.935481] Run /bin/mknod /dev/kernel-roulette c 246 0
[17575.447283] Panic probability: 49/100

今度はすぐ死ねそうですね。

$ cat /dev/kernel-roulette
Survived... sampled value is 86, which is >= 49
$ cat /dev/kernel-roulette
Segmentation fault

お、2回目でセグメンテーションフォールトが発生しました。何が起こっているか、dmesgで確認してみましょう。

$ dmesg | tail -n 100
[17633.158768] Rust panic @ src/roulette.rs:33
[17633.158774] Boom!
[17633.158793] ------------[ cut here ]------------
[17633.158807] Kernel BUG at ffffffffc04bb24e [verbose debug info unavailable]
[17633.158822] invalid opcode: 0000 [#1] SMP 
# トレース省略
[17633.172695] fbcon_switch: detected unhandled fb_set_par error, error code -16
[17633.173673] fbcon_switch: detected unhandled fb_set_par error, error code -16
[17633.174673] ---[ end trace ff2742f73b763c6d ]---

まずRustでpanicが発生していて、その後は、おなじみのLinux kernelのトレース情報です。
KernelのBUG()が呼び出されて、invalid opcodeになっていますね。

kernel-roullete作者の恩情で、panic()ではなく、BUG()を呼ぶようにしてくれているため、Kernelがpanicしてシステムが止まるような事態は発生しません。

ちなみに、この後、roullete driverをrmmodできなくなります。
原因は解明できていないです。

$ sudo rmmod roulette
rmmod: ERROR: Module roulette is in use

ソースコード解析

さて、Linux kernel driverの一部として、Rustを使うにはどのようなトリックが必要なのでしょうか?
ここまでで動作を確認したkernel-rouletteのソースコードを解析してみましょう。
ソースファイルやMakefileに丁寧にコメントが添えられているため、非常に解析しやすいソースコードでした。作者の技術レベルの高さが伺えます。
紹介するソースコードは、1.31.0-nightlyでビルドできるように修正を加えたものです。

構成

まずは、ディレクトリ構成から全体像を把握していきます。shim.ckbuild.mkがある以外は、普通のRustプロジェクト、という感じでしょうか。

$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── kbuild.mk
├── LICENSE.txt
├── Makefile
├── README.md
├── src
│   ├── io
│   │   └── mod.rs
│   ├── lang.rs
│   ├── lib.rs
│   ├── mem
│   │   └── mod.rs
│   ├── roulette.rs
│   └── shim.c
├── x86_64-unknown-none-gnu.json
└── Xargo.toml

3 directories, 14 files

Cargo.tomlを見てみましょう。

Cargo.toml
[package]
name = "kernel-roulette"
version = "0.1.0"
authors = ["Souvik Banerjee <souvik@souvik.me>"]

[lib]
name = "roulette"
crate-type = ["staticlib"]

[dependencies]
lazy_static = { version = "1.0.0", features = ["spin_no_std"] }
spin = "0.4.9"
rand = { version = "0.4.2", default-features = false }
rlibc = "1.0"

crate-typeがstaticlibになっています。これはRustのアーカイブファイルを作って、C言語のオブジェクトファイルとリンクするアレですね、きっと。
検討がついたところで、Makefileを見ましょう。kernel module名はrouletteです。

Makefile
# Configuration that will be passed to sub-make

# Name of the kernel module
export KERNEL_MODULE := roulette

# Path to Linux kernel headers
export KERNEL_BUILD_PATH := /lib/modules/$(shell uname -r)/build

# Find all C and Rust source files
export C_FILES := $(shell find src/ -type f -name "*.c")
export RUST_FILES := $(shell find src/ -type f -name "*.rs") Cargo.toml Cargo.lock

# Define the architecture; this will be used for the LLVM target specification
export UTS_MACHINE = x86_64

# The Rust compiler and cross-compiler
export CARGO=cargo
export XARGO=xargo

# A JSON file specifying an LLVM target
export LLVM_TARGET_SPEC=$(UTS_MACHINE)-unknown-none-gnu.json

# Top-level project directory
export BASE_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))

# The build directory
export BUILD_DIR := build

# The Makefile that is copied to $(BUILD_DIR)
export KBUILD := kbuild.mk

all: $(BUILD_DIR)/Makefile Makefile
# The kbuild makefile has been copied to $(BUILD_DIR), so now we can invoke kbuild from
# the kernel headers.
    $(MAKE) -C "$(KERNEL_BUILD_PATH)" M="$(BASE_DIR)/$(BUILD_DIR)" modules

$(BUILD_DIR)/Makefile : $(KBUILD)
    @mkdir -p "${BUILD_DIR}/src"
    cp "$(KBUILD)" "$(BUILD_DIR)/Makefile"

clean:
# cleanup is really simple, we just blow away the $(BUILD_DIR) and call `xargo clean`
    rm -rf "$(BUILD_DIR)"
    xargo clean

kbuild.mkをMakefileにリネームした上で、buildディレクトリに移動して、kbuild.mkをmakeコマンドで叩いています。
kbuildで引き継ぐ環境変数も多数定義していますね。
kbuild.mkを見てみます。

kbuild.mk
# Define the Rust target. Xargo will create an ar archive file with the
# following name in the target folder
RUST_TARGET := lib$(KERNEL_MODULE).a

# Enumerate the object files that the C files will compile to
C_OBJECTS := $(patsubst %.c,%.o,$(C_FILES))

# Tell kbuild which files to build
obj-m := $(KERNEL_MODULE).o

# Tell kbuild where the source files are
src := $(BASE_DIR)

# The kernel module will be linked from the C object files and the Rust archive
# The order is important: C objects must come first
$(KERNEL_MODULE)-objs := $(C_OBJECTS) $(RUST_TARGET)

# Strip unused symbols from the input object file
EXTRA_LDFLAGS += --gc-sections --entry=init_module --undefined=cleanup_module
EXTRA_LDFLAGS += $(if $(RELEASE),--strip-all)

# Fix file paths (since this script will be run from the kbuild's working directory)
C_FILES    := $(foreach filepath,$(C_FILES)   ,$(BASE_DIR)/$(filepath))
RUST_FILES := $(foreach filepath,$(RUST_FILES),$(BASE_DIR)/$(filepath))
LLVM_TARGET_SPEC := $(foreach filepath,$(LLVM_TARGET_SPEC),$(BASE_DIR)/$(filepath))

# Determine target directory of cargo's module build
CARGO_BUILD_DIR := $(BASE_DIR)/target/$(UTS_MACHINE)-unknown-none-gnu/$(if $(RELEASE),release,debug)

$(obj)/$(RUST_TARGET): $(RUST_FILES) $(LLVM_TARGET_SPEC) $(BASE_DIR)/$(KBUILD)
# We set RUST_TARGET_PATH because of a bug in cargo/xargo/rustc: the --target flag is relative to the current working
# directory but subsequent invokations of cargo/xargo/rustc might change their working directory. Setting
# RUST_TARGET_PATH ensures that the compiler can find the LLVM target specification.
# We also have to `cd` into $(BASE_DIR) since we are currently in the kernel headers directory.
    (cd $(BASE_DIR); env RUST_TARGET_PATH=$(BASE_DIR) $(XARGO) rustc $(if $(RELEASE),--release) $(if $(VERBOSE),--verbose) --target $(UTS_MACHINE)-unknown-none-gnu -- -C code-model=kernel -C relocation-model=static -C panic=abort)
# After the archive is compiled, copy it to the build directory
    cp "$(CARGO_BUILD_DIR)/$(RUST_TARGET)" $(obj)

$(obj)/%.c : $(BASE_DIR)/%.c $(BASE_DIR)/$(KBUILD)
# KBUILD_CFLAGS is automatically generated by kbuild
    $(CC) $(KBUILD_CFLAGS) -c $(BASE_DIR)/$*.c -o $*.o

そこそこボリュームがあるのですが、下を見ると、先ほどの予測が的中していることが分かります。

# The kernel module will be linked from the C object files and the Rust archive
# The order is important: C objects must come first
$(KERNEL_MODULE)-objs := $(C_OBJECTS) $(RUST_TARGET)

kernel moduleを作るためのオブジェクトファイルは、C言語とRust、それぞれ個別で作られています。
C言語、普通にkernel moduleのオブジェクトファイル作っています。

$(obj)/%.c : $(BASE_DIR)/%.c $(BASE_DIR)/$(KBUILD)
# KBUILD_CFLAGS is automatically generated by kbuild
    $(CC) $(KBUILD_CFLAGS) -c $(BASE_DIR)/$*.c -o $*.o

Rust、relocation-modelをstaticにする、panicをabortにする、以外は普通のクロスビルドでしょうか。

    (cd $(BASE_DIR); env RUST_TARGET_PATH=$(BASE_DIR) $(XARGO) rustc $(if $(RELEASE),--release) $(if $(VERBOSE),--verbose) --target $(UTS_MACHINE)-unknown-none-gnu -- -C code-model=kernel -C relocation-model=static -C panic=abort)

RustとC言語ソースファイルの構成をざっと見てみましょう。

├── src
│   ├── io
│   │   └── mod.rs
│   ├── lang.rs
│   ├── lib.rs
│   ├── mem
│   │   └── mod.rs
│   ├── roulette.rs
│   └── shim.c
ファイル 説明
shim.c Linux kernel driver。init()やopen()でRust関数を呼び出す
io/mod.rs printkのラッパーでprint!マクロを定義
mem/mod.rs kmallocのラッパーでヒープアロケータを実装
lang.rs panic_handlerおよびalloc_error_handlerを定義
lib.rs init(), exit()で呼ばれる関数を定義
roulette.rs 一定確率でpanicする処理を定義

ソースコード

では、コードを見ていきましょう。

初期化/終了処理

driverロード

まずは、初期化を追っていきます。
kernel driverのエントリポイントなので、C言語からです。
最後の一行以外は、普通のkernel driverです。
キャラクタデバイスを登録しています。これは後ほど改めて解説します。

shim.c
/*
 * The entry points in C
 */
static int _mod_init(void) {

  // Register a character device
  rl_dev_major_num = register_chrdev(0 /* allocate a major number */, rl_device_name, &rl_driver_fops);

  if (rl_dev_major_num < 0) {
    printk(KERN_ALERT "failed to register character device: got major number %d\n", rl_dev_major_num);
    return rl_dev_major_num;
  }

  printk(KERN_INFO "Registered %s with major device number %d\n", rl_device_name, rl_dev_major_num);
  printk(KERN_INFO "Run /bin/mknod /dev/%s c %d 0\n", rl_device_name, rl_dev_major_num);

  return rust_mod_init();
}
init(_mod_init);

rust_mod_init()は、Rustの関数呼び出しです。shim.c内でexternされています。

shim.c
/*
 * The entry points for the kernel module in Rust. We define
 * entry points in C for the module_init and module_exit macros.
 */
extern int rust_mod_init(void);

Rust側の実装を見てみましょう。
no_mangleでマングルされないようにしている以外は、普通のRustに見えますね。

lib.rs
// Entry points
#[no_mangle]
pub extern "C" fn rust_mod_init() -> i32 {
    print!("Panic probability: {}/{}\n", CONFIG.lock().chance, MAX_RAND);
    0
}

ただ、裏では色々と動いています。
順番に見ていきましょう。
print!マクロ、ヒープアロケータ、CONFIG、の順番に見ていきます。

print!マクロ

まずは、print!マクロです。
標準ライブラリを使っていないため、printk()をラップする仕組みを自作しています。

io.rs
/// Like the `print!` macro in the standard library, but calls printk
#[allow(unused_macros)]
macro_rules! print {
    ($($arg:tt)*) => ($crate::io::print(format_args!($($arg)*)));
}

このcrateのioモジュールのprint()を読んでいますね。
つまり、同io.rs内のprint()関数です。

io.rs
pub fn print(args: fmt::Arguments) {
    use core::fmt::Write;
    let mut writer = PRINTK_WRITER.lock();
    writer.write_fmt(args).unwrap();
    writer.flush();
}

PRINTK_WRITERのロックを取得し、writerが実装しているWrite traitのwrite_fmt()を呼び出しています。
このPRINTK_WRITERは、Mutex付きのKernelDebugWriterです。
lazy_static!マクロを利用しているので、最初に使われるときに初期化されます。

io.rs
lazy_static! {
    /// Used by the `print!` and `println!` macros.
    pub static ref PRINTK_WRITER: Mutex<KernelDebugWriter> = Mutex::new(KernelDebugWriter::default());
}

KernelDebugWriterは、最終的に、C言語のprintk()関数を呼び出します。
printk()関数に渡す文字列は、Vec<u8>でバッファリングします。

io.rs
#[derive(Default)]
pub struct KernelDebugWriter {
    buffer: Vec<u8>,
}

extern "C" {
    // printk()のラッパー
    fn puts_c(len: u64, c: *const u8);
}

// `buffer`を拡張して、`buffer`に文字列を追加する。
impl fmt::Write for KernelDebugWriter {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        // Add the bytes from `s` to the buffer
        self.buffer.extend(s.bytes());
        Ok(())
    }
}

impl KernelDebugWriter {
    // 実際に文字を出力する。出力が終わると、`buffer`をクリアする。
    fn flush(&mut self) {
        // Call c_puts with the string length and a pointer to its contents
        unsafe { puts_c(self.buffer.len() as u64, self.buffer.as_ptr() as *const u8) };
        self.buffer.clear();
    }
}

Vecを使っているので、ヒープアロケータが必要です。これは後で説明します。
write_str()でbufferに文字列のデータを追加し、flush()でC言語のput_c()を呼び出します。

C言語の世界に戻ります。put_c()はprintk()のラッパーですね。

shim.c
/*
 * A utility function to print a string.
 * NOTE: `str` is not necessarily NULL-terminated.
 */
void puts_c(u64 length, char* str) {
  printk(KERN_DEBUG "%.*s", (int)length, str);
}

これで、Rustからkernelログを出力できる仕組みがわかりました。

ヒープアロケータ

kmalloc()をラップして、Vecの利用を解禁します。

Rustのベアメタルヒープアロケータについては、こちらの記事で書きました。詳細に興味があればご覧下さい。
Redox Slab Allocatorで学ぶRustベアメタル環境のヒープアロケータ

print!マクロでは、文字列をVec<u8>のbufferでバッファリングして、printk()に出力を委譲していました。
Vecを使うためには、ヒープ領域を確保する必要があります。ヒープ領域を使うためには、GlobalAlloc traitを実装したアロケータを#[global_allocator]アトリビュートを付けて定義します。
アロケータは次のように定義されています。

lib.rs
// Set up the global allocator
#[global_allocator]
static ALLOCATOR: mem::KernelAllocator = mem::KernelAllocator::new();

KernelAllocatorの内部を追っていきます。重要なのはGlobalAlloc traitの実装です。

mem/mod.rs
#[derive(Default)]
pub struct KernelAllocator;

impl KernelAllocator {
    pub const fn new() -> Self {
        Self {}
    }
}

// Use shim functions to avoid hardcoding the GFP_KERNEL constant
#[allow(dead_code)]
extern "C" {
    fn kmalloc_c(size: usize) -> *mut u8;
    fn kfree_c(ptr: *mut u8);
    fn krealloc_c(ptr: *mut u8, size: usize) -> *mut u8;
}

unsafe impl GlobalAlloc for KernelAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        // A side effect of the buddy allocator is that allocations are aligned to
        // the power-of-two that is larger than the allocation size. So if the
        // request needs to be aligned to something larger than the allocation size,
        // we can just pass max(size, align) to kmalloc to get something reasonable
        // at the cost of a few extra wasted bytes.
        use core::cmp::max;
        let p = kmalloc_c(max(layout.size(), layout.align()));
        if p.is_null() {
            0 as *mut u8            
        } else {
            p
        }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
        kfree_c(ptr);
    }
}

alloc()では、kmalloc_c()を呼び出しています。これはC言語のkmallocのラッパーです。

shim.c
/*
 * Re-exported memory management functions
 */
void* kmalloc_c(size_t size) {
  return kmalloc(size, GFP_KERNEL);
}

この薄いラッパー1枚を作るだけで、VecやBoxなどの便利機能が解禁となるわけです。これは朗報ですね!

CONFIG

Rust側の初期化処理を再掲載します。

lib.rs
// Entry points
#[no_mangle]
pub extern "C" fn rust_mod_init() -> i32 {
    print!("Panic probability: {}/{}\n", CONFIG.lock().chance, MAX_RAND);
    0
}

CONFIG.lock().chanceはkernel panicが発生する確率です。どのように初期化されているか見ていきましょう。

roullete.rs
lazy_static! {
    // CONFIGはRouletteConfigをMutexで包んでいます
    pub static ref CONFIG: Mutex<RouletteConfig> = Mutex::new(RouletteConfig::new());
}

#[derive(Default)]
pub struct RouletteConfig {
    pub chance: u8,
}

impl RouletteConfig {
    pub fn new() -> Self {
        // MIN_RAND(0)からMAX_RAND(100)の範囲で乱数を生成します
        let chance = RNG.lock().gen_range(MIN_RAND, MAX_RAND);
        RouletteConfig { chance: chance }
    }
}

正確には、0~99の範囲で乱数を生成します。APIの解説は次の通りです。

Generate a random value in the range [low, high), i.e. inclusive of low and exclusive of high.

RNGは次のように定義されています。JitterRngという乱数生成器をMutexで包んでいます。

roullete.rs
lazy_static! {
    static ref RNG: Mutex<rand::JitterRng> = Mutex::new(rand::JitterRng::new_with_timer(|| unsafe { nanosecond_timer_c() }));
}

JitterRngはCPU実行時間のジッタとメモリアクセスのジッタとを使った、乱数ジェネレータ、とのことです。
new_with_timer()でインスタンスを生成しています。これは、no_std環境でJitterRngを使う場合のAPIです。

jitter.rs
pub fn new_with_timer(timer: fn() -> u64) -> JitterRng

Create a new JitterRng. A custom timer can be supplied, making it possible to use JitterRng in no_std environments.

new_with_timer()で渡しているクロージャでは、C言語のnanosecond_timer_c()を呼んでいます。
この中ではjiffiesを取得します。

shim.c
/*
 * This isn't really a nanosecond timer but its close enough.
 */
u64 nanosecond_timer_c(void) {
  return get_jiffies_64();
}

少し整理すると、RNGはMutexで包まれた乱数ジェネレータです。
RNG.gen_range(MIN_RAND, MAX_RAND)を呼び出すと、クロージャでjiffiesを取得し、0から99の範囲で乱数を生成します。
CONFIGはインスタンス生成時に、RNG.get_range()を呼び出し、panic確率であるchanceを初期化します。

終了処理は特に難しいことをしていないので、省略します。

ロシアンルーレット実行

それでは、肝心のロシアンルーレット実行部分を見ていきます。
記事の最初の方でやった通り、/dev/kernel-rouletteをcatしたとき(より正確にはopen()呼び出し時)に乱数を生成し、chanceより小さな値が出るとpanicします。

$ cat /dev/kernel-roulette
Survived... sampled value is 86, which is >= 49
$ cat /dev/kernel-roulette
Segmentation fault

残念なことに、デバイスファイルの操作はほとんどC言語で処理しています。
まず、キャラクタデバイスですが、_mod_init()で登録していました。

shim.c
static int _mod_init(void) {

  // Register a character device
  rl_dev_major_num = register_chrdev(0 /* allocate a major number */, rl_device_name, &rl_driver_fops);
...

キャラクタデバイスドライバには、open(), release(), read()が実装されています。

shim.c
static int rl_device_open(struct inode* inode, struct file* filp);
static int rl_device_release(struct inode* inode, struct file* filp);
static ssize_t rl_device_read(struct file* filp,    /* see include/linux/fs.h   */
                              char __user* buffer,  /* buffer to fill with data */
                              size_t length,    /* length of the buffer  */
                              loff_t* offset /* the file offset */);
static struct file_operations rl_driver_fops = {
  .read = rl_device_read,
  .open = rl_device_open,
  .release = rl_device_release,
};
static int rl_dev_major_num = 0;
static const char rl_device_name[] = "kernel-roulette";

release()とread()からはRustを呼んでいないため、open()だけ解説します。
Rustを呼んでいるのは、sample()の部分です。

shim.c
static int rl_device_open(struct inode* inode, struct file* filp) {
  u8 sampled;
  struct rl_state* state;
...
  // Get a sample from Rust
  sampled = sample(); // may panic!
...
  // read()で読みだすデータをここで作成します
  // Format data and store in string
  snprintf(state->data, MAX_BUFFER_SIZE, "Survived... sampled value is %d, which is >= %d\n", sampled, get_chance());
...
  // Set private_data to the state we allocated
  filp->private_data = state;
  return SUCCESS;
}

Rustのsample()は次の通りです。

roulette.rs
#[no_mangle]
pub extern "C" fn sample() -> u8 {
    let sampled = RNG.lock().gen_range(MIN_RAND, MAX_RAND);
    if sampled < CONFIG.lock().chance {
        panic!("Boom!");
    } else {
        sampled
    }
}

新たに生成した乱数が、chanceより小さければ、"Boom"を出力して、panicします。
panic_handlerは次のようになっています。

lang.rs
#[panic_handler]
#[no_mangle]
pub fn rust_begin_panic(info: &PanicInfo) -> ! {
    // Print the file and line number
    if let Some(location) = info.location() {
        println!("Rust panic @ {}:{}",
            location.file(), location.line());
    }

    // Print the message and a newline
    if let Some(message) = info.message() {
        println!("{}", message);
    }

    unsafe {
        // In a real kernel module, we should use abort() instead of panic()
        abort() // replace with panic_c() if you want
    }
}

最終的にC言語のabort()を呼んでいます。abort()はBUG()を呼び出します。

shim.c
/*
 * Define the abort() function. We use the BUG() macro, which generates a ud2 instruction.
 */
void abort(void) {
  BUG();
}

ちなみに、abort()の代わりに、panic_c()を呼び出すと…

shim.c
void panic_c(void) {
  panic("Panic!");
}

本当にフリーズします。

以上で主要な処理の解析は完了です。
いかがでしたでしょうか。

まとめ

  • Linux kernel moduleからRustを呼べます。Rustをライブラリでビルドしておき、C言語のkernel module objectとリンクするだけです。
  • kmalloc()のラッパーを用意することで、Vecなど、ヒープ領域を使う機能が使えます。
  • printk()のラッパーを用意することで、print!マクロを作って、標準ライブラリのように使えます。
  • 残念ながらデバイスファイルの操作はほとんどC言語でした。

参考

kernel-roulette

30
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
11