Skip to content

Week 7 — Writing, Building, and Testing Kernel Modules

Goal

Write a kernel module from scratch, build it, load it into your QEMU guest, and interact with it. Then write a networking-related module that hooks into the packet path.

Why This Matters

Modules are how you'll test ideas without rebooting. They're also how most kernel contributions start — you change or add a module, test it, and submit a patch. Nearly all network drivers are modules.


Your First Module

Create a directory for your module work:

mkdir -p ~/kernel-modules/hello
cd ~/kernel-modules/hello

Create hello.c:

// SPDX-License-Identifier: GPL-2.0
/*
 * hello.c - A minimal kernel module
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    pr_info("hello: module loaded\n");
    return 0;  // 0 = success, negative = error
}

static void __exit hello_exit(void)
{
    pr_info("hello: module unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Carlos");
MODULE_DESCRIPTION("A minimal kernel module");

Key elements:

  • SPDX-License-Identifier — required by kernel coding style. GPL-2.0 for kernel code.
  • __init — this function runs once at load, then its memory is freed.
  • __exit — runs when the module is unloaded.
  • module_init() / module_exit() — macros that register your init/exit functions.
  • MODULE_LICENSE("GPL") — required. Non-GPL modules can't use many kernel symbols.

Create the Makefile:

obj-m += hello.o

KDIR ?= ~/linux

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

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

Build it:

make

Expect: hello.ko is produced. This is your loadable module.

Loading and Testing

Copy the module to your QEMU guest (via 9p share, scp, or by mounting the disk image) and:

# Load the module
insmod hello.ko

# Check it loaded
lsmod | grep hello

# See the message
dmesg | tail -5

# Unload
rmmod hello

# See the exit message
dmesg | tail -5

Module Parameters

Make your module configurable at load time:

// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/moduleparam.h>

static int count = 1;
static char *name = "world";

module_param(count, int, 0644);   // readable/writable in sysfs
MODULE_PARM_DESC(count, "Number of greetings");

module_param(name, charp, 0444);  // read-only in sysfs
MODULE_PARM_DESC(name, "Who to greet");

static int __init hello_init(void)
{
    int i;
    for (i = 0; i < count; i++)
        pr_info("hello: Hello, %s!\n", name);
    return 0;
}

static void __exit hello_exit(void)
{
    pr_info("hello: Goodbye, %s!\n", name);
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

Load with parameters:

insmod hello.ko count=3 name="Carlos"

# Parameters visible in sysfs
cat /sys/module/hello/parameters/count
cat /sys/module/hello/parameters/name

A Networking Module: Packet Counter

Now let's write something useful — a module that hooks into the network stack and counts packets using a Netfilter hook:

// SPDX-License-Identifier: GPL-2.0
/*
 * pktcount.c - Count incoming packets using Netfilter
 */
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>

static atomic_t pkt_count = ATOMIC_INIT(0);

static unsigned int pkt_hook(void *priv,
                             struct sk_buff *skb,
                             const struct nf_hook_state *state)
{
    struct iphdr *iph;

    if (!skb)
        return NF_ACCEPT;

    iph = ip_hdr(skb);
    atomic_inc(&pkt_count);

    if (atomic_read(&pkt_count) % 100 == 0)
        pr_info("pktcount: %d packets seen (last src: %pI4)\n",
                atomic_read(&pkt_count), &iph->saddr);

    return NF_ACCEPT;  // Let the packet through
}

static struct nf_hook_ops pkt_hook_ops = {
    .hook     = pkt_hook,
    .pf       = NFPROTO_IPV4,
    .hooknum  = NF_INET_PRE_ROUTING,
    .priority = NF_IP_PRI_FIRST,
};

static int __init pktcount_init(void)
{
    int ret;

    ret = nf_register_net_hook(&init_net, &pkt_hook_ops);
    if (ret) {
        pr_err("pktcount: failed to register hook: %d\n", ret);
        return ret;
    }

    pr_info("pktcount: module loaded, counting packets\n");
    return 0;
}

static void __exit pktcount_exit(void)
{
    nf_unregister_net_hook(&init_net, &pkt_hook_ops);
    pr_info("pktcount: module unloaded, total packets: %d\n",
            atomic_read(&pkt_count));
}

module_init(pktcount_init);
module_exit(pktcount_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple packet counter using Netfilter");

Update the Makefile:

obj-m += hello.o
obj-m += pktcount.o

KDIR ?= ~/linux

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

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

Test it:

insmod pktcount.ko
ping -c 200 10.0.2.2
dmesg | grep pktcount
rmmod pktcount
dmesg | tail -3  # See total count

What this teaches you:

  • How Netfilter hooks work (you'll see this pattern everywhere in iptables/nftables)
  • How sk_buff represents a packet
  • How to safely access packet headers
  • Atomic operations for concurrent access (packets arrive on multiple CPUs)

Creating a /proc Entry

Expose your packet count via /proc instead of just printk:

#include <linux/proc_fs.h>
#include <linux/seq_file.h>

static int pktcount_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%d\n", atomic_read(&pkt_count));
    return 0;
}

static int pktcount_open(struct inode *inode, struct file *file)
{
    return single_open(file, pktcount_show, NULL);
}

static const struct proc_ops pktcount_proc_ops = {
    .proc_open    = pktcount_open,
    .proc_read    = seq_read,
    .proc_lseek   = seq_lseek,
    .proc_release = single_release,
};

// In init:
proc_create("pktcount", 0444, NULL, &pktcount_proc_ops);

// In exit:
remove_proc_entry("pktcount", NULL);

Now you can read the count with:

cat /proc/pktcount

Kernel Coding Style

Before writing more code, internalize the kernel coding style:

# In the kernel source
nvim Documentation/process/coding-style.rst

Key rules:

  • Tabs for indentation (8 spaces wide)
  • 80-column lines (soft limit, 100 for special cases)
  • Opening brace on same line (except functions)
  • No typedefs for structs
  • Comments: /* C style */, not //

Check your code:

~/linux/scripts/checkpatch.pl --file hello.c

This is the same tool used to review patches. Get used to its output now.

Exercises

  1. Write, build, and load the hello module. Verify with dmesg.
  2. Add a module parameter for the greeting count. Load with different values.
  3. Build and test the packet counter module. Ping the guest and verify counts.
  4. Add the /proc/pktcount entry. Read it while generating traffic.
  5. Run checkpatch.pl on your modules. Fix any warnings.
  6. Modify the packet counter to also count TCP vs UDP packets separately. Expose both counts via /proc.

What's Next

Next week we dive into the networking architecture — how the kernel processes packets from NIC to socket, the role of sk_buff, and how protocol layers are organized.