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 moduleclean- that cleans the modulerust-analyzer- that build therust-project.jsonfile 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/.
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:
- copy the
empty.koin to$INIT_RAM_FS - rebuild the RAM disk so that it includes the module
- 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.
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:
- Build the module
- Copy the driver to
$INIT_RAM_FS - Rebuild the RAM disk
- Run QEMU with the nu RAM disk
- 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
Make sure to export both $KDIR and $INIT_RAM_FS variables before running the script.
Exercises​
- Modify the
Module::initfunction to return anError. Try loading the module with different errors and see what the kernel prints. - Modify the module to print several types of messages using different
pr_*and see what the kernel prints. - Print the current process PID, current CPU ID and current user ID in the
Module::initfunction. (Hint: use thecurrent!macro and theTaskstructure.
Bonus​
Add two u8 parameters to the module and print their sum in the init message. Make sure you:
- boot the
nextversion of the kernel - set the correct
$KDIRpath to thenextversion of the kernel - run
make rust-analyuzerwith the correct$KDIRpath pointing to the next version of the kernel