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.

gcc/doc/install.texi:841-844

Flavourbuildhosttarget
nativex86_64-linux-gnusamesame
crossx86_64-linux-gnusamearm-linux-gnueabihf
Canadian crossx86_64-linux-gnux86_64-w64-mingw32arm-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 codeTarget code
Examplesgcc, cc1, cc1plus, lto1, collect2, lto-wrapperlibgcc.a, libstdc++.so, crt1.o, crtbegin.o
Runson your machine, as a processnever — it is linked into what your compiler produces
Built bythe build machine's system compilerthe compiler this build just produced
Lives inbin/, 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/ and bin/ are host. <target>/lib/ and the sysroot are target. lib/gcc/<target>/<version>/ is a mix — host data such as the specs file and GCC's own headers, alongside target objects such as libgcc.a and crtbegin.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 headers
  • libc.so, libm.so
  • crt1.o, crti.o, crtn.o

GCC's, under the prefix — a sysroot never touches these:

  • libgcc.a, libgcc_eh.a, libgcc_s.so.1
  • crtbegin.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:

VariableWhat it isDefault
$prefixroot of the entire install--prefix=, else /usr/local
$exec_prefixroot for machine-dependent files--exec-prefix=, else $prefix
$libdirhost libraries$exec_prefix/lib

$prefix and $exec_prefix are different variables that merely default to the same valuegcc/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:

NameDefaultContainsSysrooted?
prefix--prefixeverything GCC installsno
libsubdir$libdir/gcc/<target>/<ver>/GCC's target objects and host datano
tooldir$exec_prefix/<target>/bin/ lib/ include/ sys-include/no
toolexeclibdir$tooldir/lib (cross), $libdir (native)target libraries such as libstdc++.sono
sysroot--with-sysroot, else $tooldir/sys-rootthe C library's entire worldit 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.

gcc/doc/install.texi:2721-2723

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:

PathContainsWorld
/opt/gcc-arm/bin/arm-linux-gnueabihf-gcc, -g++, gcc-arhost
/opt/gcc-arm/libexec/gcc/<target>/<ver>/cc1, cc1plus, lto1, collect2host
/opt/gcc-arm/lib/gcc/<target>/<ver>/specs, include/, crtbegin*.o, libgcc.amixed
/opt/gcc-arm/<target>/lib/libstdc++.so, newlib's libc.atarget
/opt/gcc-arm/share/docs, man pageshost data
the sysroot, wherever it islibc.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:

  1. Target libraries need a target compiler. Fine — the build makes one first, an uninstalled xgcc.
  2. 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)
  3. And libstdc++ needs a libc it can actually link against, not merely headers — its configure runs hundreds of link probes.
  4. 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.