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:
tristatemeans three values:y(built into the kernel image),m(a loadable.komodule), or unset.boolsymbols only offeryor unset.selectforces a dependency on, without asking. That is why turning on one option silently turns on four others.depends onhides an option when its prerequisite is off. If a symbol you need "isn't in menuconfig", it is almost always hidden by an unmetdepends 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:
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:
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
munder the root filesystem. If the driver that mounts/is a module and there is no initramfs to load it, the kernel panics withVFS: Unable to mount root fs. Root filesystem, storage controller and console drivers belong in the image asy. - A bare
CONFIG_FOO=nin a fragment is ignored. Use# CONFIG_FOO is not set. selectcannot be overridden. If something keeps turning back on, find who selects it:grep -rn "select FOO" --include=Kconfig .- Silent console.
CONFIG_SERIAL_..._CONSOLE=yand a matchingconsole=inbootargsare 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.