Skip to content

03 · Kernel Configuration & Modules

In Level 1 you booted a kernel someone else built. In a product you own that kernel: which drivers are compiled in, which are modules, which are absent, and how a customer-specific driver gets loaded. This module covers the configuration system (Kconfig), the difference between y and m, config fragments — the only sane way to carry changes across kernel upgrades — and building an out-of-tree module against your kernel's build tree.

Kconfig: what the kernel actually reads

The kernel does not read menuconfig output directly. Everything funnels into one generated file, .config, in the build directory:

$ cd linux
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
#
# configuration written to .config
#
$ grep -c . .config
2114

Each line is a symbol declared by a Kconfig file somewhere in the tree:

config EXT4_FS
    tristate "The Extended 4 (ext4) filesystem"
    select JBD2
    select CRC16
    help
      This is the next generation of the ext3 filesystem.

Three facts follow from that snippet, and they explain most kernel-config confusion:

  • tristate means three values: y (built into the kernel image), m (a loadable .ko module), or unset. bool symbols only offer y or unset.
  • select forces a dependency on, without asking. That is why turning on one option silently turns on four others.
  • depends on hides an option when its prerequisite is off. If a symbol you need "isn't in menuconfig", it is almost always hidden by an unmet depends on, not missing from your tree.

Driving the configuration

menuconfig is the ncurses browser; the scriptable interfaces matter more inside a build system:

$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
$ make ARCH=arm64 olddefconfig        # accept defaults for new symbols
$ ./scripts/config --enable  CONFIG_OVERLAY_FS
$ ./scripts/config --module  CONFIG_USB_SERIAL_FTDI_SIO
$ ./scripts/config --disable CONFIG_DEBUG_INFO

Search inside menuconfig with / — it shows the symbol name, its dependencies, and which menu it lives in. That search is the fastest way to answer "why can't I select this?".

To find a board's starting point:

$ ls arch/arm64/configs/
defconfig
$ make ARCH=arm64 defconfig      # arm64 has one unified defconfig

On 32-bit ARM you get per-family files instead (imx_v6_v7_defconfig, multi_v7_defconfig). A vendor BSP always states which defconfig its board expects — using the wrong one produces a kernel that boots to a blank serial console.

Config fragments: the only maintainable approach

Never ship a 5,000-line .config as your product configuration. You cannot review a diff of it, and it does not survive a kernel version bump. Ship a fragment — just the deltas:

# my-appliance.cfg
CONFIG_OVERLAY_FS=y
CONFIG_SQUASHFS=y
CONFIG_SQUASHFS_XZ=y
CONFIG_WATCHDOG=y
CONFIG_WATCHDOG_SYSFS=y
# CONFIG_DEBUG_INFO is not set

Note the comment form: # CONFIG_FOO is not set is how Kconfig spells "off". A bare CONFIG_FOO=n line is ignored by the merge tooling — a classic silent failure.

Merge it:

$ ARCH=arm64 ./scripts/kconfig/merge_config.sh -m .config my-appliance.cfg
Using .config as base
Merging my-appliance.cfg
$ make ARCH=arm64 olddefconfig

Always verify afterwards that the merge actually took, because select and depends on can quietly override you:

$ grep -E "OVERLAY_FS|SQUASHFS_XZ" .config
CONFIG_OVERLAY_FS=y
CONFIG_SQUASHFS_XZ=y

In Yocto the same fragment goes in a .bbappend beside the kernel recipe, and the kernel-yocto class runs the merge for you:

# meta-mylayer/recipes-kernel/linux/linux-imx_%.bbappend
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
SRC_URI += "file://my-appliance.cfg"

Building and installing

$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image dtbs modules
  ...
  LD      vmlinux
  OBJCOPY arch/arm64/boot/Image
  DTC     arch/arm64/boot/dts/freescale/imx95-19x19-evk.dtb
$ make ARCH=arm64 INSTALL_MOD_PATH=/srv/rootfs modules_install
  INSTALL /srv/rootfs/lib/modules/6.6.23/kernel/fs/overlayfs/overlay.ko
  DEPMOD  /srv/rootfs/lib/modules/6.6.23

INSTALL_MOD_PATH is not optional when cross-building — omit it and you install target modules into your build host's /lib/modules, which at best does nothing and at worst confuses the host's own kernel.

Out-of-tree modules

Vendor and customer drivers usually live outside the kernel tree. The minimal case:

