Linux is capable of many interesting things, and loadable module functionality is yet another. Linux kernel modules simply put are just pieces of binary that can get linked on-the-fly against a running kernel.

This approach is especially nice for speeding up your write-compile-run cycle, systems that frown upon downtimes or creating drivers that require minimum work when porting between kernel versions.

Today, we’re going to harness that power and write a module that will print two messages on module load and unload, respectively.


  • A (virtual) machine running Linux - If you’re already running Linux the VM is optional, as it’s hard to break anything with this particular module
  • Basic understanding of C and simple GNU Makefiles - There’s finally gonna be C code involved and for our build, we’ll use a simple Makefile

Coding style

Before we write any code it’s important to mention how to write it. Some of the most notable guidelines involve:

  • 8-space-wide tabs - Most programmers would probably expect 4 spaces for indentation; with Linux we’re using actual tab characters set to the width of 8 spaces; Vim users will be happy to know that a plugin called vim-sleuth can deduce the indentation from the current buffer and comply with it accordingly, thus eliminating the need to adjust the settings manually
  • 80-column line width limit - Linux is not much different here from other coding conventions, i.e. if a longer line significantly benefits readability, it’s acceptable to leave it be
  • When in doubt, comply with K&R - When Brian W. Kernighan and Dennis Ritchie wrote “The C programming language” (a.k.a. K&R), their creation also established a robust standard for C coding style. In fact, what Linux is using can be thought of as a compatible subset of those rules

For a full explanation of the preferred coding style, see this doc.

The code

At last, some source code. Here’s what my module, called hello.c, looks like:

#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* For KERN_INFO */

int __init hello_init(void)
        printk(KERN_INFO "Hello, world!\n");

        return 0;

void __exit hello_exit(void)
        printk(KERN_INFO "Goodbye, world!\n");



MODULE_DESCRIPTION("My \"Hello, World!\" module");
MODULE_AUTHOR("Stan Drozd <>");

You can also find the code in the course’s repository.

Not too scary, right? Let’s talk about what the module does.

First, we include linux/module.h - a header essential for all Linux kernel modules. Then, we want linux/kernel.h for printk() and log levels. Both of those headers are to be found in the ./include/linux/ directory of your kernel’s source tree or header directory.

hello_init is our module’s init function - it’s supposed to make all preparations necessary to get a module ready to do its job (in our case we only want to greet the globe). Init functions are usually called only once on module load. The __init attribute tells your kernel to free the memory hello_init was occupying. For printing the message we’re using printk() - a printf() kernel equivalent - with a log level macro called KERN_INFO, which is meant for information that is neither error nor debug related. The init function has no parameters and is supposed to return an int that is either equal to 0 or a negative error code, e.g. -ENOMEM if a memory allocation fails. To learn some more error codes and their meaning, see this header.

hello_exit on the other hand cares about everything that has to do with cleaning up when your module unloads. It too has an attribute, __exit, which tells your compiler to ignore hello_exit when building your code as a built-in module, as those aren’t unloadable anyway (We sure will talk about in-tree modules in the future :slightly_smiling_face:). Exit functions don’t take arguments and have no return values.

Last but not least, we specify some general info about our module. The macros module_init and module_exit will tell kbuild which functions you want to use for module initialization and cleanup, respectively. As you might expect, the last three MODULE_* macros specify the module’s description, author and license.

The build

The Makefile for hello.c is as simple as it gets:

obj-m += hello.o

KERNEL_VERSION := $(shell uname -r)

KDIR ?= /lib/modules/$(KERNEL_VERSION)/build

	$(MAKE) -C $(KDIR) M=$(PWD) modules

	$(MAKE) -C $(KDIR) M=$(PWD) clean

If you have enough of a grasp on Makefiles, you’ll quickly see that all we do here is just visit a kernel directory and run its Makefile. We specify an M parameter which stands for the path to our module’s sources. I added a KDIR variable for specifying the desired kernel tree, which defaults to your currently running kernel.

obj-m is a list of objects to compile as loadable modules. Other kernel Makefiles will also specify obj-y and obj-n as the collections of modules to build into the kernel and not to compile at all, respectively.

Compiling against the running kernel

To build our module, all we need to do is run make -jN:

$ make -j4
make -C /lib/modules/4.10.4-1-MANJARO/build M=/home/drozdziak1/kernel-safari/dsp17-3 modules
make[1]: Entering directory '/usr/lib/modules/4.10.4-1-MANJARO/build'
  CC [M]  /home/drozdziak1/kernel-safari/dsp17-3/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/drozdziak1/kernel-safari/dsp17-3/hello.mod.o
  LD [M]  /home/drozdziak1/kernel-safari/dsp17-3/hello.ko
make[1]: Leaving directory '/usr/lib/modules/4.10.4-1-MANJARO/build'

Compiling against a specific kernel tree

If you’re not using Linux on your machine or simply don’t want to run your module against the running kernel, you can just take a virtual machine (like the one I showed you here) and use its’ kernel instead. To do that, we’re going to overwrite KDIR in our make invocation:

$ make -j4 KDIR=../linux
make -C ../linux M=/home/drozdziak1/kernel-safari/dsp17-3 modules
make[1]: Entering directory '/home/drozdziak1/kernel-safari/linux'
  CC [M]  /home/drozdziak1/kernel-safari/dsp17-3/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/drozdziak1/kernel-safari/dsp17-3/hello.mod.o
  LD [M]  /home/drozdziak1/kernel-safari/dsp17-3/hello.ko
make[1]: Leaving directory '/home/drozdziak1/kernel-safari/linux'

When building against a kernel tree of your choosing, remember to compile that kernel first before pointing at it with KDIR.

Loading and unloading of kernel modules

