Ch20.5: What the Defense Never Does: Build a Cross Compiler
1. The Shape of the Argument
Search for a rebuttal to the 2004 message and you will find thousands, and they almost all have the same shape: C++ is a fine language for kernels, look how much nicer RAII is, and by the way exceptions are only slow when they are thrown. The one thing none of them do is install a cross compiler.
Which is the whole problem, because "C++ is fine for kernels" is a claim about a freestanding toolchain, and the person making it is almost always typing into a hosted one, on the host the compiler was built for, with the host's headers on the default search path. What they have verified is that C++ compiles for the machine they are sitting at. The question was about a machine with no operating system.
2. What Happens When You Point a Hosted Compiler at a Bare Target
Here is a hosted Clang, the same one that builds a hello world on this desktop, asked to target a bare-metal AArch64 board. First, ask it where it looks for headers:
$ clang++ --target=aarch64-none-elf -E -v -x c++ /dev/null
#include "..." search starts here:
#include <...> search starts here:
/home/cqwrteur/softwares/llvm_native_with_gcc/lib/clang/24/include
End of search list.
One entry, and it is the compiler's own resource directory — the C headers that ship
with the compiler itself. There is no C++ header path at all, because the C++ part of a
toolchain is a library, and this installation has no library for
aarch64-none-elf. So:
#include <cstddef>
#include <array>
int x;
$ clang++ -std=c++26 --target=aarch64-none-elf -ffreestanding -c bare.cc -o /dev/null
bare.cc:1:10: fatal error: 'cstddef' file not found
1 | #include <cstddef>
| ^~~~~~~~~
1 error generated.
The very first header of the freestanding list, the one header every C++ program has, does not exist. That is the situation Linus was describing: not "C++ is slow", but "the C++ you are describing is not installed for the machine you are describing".
Compare the hosted build of the same compiler, where the same file compiles without a
complaint because the include path is a full libc++ for x86-64 Linux. Same compiler, same
flags, same source: one produces an object file, the other cannot find
cstddef. The difference is not the standard, it is what was installed.
3. Why osdev Insists on a Cross Compiler
The OS development community has a page about this that says it better than any C++ tutorial: Why do I need a Cross Compiler? The three reasons are worth reproducing in your own words, because they are the reasons C++ freestanding failed:
-
A native compiler is configured for its host. It has a built-in sysroot,
built-in include paths and a built-in linker target. Pointing it at another target with
flags is a hack that mostly works, until the day it silently uses the host's
stddef.h. -
Host-specific behaviour leaks in. The host build enables hardening
defaults, stack protector support, a host C library's assumptions about
errno, sometimes a host ABI for variadic calls. Your kernel then depends on a runtime it does not have, and the failure appears at run time on real hardware. -
You cannot tell your own bugs from your toolchain's. If the compiler was
never built for your target, every crash is ambiguous: is this my scheduler or the host
build's idea of
__builtin_memcpy?
A cross compiler is the answer to all three: --target, a sysroot that contains
only your target's headers, and start files written for your target. It is also the reason
the argument about C++ in kernels is unanswerable from a desktop: the people who have the
toolchain are the ones with the evidence, and the ones with the opinions do not.
4. How to Tell Whether You Have One
Three commands, and they cost nothing:
| Command | A hosted toolchain says | What a freestanding one should say |
|---|---|---|
g++ -print-sysroot |
An empty line, meaning "the host's /usr" |
A path you created that contains only your target's headers and libraries |
g++ -print-file-name=include |
The host GCC's own private header directory | A directory for your target triple |
clang++ --target=... -E -v -x c++ /dev/null |
Several host C++ include paths | Your sysroot's C++ headers, and nothing from the host |
If the first two return the host's paths, then -ffreestanding on that
toolchain is a lie you are telling the compiler, and
20.1 showed what happens next: the
hosted library refuses the header with #error "This header is not available in
freestanding mode." That error is not a bug. It is the library telling you that you
are in the wrong world.
5. The Point
Every claim in this chapter is verifiable, but only on a toolchain that was built for the
target. That is why the chapter is full of nm -u output rather than opinions:
an object file's undefined symbols are a fact about the mechanism, and they do not change
depending on who is arguing. A legacy throw needs eleven of them; a herbception needs one;
a fast_io program with a caller-supplied heap needs none
(20.13). Those numbers are the argument, and
they are why the conclusion is not "C++ is bad" but "these three features had to go".