/* hello_embed.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/utsname.h>

static int __init hello_init(void)
{
    pr_info("hello_embed: loaded on %s\n", init_utsname()->machine);
    return 0;
}

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

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("You");
MODULE_DESCRIPTION("Minimal out-of-tree module example");
# Makefile
obj-m := hello_embed.o

KDIR ?= /srv/linux
all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KDIR=/srv/linux
make -C /srv/linux M=/home/dev/hello modules
  CC [M]  /home/dev/hello/hello_embed.o
  MODPOST /home/dev/hello/Module.symvers
  LD [M]  /home/dev/hello/hello_embed.ko

MODULE_LICENSE("GPL") is load-bearing, not paperwork: a non-GPL string taints the kernel and denies the module access to GPL-only exported symbols, which surfaces as an undefined-symbol error at insmod time.

On target:

root@qemuarm64:~# insmod hello_embed.ko
root@qemuarm64:~# dmesg | tail -1
[  142.883021] hello_embed: loaded on aarch64
root@qemuarm64:~# lsmod
Module                  Size  Used by
hello_embed            16384  0
root@qemuarm64:~# modinfo hello_embed.ko
filename:       /root/hello_embed.ko
description:    Minimal out-of-tree module example
license:        GPL
vermagic:       6.6.23 SMP preempt mod_unload aarch64
root@qemuarm64:~# rmmod hello_embed

insmod takes a path and does nothing clever. modprobe takes a name, resolves dependencies from modules.dep, and honours /etc/modprobe.d/*.conf — use modprobe everywhere except when hand-testing a freshly built .ko.

Traps

Kernel config traps

  • vermagic mismatch. A module built against 6.6.23 refuses to load on 6.6.24 (insmod: ERROR: could not insert module: Invalid module format). Modules are bound to the exact kernel build — rebuild them whenever the kernel changes, and ship the two together in one image.
  • Filesystem driver as m under the root filesystem. If the driver that mounts / is a module and there is no initramfs to load it, the kernel panics with VFS: Unable to mount root fs. Root filesystem, storage controller and console drivers belong in the image as y.
  • A bare CONFIG_FOO=n in a fragment is ignored. Use # CONFIG_FOO is not set.
  • select cannot be overridden. If something keeps turning back on, find who selects it: grep -rn "select FOO" --include=Kconfig .
  • Silent console. CONFIG_SERIAL_..._CONSOLE=y and a matching console= in bootargs are separate requirements; missing either gives you a board that boots perfectly and says nothing — indistinguishable from a brick until you attach JTAG.
  • Forgetting ARCH= on a later make. The build silently falls back to the host architecture and reconfigures your tree.

Cheat sheet

Command / item Purpose
make ARCH=arm64 defconfig Start from the architecture's default config
make menuconfig Interactive config browser (/ to search)
make olddefconfig Accept defaults for newly introduced symbols
./scripts/config --enable/--module/--disable Scriptable single-symbol edits
scripts/kconfig/merge_config.sh -m .config frag.cfg Merge a config fragment
# CONFIG_FOO is not set The correct way to spell "off"
y / m / unset Built-in / loadable module / absent
make Image dtbs modules Build kernel image, device trees, modules
INSTALL_MOD_PATH=<rootfs> make modules_install Install modules into a target rootfs
obj-m := foo.o + make -C $KDIR M=$PWD Out-of-tree module build
insmod / rmmod Load/unload one exact .ko file
modprobe / modprobe -r Load/unload by name, resolving dependencies
lsmod / modinfo List loaded modules / inspect a module's metadata
depmod -a Regenerate modules.dep after installing modules
/etc/modules-load.d/*.conf Modules to load at boot (systemd)
/etc/modprobe.d/*.conf Module options, aliases, blacklists

On verification

The Kconfig, fragment and kbuild syntax on this page follows the kernel's documented rules, but a full cross-kernel build was not run while writing it. Budget one build cycle to confirm symbol names against your kernel version — symbols are added, renamed and retired between releases.

How It Actually Works

Kconfig is a constraint solver over a dependency DAG, not a flat list of switches. Every Kconfig file's config FOO stanza declares depends on, select, and default edges; make menuconfig's ncurses frontend and make olddefconfig's batch mode both run the same libkconfig evaluator, which propagates those constraints — turning on CONFIG_USB_GADGET can silently force CONFIG_USB_COMMON=y via select even though you never touched it, and turning off a dependency can silently drop something you explicitly set (.config's next olddefconfig pass removes an option whose depends on no longer holds). This propagation is why "hand-editing .config" is fragile — you're mutating one node in a graph, and only re-running the evaluator (olddefconfig) makes the file internally consistent again.

Fragments avoid full-file diffs because the merge tool replays them as if typed at the prompt. scripts/kconfig/merge_config.sh doesn't line-merge text files — it starts from a base .config (or empty), concatenates each fragment's CONFIG_X=y lines onto it in order, then runs the Kconfig evaluator once over the result via olddefconfig. A later fragment's setting for the same symbol always wins, and any resulting inconsistency (setting something whose dependency is off) gets silently resolved by the evaluator rather than erroring — which is why merge_config.sh -y is worth diffing against your fragments afterward.

A module is a stripped-down, relocatable ELF object, and insertion is runtime linking. .ko files are ET_REL ELF objects containing an __versions section (symbol CRCs when CONFIG_MODVERSIONS=y) and a .modinfo section (license, vermagic string encoding the exact kernel version/config it was built against). insmod/modprobe call the init_module()/finit_module() syscall, which the kernel's module loader uses to relocate the object against the running kernel's symbol table (kallsyms) — resolving EXPORT_SYMBOL references the same way a userspace dynamic linker resolves shared-library symbols, except the "shared library" is the live kernel image itself. A vermagic mismatch (module built against a different kernel Makefile version/config) is rejected right there, before any relocation happens — that's the mechanism behind "invalid module format," not a generic compatibility guess.

Exercise

(1) Starting from arch/arm64/configs/defconfig, write a fragment that enables CONFIG_OVERLAY_FS and CONFIG_SQUASHFS as built-ins and disables CONFIG_DEBUG_INFO, merge it with merge_config.sh, and prove with grep that all three landed — then explain why CONFIG_DEBUG_INFO=n in the fragment would not have worked. (2) In menuconfig, search for a symbol you cannot select (try CONFIG_UBIFS_FS with MTD off) and write down the exact depends on chain hiding it. (3) Build the hello_embed module out-of-tree, load it in QEMU, capture dmesg, lsmod and modinfo; then rebuild your kernel with any config change and show the resulting vermagic failure when you load the old .ko. (4) One paragraph: your storage driver currently ships as m, and the device now boots from that storage. Describe what breaks, the two valid fixes (built-in versus initramfs), and which one you would choose for a product that must also support field kernel upgrades.