Writing a simple linux kernel module with rust from the ground up

2021-04-27

Historically, if we wanted to write a kernel module, our only choice was to deal with the C libraries the kernel provides for such things. However, a recent RFC adds limited support for rust as an alternative.

Setting up the toolchain

If you have a rust toolchain installed on your machine, then you probably also have the llvm toolchain and rustup installed, so I won't get into that.

Then, we need the actual rust-for-linux kernel tree. It is a lengthy download over ssh so I recommend downloading as a zip. We will unzip it in our directory of choice. The next step is configuring our build for rust support. The process is documented throughly in Documentation/rust/quick-start.rst The next step is to build the kernel.

$ make LLVM=1 -j $(nproc)

As of the day this post was written, nightly rust is needed for building. We also need the standard library for compiling core and alloc.

$ rustup default nightly-2021-02-20
$ rustup component add rust-src

Writing the code

The rust-for-linux project provides a standard toolset for writing a kernel module. If you are familiar with the hello world module described in the book Linux Device Drivers (which if you haven't read, you should probably check out before proceeding) then this will look familiar.

First, for the hello_world.rs file:

//! Hello world kernel module.

#![no_std]
#![feature(allocator_api, global_asm)]

use kernel::prelude::*;

module! {
    type: HelloWorld,
    name: b"hello_world",
    author: b"Carlos Carral",
    description: b"Hello world kernel module",
    license: b"GPL v2",
    params: {
    },
}

struct HelloWorld;

impl KernelModule for HelloWorld {
    fn init() -> KernelResult<Self> {
        pr_info!("Hello world, from the kernel!\n");

        Ok(HelloWorld {})
    }
}

impl Drop for HelloWorld {
    fn drop(&mut self) {
        pr_info!("Goodbye!\n");
    }
}

So far so good. This is almost the exact same source code as the one in samples/rust/rust_minimal.rs.

Next comes the makefile.

obj-m += hello_world.o

# Simply expanded variable ":="
KERNELDIR :=/path/to/rust-for-linux/linux

# Shell command expands to current working directory
PWD := $(shell pwd)

all: 
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:  
	rm -rf *.o *~ core .depend *.mod.o .*.cmd *.ko *.mod.c *.mod
	rm -rf *.tmp_versions *.markers .*.symvers modules.order
	rm -rf Module.symvers
	rm -rf *.rmeta 

We run make on our current working directory and if all went well, then the output of ls should look something like this.

$ make LLVM=1
$ ls -1
hello_world.ko
hello_world.mod
hello_world.mod.c
hello_world.mod.o
hello_world.o
hello_world.rs
libhello_world.rmeta
Makefile
modules.order
Module.symvers

Great! Now what?

Lets see what information we can get out of hello_world.ko

$ modinfo hello_world.ko

filename:       /home/carlos-II/Proyectos/rust/hello-world-kmod/hello_world.ko
author:         Carlos Carral
description:    Hello world kernel module
license:        GPL v2
depends:        
retpoline:      Y
name:           hello_world
vermagic:       5.12.0-rc4rust1 SMP mod_unload 

All seems well. Now let's try to actually load it. You can install the kernel image into your system with make install or you can use some sort of virtual machine to load it. For this tutorial I used eudyptula-boot.

Loading our module

$ insmod hello_world.ko 

An we unload it with rmmod

$ rmmod hello_world

Now, when we run dmesg, we can actually see how our module was loaded into the kernel and then unloaded.

dmesg

An this is how we compile and load a rust kernel module.