Rust Kernel Module: Hello World

Wu Yu Wei published on
9 min, 1636 words

In previous post, we set up our environment to develop Linux kernel module in Rust. Now we can kick start our journey. Like every programming tutorial, we must start with a "Hello World" example. It is the law!

While there are already many samples under samples/rust directory, I'll still write a simple example to showcase how to add a module to our compiled kernel. Once you learn how to do it, you can add as many as you want.

The Simplest Module

To add a kernel module, there are a few files need to configure since we choose to compile Linux kernel on our own. But this is safer approach IMHO. This doesn't need to be the final workflow for you, but you should follow the safety measurement it obeys. First of all, let's add additional option to build a new kernel module called rust_hello. Try to add following configurations and files under other modules:

  1. In samples/rust/Kconfig:
config SAMPLE_RUST_HELLO
	tristate "Hello"
	help
	  This option builds the Rust hello module sample.

	  To compile this as a module, choose M here:
	  the module will be called rust_hello.

	  If unsure, say N.
  1. In samples/rust/Makefile:
obj-$(CONFIG_SAMPLE_RUST_HELLO)	+= rust_hello.o
  1. Add a new rust file samples/rust/rust_hello.rs:
// SPDX-License-Identifier: GPL-2.0

//! Rust hello sample.

use kernel::prelude::*;

module! {
    type: RustHello,
    name: b"rust_hello",
    author: b"Rust for Linux Contributors",
    description: b"Rust hello sample",
    license: b"GPL v2",
}

struct RustHello {
    message: String,
}

impl kernel::Module for RustHello {
    fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
        pr_info!("Rust hello sample (init)\n");
        pr_info!("Am I built-in? {}\n", !cfg!(MODULE));

        Ok(RustHello {
            message: "on the heap!".try_to_owned()?,
        })
    }
}

impl Drop for RustHello {
    fn drop(&mut self) {
        pr_info!("My message is {}\n", self.message);
        pr_info!("Rust hello sample (exit)\n");
    }
}

Build and Run the Kernel again

This is in fact just rust_minimal example with renaming. It's okay to skip adding this if you just want to learn how to do it instead of practicing yourself. To enable the module, run make menuconfig and toggle it to M in Kernel hacking --> Sample kernel code --> Rust samples --> Hello. Next, we have to build it and include it into our file system:

  1. Build Linux kernel again. But this time, it should just build the kernel module we added:
make LLVM=1 -j$(nproc)
  1. Build new image to include our kernel module. In .github/workflows/qemu-initramfs.desc, add another line under other modules and then build it again:
file /rust_hello.ko samples/rust/rust_hello.ko 0755 0 0
usr/gen_init_cpio .github/workflows/qemu-initramfs.desc > qemu-initramfs.img
  1. Also add another command under other modules in .github/workflows/qemu-init.sh. This script is what we tell busybox to do upon initialization:
busybox modinfo rust_hello.ko
busybox insmod rust_hello.ko
busybox lsmod
busybox rmmod rust_hello.ko

Run the kernel via qemu again. This time you should see the log from the kernel we just built.

Command Helpers and Tools

We'll introduce a few commands relate to kernel module. modinfo will show the info of our kernel module. lsmod will show what modules are already loaded within your current kernel. Modules are stored within /proc/modules, so you can also see them with cat /proc/modules. inmosd will load the module you provide, and rmmod will remove it. If you follow this guide from the start, you should see the logs about kernel module when running qemu. But if you run the kernel natively, you can run journalctl to get the logs. Or if you are debugging with GDB like we mentioned before, run lx-dmesg in debugger to see them.

Now we know how to create, compile, and install the new kernel module. Let's see how the kernel module actually works. Before we deep dive into the code, please allow me to introduce a few tools and commands that will benefit us significantly. The first is documentation, Linux can build and read rustdoc for its kernel crate with this command:

make LLVM=1 rustdoc
xdg-open rust/doc/kernel/index.html

The documentation is pretty comprehensive, I believe many people can even read it themselves and learn even more beyond our guides could teach. Next is formatting and lints. Linux also supports rustfmt and clippy, so you shouldn't be afraid of inconsistent code style. Rust will guide you how to comply:

make LLVM=1 rustfmt
make LLVM=1 CLIPPY=1

Finally, we have language server protocol support. Yes! Linux supports rust_analyzer too! Run the following command to create rust-project.json configuration and you will gain full LSP support on whatever IDE you uses:

make LLVM=1 rust-analyzer

Macros and Types from kernel Crate

Alright! Everything is set! Let's see how does the code work. Unlike normal Rust program, we can't simply use standard library std. But we can still import core and alloc crates, types require heap allocation like String is still possible to use. And the most important crate is probably the kernel crate. It cantains kernel APIs that have been ported or wrapped for usage by Rust code in the kernel and is shared by all of them. Like most rust crates, it provides a prelude module for us to import some common items.

