Module Dependencies and depmod
A loadable kernel module almost never stands alone: it calls functions and reads variables that live in other modules (or in the built-in kernel). The way one module “depends on” another in Linux is concrete and mechanical — module A depends on module B iff A references a symbol that B exports with [[Symbol Export and Module Namespaces|
EXPORT_SYMBOL]]. That relationship is computed twice: once at build time bymodpost, which writes the dependency list into the module’s own metadata as adepends=string, and once at install time bydepmod(8), which scans every.kounder/lib/modules/$(uname -r)/, reconciles each module’s imported symbols against the modules that export them, and emitsmodules.dep(plus a binarymodules.dep.binand companion index files). At load time, [[Module Loading insmod modprobe and kmod|modprobe]] readsmodules.dep.binand loads every prerequisite before the requested module, bottom-up. This note owns that dependency-resolution machinery end to end; the metadata macros themselves live in Module Parameters and Metadata Macros.
A loadable kernel module (LKM) is a relocatable .ko ELF object the kernel links into itself at runtime — see Loadable Kernel Modules for the object itself. This note pins all version-sensitive claims to Linux 6.12 LTS (released 2024-11-17), cross-checked against 6.18 LTS where the mechanism changed.
Mental Model: Dependencies Are Just Symbol Imports
The single idea to internalize: in Linux there is no separate, hand-maintained dependency manifest. A dependency is an unresolved symbol reference that some other module satisfies. When you compile a module, the C compiler leaves the addresses of functions it calls in other modules as undefined symbols in the .ko’s symbol table — exactly as a userspace .o leaves undefined references to libc. The kernel’s build and load tooling then plays the role of a linker: it walks those undefined symbols, finds which module exports each one, and records “you need that module first.”
flowchart TB subgraph BUILD["Build time (modpost)"] A[".ko has undefined<br/>symbol foo()"] -->|"modpost matches against<br/>Module.symvers / other .ko"| B["foo is exported by drv_b"] B --> C["emit MODULE_INFO(depends,<br/>"drv_b") into .modinfo"] end subgraph INSTALL["Install time (depmod)"] C --> D["depmod scans<br/>/lib/modules/VER/"] D --> E["reads each module's<br/>imports + exports"] E --> F["writes modules.dep:<br/>drv_a.ko: drv_b.ko"] F --> G["+ modules.dep.bin (hashed),<br/>modules.alias, modules.symbols,<br/>modules.builtin"] end subgraph LOAD["Load time (modprobe)"] G --> H["modprobe drv_a reads<br/>modules.dep.bin"] H --> I["load drv_b first,<br/>then drv_a"] end
The lifecycle of a dependency, from compiler to load. What it shows: the same fact — “drv_a uses a symbol from drv_b” — is computed at build (modpost, into .modinfo), re-derived and indexed at install (depmod, into modules.dep), and consumed at load (modprobe, loading bottom-up). The insight: there is no authoritative central dependency list shipped with the kernel; modules.dep is a derived cache that depmod regenerates from the symbol facts baked into each .ko, which is why you must rerun depmod after adding or replacing modules.
How modpost Computes the depends= String at Build Time
The first half of dependency resolution happens during the kernel build, in the host program scripts/mod/modpost. After all object files are compiled, modpost parses each module’s ELF, collects its undefined symbols (the things it needs) and its defined exported symbols (the things it provides), and cross-references them against Module.symvers — the table of every symbol the vmlinux core and the in-tree modules export. For each unresolved symbol that turns out to be owned by another module (not by the built-in vmlinux), modpost records that owning module as a dependency.
The emission is small and worth reading literally. modpost’s add_depends() walks the module’s unresolved-symbol list, dedupes the owning modules with a seen flag (so a module you import ten symbols from appears once), strips the directory prefix off each owner’s path, and prints a single comma-joined string (modpost.c:1854-1885, v6.12):
static void add_depends(struct buffer *b, struct module *mod)
{
struct symbol *s;
int first = 1;
/* Clear ->seen flag of modules that own symbols needed by this. */
list_for_each_entry(s, &mod->unresolved_symbols, list)
if (s->module)
s->module->seen = s->module->is_vmlinux; /* vmlinux pre-seen => skipped */
buf_printf(b, "MODULE_INFO(depends, \"");
list_for_each_entry(s, &mod->unresolved_symbols, list) {
if (!s->module || s->module->seen) /* skip vmlinux + dupes */
continue;
s->module->seen = true;
p = strrchr(s->module->name, '/'); /* keep basename only */
buf_printf(b, "%s%s", first ? "" : ",", p ? p + 1 : s->module->name);
first = 0;
}
buf_printf(b, "\");\n");
}Two subtleties fall straight out of this code. First, the line s->module->seen = s->module->is_vmlinux; pre-marks the built-in kernel as “already seen,” so symbols satisfied by vmlinux never produce a dependency — depending on the core kernel is implicit. Second, the result is written into a generated <module>.mod.c file as MODULE_INFO(depends, "drv_b,drv_c"), which the second compilation pass bakes into the module’s .modinfo ELF section. So the dependency list is part of the module’s own metadata, visible later with modinfo <mod> | grep depends. (MODULE_INFO is the same metadata-emitting macro family covered in Module Parameters and Metadata Macros.)
This is why an out-of-tree module (Out-of-Tree Modules and the Kernel ABI) must be built against the exact kernel’s Module.symvers: modpost can only mark a symbol “exported by module X” if X’s export is in that table. Build against the wrong tree and the dependency string is wrong or the symbol is reported as wholly unresolved.
What depmod Does at Install Time
depmod(8) is the userspace tool (part of the kmod package) that turns a directory full of .ko files into the index files modprobe consults. Per its man page, “depmod creates a list of module dependencies by reading each module under <BASEDIR>/<MODULEDIR>/<version>. By default <MODULEDIR> is /lib/modules and <BASEDIR> is empty,” and it “determines what symbols each module exports and needs” — when “a second module uses this symbol, that second module clearly depends on the first module” (depmod(8)).
Note that depmod does not trust the depends= string blindly; it re-derives the dependency graph from the actual symbol tables of the installed .ko files. That redundancy is deliberate: a distribution can drop a third-party .ko into /lib/modules/<ver>/extra/ and a single depmod -a will splice it into the dependency graph alongside the in-tree modules, because depmod is reading symbols, not a precomputed manifest. depmod is invoked automatically by make modules_install, by kernel package post-install scripts, and (with -a) on demand.
The files depmod writes into /lib/modules/<version>/ are:
modules.dep— the human-readable dependency list. Each line isfilename: [filename]*, listing the complete (transitively flattened) dependencies of the first module in load order. Per the man page, ifa.kodepends onb.koandc.ko, andc.kodepends onb.ko, the file reads (modules.dep(5)):
The dependencies are listed in the order they must be loaded for that module, and the list is the full transitive closure, not just direct edges —kernel/a.ko: kernel/c.ko kernel/b.ko kernel/b.ko: kernel/c.ko kernel/c.ko: kernel/b.koa.ko’s line names bothc.koandb.koeven thoughaonly directly referencesc.modules.dep.bin— a binary, hashed index of the same data. This is the filemodprobeactually reads; the man page is explicit that “the text version is maintained only for ease of reading by humans and is in no way used by any kmod tool,” while the.binform “is used by kmod tools such as modprobe and libkmod” (modules.dep(5)).modules.alias— maps wildcard modalias strings to module names, collected from each module’sMODULE_ALIAS/MODULE_DEVICE_TABLE-derived aliases. This is what powers hotplug autoloading; the matching mechanics belong to Device-Driver Matching, and the macros to Module Parameters and Metadata Macros. depmod merely harvests the aliases out of each.modinfoand indexes them.modules.symbols(andmodules.symbols.bin) — maps every exported symbol to the module that provides it, assymbol:<name>aliases, somodprobe symbol:foocan find the right module.modules.devname— special device-node names some modules request for/devpre-population at boot (thedevname:modinfo), letting systemd/udev create the node before the module is loaded.modules.builtin— lists every module compiled into the kernel rather than as a.ko. Per Kbuild docs this is “used by modprobe to not fail when trying to load something builtin” (Kbuild docs). Without it,modprobe ext4on a kernel with ext4 built-in would error “module not found” instead of silently succeeding. Notemodules.builtinis generated by Kbuild, not depmod — depmod reads it.
modules.order and Deterministic Alias Resolution
modules.order “records the order in which modules appear in Makefiles” and “is used by modprobe to deterministically resolve aliases that match multiple modules” (Kbuild docs). The problem it solves: when two modules both claim the same modalias (two drivers that can both bind a given device), which does modprobe <modalias> load? For built-in code the answer is implicit — the link order set by the Makefile determines which module_init runs first. For loadable modules that link order is otherwise lost. So Kbuild emits modules.order, and depmod sorts its alias index to honor it, giving loadable modules the same priority order the kernel Makefiles intended (LKML “depmod: sort output according to modules.order”).
How modprobe Consumes the Dependency Graph
When you run modprobe drv_a, modprobe reads modules.dep.bin, finds drv_a’s line, and loads every listed prerequisite first, then drv_a itself — the man page states it “uses this to add or remove these dependencies automatically” (modprobe(8)). Each individual insertion uses the finit_module(2) syscall (the mechanism owned by Module Loading insmod modprobe and kmod). Because modules.dep already flattens transitivity and lists deps in load order, modprobe does not need to do graph traversal at runtime; it walks the precomputed list.
You can see exactly what modprobe would load without loading anything:
$ modprobe --show-depends mac80211
insmod /lib/modules/6.12.0/kernel/net/wireless/cfg80211.ko
insmod /lib/modules/6.12.0/kernel/net/mac80211/mac80211.ko--show-depends “lists the dependencies of a module (or alias), including the module itself,” and is “typically used by distributions to determine which modules to include when generating initrd/initramfs images” (modprobe(8)) — note cfg80211 is listed before mac80211, the load order. Removal runs in reverse: modprobe -r drv_a unloads drv_a then any now-unused dependencies.
Soft, Weak, and Pre/Post Dependencies
The depends= graph captures only hard symbol dependencies. There are three softer relationships, all expressed in module metadata or modprobe.d config rather than derived from symbols.
A soft dependency is an optional companion module — modulename works without it but with reduced functionality. It is declared either in the module source with MODULE_SOFTDEP("pre: foo post: bar") (which emits a softdep= modinfo string, see Module Parameters and Metadata Macros) or in /etc/modprobe.d/*.conf with a softdep line. The pre:/post: keywords set ordering relative to the main module: per modprobe.d(5), running modprobe c with softdep c pre: a b post: d e “is equivalent to modprobe a b c d e” — pre-deps load before, post-deps after (modprobe.d(5)). The critical distinction from a hard dependency: a missing or failing softdep does not abort the load; “modulename can be used without these optional modules installed, but usually with some features missing.”
A weak dependency (MODULE_WEAKDEP / weakdep in modprobe.d) is softer still. Per the man page, “unlike softdeps, userspace doesn’t attempt loading them; the kernel requests them during probe if needed” (modprobe.d(5)). It expresses “this driver can use that module” without eagerly pulling it in at modprobe time — the in-kernel request_module() path decides at runtime.
Uncertain
Verify: that
MODULE_WEAKDEP/weakdep-based weak dependencies are fully wired in 6.12 (theweakdep=modinfo is emitted by the macro permodule.h:180, but the runtime/userspace handling split — “kernel requests during probe” — comes from themodprobe.d(5)man page, not directly traced in 6.12 source for this note). Reason: the load-time vs probe-time division is documented but the exact 6.12 code path was not read. To resolve: tracerequest_module()call sites and howkmod/libkmod consumesweakdep=vssoftdep=. uncertain
Configuration and Diagnostics
Inspecting the dependency facts of a module is done with modinfo (which reads the .modinfo section, see Module Parameters and Metadata Macros):
$ modinfo -F depends snd_hda_intel
snd-hda-codec,snd-hda-core,snd-pcm,sndThe -F depends flag prints only the depends field — the build-time MODULE_INFO(depends, ...) string modpost emitted. Compare that against what modprobe would actually do:
$ depmod -n drv_a.ko | head # dry-run: print to stdout, write nothing (-n)
$ sudo depmod -a # regenerate all index files for running kernel
$ sudo depmod -a 6.18.0 # regenerate for a specific installed kernel version
$ depmod -e -F /boot/System.map-6.12.0 6.12.0 # also report unresolved symbols (-e needs -F)The -n/--dry-run option writes the generated files to stdout instead of touching the filesystem — useful for seeing what depmod would produce. -a/--all (the default when no module names are given) probes every module. -A/--quick exits silently if nothing is newer than modules.dep, which is why package managers can call it cheaply. -e combined with -F System.map makes depmod report symbols no module or the kernel satisfies — the canonical way to catch a half-installed module set (depmod(8)).
Failure Modes
**“Unknown symbol in module” / modprobe: ERROR: could not insert ... Unknown symbol".** The module references a symbol no loaded module or the kernel exports. Causes: a prerequisite module is not loaded (you insmod-ed directly, bypassing modprobe's dependency loading), modules.depis stale (you copied a.koin without runningdepmod), or a [[Symbol Export and Module Namespaces|namespace]] was not imported. Diagnose with modinfo -F depends, depmod -e -F System.map, and cat /sys/module/
Stale modules.dep after manual install. Dropping a .ko into /lib/modules/<ver>/extra/ without depmod -a means modprobe cannot find it and cannot compute its dependencies. The fix is always depmod -a. This is the single most common self-inflicted modprobe failure.
A built-in “module” reported as not found. modprobe foo errors because foo is compiled into vmlinux, but modules.builtin is missing or stale. Without that file, modprobe cannot distinguish “built-in, nothing to do” from “does not exist.” Regenerate with depmod (and ensure Kbuild produced modules.builtin at install).
Dependency cycle. Two modules that export symbols to each other (as in the b.ko/c.ko man-page example) are legal — modules.dep records the closure and modprobe loads them together, but a genuine initialization cycle (each module_init needs the other already running) will deadlock or fail. The design discourages this; it usually signals a layering mistake.
Wrong-kernel .ko with a bad depends= string. A module built against a different Module.symvers may carry an incorrect dependency list or vermagic mismatch; the kernel’s check_modinfo() will reject it on vermagic before dependencies even matter (main.c:2094, v6.12). See Out-of-Tree Modules and the Kernel ABI.
Alternatives and Boundaries
The depmod/modules.dep scheme is specific to the kmod userspace (the modern successor to the old module-init-tools). The kernel’s own in-memory dependency tracking is a different thing: when a module actually loads and resolves a symbol from another, the kernel records a module_use edge (the source_list/target_list in struct module) and bumps the providing module’s refcount, so rmmod of a still-used module fails with -EBUSY. That live refcount graph — surfaced as /sys/module/<mod>/holders/ and the “Used by” column of lsmod — is the runtime truth; modules.dep is the pre-load prediction depmod computes so modprobe loads things in the right order in the first place. The two agree by construction but are maintained by different layers (kmod userspace vs the kernel module loader). Minimal/embedded systems sometimes skip kmod entirely and statically insmod modules in a fixed order from an init script, sidestepping modules.dep altogether — viable only when the dependency order is known and small.
See Also
- Loadable Kernel Modules — the
.koobject whose symbols create these dependencies - Module Loading insmod modprobe and kmod — the syscalls and tools that consume
modules.dep - Module Parameters and Metadata Macros —
MODULE_SOFTDEP/MODULE_WEAKDEP/MODULE_ALIASand the.modinfosection depmod harvests - Symbol Export and Module Namespaces —
EXPORT_SYMBOL, the source of every hard dependency - Device-Driver Matching — how
modules.aliasmodalias strings drive hotplug autoloading - Out-of-Tree Modules and the Kernel ABI — why dependency computation needs the matching
Module.symvers - Linux Device Drivers and Device Model MOC — §6, the kernel module system