Making your first kernel module

20 March 2021

Linux kernel modules are pieces of code that can be loaded and unloaded into the kernel without rebooting the system. Running code inside kernel space instead of user space is similar to what we can do with eBPF (intro to BPF here). Although eBFP is newer and provides safety guarantees, there are more constraints on what you can write compared to a kernel module.

If you want to skip to the full source code, it is on GitHub here.

A working setup

An error in our kernel module can crash our system, so my first piece of advice is don’t test your kernel module in the kernel of the host system you are developing on! My quickest way to get set up is to use Vagrant, Docker will not work here because it shares the same kernel as the host. On a Ubuntu host machine this should get Vagrant running if you do not already have it:

apt install virtualbox

curl -SL --progress-bar https://releases.hashicorp.com/vagrant/2.2.14/vagrant_2.2.14_linux_amd64.zip --output /tmp/vagrant.zip
unzip -o /tmp/vagrant.zip -d /usr/local/bin
rm  /tmp/vagrant.zip

# libarchive-tools not default in ubuntu 20.04
apt-get install libarchive-tools

vagrant --version

vagrant plugin install vagrant-vbguest

With a barebones Vagrantfile to mount our working directory in a virtualbox:

# Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "debian/buster64"
  config.vm.synced_folder ".", "/home/vagrant/src"
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "1024"
  end

  config.vm.provision "shell", inline: <<-SHELL
    mkdir -p /home/vagrant/src
  SHELL
end

We should be able to run vagrant up && vagrant ssh, then cd src to drop us into a safe shell with our working directory to run kernel modules in. Before we get started you will need to install the kernel headers for your kernel version, using apt-get install build-essential linux-headers-`uname -r`

The module

There are only a few pieces to the kernel module API we need for a fully running hello world.

1) linux headers

Add these at the top of the file, required for compiling:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

2) licence

Add a licence for the module

MODULE_LICENSE("GPL");

3) the functions

There are two function signatures we need, an init function that runs when the module is loaded and an exit function that runs when the module is removed:

static int hello_init(void) { return 0; }
static void hello_exit(void) {}

We want a way to verify this module is actually running, so we are going to add a print line. The kernel module does not have access to print, so it has its own printk, that takes a kernel log level with a message. Let’s add in hello and goodbye to give us:

static int hello_init(void)
{
  printk(KERN_ALERT "Hello world\n");
  return 0;
}

static void hello_exit(void)
{
  printk(KERN_ALERT "Goodbye world\n");
}

4) register the functions

Using our headers we need to register our functions:

module_init(hello_init);
module_exit(hello_exit);

And this takes us to a fully working hello.c module file

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Andrew Klotz");
MODULE_DESCRIPTION("A Simple Hello World Module");

static int hello_init(void)
{
  printk(KERN_ALERT "Hello world\n");
  return 0;
}

static void hello_exit(void)
{
  printk(KERN_ALERT "Goodbye world\n");
}

module_init(hello_init);
module_exit(hello_exit);

Compiling

The next part is to compile our modules with a Makefile:

obj-m := hello.o

all:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

.PHONY: setup
setup:
  apt-get install build-essential linux-headers-`uname -r`

What is interesting here is if we look into cat /lib/modules/$(uname -r)/build, we will find a Linux Makefile that is doing the heavy lifting for us. What we need to do is call out to it with some commands, our current directory, and include our .c files in obj-m.

Running

Now with everything setup:

  1. Run make all, which should output a hello.ko file
  2. Install our module with sudo /sbin/insmod hello.ko
  3. Remove our module with sudo /sbin/rmmod hello
  4. Verify out module logged correctly with sudo tail /var/log/kern.log

If everything worked correctly you should see something like this:

Mar 20 20:48:21 buster kernel: [   93.240717] Hello world
Mar 20 20:48:27 buster kernel: [  100.023905] Goodbye world

Congrats, you have a working kernel module! Don’t forget to run vagrant destroy to clean up after yourself. The full code is on GitHub if you want to take a look.