I straced modprobe on Linux

The linux kernel does almost nothing when you run modprobe.

You type:

modprobe hid-logitech

Your mouse starts working.

The explanation sounds simple - "The kernel loads a driver".

But if you trace what actually happens, the story changes.

The kernel does almost nothing until the very end.

Files are opened. Directories are scanned. Configuration files are parsed. Dependency databases are mapped into memory.

All of that happens in user space.

The kernel stays idle until user space hands it a finished binary.

Why user space does most of the work for something need to be run on kernel?

Because of a core Unix idea:

User space decides policy. The kernel provides mechanism.

This article traces one complete journey.

From modprobe to the instant the kernel accepts a module.

Not to teach module writing, but to show what load actually means.

The Command That Does Everything

The trace starts with this line:

execve("/usr/sbin/modprobe", ["modprobe", "hid-logitech"], ...) = 0

The output scrolls past hundreds of lines before anything related to module loading appears.

You see dynamic libraries loading.

Memory being mapped.

Files opened and closed.

The kernel services sys calls, but it is not involved in module loading yet.

Then, near the end, something comes up:

finit_module(...)

That is the moment the kernel finally participates.

Everything before it happened in user space.

This surprises most of us.

The name modprobe sounds like a kernel tool.

The manual says it “adds and removes modules from the Linux kernel.”

The phrasing makes it sound like modprobe reaches into kernel space and installs code directly.

It does not.

Modprobe cannot load anything by itself.

It prepares.

The kernel decides.

This split is intentional.

The kernel is not a general-purpose loader. It is a gatekeeper.

Unix systems keep the kernel small by pushing policy into user space.

Modprobe is policy code.

It decides where modules live, how dependencies work, and what parameters to pass.

The kernel is mechanism code.

It maps pages, checks signatures, and links symbols.

This pattern is fundamental to how Linux works.

If you understand this handoff, you understand the system's philosophy.

Finding Something That Does Not Exist Yet

You typed a name "hid-logitech" as argument to modprobe.

Not a path.

Not a file.

That name does not exist anywhere on disk.

So the first problem modprobe must solve is simple and strange:

How do you load something that does not exist yet?

The answer lives entirely in userspace.

Modprobe turns that label into a file by reading userspace databases:

/lib/modules/$(uname -r)/modules.dep.bin
/lib/modules/$(uname -r)/modules.alias.bin
/lib/modules/$(uname -r)/modules.symbols.bin
/lib/modules/$(uname -r)/modules.dep

These files are not built by the kernel.

They are generated by depmod, another userspace program.

depmod audits the filesystem.

It reads ELF headers.

It extracts symbols.

It builds dependency graphs.

It writes everything down as plain data.

The kernel never sees this process.

It runs at boot or install time, scheduled by init systems and package managers. If it breaks, you fix userspace. The kernel does not care.

To choose the correct database, modprobe need the right kernel:

uname -r

That tells it which kernel is running:

6.6.87.2-microsoft-standard-WSL2

Once it finds an entry for hid-logitech, it finally has a real path:

/lib/modules/(uname -r)/kernel/drivers/hid/hid-logitech.ko

The Dependency Graph

Now modprobe knows which file to load.

But what about it's dependencies?

It reads another file called modules.dep:

/lib/modules/(uname -r)/modules.dep

The modules.dep file contains entries like this:

kernel/drivers/hid/hid-logitech.ko:
    kernel/drivers/hid/usbhid/usbhid.ko
    kernel/drivers/hid/hid.ko
    kernel/drivers/usb/core/usbcore.ko
    kernel/drivers/usb/common/usb-common.ko

Then it picks the first dependency, go through the modules.dep and find it's dependency and follow the same for all the above listed modules, and at the end create the graph like this:

hid-logitech.ko
├── usbhid.ko
│   ├── hid.ko
│   └── usbcore.ko
│       └── usb-common.ko
├── hid.ko
├── usbcore.ko
│   └── usb-common.ko
└── usb-common.ko

