Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion docs/hackathon/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
slide_position: 6
sidebar_position: 8
title: 🦀 Rust Open-Source Hackathon
---

Expand Down
298 changes: 298 additions & 0 deletions docs/linux_kernel_development/1.empty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
# 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`.

```makefile
# 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.

```makefile
# 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](https://rust-analyzer.github.io/).

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.

```shell
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`.


```rust
```

### 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!`](https://rust.docs.kernel.org/kernel/macro.pr_info.html),
[`pr_error!`](https://rust.docs.kernel.org/kernel/macro.pr_error.html),
[`pr_warn!`](https://rust.docs.kernel.org/kernel/macro.pr_warn.html),
[`pr_debug!`](https://rust.docs.kernel.org/kernel/macro.pr_debug.html) and
[`pr_alert`](https://rust.docs.kernel.org/kernel/macro.pr_alert.html),

A module is declared using the [`module!`](https://rust.docs.kernel.org/macros/macro.module.html) 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`](https://rust.docs.kernel.org/kernel/error/struct.Error.html) [code](https://rust.docs.kernel.org/kernel/error/code/index.html) if the module cannot be loaded. The kernel will try several times
and print the error if it still fails.

```rust
// 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.

```shelll
$ 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](../linux_kernel_development#build-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.

```shell
$ 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.

```bash
$ 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).

```rust
$ 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.

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

To read the value of a parameter, use

```rust
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:

```shell
$ 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:

```bash
#!/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!`](https://rust.docs.kernel.org/kernel/macro.current.html) macro and the [`Task`](https://rust.docs.kernel.org/kernel/task/struct.Task.html#method.current) 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
Loading