Skip to main content

Empty Module

The first exercise is to write an empty module. The module will print a message when it loads and a message when it is unloaded.

Tree Structure​

Kernel module are split imn two cageories:

  • in tree - modules that reside within the kernel's source code tree
  • out of tree - modules that are built outside the kernel

We will build out of tree modules. The folder structure of a kernel module is different from the one of a program. The kernel uses its own tools to build the module, while applications use cargo. The kernel uses make and Kbuild.

Kbuild File​

The Kbuild file defines the object files that the module provides. In our case, the object file will be called empty.o.

# SPDX-License-Identifier: GPL-2.0

obj-m := empty.o

Makefile​

We need to use a special makefile that connects to the kernel's source build infrastructure.

# SPDX-License-Identifier: GPL-2.0

# Ask the rust compiler to rewrite the file names that start with ../ to ./
# when dispaying errors, warnings and notes.
#
# Example: ../source.rs will be displayed as source.rs
#
# This is needed as we use the ./build folder for compiling and the compiler
# considers the source files to be in ../
export KRUSTFLAGS := --remap-path-prefix=../=

KDIR ?= /lib/modules/`uname -r`/build

default:
echo $$RUSTFLAGS
$(MAKE) -C $(KDIR) LLVM=1 M=$$PWD MO=$$PWD/build

clean:
$(MAKE) -C $(KDIR) M=$$PWD MO=$$PWD/build clean

rust-analyzer:
$(MAKE) -C $(KDIR) M=$$PWD rust-analyzer

This makefile defines three important targets:

  • default - that build the module
  • clean - that cleans the module
  • rust-analyzer - that build the rust-project.json file used by rust-analyzer.

The makefile assumes that we will set the $KDIR variable to point to the kernel's soutrce code. In our case, this variable will be similar to ../linux-6.18-rc5/.

warning

Please make sure you export this variable before running any make targets.

export KDIR="../linux-6.18-rc5"

You can allways define the variable in the make command line: make KDIR=../linux-6.18-rc5 ....

The KRUSTFLAGS

Source Code​

The main source code file of our module is empty.rs. It has to have the same name ast the object file defined in KBuild.

Enabling Rust Analyzer​

To help us with code completion, we want to activate rust-analyzer. As this is not a standard rust application, we have to run make rust-analyzer to obtgain the rust-project.json file which rust-analyzer can use instead of Cargo.toml.

The Module​

Printing to the kernel console is done using the pr_* macros such as pr_info!, pr_error!, pr_warn!, pr_debug! and pr_alert,

A module is declared using the module! macro. It defines the name, authors, description and the license of the module and the data type that implements the Module and Drop trais. In this exmple, this is the Empty type.

The Module::init function may return an Error code if the module cannot be loaded. The kernel will try several times and print the error if it still fails.

// SPDX-License-Identifier: GPL-2.0

//! Rust Empty Module

use kernel::prelude::*;

module! {
type: Empty,
name: "empty",
authors: ["Rust Workshop"],
description: "Rust empty sample",
license: "GPL",
}

struct Empty;

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

Ok(Empty)
}
}

impl Drop for Empty {
fn drop(&mut self) {
pr_info!("Empty Module (exit)\n");
}
}

Build the module​

To build the module we use the make command. This will build all the Rust code and all the necessary C glue code and output the kernel object file build/empty.ko. This is actually a static relocatable ELF file.

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

Loading the module​

To load the module into the kernel we have to perform the following steps:

  1. copy the empty.ko in to $INIT_RAM_FS
  2. rebuild the RAM disk so that it includes the module
  3. Boot the kernel

The module will not be automatically loaded by the kernel, we have to load it manually using the insmod command.

$ insmod empty.ko 
empty: loading out-of-tree module taints kernel.
empty: Empty Module (init)

If everything works, we should see the module's init message.

We can see the loaded module using lsmod to list all the kernel modules.

$ lsmod
empty 12288 0 - Live 0xffffffffa0000000 (O)

We can see here the address at which the module is loaded.

Unload the module​

Unloading a module is done by using the rmmod command. It receives one single parameter that is the name of the module (without the .ko extension).

$ rmmod empty
empty: Empty Module (exit)

We should see the drop message.

Module Parameters​

Modules can receive parameters from the command line when loaded.

warning

The parameters API in Rust is not yet available in the mainstream kernel. We will have to use the next version of the kernel.

To download this version, please use git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git. To compile this version with the same configuration, please copy the .config file from the stable kernel folder to this kernel folder and run make -jn where n is replaced by the number of cores that your laptop has.

Parameters are defined in the module! macro using the params filed.

module! {
// ...
params: {
first_param: u8 {
default: 1,
description: "This parameter has a default of 1",
},
},
}

To read the value of a parameter, use

module_parameters::first_param.value()

where first_param is the name of the parameter.

Parameter values are assigned values when the module is loaded with insmod. The synatx is:

$ insmod module.ko parameter_1=value parameter_2=value ...

Run Script​

Every time we change the module, we have to perform the following steps:

  1. Build the module
  2. Copy the driver to $INIT_RAM_FS
  3. Rebuild the RAM disk
  4. Run QEMU with the nu RAM disk
  5. Load the module

We can use a run.sh script like the following placed in the module's folder to automate this:

#!/bin/sh

MODULE=empty.ko
BUILD_DIR="$(pwd)/build"

set -e

if [ -z $KDIR ]; then
echo "Kernel folder not set, use export KDIR=..."
exit 1
fi

if [ -z $INIT_RAM_FS ]; then
echo "initramfs folder not set, use export INIT_RAM_FS=..."
ecit 1
fi

echo "Building module"
make

echo "Kernel folder $KDIR"
echo "initramfs folder $INIT_RAM_FS"

KVERSION=$(cd "$KDIR" && make kernelversion)

echo "Kernel version $KVERSION"

echo "Copying driver"
MODULES_DIR="$INIT_RAM_FS/lib/modules/$KVERSION"
mkdir -p "$MODULES_DIR"
cp build/empty.ko "$MODULES_DIR"

echo "Compressing initramfs"
(cd "$INIT_RAM_FS" && find . -print0 | cpio --null -ov --format=newc | gzip -9 > "$BUILD_DIR/initramfs.cpio.gz")

echo "Running QEMU"
qemu-system-x86_64 -kernel "$KDIR/arch/x86_64/boot/bzImage" --initrd build/initramfs.cpio.gz -nographic -append "console=ttyS0" -s
note

Make sure to export both $KDIR and $INIT_RAM_FS variables before running the script.

Exercises​

  1. Modify the Module::init function to return an Error. Try loading the module with different errors and see what the kernel prints.
  2. Modify the module to print several types of messages using different pr_* and see what the kernel prints.
  3. Print the current process PID, current CPU ID and current user ID in the Module::init function. (Hint: use the current! macro and the Task structure.

Bonus​

Add two u8 parameters to the module and print their sum in the init message. Make sure you:

  • boot the next version of the kernel
  • set the correct $KDIR path to the next version of the kernel
  • run make rust-analyuzer with the correct $KDIR path pointing to the next version of the kernel