Building a simple kernel module on Linux

This article is about building the first kernel module is presented in the book Linux Device Drivers on a machine running a modern kernel

I’ve been using Linux for a long time and currently using the Arch distribution. Only the command to install kernel headers is Arch specific here—the rest should apply to other distros.

Wanting to learn about Linux kernel programming, I started reading the book Linux Device Drivers. This is quite old (in computing terms)—it was written in 2005, and the examples are based on the 2.6 Linux kernel.

I’m writing this in case anyone else is wondering, does this code sample even work on machine running a modern kernel? The answer is that it does, but you might encounter some issues, which are hopefully all addressed here.

If you are trying to follow this, please refer to the errors below—there are errors around building the module, and errors around loading it. These are distinct and need to be resolved separately.

The challenge

I wanted to run the initial “hello, world” example, given in the book. This is a tiny kernel module, which, when loaded into the kernel, writes a message to a log.

First problem — I can’t build the 2.6 kernel

The book is based on the 2.6 kernels, and I downloaded the source and tried to build it. No success. It seems that in 15 years, the latest version of the compiler shipped with most Linux systems won’t build it. Or at least, won’t build it easily. I found many hints online, but nothing conclusive. So I gave up on this track.

Next challenge — building the sample against an up-to-date kernel.

Next, I tried to build the “hello, world” module against a modern (6.11 in my case) kernel. This took me a while to figure out — the MakeFile in the book specifically refers to the header versions, and in the end, I used another Makefile I found online. I also didn’t know where to put the source files. Eventually, I got this to build, details are below.

Big Challenge — actually loading the compiled module

After this step, I could build the module, but when I tried to load it with:

sudo insmod ./hello.ko

I got an error saying:

insmod: ERROR: could not insert module ./hello.ko: Invalid module format

I didn’t know what this meant but eventually figured out it was because my module wasn’t based on the same headers as my running kernel. To get my running kernel version I used:

uname -r

To get the version of the headers the module was compiled against, I used:

modinfo hello.ko

The result was different versions — so I was trying to load a module intended for one kernel version while running a different version.

To clean this up, I removed the custom kernel I had built and was running and updated my Arch system so I had the latest kernel version. I’m not sure what had gone wrong when I built my custom kernel — but I had somehow ended up with headers located in incorrect folders.

Missing folder!

After I’d cleared up my kernel situation, so my system, at boot, reported just one available kernel (the latest one for Arch), I tried the build again, and it failed as it was missing a folder called /lib/modules/{version}/ build where {version} is the kernel version.

To fix this I installed the kernel headers:

sudo pacman -S linux-headers

Success

Then, it all worked. And I was glad.

Solution — so, the instructions are:

Create a folder in your home directory called, hello-module (or whatever). In that directory, add a file called hello.c, containing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

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

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

module_init(hello_init);
module_exit(hello_exit);

Also, add a file called Makefile. Use this one:

1
2
3
4
5
6
obj-m := hello.o
    
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
    
all: default

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

clean: rm -rf *.o ~ core .depend ..cmd *.ko *.mod.c .tmp_versions

Note that the lines after default: and clean: are indented with a tab, not spaces.

Open a terminal in that folder and run:

make

It should succeed, and you should have a file (along with some others) called hello.ko.

Run:

modinfo hello.ko

This should show the same version as your running kernel (which you can get from uname -r) .

If so, all is good. Then run:

sudo insmod ./hello.ko

This should succeed silently, but by running:

sudo dmesg

You should see in the log, the message emitted from the loading module! Yay. Your code is running in the kernel.