How does Building and Loading Kernel Modules work in Linux-Free Linux Device Drivers Course

PREV_LEC NEXT_LEC

Building and Loading Kernel Modules

Compile a Linux driver out-of-tree, wire it into the kernel source tree, and load it safely with insmod, modprobe and depmod on a modern kernel.

Kbuild System
Out-of-Tree Builds
Module Loading Tools

Free linux kernel development course learners often write correct driver code and then get stuck at the very next step: turning that C file into something the kernel will actually run. This lecture is part of our free linux device drivers course and covers the two ways to build a driver — out-of-tree as a standalone module, and in-tree as part of the kernel source — and then the full lifecycle of loading, inspecting and removing that module safely. If you’re following this free embedded linux course sequentially, this picks up right after you’ve written your first character device driver skeleton.

What You Will Learn

Kbuild and the kernel build system Out-of-tree Makefiles In-tree drivers and Kconfig insmod, rmmod, lsmod, modinfo depmod and modprobe udev and automatic loading

Prerequisites

This lecture assumes you already have a buildable kernel source tree for your target (or a matching linux-headers package for your host, if you’re building for the machine you’re sitting at), and that you’ve written a minimal character device driver in a previous lecture of this free linux kernel development course. You should be comfortable with basic Makefile syntax and running commands as root on a Linux target.

Two Ways to Build a Driver

A Linux driver reaches the kernel one of two ways. It can live outside the kernel source tree entirely and be compiled as a loadable kernel module (an .ko file) using the kernel’s own build machinery — this is called an out-of-tree or “external module” build. Or it can live inside the kernel source tree itself, alongside drivers like drivers/char or drivers/net, and be built as part of a full kernel build or as a module selected through Kconfig.

Neither approach recompiles the driver from scratch with a plain gcc command. Both go through Kbuild, the kernel’s own recursive Makefile system, because a kernel module has to be compiled with the exact same compiler flags, structure layout, and ABI as the running kernel. A module built against the wrong kernel version, or with a mismatched CONFIG_ option, will simply refuse to load.

Two Build Paths to a Loadable Module
ep_modtest.c (your driver source) | | out-of-tree path in-tree path v v Your own Makefile drivers/char/Makefile calls into Kbuild obj-m/obj-y += ep_modtest.o | | v v Kernel build system (Kbuild) <——————— (same engine either way) | v ep_modtest.ko (built for THIS kernel version + config only)

Building Out-of-Tree with Kbuild

An out-of-tree module needs a tiny Makefile that does almost nothing itself — it just hands control to the real kernel build system, pointed at your kernel’s build directory. On a development host targeting the host’s own running kernel, that directory is normally /lib/modules/$(uname -r)/build, a symlink into the matching kernel headers package. When cross-compiling for an embedded target, point it at your configured and built kernel source tree instead.

# Makefile
obj-m += ep_modtest.o

KDIR ?= /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

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

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

The obj-m += ep_modtest.o line is a Kbuild directive, not a normal Makefile rule — it tells Kbuild “there is a module target here, build it from ep_modtest.c.” The -C $(KDIR) switches make into the kernel source tree first, and M=$(PWD) tells that outer Makefile to come back into your directory to actually build the module. This two-step indirection is exactly what makes the module get built with the running kernel’s configuration and compiler flags.

Here is a minimal driver to build against that Makefile — deliberately small, just enough to prove the build and load pipeline works end to end:

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

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

static void __exit ep_modtest_exit(void)
{
	pr_info("ep_modtest: unloaded\n");
}

