Operating Systems · Kernel Engineering
Kconfig Options and the Shape of the Compiled Kernel
How a configuration symbol turns into preprocessor structure, why IS_ENABLED differs from #ifdef, and how a build-time option silently changes struct layout and the module ABI.
The kernel source is one tree that compiles into a range of binaries differing by megabytes and by whole subsystems. The mechanism that selects which parts exist is not a runtime switch but a set of symbols resolved before the compiler runs, and the consequences reach further than the presence or absence of a feature: they determine which functions are inlined, which structures have which fields, and whether a module built elsewhere can be loaded at all.
From a question in a text file to a value in a header
A Kconfig file declares symbols with a type, a prompt, and a default, plus dependencies expressed as boolean expressions over other symbols. config PREEMPT might be a bool depending on ARCH_NO_PREEMPT being false; config NR_CPUS might be an int with a range and a default derived from the architecture. The declaration says nothing about what the symbol controls. It is a schema, and the meaning is entirely in where the C code tests it.
make oldconfig or make menuconfig produces a .config file of CONFIG_<name>=<value> lines. The build then generates, from that file, a header of #defines, plus a set of files that record dependency information so that a later make can tell whether a change to .config invalidates previously compiled objects. Three conventions matter:
- Boolean symbols that are enabled become
#define CONFIG_FOO 1; those that are disabled are not defined at all. - Tristate symbols can be
y,m, orn.ydefinesCONFIG_FOOitself;mdefines only the companionCONFIG_FOO_MODULE;ndefines neither. - Integer and string symbols become their literal value.
The asymmetry between 1 and undefined is the source of a common mistake. #ifdef CONFIG_FOO is true when the symbol is y and false when it is m, which is usually wrong for a tristate: a driver that is being built as a module should see the same code as one built in. #if defined(CONFIG_FOO) && CONFIG_FOO distinguishes y from m correctly but reads badly and cannot be used in an expression. The idiom that survives both cases is IS_ENABLED(CONFIG_FOO), which the preprocessor expands to 1 when the symbol is y, 1 when it is m, and 0 when it is undefined, a value usable in a normal C conditional, so the compiler can fold it and discard the dead branch without the preprocessor having decided anything.
That distinction is the whole design argument. #ifdef removes code before the compiler parses it, which means the removed code is not type-checked, not syntax-checked, and can rot for years unnoticed. IS_ENABLED keeps both branches in the translation unit, so both are compiled and both must be valid C. The cost is that the disabled branch must still reference symbols that exist in every configuration, which is not always possible; where it is not, #ifdef remains the correct tool, and the code inside it is code that no configuration in the build matrix exercises.
Why the build matrix is the real specification
Because #ifdef branches are invisible to the compiler in configurations that exclude them, the only thing that keeps them working is somebody building them. The kernel’s answer is generated configurations rather than curated ones. allmodconfig sets every tristate symbol to m, producing a build that compiles the largest possible set of drivers as modules. allyesconfig sets everything to y. allnoconfig sets everything to n, producing the smallest kernel the tree can express. randconfig generates random valid assignments, respecting the dependency graph, and is the one that finds the interesting bugs: a combination of two unrelated options that a human would never select, which happens to make a structure field conditional and a function that uses it unconditional.
A randconfig failure is usually not a bug in the feature being tested. It is a bug in a dependency declaration: config FOO selects BAR but does not depend on BAZ, which BAR requires, so the generated configuration is internally inconsistent and the code that assumes BAZ is present fails to compile. The dependency graph in Kconfig files is therefore a specification with the same standing as the C code, and it is checked by the same builds.
When a configuration option moves a field
The layout consequence is the one that produces bugs nobody can reproduce. Consider a structure whose members are conditional:
struct request {
unsigned int flags;
#ifdef CONFIG_ACCOUNTING
struct acct_record acct; /* 24 bytes when present */
#endif
u64 submitted_at;
};
With accounting enabled, submitted_at sits 24 bytes further into the structure. Any code that was compiled without the option and computes an offset (a hand-written offset constant, a debugger script, a serialised format, a tracepoint field description) is now wrong by 24 bytes. The kernel does not attempt to keep layouts stable across configurations; it deliberately lets them differ, because the alternative is padding every structure for every feature that might be enabled.
Two defences exist. The first is that conditional fields are placed at the end of a structure where possible, so that enabling an option only extends the structure rather than shifting what follows. This is a convention, not a rule, and it fails when the field must sit near the others for cache-locality reasons. The second is pahole, which reads the debug information in a built object and prints the exact offsets and padding of every structure. Comparing its output between two builds is the reliable way to answer whether a layout changed, and it is also how padding waste is found: a structure with a u8 between two u64 members carries seven bytes of padding, and reordering the declarations removes them. The alignment rules that decide where that padding lands in the first place are the subject of Word Size, Endianness, and Structure Layout.
For modules the layout difference becomes a load failure rather than a wrong offset, because of the compatibility checks described in module symbol resolution. The version CRC covers a function’s signature, and vermagic covers a fixed list of configuration tags, but neither enumerates every CONFIG_ option that changes a layout. A module built against a kernel with the same release string and the same preemption model, but with one accounting option flipped, will load and then misbehave. The struct-size check on struct module itself catches the case where the loader’s own bookkeeping structure changed; it cannot catch anything else.
Reading a configuration as documentation
A useful habit when approaching unfamiliar kernel code is to read the Kconfig entry before the C file. The help text states the constraint that motivated the option (why it exists, what it costs, and what it excludes), which is often absent from the code comments. The depends on line states which other options must hold, which is the quickest way to understand why a feature is not available on a given architecture. The default line states what the option’s authors expect most users to want, and a default that differs between architectures is a statement about which platform the feature was designed for.
The select versus depends on distinction is worth knowing because it inverts the direction of the dependency. depends on means the option is only offered when its prerequisite holds, and the user cannot enable it otherwise. select means enabling this option forces another one on, regardless of whether the user wanted it. This is the stronger and more dangerous statement, because it can silently enable a library that the user’s configuration did not intend, and because select does not check that the selected symbol’s own dependencies are satisfiable. The kernel’s own documentation recommends depends on for anything with real prerequisites and reserves select for small library symbols with no prompts, precisely because a select chain can produce a configuration that cannot be built and cannot be explained by any single menu entry.
Limits of the mechanism
Configuration is resolved at build time, so a kernel cannot change its feature set after boot; the closest thing is a module, which is still built for a specific configuration. It also means the running kernel’s configuration is not fully recoverable from the image: /proc/config.gz exists only when the option to embed it was itself enabled, and a stripped vendor kernel may not carry it at all. Where it is absent, the module loader’s vermagic string and the exported symbol set are the remaining evidence of what was built, and they are enough to reject an incompatible module but not enough to explain why a structure has the offsets it does. That explanation lives in the exact headers and the exact .config used for the build, which is why keeping the build configuration alongside any kernel binary is not bookkeeping but part of the artifact.
References
- Kconfig Language — Linux kernel documentation
- Kbuild: the kernel build system — Linux kernel documentation
- Linux Kernel Makefiles — Linux kernel documentation