07 · Kernel & Driver Intro

The Linux kernel is written in C — ~30 million lines of it as of the 6.x series. Writing kernel code is a specialty, not a hobby: real bugs corrupt the page cache, brick filesystems, and, if you’re unlucky, void warranties. Yet you cannot claim “systems C fluency” without at least understanding what a kernel module looks like, what kmalloc does that malloc doesn’t, and why copy_to_user is not just a memcpy. This file gives you a hello-world module, the four APIs that matter, and a firm warning about when not to write kernel code.

Why touch this at all

Two reasons, and only two.

  1. study literacy. Systems roles at Nvidia, Cloudflare, TigerBeetle, and any chip-adjacent shop will ask “have you written a kernel module?” A one-week hello-world in a VM is the honest answer that passes.

  2. Reading eBPF / driver code. eBPF is where kernel meets application ML infra in 2026 — packet filtering, tracing, security. Every serious observability tool (Cilium, Falco, Pixie) ships eBPF. You do not need to write it, but you must be able to read the C side.

If you want to actually maintain kernel code, budget years, not weeks. That is a different career from ML inference and does not overlap with your Zoho trajectory. This file is the minimum viable exposure, not a roadmap.

Setup: run everything in a throwaway VM

Do not compile kernel modules against your laptop’s running kernel. A misbehaving module can wedge the machine, corrupt disk, or trigger a kernel panic. Use:

  • UTM / QEMU on Apple Silicon with an aarch64 or x86_64 Debian guest, or

  • Multipass for a quick Ubuntu 24.04 VM, or

  • Docker with --privileged + a matching-kernel Ubuntu image — fine for tinkering, not production-safe.

Inside the VM: sudo apt install build-essential linux-headers-$(uname -r).

Hello, kernel

// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("You");
MODULE_DESCRIPTION("Minimal kernel module.");
MODULE_VERSION("0.1");

static int __init hello_init(void) {
    pr_info("hello: loaded\n");
    return 0;
}

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

module_init(hello_init);
module_exit(hello_exit);
# Makefile
obj-m += hello.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
make
sudo insmod hello.ko
dmesg | tail -2      # shows "hello: loaded"
sudo rmmod hello
dmesg | tail -2      # shows "hello: unloaded"

That is a kernel module. pr_info is a printf that goes to the kernel ring buffer, viewable with dmesg or journalctl -k. The __init and __exit annotations tell the kernel to free the setup code after boot and only run the teardown when explicitly unloaded.

The four APIs to actually understand

kmalloc(size, flags) / kfree(ptr)

Kernel allocator. Returns kernel-virtual memory. flags controls behavior:

  • GFP_KERNEL — normal, may sleep, use in process context.

  • GFP_ATOMIC — must not sleep, use in interrupt handlers.

  • GFP_DMA — physically contiguous, DMA-safe.

Getting the flag wrong is a classic “works on my machine, deadlocks in production” bug. The rule: in interrupt context, only GFP_ATOMIC. Everywhere else, GFP_KERNEL.

copy_to_user(dst, src, n) / copy_from_user(dst, src, n)

These are not memcpy. User pointers cannot be dereferenced directly — they belong to a different address space, may not be present (paged out), and may point at unmapped memory. copy_to_user handles the page fault, the access-check (access_ok), and returns the number of bytes not copied on failure. Every syscall that returns data to userspace uses this.

A memcpy where copy_to_user was needed is the source of CVE-worthy info-leak bugs. See the classic CVE-2016-5195 (“Dirty COW”) for a race in the copy-on-write path — not the same bug but the same address-space seam.

ioctl handlers

ioctl(fd, cmd, arg) is the escape hatch when read/write aren’t enough. Every driver defines a set of _IO, _IOR, _IOW, _IOWR command numbers and a switch statement in its file_operations.unlocked_ioctl. The interface is unsafe: arg is a user pointer, must go through copy_from_user. Half of all Linux privilege-escalation CVEs live in ioctl handlers because developers forget to validate.

The file_operations struct

The interface between userspace open/read/write/ioctl and your driver. Register it in your init with register_chrdev (character devices) or the miscdevice framework. Every device driver in the tree — drivers/net/*, drivers/gpu/*, drivers/tty/* — is at heart a file_operations struct.

What you can build in a weekend

  • A /dev/reverse character device: reads what you wrote to it, reversed. Exercises file_operations, copy_from_user, copy_to_user, a per-open buffer.

  • A /proc/hello entry printing uptime and the module load time. Exercises proc_create and seq_file.

  • A tiny netfilter hook that logs the source IP of every incoming SYN packet. Exercises nf_register_net_hook and skb parsing.

One of these, in a VM, with a short write-up. That is enough. Do not deploy any of them.

The kernel coding style, briefly

  • Read Documentation/process/coding-style.rst in the kernel tree. Written by Linus, still authoritative.

  • 8-space tabs, no exceptions. K&R braces on functions, cuddled on control flow.

  • Function names in snake_case, macros in SCREAMING_SNAKE_CASE.

  • Max function length: 24 lines is the aspiration. “If you need more, break it up.”

  • goto is idiomatic for cleanup. Every kernel function that acquires resources ends with goto out_free_foo; ... out_free_foo: kfree(foo); return err; cascades. Do not fight this pattern; it is the safest pattern in C for error paths without RAII.

eBPF: the modern replacement for many kernel modules

Most “I want to observe / filter / trace” tasks that would have been kernel modules ten years ago are now eBPF programs — verified, sandboxed byte-code loaded into the kernel at runtime with bpf() syscall, safe by construction. Frameworks:

  • libbpf-bootstrap — the modern, CO-RE-based way to write eBPF in C.

  • bcc — older, Python front-end.

  • bpftrace — awk-like DSL, best for ad-hoc tracing.

If you’re on-call at Zoho and need to trace “which process is hammering my page cache,” you write a 20-line bpftrace program. You do not build a kernel module. This is the pragmatic 2026 path.

What most people get wrong about kernel code

They treat it as “harder userspace C.” It isn’t. Kernel code has no libc, no stdio.h, no malloc (only kmalloc), no floating point in most paths (the FPU state isn’t saved on kernel entry), no errno (you return -EINVAL etc. directly), and a completely different concurrency model (per-CPU variables, RCU, spinlocks vs mutexes based on preemptibility). Every skill you built in Phases 1–5 carries over syntactically; almost none of it carries over semantically. If you find yourself “just using” a stdlib idiom in kernel code, stop — you are almost certainly doing something dangerous. Read Documentation/ in the kernel tree before you write, not after.


Return to README.md · Next: 08_ffi_c_from_python.md