It builds a load order: dependencies first, requested module last.

So, the load order would be:

usb-common.ko -> usbcore.ko -> hid.ko -> usbhid.ko -> hid-logitech.ko

The kernel never sees the graph. It never computes the order. It receives modules one at a time, in the correct sequence, from userspace.

But the order matters.

If hid-logitech loads before usbhid, symbol resolution fails. The kernel will reject it.

Dependency resolution is policy.

The kernel does not implement policy.

Userspace must get it right before knocking on the kernel’s door.

The Parameter Problem

Modules accept parameters. You can pass them on the command line:

modprobe hid-logitech.ko debug=2

Or in configuration files:

options hid-logitech.ko debug=2

Modprobe reads /etc/modprobe.d/ and scans for lines matching the module name. It builds a string of parameters.

It merges configuration defaults with command-line options.

It builds a raw parameter string.

That string is passed to the kernel as part of the load request.

The kernel does not parse it.

Instead, the kernel hands the string to the module’s init function.

The kernel provides helpers like module_param(), which generate parsing code. But that code lives inside the module, not in core kernel logic.

The module decides what parameters mean.

The kernel provides infrastructure. It does not interpret policy.

Once again: userspace decides. The kernel enables.

The Binary Handoff

Now modprobe opens the .ko files one by one, in the correct order, using a regular open() system call.

It obtains file descriptors.

These are ordinary descriptors, no different from those used to read text files or sockets.

Then it calls finit_module(fd, params, flags).

The system call is simple. Three arguments:

A file descriptor pointing to an ELF binary

A string of parameters

A set of flags

In strace, you will see similar lines:

finit_module(3, "", 0) = 0         // usb-common.ko
finit_module(4, "", 0) = 0         // usbcore.ko
finit_module(5, "", 0) = 0         // hid.ko
finit_module(6, "", 0) = 0         // usbhid.ko
finit_module(7, "", 0) = 0         // hid-logitech.ko

This is the handoff.

Everything before this point happened in userspace.

Everything after happens in the kernel.

That boundary is easy to miss.

This is strange if you think about it.

The code that will run in kernel space, with full hardware access and zero memory protection, starts its journey as an ordinary file that any user with read permissions could open.

At this stage, modprobe is just reading bytes.

The security comes later.

But already, important decisions have been made.

Which directory to search. Which version of the kernel to match. Which file format to expect. How to parse the dependency database. What parameters to pass.

Now the kernel takes over.

The Verification Gate

Once finit_module is called, the kernel wakes up.

It checks:

  • Is this a valid ELF file?
  • Does it match the running architecture?
  • Is it signed, if signatures are required?

If any check fails, the load stops.

The kernel does not ask why you want the module. It does not attempt recovery.

This brevity is intentional. Error handling is userspace's job.

The kernel just want to know one thing: "is this binary safe to execute in kernel space?"

If the answer is no, it rejects it.

If all checks pass, the kernel moves to the next phase: symbol resolution.

The Linking Phase

If verification succeeds, the kernel links the module.

It resolves symbols against the kernel and already-loaded modules. Addresses are patched. Relocations applied.

If a required symbol is missing, the load fails.

If linking succeeds, the kernel calls the module’s init function.

At that moment, the module becomes part of the kernel.

The handoff is complete.

Why Userspace Leads

This design keeps the kernel small.

If the kernel searched directories, it would need filesystem policy.

If it resolved dependencies, it would need graph logic.

If it parsed configuration, it would need string handling.

All of that would live in kernel space.

Linux avoids this by pushing complexity outward.

Userspace can change without rebooting.

Userspace can fail without crashing the system.

Userspace can evolve while the kernel stays stable.

The cost is strictness.

If userspace is wrong, the kernel will not compensate.

That is the trade-off.

Once you see this handoff, the system stops feeling magical.

It starts feeling disciplined.

Userspace decides what to do.

The kernel decides whether it is allowed.

Subscribe to Vishnu

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe