Ch22.2: Sysroot

中文(简体)

1. The Rule

Rule. A toolchain needs a C library for its target. You either provide one in a sysroot, or the target is freestanding and the toolchain must be configured as such. Everything else — the compiler runtime, the C++ standard library, the sanitizers — is provided by GCC or by LLVM.

This is the part that is missing from most cross-compilation tutorials. They tell you to install a compiler and pass --target, and then a link fails with cannot find crt1.o or cannot find -lc. The compiler is not supposed to supply those files. A compiler is a code generator plus a driver; the operating system's ABI lives in the C library and in the start-up objects that come with it.

There is a stronger form of the rule, and it is the one that matters when you bootstrap a new platform: a C/C++ sysroot is the minimum required to build a compiler at all. GCC and Clang are C++ programs. Before either can be built, the build machine needs a working C++ compiler, the C++ standard library, libc, and the platform headers. The compiler you are building may target a freestanding board, but the compiler binary itself still runs on a hosted machine and still links a C++ runtime.

The floor. Every other language toolchain on the platform sits on the C/C++ sysroot. CPython's interpreter and its extension modules are C and link libc; Go's cgo and platform packages use the C ABI and the platform's syscall layer; Rust's standard library links libc and invokes a C linker; Node.js and V8 are C++. None of them defines a second sysroot. If the C/C++ sysroot is missing or wrong, they cannot be built for that platform either.

That is why the first thing a new operating-system port needs is not a Python port or a Rust port. It is a C/C++ toolchain and a sysroot. Once those exist, the other runtimes are ordinary programs built against them, and "porting Python" becomes a build of CPython rather than a new platform definition. The C/C++ layer is the platform contract; everything else is a consumer.

In a Canadian build the same rule applies twice. The compiler binary runs on the host, so the host sysroot must be complete enough to link a C++ program: libc, the C++ standard library, and the C++ headers. The target sysroot must provide the C library and headers that the target runtime libraries are built against. Both are C/C++ sysroots; they are just for different triples.

The split is clean once you see it:

Component Provided by Examples
C library The sysroot glibc, musl, bionic, newlib, mingw-w64 CRT, Apple libSystem
Start-up objects The sysroot (from libc) crt1.o, crti.o, crtn.o, Scrt1.o
Kernel headers The sysroot linux/, asm/, asm-generic/
Dynamic loader The sysroot (from libc) ld-linux-x86-64.so.2, ld-musl-aarch64.so.1
Compiler runtime GCC or LLVM libgcc, libgcc_eh, compiler-rt builtins
C++ standard library GCC or LLVM for the target; the host's copy is part of the host C/C++ environment libstdc++, libc++, libc++abi, libunwind
Compiler headers GCC or LLVM stddef.h, stdarg.h, stdint.h, limits.h

The C++ standard library depends on the C library, which is why it is built after the libc and against the sysroot. But it is still part of the compiler distribution: you install libstdc++ with GCC and libc++ with LLVM, not with the operating system. On a Canadian build, that distinction decides which artifact must be built for the host and which for the target.

The host side is the exception that proves the rule. The C++ compiler and C++ standard library used to build the new compiler are part of the build machine's existing C/C++ environment. They are not produced by the compiler you are building. This is why the first requirement on a new build machine is a working C/C++ toolchain, not a copy of the new compiler.

2. What a Sysroot Looks Like

A sysroot is a directory tree that looks like the target's root filesystem. GCC and Clang use the same idea: point the compiler at one directory, and the driver resolves its default header, library and program search paths inside it. The usual tree is just:

/path/to/sysroot/
  bin/       target programs and helper tools
  include/   C and C++ headers
  lib/       libraries, startup objects and the dynamic loader
  share/     target data files

Some systems merge /usr and put the same directories under usr/, so the compiler may search usr/include and usr/lib instead of include and lib. That is a path detail, not a different sysroot format. A target may also add lib64, libexec or a multiarch subdirectory. The root is still one directory.

GCC and Clang use the same root layout; both accept --sysroot. Their drivers may have different default search paths, but that is a driver detail, not a different sysroot format. A GCC cross driver often defaults its sysroot to $PREFIX/$TARGET; Clang is often invoked with an explicit --sysroot= pointing at an SDK. Both are pointing at a root tree. If the compiler's search path and the files on disk disagree, the link fails; the fix is to inspect the actual search path, not to rename the tree.

A sysroot is not a "GCC layout" or a "Clang layout". It is a target root. The compiler's default paths are a property of the driver and the target configuration; --sysroot only changes the root against which those paths are resolved.

The start-up objects are the usual first failure. A dynamically linked C program on Linux needs crt1.o, crti.o and crtn.o from libc, plus crtbegin.o and crtend.o from the compiler runtime, plus libc.so. A static program needs libc.a, libm.a and the static variants of the start-up objects. If any one is missing, the link fails before your first line of code is involved.

3. Hosted or Freestanding

There are two valid answers to "where is libc", and only two:

The mistake is mixing the two. A triple that says -linux-gnu is a promise that the target has glibc and a Linux ABI. If you build a compiler for that triple without a sysroot, the build may succeed and every link will fail. Conversely, a triple that says -elf or -none- has no libc by definition; adding a Linux sysroot to it does not make it Linux, it only hides the mismatch until something calls a syscall that the target does not have.

Some embedded targets use newlib, which is a small C library with a porting layer. That is a hosted target in the C sense, but it is not POSIX and it is not glibc. It still belongs in the sysroot, and the compiler still provides only its own runtime. "Freestanding" means no libc at all, not "a small libc".

4. Where the Sysroot Comes From

There are three practical sources, and they fail in different ways:

  1. Build the libc from source. For a Linux target this is the normal path: install kernel headers, build glibc or musl with the stage-1 cross compiler, install it into the sysroot, then rebuild GCC against it. The sysroot is then exactly the ABI you chose, at the cost of a long build.
  2. Extract it from an SDK or an OS image. Android's NDK ships a sysroot with bionic headers and stub libraries. A Windows SDK ships the MSVC CRT and import libraries. A macOS SDK ships libSystem and the frameworks. This is faster but ties you to the vendor's layout and license.
  3. Reuse another toolchain's sysroot. If a GCC cross toolchain already exists, clang can use it with --gcc-toolchain=$PREFIX --sysroot=$SYSROOT. This is how most people get a working clang cross compiler without rebuilding the world. It is also how the LLVM page in this chapter works.

For a Canadian build, the sysroot must be built for the target, and it must be visible at the path the compiler will record. GCC's --with-sysroot=$SYSROOT bakes that path into the compiler. If you later move the toolchain to another machine and the path does not exist there, the compiler starts and then fails to find its headers. Either install the sysroot at the same path on the host, or pass --sysroot explicitly at every invocation.

During the build there is a second path: --with-build-sysroot. It tells the build system where the target headers and libraries are on the build machine while the compiler is being produced. It is not recorded in the compiler. Confusing the two is a common reason a cross compiler works during make and fails after installation.

5. The Compiler's Own Files

It is worth being precise about what "GCC provides" and "LLVM provides" means, because the files are installed in different places:

GCC LLVM Built for
libgcc.a, libgcc_s.so libclang_rt.builtins.a Target
libstdc++.so, libstdc++.a libc++.so, libc++.a Target
crtbegin.o, crtend.o crtbegin.o, crtend.o (where compiler-rt provides them) Target
libatomic, libgomp, libquadmath libomp, compiler-rt sanitizers Target, optional
cc1, cc1plus, collect2, lto-wrapper clang, lld, llvm-ar, llvm-objcopy Host
libgcc for the compiler itself LLVM/Clang libraries linked into the tools Host

This table is the reason a Canadian build is not one build. The host-side tools (cc1plus, clang, ld) must run on the host. The target-side runtimes (libgcc, libstdc++) must run on the target. The sysroot's libc must match the target. A single make cannot do all three at once on one machine; the build has to keep the triples straight.

6. Verifying a Sysroot

Before blaming the compiler, ask it what it thinks it is:

$TARGET-gcc -print-sysroot
$TARGET-gcc -print-search-dirs
$TARGET-gcc -v -E -x c /dev/null

clang --target=$TARGET --sysroot=$SYSROOT -print-search-dirs
clang --target=$TARGET --sysroot=$SYSROOT -v -E -x c /dev/null

The -v -E output lists the include search path. Check that the first entries are inside the sysroot and that they contain the headers you expect. Then compile a trivial file with -nostdlib -Wl,--verbose to see which start-up objects and which libc the linker finds. If -print-sysroot is empty, the compiler is using its default prefix layout, which may still be correct; if it prints a path that does not exist on the current machine, that is your bug.

For a freestanding toolchain, the same commands should show no libc and no sysroot, and a link should succeed only when you pass -nostdlib and provide your own start-up code. If a freestanding toolchain silently finds the build machine's glibc, it will produce binaries that link and then cannot run.