
Writing a Simple Linux Kernel Module
- Transfer

Capture the Golden Ring-0
Linux provides a powerful and comprehensive API for applications, but sometimes it is not enough. To interact with equipment or perform operations with access to privileged information in the system, a kernel driver is needed.
The Linux kernel module is a compiled binary that is inserted directly into the Linux kernel, working in ring 0, the internal and least protected ring of command execution on the x86-64 processor. Here the code is executed completely without any checks, but at an incredible speed and with access to any system resources.
Not for mere mortals
Writing a Linux kernel module is not for the faint of heart. Changing the kernel, you risk losing data. Kernel code does not have standard protection, as in regular Linux applications. If you make a mistake, hang the whole system.
The situation is aggravated by the fact that the problem does not necessarily appear immediately. If the module hangs up the system immediately after booting, then this is the best failure scenario. The more code there is, the higher the risk of endless loops and memory leaks. If you are careless, then the problems will gradually increase as the machine operates. In the end, important data structures and even buffers can be overwritten.
You can mostly forget the traditional application development paradigms. In addition to loading and unloading the module, you will write code that responds to system events, and does not work according to a sequential pattern. When working with the kernel, you write the API, not the applications themselves.
You also do not have access to the standard library. Although the kernel provides some functions like
printk
(which serves as a replacement printf
) and kmalloc
(works similar to malloc
), basically you are left alone with the hardware. In addition, after unloading the module, you should completely clean it. There is no garbage collection.Required Components
Before you begin, you should make sure that you have all the necessary tools for the job. Most importantly, you need a machine under Linux. I know this is unexpected! Although any Linux distribution is suitable, in this example I use Ubuntu 16.04 LTS, so if you use other distributions, you may need to slightly modify the installation commands.
Secondly, you need either a separate physical machine or a virtual machine. Personally, I prefer to work in a virtual machine, but choose for yourself. I do not recommend using your main machine due to data loss when you make a mistake. I say “when,” not “if,” because you must hang the car at least several times in the process. Your last changes in the code may still be in the write buffer at the time of the kernel panic, so your sources may be damaged. Testing in a virtual machine eliminates these risks.
And finally, you need to know at least a little C. The C ++ working environment is too big for the kernel, so you need to write in pure bare C. Some knowledge of assembler will not hurt to interact with the equipment.
Install Development Environment
On Ubuntu you need to run:
apt-get install build-essential linux-headers-`uname -r`
We install the most important development tools and kernel headers necessary for this example.
The examples below assume that you are working as a normal user, not root, but that you have sudo privileges. Sudo is needed to load kernel modules, but we want to work as far as possible outside the root.
Getting started
Let's start writing the code. Prepare our environment:
mkdir ~/src/lkm_example
cd ~/src/lkm_example
Launch your favorite editor (in my case it’s vim) and create a file with the
lkm_example.c
following contents:#include
#include
#include
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);
We have constructed the simplest possible module, consider in more detail the most important parts of it:
- In the
include
listed header files necessary for the development of the Linux kernel. - You
MODULE_LICENSE
can set different values in, depending on the license of the module. To view the complete list, run:grep “MODULE_LICENSE” -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h
- We set
init
(load) andexit
(unload) as static functions that return integers. - Pay attention to use
printk
insteadprintf
. Also optionsprintk
are different fromprintf
. For example, the KERN_INFO flag for declaring logging priority for a particular line is specified without a comma. The kernel parses these things inside the functionprintk
to save stack memory. - At the end of the file can be accessed
module_init
andmodule_exit
and point loading and unloading functions. This enables arbitrary function naming.
However, while we can not compile this file. Need a Makefile. Such a basic example is enough for now. Note that it is
make
very picky about spaces and tabs, so be sure to use tabs instead of spaces 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 must successfully compile our module. The result will be a file lkm_example.ko
. If any errors pop up, check that the quotes in the source code are set correctly, and not accidentally in the UTF-8 encoding. Now you can implement the module and check it. To do this, run:
sudo insmod lkm_example.ko
If everything is fine, then you will not see anything. The function
printk
provides output not to the console, but to the kernel log. To view, you need to run:sudo dmesg
You should see the line “Hello, World!” with a timestamp at the beginning. This means that our kernel module has loaded and successfully written to the kernel log. We can also check that the module is still in memory:
lsmod | grep “lkm_example”
To remove the module, run:
sudo rmmod lkm_example
If you run dmesg again, you will see the entry “Goodbye, World!” In the journal. You can run lsmod again and verify that the module is unloaded.
As you can see, this testing procedure is a little tedious, but it can be automated by adding:
test:
sudo dmesg -C
sudo insmod lkm_example.ko
sudo rmmod lkm_example.ko
dmesg
at the end of the Makefile, and then running:
make test
to test the module and check the output to the kernel log without the need to run separate commands.
Now we have a fully functional, albeit absolutely trivial kernel module!
A bit more interesting
Digging a little deeper. Although kernel modules can perform all kinds of tasks, interacting with applications is one of the most common use cases.
Since applications are not allowed to view memory in kernel space, you must use the API to interact with them. Although technically there are several methods for this interaction, the most common is to create a device file.
You have probably dealt with device files before. Commands with a mention
/dev/zero
, /dev/null
and the like, interact with the “zero” and “null” devices, which return the expected values. In our example, we return “Hello, World”. Although this is not a particularly useful feature for applications, it still demonstrates the process of interacting with the application through the device file.
Here is the full listing:
#include
#include
#include
#include
#include
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“Robert W. Oliver II”);
MODULE_DESCRIPTION(“A simple example Linux module.”);
MODULE_VERSION(“0.01”);
#define DEVICE_NAME “lkm_example”
#define EXAMPLE_MSG “Hello, World!\n”
#define MSG_BUFFER_LEN 15
/* Prototypes for device functions */
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);
static int major_num;
static int device_open_count = 0;
static char msg_buffer[MSG_BUFFER_LEN];
static char *msg_ptr;
/* This structure points to all of the device functions */
static struct file_operations file_ops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
/* When a process reads from our device, this gets called. */
static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) {
int bytes_read = 0;
/* If we’re at the end, loop back to the beginning */
if (*msg_ptr == 0) {
msg_ptr = msg_buffer;
}
/* Put data in the buffer */
while (len && *msg_ptr) {
/* Buffer is in user data, not kernel, so you can’t just reference
* with a pointer. The function put_user handles this for us */
put_user(*(msg_ptr++), buffer++);
len--;
bytes_read++;
}
return bytes_read;
}
/* Called when a process tries to write to our device */
static ssize_t device_write(struct file *flip, const char *buffer, size_t len, loff_t *offset) {
/* This is a read-only device */
printk(KERN_ALERT “This operation is not supported.\n”);
return -EINVAL;
}
/* Called when a process opens our device */
static int device_open(struct inode *inode, struct file *file) {
/* If device is open, return busy */
if (device_open_count) {
return -EBUSY;
}
device_open_count++;
try_module_get(THIS_MODULE);
return 0;
}
/* Called when a process closes our device */
static int device_release(struct inode *inode, struct file *file) {
/* Decrement the open counter and usage count. Without this, the module would not unload. */
device_open_count--;
module_put(THIS_MODULE);
return 0;
}
static int __init lkm_example_init(void) {
/* Fill buffer with our message */
strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN);
/* Set the msg_ptr to the buffer */
msg_ptr = msg_buffer;
/* Try to register character device */
major_num = register_chrdev(0, “lkm_example”, &file_ops);
if (major_num < 0) {
printk(KERN_ALERT “Could not register device: %d\n”, major_num);
return major_num;
} else {
printk(KERN_INFO “lkm_example module loaded with device major number %d\n”, major_num);
return 0;
}
}
static void __exit lkm_example_exit(void) {
/* Remember — we have to clean up after ourselves. Unregister the character device. */
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO “Goodbye, World!\n”);
}
/* Register module functions */
module_init(lkm_example_init);
module_exit(lkm_example_exit);
Testing an Improved Example
Now our example does more than just display a message when loading and unloading, so a less rigorous testing procedure is needed. Change the Makefile only to load the module, without unloading it.
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
test:
# We put a — in front of the rmmod command to tell make to ignore
# an error in case the module isn’t loaded.
-sudo rmmod lkm_example
# Clear the kernel log without echo
sudo dmesg -C
# Insert the module
sudo insmod lkm_example.ko
# Display the kernel log
dmesg
Now after launch,
make test
you will see the issuance of the senior device number. In our example, the kernel automatically assigns it. However, this number is needed to create a new device. Take the number obtained as a result of execution
make test
and use it to create a device file so that you can establish communication with our kernel module from user space.sudo mknod /dev/lkm_example c MAJOR 0
(in this example, replace MAJOR with the value obtained from the execution of
make test
or dmesg
) The parameter
c
in the command mknod
tells mknod that we need to create a character device file. Now we can get the contents from the device:
cat /dev/lkm_example
or even through the command
dd
:dd if=/dev/lkm_example of=test bs=14 count=100
You can also access this file from applications. These do not have to be compiled applications - even Python, Ruby, and PHP scripts have access to this data.
When we are done with the device, delete it and unload the module:
sudo rm /dev/lkm_example
sudo rmmod lkm_example
Conclusion
I hope you enjoyed our pranks in kernel space. Although the examples shown are primitive, these structures can be used to create your own modules that perform very complex tasks.
Just remember that everything in your kernel space is your responsibility. There is no support or second chance for your code. If you are doing a project for a client, plan ahead for double if not triple time for debugging. The kernel code should be as perfect as possible in order to guarantee the integrity and reliability of the systems on which it runs.