module_init(ep_modtest_init);
module_exit(ep_modtest_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Minimal module build/load demo");
$ make
make -C /lib/modules/6.11.0-generic/build M=/home/dev/ep_modtest modules
  CC [M]  ep_modtest.o
  MODPOST Module.symvers
  CC [M]  ep_modtest.mod.o
  LD [M]  ep_modtest.ko
$ ls
ep_modtest.c  ep_modtest.ko  ep_modtest.mod  ep_modtest.mod.c
ep_modtest.mod.o  ep_modtest.o  Module.symvers  modules.order  Makefile

Notice all the extra files: Module.symvers records exported symbol versions, modules.order lists build order for multi-module projects, and *.mod.c/*.mod.o hold the auto-generated module metadata (license, parameters, module info) that Kbuild compiles in separately. Only ep_modtest.ko matters for loading; the rest are build artifacts you can safely make clean away.

Building In-Tree

If the driver is meant to ship as part of a board’s kernel rather than as a standalone module, it belongs inside the kernel source tree, in the directory matching its subsystem — a simple character device would go under drivers/char, for example. Two lines are enough to hook it into the existing Kbuild Makefile for that directory:

# drivers/char/Makefile
obj-m += ep_modtest.o      # always built as a loadable module
# or
obj-y += ep_modtest.o      # always built directly into the kernel image

Hard-coding obj-y or obj-m means the driver is always built one way or the other, for every board using that Makefile. In real kernel trees this is almost always done conditionally instead, driven by a Kconfig option so the driver can be turned on or off per board configuration:

# drivers/char/Kconfig
config EP_MODTEST
	tristate "EP module build/load demo driver"
	default n
	help
	  Minimal demo driver used to illustrate kernel module
	  build and load mechanics. Say M to build as a module.
# drivers/char/Makefile
obj-$(CONFIG_EP_MODTEST) += ep_modtest.o

With tristate, running make menuconfig lets you pick y (built-in), m (module), or leave it out entirely — obj-$(CONFIG_EP_MODTEST) then expands to obj-y, obj-m, or nothing at all, depending on what was selected during configuration.

Loading and Unloading Modules

Once you have a .ko file, four small utilities from kmod cover almost everything you need day to day:

CommandPurpose
insmodLoad a module from an explicit .ko path, no dependency resolution
rmmodUnload a module by name
lsmodList currently loaded modules, their size, and use count
modinfoInspect a .ko file’s metadata without loading it
$ sudo insmod ./ep_modtest.ko
$ dmesg | tail -n 2
[ 1942.113092] ep_modtest: loaded

$ lsmod | grep ep_modtest
ep_modtest             16384  0

$ modinfo ep_modtest.ko
filename:       /home/dev/ep_modtest/ep_modtest.ko
description:    Minimal module build/load demo
author:         EmbeddedPathashala
license:        GPL
srcversion:     4C3A9E9F2B1D7E6ABCF1234
depends:
vermagic:       6.11.0-generic SMP preempt mod_unload

$ sudo rmmod ep_modtest
$ dmesg | tail -n 1
[ 1958.501774] ep_modtest: unloaded

The vermagic line in modinfo output is what enforces the “only loads on the kernel it was built against” rule — insmod checks it against the running kernel’s version string and refuses to load on a mismatch, which is by far the most common “why won’t my module load” problem for beginners.

Installing Modules Properly with depmod and modprobe

insmod is fine for a module you just built and are testing by hand, but it doesn’t understand dependencies between modules, and it needs the exact file path every time. For anything installed permanently, modules are copied into /lib/modules/<kernel-release>/ (often under a subdirectory such as extra/ or updates/), and then depmod is run to (re)build the dependency and alias databases that live alongside them:

$ sudo cp ep_modtest.ko /lib/modules/$(uname -r)/extra/
$ sudo depmod -a
$ ls /lib/modules/$(uname -r)/
kernel  modules.alias  modules.alias.bin  modules.builtin
modules.builtin.bin  modules.dep  modules.dep.bin  modules.devname
modules.order  modules.softdep  modules.symbols  modules.symbols.bin

With that database in place, modprobe replaces insmod/rmmod for everyday use — it looks the module up by name instead of by path, pulls in any modules it depends on automatically, and can also remove a module along with any dependents nobody else still needs:

$ sudo modprobe ep_modtest
$ sudo modprobe -r ep_modtest

This dependency database is also what powers automatic module loading. When new hardware appears — a USB device being plugged in is the classic example — the kernel raises a uevent, udevd picks it up, reads the vendor and product identifiers the hardware reports, and searches modules.alias for a module whose declared device table matches. If it finds one, it calls modprobe for you, and the right driver loads with no user action at all. This only works for drivers that declare their supported devices with MODULE_DEVICE_TABLE(), a topic covered in full in the next lecture on device trees and platform data.

Common Mistakes and Troubleshooting

  • “Invalid module format” on insmod — almost always a vermagic mismatch; rebuild against the exact running kernel’s KDIR, don’t reuse a .ko across kernel versions.
  • Module builds but nothing shows in dmesg — check dmesg permissions/log level rather than assuming the module didn’t load; confirm with lsmod first.
  • “Unknown symbol” at load time — the module depends on a symbol exported by another module that isn’t loaded yet; check depends: in modinfo output and load that module (or let modprobe do it for you) first.
  • Forgetting depmod -a after copying a module into /lib/modules/ — modprobe will report “module not found” even though the file is right there, because the alias database hasn’t been regenerated.

Best Practices

  • Always set MODULE_LICENSE() honestly — an incorrect or missing license string taints the kernel and disables access to GPL-only exported symbols.
  • Prefer modprobe over insmod/rmmod for anything beyond quick iteration during development, so dependency handling stays automatic.
  • Use a Kconfig tristate option rather than hard-coded obj-y/obj-m for any in-tree driver so downstream board configurations can opt in or out.
  • Keep a clean make clean step in out-of-tree Makefiles — stray build artifacts from a previous kernel version are a frequent source of confusing build errors.

Summary

Every Linux driver, whether it lives permanently in the kernel source tree or is built as a standalone module, goes through the same Kbuild engine so that it stays binary-compatible with the exact kernel it will run on. Out-of-tree builds use a thin Makefile that redirects into the kernel’s build directory; in-tree builds hook into an existing subsystem Makefile, usually gated by a Kconfig tristate option. Once built, insmod/rmmod give direct, dependency-free control for quick testing, while depmod plus modprobe is the production path that resolves dependencies and enables udev-driven automatic loading. With that build-and-load pipeline solid, the next lecture moves on to how drivers actually get matched to hardware in the first place, through device trees and platform data.

FAQ

Why does my module fail to load with “invalid module format”?

This is almost always a kernel version mismatch — the module’s embedded vermagic string doesn’t match the running kernel. Rebuild the module against /lib/modules/$(uname -r)/build for the kernel you’re actually loading into.

What’s the difference between insmod and modprobe?

insmod loads a single .ko file from an explicit path with no dependency resolution. modprobe looks a module up by name using the depmod-generated database, automatically loading any modules it depends on first.

Do I need to run depmod every time I build a module?

Only when you install or update a module inside /lib/modules/<kernel-release>/. Modules loaded directly by path with insmod during development don’t need it.

What does obj-m vs obj-y actually control?

obj-m builds the source file as a separate loadable .ko module; obj-y links it directly into the kernel image (vmlinux) as a built-in driver. A Kconfig tristate option lets a board choose between the two, or neither.

Why do I get “unknown symbol” errors when loading my module?

Your module references a symbol exported by another module (via EXPORT_SYMBOL/EXPORT_SYMBOL_GPL) that isn’t currently loaded. Check the depends: field from modinfo and load that module first, or use modprobe which resolves this automatically.

Can a module I built on my PC run on my embedded board?

Only if it was cross-compiled against that board’s exact kernel source tree and configuration. A module built against your PC’s kernel headers will not load on a different architecture or kernel version.

What’s the point of MODULE_LICENSE?

It declares the module’s license to the kernel at load time. An incorrect or missing GPL-compatible license “taints” the kernel and blocks access to symbols exported with EXPORT_SYMBOL_GPL.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ {“@type”: “Question”, “name”: “Why does my module fail to load with \”invalid module format\”?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “This is almost always a kernel version mismatch — the module’s embedded vermagic string doesn’t match the running kernel. Rebuild the module against the exact kernel build directory for the kernel you’re loading into.”}}, {“@type”: “Question”, “name”: “What’s the difference between insmod and modprobe?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “insmod loads a single .ko file from an explicit path with no dependency resolution. modprobe looks a module up by name using the depmod-generated database and automatically loads any modules it depends on.”}}, {“@type”: “Question”, “name”: “Do I need to run depmod every time I build a module?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Only when you install or update a module inside the /lib/modules// tree. Modules loaded directly by path during development don’t require it.”}}, {“@type”: “Question”, “name”: “What does obj-m vs obj-y actually control?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “obj-m builds the source as a separate loadable .ko module; obj-y links it directly into the kernel image as a built-in driver.”}}, {“@type”: “Question”, “name”: “Why do I get \”unknown symbol\” errors when loading my module?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “The module references a symbol exported by another module that isn’t currently loaded. Check the depends field from modinfo and load that module first, or use modprobe.”}}, {“@type”: “Question”, “name”: “Can a module I built on my PC run on my embedded board?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Only if it was cross-compiled against that board’s exact kernel source tree and configuration.”}}, {“@type”: “Question”, “name”: “What’s the point of MODULE_LICENSE?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “It declares the module’s license to the kernel at load time; an incompatible or missing license taints the kernel and blocks GPL-only exported symbols.”}} ] }

Ready to Match Drivers to Hardware?

Next up in this free linux device drivers course: how the kernel discovers hardware and links it to the right driver using device trees and platform data.

Continue to Next Lecture Back to Course Index
PREV_LEC NEXT_LEC

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *