How does the Linux kernel build system use built-in.a archives when producing vmlinux? I understand that:
- Each subdirectory containing
obj-yproduces its ownbuilt-in.aarchive during compilation. - These archives are collected for the final link.
scripts/link-vmlinux.shinvokesldto generate thevmlinuxELF image.
Does scripts/link-vmlinux.sh pass a single top-level aggregated built-in.a (for example, from the kernel source root) to ld, or does it explicitly pass each subdirectory’s built-in.a individually to ld during the final link? When and how are all compiled object files — including drivers — merged into the final vmlinux?
When you build linux kernel, scripts/link-vmlinux.sh doesn’t hand ld a single monolithic archive that already contains every directory’s objects. Instead the kernel build system passes one top-level vmlinux.a (objects that must always be linked) plus a list of per-directory archives collected in KBUILD_VMLINUX_LIBS; the script calls ld with --whole-archive for vmlinux.a and --start-group/–end-group for the other archives so the linker extracts the needed members. Built‑in drivers (obj‑y) are included via those archives and get merged into vmlinux during that final ld invocation; drivers built as modules (obj‑m) become .ko files and are not linked into vmlinux.
Contents
- How the linux kernel building process collects built-in.a (overview)
- How the kernel build system links built-in.a into vmlinux (ld stage)
- Where drivers end up: built-in vs modules
- When and how objects are merged into vmlinux
- Practical diagnostics: inspect archives and vmlinux
- Notes: archives, indexing, special cases (LTO / BTF)
- Sources
- Conclusion
How the linux kernel building process collects built-in.a (overview)
The build starts in each directory: .c files compile to .o and Kbuild groups those .o files into a directory-level archive (commonly named built-in.a) or a single relocatable object (built-in.o) depending on the Makefile rules. For example, the linux-insides write-up walks through how subdirectories produce a built-in object/archive (it shows the ld -r merge step used in some places) and how those per-directory outputs are then presented to higher-level make rules (linux-insides).
As the tree is walked, Kbuild collects the paths of those archives into a list variable (KBUILD_VMLINUX_LIBS). Separately, a small set of objects that must always be present in the kernel image are gathered into a top-level archive (vmlinux.a). The linking script then uses those two inputs: the unconditional archive (vmlinux.a) and the list of conditional archives (the entries in KBUILD_VMLINUX_LIBS). The kernel documentation on Kbuild and makefiles explains the macros and link target conventions that produce these variables and artifacts (kernel.org kbuild docs).
How the kernel build system links built-in.a into vmlinux (ld stage)
Open the actual script and you’ll see the pattern: the linker is invoked with vmlinux.a passed under --whole-archive and the other archives wrapped in a group. The core fragment looks like this (simplified from scripts/link-vmlinux.sh):
objs=vmlinux.a
libs="${KBUILD_VMLINUX_LIBS}"
${ld} ${ldflags} -o ${output} ${wl}--whole-archive ${objs} ${wl}--no-whole-archive \
${wl}--start-group ${libs} ${wl}--end-group ${kallsymso} ${btf_vmlinux_bin_o} ${arch_vmlinux_o} ${ldlibs}
(see the live script for full context: scripts/link-vmlinux.sh)
What that means in practice:
- vmlinux.a is included with --whole-archive, so every member of that archive is forcibly pulled into the link. Kbuild puts objects there that must be present even if no other object references them (boot/startup code is a typical example).
- The archives listed in KBUILD_VMLINUX_LIBS are passed separately (each archive is an argument). They’re wrapped by --start-group/–end-group so the linker can resolve circular inter-archive dependencies: ld will repeatedly search the set of archives to satisfy undefined symbols across them.
- For the archives in the group ld will not include all members automatically; it extracts only the archive members that are needed to resolve symbols (unlike --whole-archive). This keeps vmlinux smaller and avoids pulling in unused code.
- Kbuild sometimes aggregates smaller directories into combined archives (for example lib/lib.a or other aggregated .a files) to reduce the number of items on the linker command line, but that aggregation is done during the make recursion — the link script still receives a list of archive file arguments rather than one pre-merged mega-archive.
You can inspect the final invocation by building with verbosity on (make V=1) and watching the ld line; that shows exactly which .a files are being passed to ld.
Where drivers end up: built-in vs modules
Short answer: built‑in drivers (obj‑y) are merged into vmlinux because their .o files are packed into built-in.a archives and those archives are on the linker command line; module drivers (obj‑m) are compiled into .ko files and are never linked into vmlinux.
Why that matters: the semantics of obj-y vs obj-m are chosen in Kconfig/Makefiles. If a driver is configured as built‑in, its object becomes part of the directory’s built-in.a and is therefore available to the final link (subject to the linker’s normal archive-member selection rules or --whole‑archive if placed in vmlinux.a). If it’s a module, the build produces a .ko and that .ko is loaded dynamically at runtime — it’s not part of the vmlinux ELF.
To check membership:
- List archive members: ar -t path/to/built-in.a
- Search the final vmlinux for a symbol: nm vmlinux | grep MyDriverSymbol
- Or list exports: readelf -s vmlinux | grep driver_name
When and how objects are merged into vmlinux
All actual merging happens at the final link (ld) step invoked by scripts/link-vmlinux.sh. The earlier steps merely produce object files and archives:
- compile: .c → .o
- per-directory packaging: .o → built-in.a or built-in.o (via ar or ld -r)
- Kbuild gathers archive paths in KBUILD_VMLINUX_LIBS and decides which objects belong in vmlinux.a
- scripts/link-vmlinux.sh runs ld with vmlinux.a and the list of archives
- –whole-archive vmlinux.a forces all members into the link
- –start-group/–end-group around the rest lets ld resolve circular dependencies and pull in only needed members
- ld produces the vmlinux ELF by merging sections and resolving symbols
- post-link steps (kallsyms generation, BTF, optional re-linking or objcopy/strip) may modify or regenerate artifacts before producing the final bootable image (bzImage, etc.)
So, merging is done by the linker, not by concatenating object files ahead of time. The linker decides which archive members become real sections inside the vmlinux ELF.
A couple of caveats: some build targets and features (LTO, specific arch toolchain quirks) alter the exact flags and flow, and Kbuild may run a short link to produce symbol lists that are then turned into source and re-linked. But the fundamental point stands — the final combination into an ELF is ld’s job, driven by the vmlinux.a + KBUILD_VMLINUX_LIBS inputs.
Practical diagnostics: inspect archives and vmlinux
Want to see what’s actually passed to ld and whether a particular driver ended up inside vmlinux? Try these:
- Build with full verbosity: make V=1 2>&1 | less — watch the ld invocation.
- Inspect the link script: scripts/link-vmlinux.sh (script link).
- List archive contents: ar -t path/to/some/built-in.a
- Search vmlinux for a symbol: nm vmlinux | grep driver_symbol or readelf -s vmlinux | grep driver_symbol
- Count built-in archives: find . -name ‘built-in.a’ | wc -l
- See which archives are in KBUILD_VMLINUX_LIBS: the linker command printed with V=1 contains that expanded list.
These commands let you confirm whether a given object was put into a built-in.a, whether that archive was passed to ld, and whether ld pulled that member into vmlinux.
Notes: archives, indexing, special cases (LTO / BTF)
- Aggregation: Kbuild sometimes aggregates many small built-in.a files into larger archives (lib/lib.a, etc.) to reduce command-line length — so you might not see a distinct built-in.a for every single subdirectory even though the contents end up linked.
- Indexing: some linkers benefit from an archive index (ranlib/ar -s). Kbuild handles whatever is needed for the toolchain; the link script comment notes that KBUILD_VMLINUX_LIBS archives “do not require symbol indexes added” in typical workflows.
- kallsym and BTF: the link command includes generated objects like kallsyms and BTF helpers. Generating those can be part of the build graph (they may be produced and re-linked into the image as needed).
- LTO: when using link-time optimization you may see different behavior (plugins, different ld flags, or the linker invoking the compiler plugin). Those are special cases handled by Kbuild hooks.
If you want the authoritative, runnable code-level details, the linking behavior is visible in the actual script in-tree: scripts/link-vmlinux.sh. For higher-level explanation of how built-in objects are prepared, the linux-insides walkthrough is a useful companion (linux-insides).
Sources
- https://raw.githubusercontent.com/torvalds/linux/master/scripts/link-vmlinux.sh
- https://0xax.gitbooks.io/linux-insides/content/Misc/linux-misc-2.html
- https://stackoverflow.com/questions/79864947/role-of-built-in-a-in-vmlinux-build-process
- https://www.kernel.org/doc/html/latest/kbuild/makefiles.html?highlight=vmlinux
- https://stackoverflow.com/questions/41326607/what-is-the-use-of-vmlinux-file-generated-when-we-compile-linux-kernel
Conclusion
When you build linux kernel the final vmlinux is produced by ld from a top-level vmlinux.a plus a list of per-directory archives gathered in KBUILD_VMLINUX_LIBS; the script passes vmlinux.a with --whole-archive and then each archive in the list (usually the built-in.a files or aggregated libraries) so ld either includes all members (vmlinux.a) or selectively extracts needed members from the other archives. Built‑in drivers end up in those archives and are incorporated during that final link; modules remain separate .ko files.