Kernel & Driver Build Environments

How to build the kernel driver and out-of-tree modules like amdgpu across kernel versions in CI, covering Kbuild, vermagic/ABI, DKMS, cross-compiling, and Secure Boot signing.

Overview

Building Against the Kernel, Not Against libc

An out-of-tree kernel module is not a normal C program. It has no libc, no dynamic linker, and no stable ABI. It is compiled against one specific prepared kernel tree and is only guaranteed to load into a kernel built from that exact tree, with that exact .config and compiler. This is why amdgpu built for 6.8.0-31-generic refuses to load on 6.8.0-35-generic with invalid module format, even though the two version strings look almost identical.

The Core Build Invocation

Every out-of-tree build delegates to the kernel's own build system, Kbuild. The canonical command is make -C /lib/modules/$(uname -r)/build M=$PWD modules. The -C switches into the prepared kernel build directory so the kernel's top-level Makefile and .config drive the compile; M=$PWD tells Kbuild to build the external module in the current directory using its small Kbuild/Makefile with obj-m += mydriver.o.

Why This Matters in CI

In an AMD GPU-stack CI role you never build just once. You build the same driver across a matrix of kernel versions and architectures, on runners where the headers arrive over NFS, where Secure Boot demands signed modules, and where a kernel upgrade on a test host must not silently break the driver. That is exactly what kernel-devel packages, vermagic, DKMS, CROSS_COMPILE, and module signing collectively solve. This topic walks the practical mechanics of each.

Key concepts

Kbuild and obj-m
The kernel's recursive make-based build system. An external module ships a Kbuild or Makefile that lists obj-m += foo.o (build foo.ko as a loadable module) and, for multi-file modules, foo-y := a.o b.o. You invoke it via the prepared kernel tree with make -C M=$PWD modules, so the kernel's flags, .config, and compiler settings govern the build rather than your own.
kernel-devel / linux-headers vs full source
You do not need full kernel source to build a module. The kernel-devel (Fedora/RHEL) or linux-headers-$(uname -r) (Debian/Ubuntu) package provides the prepared build tree: headers, Kconfig/.config, Module.symvers, and scripts/ (including modpost). It is exposed at /lib/modules/$(uname -r)/build, a symlink into that package. Full source is only needed to build the kernel itself.
vermagic
A version-magic string baked into every .ko by modpost, combining kernel release, SMP, preemption model, module-versioning state, and other ABI markers (for example 6.8.0-35-generic SMP mod_unload modversions). At load time the kernel compares the module's vermagic against its own; any mismatch yields invalid module format. It is the coarse gate that stops an ABI-incompatible module from loading.
Module versioning (modversions / Module.symvers)
With CONFIG_MODVERSIONS=y, each exported symbol gets a CRC derived from its type signature, recorded in Module.symvers. A module stores the CRCs it was built against; if the running kernel's CRC for a symbol differs, load fails with disagrees about version of symbol. This catches ABI drift finer than vermagic (for example a changed struct that keeps the same kernel release).
DKMS (Dynamic Kernel Module Support)
A framework that stores driver source under /usr/src/-/ with a dkms.conf, then rebuilds and installs the module automatically whenever a new kernel is installed. Driven by dkms add, dkms build, dkms install; kernel package post-install hooks trigger rebuilds so the driver survives upgrades. This is how AMD ships amdgpu-dkms.
ARCH and CROSS_COMPILE
Two make variables controlling cross-compilation. ARCH selects the target architecture subtree (for example arm64), and CROSS_COMPILE is the toolchain prefix (for example aarch64-linux-gnu-) prepended to gcc, ld, objcopy. You must build against a kernel tree prepared for that same ARCH; mixing a host x86_64 tree with ARCH=arm64 will not work.
Module signing and Secure Boot
Under Secure Boot with kernel lockdown, unsigned modules are rejected (or the kernel is tainted and load blocked). Modules are signed with scripts/sign-file using a private key whose matching certificate is enrolled in the platform (a MOK via mokutil, or a build-time key in CONFIG_MODULE_SIG_KEY). The signature is appended to the .ko; modinfo then shows sig_id/signer.

When to use it

  • You maintain an out-of-tree driver (for example amdgpu-dkms or a custom module) that must build and load cleanly across many kernel versions in CI.
  • A module compiled successfully but fails at modprobe/insmod with invalid module format or disagrees about version of symbol, and you must trace it to an ABI/vermagic mismatch.
  • You are packaging a driver so it automatically rebuilds when a test host receives a kernel upgrade, rather than silently losing the device after reboot.
  • You must cross-compile a driver for a different architecture (for example aarch64) from x86_64 build runners.
  • Target hosts enforce Secure Boot, so every module must be signed with an enrolled key or the kernel refuses to load it (or taints and blocks under lockdown).

Interview questions

  • How do you build an out-of-tree module in CI across a matrix of kernel versions?
  • How do you package a driver with DKMS so it survives kernel upgrades?
  • How do you diagnose a vermagic/ABI mismatch at load time?
  • How do you cross-compile a driver for aarch64 from an x86_64 runner?
  • How do you sign modules so they load on a Secure Boot host?
  • How do you handle NFS-mounted kernel headers and toolchains on the runner, and keep builds reproducible?

Sign up free to read the full topic

More Linux topics

Start free on Cariara · Coding problems · System design studio