So You Want to Know All the Files Used in a Linux Kernel Build?
At some point, we all ask ourselves this question, it is kinda rite of passage:
Hey, I just want to list all the files that go into building this particular Linux kernel configuration. How hard can it be?”
Ha. Hahaha.
You sweet summer child.
(credits: Game of Thrones Stark’s Old Nan to Bran S01E03)
Let me take you through my journey of trying to do exactly that: not just for a single object, but for the entire kernel build, based on a specific configuration. Spoiler: it sounds simple, but the rabbit hole goes deep.
My Original Genius Plan (That Didn’t Survive Reality)
I thought: “It’s just a matter of combining a few logical steps.”
- Use
.config
to know which options are enabled. - Parse the
Makefile
s (Kconfig are not needed, config is supposed to be static) to find out which.c
files will actually be compiled. - Recursively parse
#include
directives to find all needed headers. - Done!
Except not.
That third point hides drangos! That’s where dreams go to die. Because parsing includes in the kernel is a nightmare wrapped in a macro inside a conditional.
Just look to this abomination:
#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)
What is TRACE_INCLUDE_FILE
?
It’s a macro.
Where is it defined?
Somewhere else... Not in this file... Possibly not even in this compilation unit... Possibly on the moon.
Static parsing? Good luck.
You Need a Preprocessor, Not Just a Text Scanner
If you really want to know what files a .c
file pulls in, you need to preprocess it with the compiler. Like, for real. Not pretend. You need the full gcc -E
or -MMD
treatment. And actually, here’s the twist, the Linux kernel already does that! The kernel’s build system uses gcc -MMD
by default, which generates a .d
file alongside each .o
or sometimes .s
file. This .d
file lists all the headers used for that .c
file during preprocessing.
Awesome, right?
Except…
The Case of the Disappearing .d
Files
I excitedly looked for these .d
files, expecting a gold mine, and found… nothing. They’re gone. Vanished. Turns out, the kernel build system generates them, uses them, and then deletes them. Like a ninja with commitment issues.
Why?
Because they’re only meant to feed into a utility called…
Enter fixdep
and the .cmd
Files
Once upon a time, the kernel used .d
files directly. But those were replaced by .cmd
files, which are more versatile and kernel-specific. Here’s how it works now:
- The compiler creates a
.d
file (thanks to-MMD
). - The kernel build system runs
fixdep
on that.d
file. fixdep
reads the dependencies, filters out junk (like system headers), adds references toCONFIG_*
flags, and writes everything into a.cmd
file.- Then it deletes the
.d
file like it never existed.
The resulting .cmd
file (e.g., net/core/.sock.o.cmd
) contains:
- The full compile command - The source file path
- A list of directly included headers
- Wildcards for
include/config/CONFIG_...
dependencies
And this is what make
(via Kbuild) uses to decide when to rebuild something.
So Are .cmd
Better Than .d
Files?
Sort of. .cmd
files are more than .d
files, but also less.
Yes:
- They’re smarter.
- They track
CONFIG_
conditionals. - They integrate cleanly with the kernel’s Makefile logic.
But:
- They’re not recursive.
- They don’t include deeply nested headers.
- They only list direct includes.
So what happens when you change a deeply nested header?
Nothing. Absolutely nothing.
Unless the header was directly listed in the .cmd
file, your object won’t rebuild.
Ask Me How I Know
I can’t count how many times I’ve changed a header, rebuilt, and then… nothing changed. No errors. No recompiled object. Just my confused face staring into the void.
Eventually I learned: just delete the .o
file manually and try again.
And now, finally, I understand why: the change wasn’t tracked because the .cmd
file didn’t know about the header I changed. Because fixdep
didn’t know. Because it’s not recursive. Because… reasons.
What Are Your Options Then?
If your goal is to get a full list of files used in a Linux kernel build, here are your tools:
Method | Pros | Cons |
---|---|---|
.d files |
Accurate, compiler-generated | Deleted after use |
.cmd files |
Tracked by Kbuild, includes CONFIG_ |
Incomplete, no nested headers |
strace the build |
Very complete | Noisy, includes false positives |
Static parsing | Fun in theory | Hell in practice (TRACE_INCLUDE etc.) |
So, Can I Use Dependency Files to List All Files Used in the Build?
Yes… But you have to be a bit clever about it.
You might think: “Hey, the kernel already uses -MMD
, so .d
files are created… I’ll just grab those!” Well… yes, they are created. But they’re also brutally deleted right after they’re used.
That rm
command? It’s not a side effect or compiler flag, it’s explicitly baked into the kernel’s build logic. You correctly traced it to scripts/Kbuild.include
, and specifically into, the logic used by the kernel’s makefiles. It’s part of this pattern:
cmd_and_fixdep = \ $(cmd); \ scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).cmd;\ rm -f $(depfile)
So it’s not that you forgot to save the .d
files, it’s that the kernel build system deliberately throws them away like yesterday’s logs. Why? Because it only needs them briefly, to feed into the fixdep
tool, which extracts top-level config header dependencies (like include/config/FOO
) and embeds them into the .cmd
files.
What Can Be Actually Done
What can be done, rather than rerun the entire kernel build with strace, or try to reverse-engineer header includes statically (good luck parsing around TRACE_INCLUDE
)...
“Wait… The
.cmd
files have the full compiler commands!”
Exactly.
Here’s what
- Search for all
*.cmd
files in the build output directory. - Extract each compile command (it’s right there in the file).
- Manually rerun that compile, with
-MMD
still in place… - …but skip running
fixdep
and don’t delete the.d
file.
This gives you full .d
files, unmolested by cleanup. Now you have the real, compiler-generated dependency lists, with every nested header, accurate and complete, not the trimmed-down config-focused ones baked into .cmd
.
Yes, it takes a bit of scripting and time. But it’s deterministic, reproducible, and lets you trace exactly what went into a particular kernel build, without hacking the build system or playing syscall whack-a-mole with strace.
And this is what I'm going to implement after an ufair fight with makfiles and friends!