After your module builds, you should end up with a bunch of new files:

├── Makefile
├── hello.c
├── Module.symvers - Information about symbols declared in your module (empty at
│                    this point)
├── hello.mod.c - Generated C code for embedding a module's info into the *.ko
│                 file
├── hello.mod.o - Same, but compiled and ready for linking
├── hello.o - A raw, freshly compiled version of your module (not linked yet)
├── hello.ko - The finished module binary, which we'll insert into the kernel
│              shortly
└── modules.order - Defines the order in which your module's dependencies
                    should be met

If you’d like to get a better idea about how Linux modules are compiled, see this Makefile.

To see the information about your module, use modinfo:

$ modinfo hello.ko
filename:       /home/drozdziak1/kernel-safari/dsp17-3/hello.ko
license:        GPL
author:         Stan Drozd <>
description:    My "Hello, World!" module
vermagic:       4.10.6-1-ARCH SMP preempt mod_unload modversions

For attaching, listing and removing our module from the kernel, we’ll use insmod, lsmod and rmmod (usually provided by the kmod package, present in the VM thanks to busybox):

/ # insmod hello.ko
[   14.15021] hello: loading out-of-tree module taints kernel.
[   14.159069] Hello, world!
/ # lsmod | grep hello
hello 16384 0 - Live 0xffffffffa0005000 (O)
/ # rmmod hello.ko
[   18.773422] Goodbye, world!
/ # lsmod
/ #

Keep in mind that messing with a running kernel requires root privileges. Use sudo as needed; lsmod will swarm you with module listings when used on the host, so grep might come in handy.

The taints kernel part indicates that after inserting a module built outside the original kernel’s directory, the kernel’s behavior may not be 100% predictible; Linux can also get tainted for various other reasons.

If you used your host’s running kernel for testing the module, and you sport a terminal emulator like gnome-terminal or xfce4-terminal, you may miss the module’s log output. That’s because kernel logs get printed in real time only to “real” terminals, and not pseudoterminals, which terminal emulators use (see here for more details).

The easiest way to see full kernel logs from anywhere is using dmesg. By default, it should spew it all out from the very boot sequence to the most recent entries at your default log level:

$ dmesg
[    0.000000] Linux version 4.10.6-1-ARCH (builduser@tobias) (gcc version 6.3.1 20170306 (GCC) ) #1 SMP PREEMPT Mon Mar 27 08:28:22 CEST 2017
[    0.000000] Command line: BOOT_IMAGE=/vmlinuz-linux root=UUID=dafec227-4989-4485-a1c5-59520fbb51be rw cryptdevice=UUID=a2feb722-1089-4620-83c8-573596dab444:root
[    0.000000] x86/fpu: Legacy x87 FPU detected.
[    0.000000] e820: BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000cfe0abff] usable
[    0.000000] BIOS-e820: [mem 0x00000000cfe0ac00-0x00000000cfe5cbff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000cfe5cc00-0x00000000cfe5ebff] ACPI data
[    0.000000] BIOS-e820: [mem 0x00000000cfe5ec00-0x00000000cfffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000e0000000-0x00000000efffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fe000000-0x00000000feffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000ffb00000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000022fffffff] usable
[    0.000000] NX (Execute Disable) protection: active
[    0.000000] SMBIOS 2.5 present.


[11623.122335] ath: Country alpha2 being used: CN
[11623.122336] ath: Regpair used: 0x52
[11623.127174] ieee80211 phy1: Atheros AR9271 Rev:1
[11623.129346] ath9k_htc 1-8:1.0 wlp0s29f7u8: renamed from wlan0
[11623.165843] IPv6: ADDRCONF(NETDEV_UP): wlp0s29f7u8: link is not ready
[11623.350130] IPv6: ADDRCONF(NETDEV_UP): wlp0s29f7u8: link is not ready
[11623.596588] IPv6: ADDRCONF(NETDEV_UP): wlp0s29f7u8: link is not ready
[11623.648622] IPv6: ADDRCONF(NETDEV_UP): wlp0s29f7u8: link is not ready
[11625.417598] IPv6: ADDRCONF(NETDEV_UP): wlp0s29f7u8: link is not ready
[11626.743869] wlp0s29f7u8: authenticate with 8c:04:ff:f5:df:9f
[11626.999492] wlp0s29f7u8: send auth to 8c:04:ff:f5:df:9f (try 1/3)
[11627.001355] wlp0s29f7u8: authenticated
[11627.002584] wlp0s29f7u8: associate with 8c:04:ff:f5:df:9f (try 1/3)
[11627.006338] wlp0s29f7u8: RX AssocResp from 8c:04:ff:f5:df:9f (capab=0x1411 status=0 aid=7)
[11627.016317] wlp0s29f7u8: associated
[11627.016374] IPv6: ADDRCONF(NETDEV_CHANGE): wlp0s29f7u8: link becomes ready
[14961.125874] Hello, world!
[14967.349120] Goodbye, world!

Looks good!

If you still don’t get your module’s log output, try explicitly printing the KERN_INFO log level with dmesg -l info.

Can I use my module with any kernel?

Short answer: No, a module that doesn’t match the running kernel will not load and get rejected.

Long answer: You can, and in some cases it would run perfectly fine. Module compatibility is guarded with the vermagic string we’ve seen earlier in modinfo output. You could load the module with modprobe, a more sophisticated module management tool, which offers a --force flag for ignoring version mismatches. Don’t ever do that unless you know what you’re doing.


This post took a surprisingly long time to write, so I’ll just hope that it proved useful to someone :upside_down_face:. If you see an error or have a question, help yourself to the comment section. See you next time, when we’ll talk about the kernel source directory and where to look for various things.

Further reading