Prerequisites
Before we get started, we need to make sure we have the correct tools for the job. Most importantly, you’ll need a Linux machine. I know that comes as a complete surprise! While any Linux distribution will do, I am using Ubuntu 16.04 LTS in this example, so if you’re using a different distribution you may need to slightly adjust your installation commands.
Secondly, you’ll need either a separate physical machine or a virtual machine. I prefer to do my work in a virtual machine, but this is entirely up to you. I don’t suggest using your primary machine because data loss can occur when you make a mistake. I say when, not if, because you undoubtedly will lock up your machine at least a few times during the process. Your latest code changes may still be in the write buffer when the kernel panics, so it’s possible that your source files can become corrupted. Testing in a virtual machine eliminates this risk.
And finally, you’ll need to know at least some C. The C++ runtime is far too large for the kernel, so writing bare metal C is essential. For interaction with hardware, knowing some assembly might be helpful.
Installing the Development Environment
On Ubuntu, we need to run:
apt-get install build-essential linux-headers-`uname -r`
This will install the essential development tools and the kernel headers necessary for this example.
The examples below assume you are running as a regular user and not root, but that you have sudo privileges. Sudo is mandatory for loading kernel modules, but we want to work outside of root whenever possible.
Getting Started
Let’s start writing some code. Let’s prepare our environment:
mkdir ~/src/lkm_example
cd ~/src/lkm_example
Fire up your favorite editor (in my case, this is vim) and create the file lkm_example.c with the following contents:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“Robert W. Oliver II”);
MODULE_DESCRIPTION(“A simple example Linux module.”);
MODULE_VERSION(“0.01”);
static int __init lkm_example_init(void) {
printk(KERN_INFO “Hello, World!\n”);
return 0;
}
static void __exit lkm_example_exit(void) {
printk(KERN_INFO “Goodbye, World!\n”);
}
module_init(lkm_example_init);
module_exit(lkm_example_exit);
Now that we’ve constructed the simplest possible module, let’s example the important parts in detail:
· The “includes” cover the required header files necessary for Linux kernel development.
· MODULE_LICENSE can be set to a variety of values depending on the license of the module. To see a full list, run:
grep “MODULE_LICENSE” -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h
· We define both the init (loading) and exit (unloading) functions as static and returning an int.
· Note the use of printk instead of printf. Also, printk doesn’t share the same parameters as printf. For example, the KERN_INFO, which is a flag to declare what priority of logging should be set for this line, is defined without a comma. The kernel sorts this out inside the printk function to save stack memory.
· At the end of the file, we call module_init and module_exit to tell the kernel which functions are or loading and unloading functions. This gives us the freedom to name the functions whatever we like.
We can’t compile this file yet, though. We need a Makefile. This basic example will work for now. Note that make is very picky about spaces and tabs, so ensure you use tab instead of space where appropriate.
obj-m += lkm_example.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
If we run “make”, it should compile your module successfully. The resulting file is “lkm_example.ko”. If you receive any errors, check that your quotation marks in the example source file are correct and not pasted accidentally as UTF-8 characters.
Now we can insert the module to test it. To do this, run:
sudo insmod lkm_example.ko
If all goes well, you won’t see a thing. The printk function doesn’t output to the console but rather the kernel log. To see that, we’ll need to run:
sudo dmesg
You should see the “Hello, World!” line prefixed by a timestamp. This means our kernel module loaded and successfully printed to the kernel log. We can also check to see if the module is still loaded:
lsmod | grep “lkm_example”
To remove the module, run:
sudo rmmod lkm_example
If you run dmesg again, you’ll see “Goodbye, World!” in the logs. You can also use lsmod again to confirm it was unloaded.
As you can see, this testing workflow is a bit tedious, so to automate this we can add:
test:
sudo dmesg -C
sudo insmod lkm_example.ko
sudo rmmod lkm_example.ko
dmesg
at the end of our Makefile and now run:
make test
to test our module and see the output of the kernel log without having to run separate commands.
Now we have a fully functional, yet completely trivial, kernel module!