Three machines, two worlds
Source references in this chapter are to GCC 15.2.0 (releases/gcc-15.2.0).
You have an ARM cross toolchain. You point --sysroot at your root filesystem
and crt1.o is found. You point it somewhere else and crtbegin.o is still
found, from the old place, and you cannot see why one moved and the other didn't.
The reason is that a GCC installation contains artifacts belonging to two different machines at once, and every configure flag and every command-line option belongs to one world or the other. Once you can look at a path and say which world it is in, most of GCC's path behaviour stops being arbitrary.
This chapter is the vocabulary the rest of Part I is written in. It is short, and everything after it depends on it.
Three machine names
Almost every autoconf package has two machine names. GCC has three, because a compiler is the one program whose output is for a machine other than the one it runs on. The manual defines them in one sentence:
The build machine is the system which you are using, the host machine is the system where you want to run the resulting compiler (normally the build machine), and the target machine is the system for which you want the compiler to generate code.
| Flavour | build | host | target |
|---|---|---|---|
| native | x86_64-linux-gnu | same | same |
| cross | x86_64-linux-gnu | same | arm-linux-gnueabihf |
| Canadian cross | x86_64-linux-gnu | x86_64-w64-mingw32 | arm-none-eabi |
The practical consequence is the one to hold on to: a single make produces
artifacts for two different machines at once. Almost everything confusing about
GCC's configure flags comes from that one fact, and the useful question to ask of
any flag is always "is this about the machine running the compiler, or the machine
running its output?"
You can ask a built compiler which target it is for:
$ arm-linux-gnueabihf-gcc -dumpmachine
That prints the target triple and exits — the quickest check that you are running the compiler you think you are.
Host code and target code
Now the split that matters when you are reading paths.
| Host code | Target code | |
|---|---|---|
| Examples | gcc, cc1, cc1plus, lto1, collect2, lto-wrapper | libgcc.a, libstdc++.so, crt1.o, crtbegin.o |
| Runs | on your machine, as a process | never — it is linked into what your compiler produces |
| Built by | the build machine's system compiler | the compiler this build just produced |
| Lives in | bin/, libexec/ | lib/gcc/<t>/<v>/, <t>/lib/, and the sysroot |
Target code is the part people misjudge. libgcc.a is not a library GCC uses; it
is a library GCC emits calls into, compiled for a machine that may not be able
to run a single instruction of the compiler that built it. When a cross toolchain
"finds the wrong libgcc.a", nothing has gone wrong with GCC's own installation
— it has picked the wrong artifact for a foreign machine.
The clean rule, which holds without exception:
libexec/andbin/are host.<target>/lib/and the sysroot are target.lib/gcc/<target>/<version>/is a mix — host data such as thespecsfile and GCC's own headers, alongside target objects such aslibgcc.aandcrtbegin.o.
Most flags move one world or the other. -B is the notable exception: it is a
single prefix applied to three separate search lists at once, and those lists span
both worlds. That is why -B feels blunt, and it is covered in
Chapter 1.5.
What a sysroot actually is
A sysroot is a directory that stands in for / on the target machine. Inside
it, usr/include/stdio.h plays the role that /usr/include/stdio.h plays on the
target.
It has to exist because your own /usr/include/stdio.h describes your libc:
wrong sizeof(long), wrong struct layouts, wrong syscall numbers, wrong
#defines. Reading it while cross-compiling would be silently catastrophic —
you would get an object file that compiles, links, and misbehaves on the device.
So the compiler needs a second, target-flavoured /. That is all a sysroot is.
Mechanically it is unglamorous. It is a string glued onto the front of certain absolute paths, by a function whose entire job is that gluing:
add_sysrooted_prefix (struct path_prefix *pprefix, const char *prefix,
const char *component,
/* enum prefix_priority */ int priority,
int require_machine_suffix, int os_multilib)
{
if (!IS_ABSOLUTE_PATH (prefix))
fatal_error (input_location, "system path %qs is not absolute", prefix);
if (target_system_root)
{
...
prefix = concat (sysroot_no_trailing_dir_separator, prefix, NULL);
Listing 2-1: add_sysrooted_prefix (gcc/gcc.cc:3177-3200).
Abridged; the omitted lines strip a trailing separator and splice in a per-multilib
sysroot suffix.
The important thing about Listing 2-1 is what it implies about everything else:
there is a sibling function, add_prefix (gcc/gcc.cc:3144), which
does the same job without the sysroot. Whether a given directory follows your
--sysroot comes down to which of the two functions registered it. That is the
whole mechanism. There is no post-processing pass that rewrites paths, and no flag
that can retroactively sysroot a prefix that was registered with add_prefix.
The rule that explains most surprising path results
The sysroot belongs to the C library. The prefix belongs to GCC.
GCC never installs anything into the sysroot, and the C library never installs anything into the prefix.
Almost every "why didn't --sysroot move that?" has the same answer: because that
thing is GCC's, not libc's. Sorting the pieces of a normal link by owner:
The C library's, in the sysroot — this is the half a sysroot moves:
<stdio.h>,<pthread.h>,<fcntl.h>and the rest of the system headerslibc.so,libm.socrt1.o,crti.o,crtn.o
GCC's, under the prefix — a sysroot never touches these:
libgcc.a,libgcc_eh.a,libgcc_s.so.1crtbegin.o,crtend.o(they are part of libgcc)libstdc++and its headers, which live in the tool directory<stddef.h>,<stdint.h>,<limits.h>,<stdarg.h>— surprisingly, GCC's; the C standard requires the compiler to supply these, and Chapter 1.9 is about why
That last group is the one that catches everyone. You will not find stdint.h in
your sysroot, you are not supposed to, and copying a sysroot from one machine to
another does not bring it along. The compiler's own headers travel with the
compiler.
The directory map
Three autoconf variables underlie every path formula in GCC:
| Variable | What it is | Default |
|---|---|---|
$prefix | root of the entire install | --prefix=, else /usr/local |
$exec_prefix | root for machine-dependent files | --exec-prefix=, else $prefix |
$libdir | host libraries | $exec_prefix/lib |
$prefix and $exec_prefix are different variables that merely default to the
same value — gcc/Makefile.in:662 and :673. Pass
--exec-prefix and paths built from one diverge from paths built from the other.
This is not hypothetical: libstdc++ installs its headers under ${prefix} and its
libraries under ${exec_prefix}, so passing --exec-prefix puts the C++ headers
and the C++ library in different trees. That is a Chapter 1.12 problem, but the
cause is here.
Then the directories themselves. Two have names you will meet constantly:
libsubdir — GCC's private per-version directory:
libsubdir = $(libdir)/gcc/$(real_target_noncanonical)/$(version)$(accel_dir_suffix)
Listing 2-2: libsubdir (gcc/Makefile.in:682).
That expands to something like /opt/gcc-arm/lib/gcc/arm-linux-gnueabihf/15.2.0/,
and it is where specs, GCC's own include/, libgcc.a and crtbegin.o live.
Note the $(version) component: two GCC releases have entirely separate
libsubdirs, which is what lets them coexist and what makes mixing GCC 12's headers
with GCC 15's cc1 a broken toolchain rather than a merely unwise one.
tooldir — the target's own subtree, defined at the top level:
tooldir='${exec_prefix}'/${target_noncanonical}
Listing 2-3: tooldir (configure.ac:3121).
That is /opt/gcc-arm/arm-linux-gnueabihf/, and inside it are bin/, lib/,
include/ and sys-include/ — a little pretend /usr for the target. A cross
libstdc++ installs there, and so does newlib.
Rounding it out:
| Name | Default | Contains | Sysrooted? |
|---|---|---|---|
| prefix | --prefix | everything GCC installs | no |
| libsubdir | $libdir/gcc/<target>/<ver>/ | GCC's target objects and host data | no |
| tooldir | $exec_prefix/<target>/ | bin/ lib/ include/ sys-include/ | no |
| toolexeclibdir | $tooldir/lib (cross), $libdir (native) | target libraries such as libstdc++.so | no |
| sysroot | --with-sysroot, else $tooldir/sys-root | the C library's entire world | it is the sysroot |
The manual documents --with-toolexeclibdir's default as ${gcc_tooldir}/lib
(install.texi:2705-2707), and it is worth noticing that
the bare --with-sysroot default sits inside the tooldir, hence inside
$exec_prefix. That is exactly the condition under which a sysroot relocates with
a moved toolchain, and the manual says so:
If the specified directory is a subdirectory of
${exec_prefix}, then it will be found relative to the GCC binaries if the installation tree is moved.
The code that implements that sentence is guarded by a macro that configure only
defines when the condition holds (gcc/configure.ac:176-182, acted
on at gcc/gcc.cc:5542-5559). It is covered in
Chapter 1.5.
What a real install looks like
Under --prefix=/opt/gcc-arm --target=arm-linux-gnueabihf:
| Path | Contains | World |
|---|---|---|
/opt/gcc-arm/bin/ | arm-linux-gnueabihf-gcc, -g++, gcc-ar | host |
/opt/gcc-arm/libexec/gcc/<target>/<ver>/ | cc1, cc1plus, lto1, collect2 | host |
/opt/gcc-arm/lib/gcc/<target>/<ver>/ | specs, include/, crtbegin*.o, libgcc.a | mixed |
/opt/gcc-arm/<target>/lib/ | libstdc++.so, newlib's libc.a | target |
/opt/gcc-arm/share/ | docs, man pages | host data |
| the sysroot, wherever it is | libc.so, crt1.o, <stdio.h> | target |
Check your own understanding against a real compiler:
$ arm-linux-gnueabihf-gcc -dumpmachine # the target triple
$ arm-linux-gnueabihf-gcc -print-sysroot # the C library's world; empty = none
$ arm-linux-gnueabihf-gcc -print-search-dirs # install root, programs, target libraries
$ arm-linux-gnueabihf-gcc -v # the configure line it was built with
An empty -print-sysroot is meaningful rather than broken, and the next chapter
explains what it implies.
Why this makes cross-toolchain builds circular
One more consequence, because it drives all of Part III.
libgcc and libstdc++ are target libraries that GCC itself ships. That is
exactly why they sit awkwardly between the two worlds, and it creates a dependency
cycle:
- Target libraries need a target compiler. Fine — the build makes one first, an
uninstalled
xgcc. - But libgcc's own source includes libc headers. The manual is blunt about it:
"When crossing to GNU/Linux, you need the headers so GCC can build the
exception handling for libgcc." (
install.texi:2765-2766) - And libstdc++ needs a libc it can actually link against, not merely headers — its configure runs hundreds of link probes.
- But the libc was itself built by a compiler.
The conventional way out is to build a deliberately crippled GCC first:
binutils → libc headers → minimal GCC (--without-headers)
→ full libc → full GCC (libgcc + libstdc++)
The mechanism that makes the minimal GCC possible is an internal configure
variable called inhibit_libc, which builds a stripped libgcc needing no C
library at all. It has one non-obvious extra condition that people trip over
constantly, and Part III opens with it.
An archaeological note
Before sysroots existed you pointed GCC at the target headers and libraries with
--with-headers and --with-libs, and configure copied them into the install
tree (configure.ac:2931). The manual now marks both
"Deprecated in favor of --with-sysroot"
(install.texi:2751-2753, :2769-2770).
Two reasons to know they existed. You will meet them in old build scripts. And
they explain why $tooldir/include and $tooldir/sys-include are still header
search paths today, on a pure-sysroot toolchain that never asked for them — the
surviving path is compiled in as TOOL_INCLUDE_DIR
(gcc/Makefile.in:2613), and you will see it again in
Chapter 1.9.
What to take from this
Two machines, one install tree. Look at any path and ask which machine's code is
in it. bin/ and libexec/ are yours; <target>/lib/ and the sysroot are the
device's; lib/gcc/<t>/<v>/ is both, which is why it is the confusing one. And a
sysroot is a string prepended by add_sysrooted_prefix, applied to libc's half of
the world and to nothing else.
Next: how to make a built compiler tell you every decision it has made.
All source references in this chapter are to GCC 15.2.0
(releases/gcc-15.2.0). Line numbers in other releases will differ; the
surrounding code rarely does. Where behaviour itself changed across a major
version, it is flagged inline.