The first one we must add is module! macro. This is the macro to declare a kernel module. You can see its description from documentation or even just hover it via rust-analyzer. You'll learn its usage, example, and all supported argument and parameter types. There are three argument types you must add to complete the macro:

  • type: The type we are going to implement the Module trait which we will demonstrate soon.
  • name: Your kernel module name.
  • license: Your kernel module license. Some examples include "GPL", "GPL v2", "GPL and additional rights", "Dual BSD/GPL", "Dual MIT/GPL", "Dual MPL/GPL" and "Proprietary".

To add logics for the kernel module, we can define any type as long as it implements Module trait. The trait's signature looks like this:

pub trait Module: Sized + Sync {
    fn init(name: &'static CStr, module: &'static ThisModule) -> Result<Self>;
}

The method init is equivalent to the module_init macro in the C API. We create the instance via this method. Since Rust ownership can manage our memory properly, it's not necessary to implement Drop trait which is equivalent to the module_exit macro in the C API. In our hello world example, we still do this to show when will the module exit.

Notice we use pr_info macro instead of println, but the usage is pretty similar. This maps to kernel's print macros. It follows most log level for different conditions. There are also other print macros like pr_err, pr_alert.

Passing Command Line Arguments to a Module

Modules can take command line arguments too, but not from env::args() you might used to. To allow arguments to be passed to your module, declare the parameter types in modules! macro. Let's see the example from samples/rust/rust_module_parameters.rs:

// SPDX-License-Identifier: GPL-2.0

//! Rust module parameters sample.

use kernel::prelude::*;

module! {
    type: RustModuleParameters,
    name: b"rust_module_parameters",
    author: b"Rust for Linux Contributors",
    description: b"Rust module parameters sample",
    license: b"GPL v2",
    params: {
        my_bool: bool {
            default: true,
            permissions: 0,
            description: b"Example of bool",
        },
        my_i32: i32 {
            default: 42,
            permissions: 0o644,
            description: b"Example of i32",
        },
        my_str: str {
            default: b"default str val",
            permissions: 0o644,
            description: b"Example of a string param",
        },
        my_usize: usize {
            default: 42,
            permissions: 0o644,
            description: b"Example of usize",
        },
        my_array: ArrayParam<i32, 3> {
            default: [0, 1],
            permissions: 0,
            description: b"Example of array",
        },
    },
}

struct RustModuleParameters;

impl kernel::Module for RustModuleParameters {
    fn init(_name: &'static CStr, module: &'static ThisModule) -> Result<Self> {
        pr_info!("Rust module parameters sample (init)\n");

        {
            let lock = module.kernel_param_lock();
            pr_info!("Parameters:\n");
            pr_info!("  my_bool:    {}\n", my_bool.read());
            pr_info!("  my_i32:     {}\n", my_i32.read(&lock));
            pr_info!(
                "  my_str:     {}\n",
                core::str::from_utf8(my_str.read(&lock))?
            );
            pr_info!("  my_usize:   {}\n", my_usize.read(&lock));
            pr_info!("  my_array:   {:?}\n", my_array.read());
        }

        Ok(RustModuleParameters)
    }
}

impl Drop for RustModuleParameters {
    fn drop(&mut self) {
        pr_info!("Rust module parameters sample (exit)\n");
    }
}

To add a parameter, we declare its type, default value, permission, and description. As we already saw the documentation of module! macro, it supports most primitive types. It can also support arrays via const generic ArrayParam<T, N>. At the time of this post, const generic only support numeric types, but this should cover most of our use cases.

In the init script .github/workflows/qemu-init.sh, insmod will fill the variables with any command line arguments that are given. The parameter that doesn't get the value from here will set to default value. We can pass the argument like this:

busybox insmod rust_module_parameters.ko \
    my_bool=n \
    my_i32=345543 \
    my_str=đŸ¦€mod \
    my_usize=84 \
    my_array=1,2,3
busybox  rmmod rust_module_parameters.ko

To read the values, some types require scoped lock, we can get it from module.kernel_param_lock(). Then you'll retrieve values from read() method. The lock will be dropped once it's out of the scope. Kernel parameters are usually used to have the module variable’s default values set, like a port or IO address. If the variables contain the default values, then perform auto-detection (explained elsewhere). Otherwise, keep the current value. This will be made clear later on.

Conclusion

And that's it! Now you should have the basic idea about how to write and build the kernel module. Feel free to play around with config files, scripts, and kernel modules either it's existing one or the one you create. The documentation is also comprehensive enough, I'm confident many people can even learn it themselves starting from here. But don't worry if you don't know how to proceed. I'll publish more posts in the future. Next time we will try to write another simple kernel module: Character Device drivers.