Ch22.3: GCC: Build, Host, Target

中文(简体)

1. Three Flags, One Build System

GCC's top-level configure accepts --build, --host and --target. They are not synonyms and none of them is "the platform":

Flag Meaning Who runs the output
--build= The machine that compiles GCC The build tools produced during make
--host= The machine where the installed GCC runs The installed gcc, cc1plus, collect2
--target= The machine whose code GCC emits The programs you compile later

Defaults: --host defaults to --build, and --target defaults to --host. So a normal native build needs no flags; a cross compiler needs --target; a Canadian build needs --host as well. --build is usually set explicitly anyway, because config.guess can guess a different canonical name than the one you use for your other toolchains.

The build system keeps three compilers apart internally, and this is the part that matters:

In a native build all three are the same program. In a cross build CC and CC_FOR_BUILD are the same and CC_FOR_TARGET differs. In a Canadian build all three can differ, and a build that conflates them produces a compiler that runs on the wrong machine.

2. Two Compilers Before You Can Build One

To produce a compiler that runs on aarch64-linux-gnu from an x86_64-linux-gnu machine, you first need an x86_64-linux-gnu → aarch64-linux-gnu cross compiler. That compiler runs on the build machine and emits AArch64 code, so it can compile:

In a Canadian native build, host equals target, so this one cross compiler does both jobs. In a Canadian cross build, host and target differ, and you need two: build→host for the compiler binary and build→target for the target libraries. For a crossback (build = target), the second is the native compiler; for an ordinary Canadian cross it is a second cross compiler. The reason is not only the target runtime: GCC is itself a C++ program, so the host it will run on needs a complete C++ sysroot. The build→host compiler is what produces a compiler binary linked against that host C++ runtime. The build order is therefore:

1. build -> host cross toolchain     (the compiler that builds the host compiler)
2. build -> target compiler          (only if host != target; builds target libs)
3. host -> target toolchain          (the product; configure --host=HOST --target=TARGET)

This is exactly the order used by the reference scripts in toolchainbuildscripts. The triplet list is a list of (build, host, target) rows, and build_common.sh builds the prerequisites before the row itself:

# from gcc/build_common.sh
if [[ ${BUILD_GCC_TRIPLET} != ${HOST_GCC_TRIPLET} ]]; then
    # canadian
    build_toolchain $BUILD_GCC_TRIPLET $HOST_GCC_TRIPLET
    if [[ ${BUILD_GCC_TRIPLET} != ${TARGET_GCC_TRIPLET} &&
          ${HOST_GCC_TRIPLET}  != ${TARGET_GCC_TRIPLET} ]]; then
        # canadian cross (non crossback)
        build_toolchain $BUILD_GCC_TRIPLET $TARGET_GCC_TRIPLET
    fi
fi
build_toolchain $HOST_GCC_TRIPLET $TARGET_GCC_TRIPLET

build_toolchain H T configures with --build=$BUILD_TRIPLET --host=H --target=T, installing into $ROOT/H/T. The final call installs into $ROOT/HOST/TARGET. That prefix convention is what makes the pieces findable: the cross compiler is at $ROOT/$BUILD/$HOST/bin/$HOST-gcc, and the product is at $ROOT/$HOST/$TARGET/bin.

3. A Canadian Native Walkthrough

The rest of this section is the concrete recipe for build=x86_64-linux-gnu, host=target=aarch64-linux-gnu. It is the same sequence the scripts automate, with the bookkeeping made explicit.

BUILD=x86_64-linux-gnu
HOST=aarch64-linux-gnu
TARGET=$HOST

ROOT=$HOME/toolchains/gnu
CROSS=$ROOT/$BUILD/$HOST          # runs on BUILD, targets HOST
FINAL=$ROOT/$HOST/$TARGET         # runs on HOST, targets TARGET
SRC=$HOME/toolchains_build
export PATH=$CROSS/bin:$PATH

Step 1: binutils for the cross compiler

mkdir -p $SRC/build/binutils-cross && cd $SRC/build/binutils-cross
$SRC/binutils-gdb/configure \
  --build=$BUILD --host=$BUILD --target=$TARGET \
  --prefix=$CROSS \
  --disable-nls --disable-werror --disable-tui --without-debuginfod
make -j"$(nproc)"
make install-strip

This produces $TARGET-as, $TARGET-ld, $TARGET-ar, $TARGET-strip and the rest, all running on the build machine. Do not skip this: a compiler installation without a matching assembler and linker is not a toolchain.

Step 2: stage-1 GCC, no libc yet

mkdir -p $SRC/build/gcc-stage1 && cd $SRC/build/gcc-stage1
$SRC/gcc/configure \
  --build=$BUILD --host=$BUILD --target=$TARGET \
  --prefix=$CROSS \
  --disable-bootstrap --disable-nls --disable-werror \
  --enable-languages=c \
  --without-headers --disable-threads --disable-shared \
  --disable-libssp --disable-libquadmath --disable-libatomic \
  --disable-libsanitizer --disable-libstdcxx --disable-multilib
make -j"$(nproc)" all-gcc all-target-libgcc
make install-strip-gcc install-strip-target-libgcc

This GCC can compile C and can build libgcc, but it has no target headers and no target C library. That is the point: a libc needs a compiler, and the compiler needs a libc. The stage-1 compiler breaks the cycle.

Step 3: kernel headers and libc into the sysroot

# Linux UAPI headers
make -C $SRC/linux headers_install ARCH=arm64 \
  INSTALL_HDR_PATH=$CROSS/$TARGET

# glibc
mkdir -p $SRC/build/glibc && cd $SRC/build/glibc
export CC=$TARGET-gcc
export CXX=$TARGET-g++
$SRC/glibc/configure \
  --build=$BUILD --host=$TARGET \
  --prefix=$PWD/install \
  --with-headers=$CROSS/$TARGET/include \
  --disable-nls --disable-werror --without-selinux
make -j"$(nproc)"
make install
cp -a install/. $CROSS/$TARGET/

glibc's libc.so is often a linker script containing absolute paths. If you install to a temporary prefix and copy, those paths still point at the temporary directory. Either install directly with --prefix=$CROSS/$TARGET, or rewrite the three small linker scripts (libc.so, libm.so) after copying. The reference scripts do the second and delete the build prefix from the scripts.

For musl the flow is shorter, and for Android/Windows/macOS you usually start from the SDK's sysroot instead of building libc at all. The invariant is the same: headers in $CROSS/$TARGET/include, libraries in $CROSS/$TARGET/lib, crt1.o and friends in the same sysroot.

Step 4: stage-2 GCC, now hosted

mkdir -p $SRC/build/gcc-stage2 && cd $SRC/build/gcc-stage2
$SRC/gcc/configure \
  --build=$BUILD --host=$BUILD --target=$TARGET \
  --prefix=$CROSS \
  --disable-bootstrap --disable-nls --disable-werror \
  --enable-languages=c,c++ \
  --enable-shared --enable-threads=posix \
  --disable-multilib
make -j"$(nproc)"
make install-strip

Now $CROSS/bin/$TARGET-g++ exists, libstdc++ is built for AArch64, and the cross toolchain is complete. If you only wanted a cross compiler, you are done.

Step 5: binutils for the host compiler

mkdir -p $SRC/build/binutils-host && cd $SRC/build/binutils-host
STRIP=$HOST-strip \
$SRC/binutils-gdb/configure \
  --build=$BUILD --host=$HOST --target=$TARGET \
  --prefix=$FINAL \
  --disable-nls --disable-werror --disable-tui --without-debuginfod
make -j"$(nproc)"
make install-strip

This time the tools run on the AArch64 host. The make install-strip step needs an $HOST-strip that runs on the build machine; because host equals target here, $TARGET-strip from step 1 is that program. In a Canadian cross build it is not, and you must point STRIP at the build→host toolchain's strip.

Step 6: the host GCC

# the product needs its own copy of the target sysroot
mkdir -p $FINAL/$TARGET
cp -a $CROSS/$TARGET/. $FINAL/$TARGET/

# GCC may look for target headers in $FINAL/$TARGET/sys-include during
# the build; the reference scripts create this temporary symlink.
mkdir -p $FINAL/$TARGET
ln -sfn $CROSS/$TARGET/include $FINAL/$TARGET/sys-include

mkdir -p $SRC/build/gcc-host && cd $SRC/build/gcc-host
export CC=$HOST-gcc
export CXX=$HOST-g++
export CC_FOR_BUILD=$BUILD-gcc
export STRIP=$HOST-strip
export ac_cv_path_STRIP_FOR_TARGET=$TARGET-strip
$SRC/gcc/configure \
  --build=$BUILD --host=$HOST --target=$TARGET \
  --prefix=$FINAL \
  --disable-bootstrap --disable-nls --disable-werror \
  --enable-languages=c,c++ \
  --enable-shared --enable-threads=posix \
  --disable-multilib
make -j"$(nproc)"
make install-strip
rm -f $FINAL/$TARGET/sys-include

Read the three compiler variables carefully:

In-tree gmp, mpfr, mpc and isl are host libraries: the compiler links them and runs on the host. They are built with CC, not with CC_FOR_TARGET. If you provide them externally, build them for the host and pass --with-gmp, --with-mpfr, --with-mpc.

4. Why the Sysroot Appears Twice

The cross toolchain at $CROSS needs a sysroot to build its own libstdc++; the final toolchain at $FINAL needs a sysroot because that is where its headers and libraries must be on the host. They are two copies: $CROSS/$TARGET and $FINAL/$TARGET. The reference scripts install libc separately for each prefix, which looks redundant until you remember that the two compilers have different lifetimes. The cross toolchain is a build tool and can be deleted; the final toolchain is the product and must be self-contained.

During the Canadian build, GCC also wants to find target headers under $FINAL/$TARGET/sys-include. The scripts create a temporary symlink from $FINAL/$TARGET/$TARGET/sys-include to the cross toolchain's include directory, then remove it after installation. This is a build-time workaround for GCC's default include layout, not something you should ship.

5. Canadian Cross, and the Crossback Quirk

If host and target differ, add the build→target compiler and set CC_FOR_TARGET explicitly. In an ordinary Canadian cross this is a second cross toolchain; in a crossback it is the native compiler, because build equals target.

# build -> target cross toolchain, installed at $ROOT/$BUILD/$TARGET
build_toolchain $BUILD $TARGET

# the Canadian cross product
export CC=$HOST-gcc
export CXX=$HOST-g++
export CC_FOR_TARGET=$TARGET-gcc
export CXX_FOR_TARGET=$TARGET-g++
export CC_FOR_BUILD=$BUILD-gcc
configure --build=$BUILD --host=$HOST --target=$TARGET ...

The roles are now distinct: CC produces the host compiler binary, CC_FOR_TARGET produces the target runtime, and CC_FOR_BUILD produces the build-time generators. Mixing up CC and CC_FOR_TARGET is the classic failure: the build succeeds, and the installed compiler either cannot execute on the host or emits code for the wrong machine.

Crossback is the special case of Canadian cross where build = target ≠ host. GCC's build system has a known quirk here: it may try to use cc instead of gcc for CC_FOR_TARGET, and cc may not exist or may be the wrong compiler. The reference scripts patch it explicitly:

if [[ "$BUILD_TRIPLET" == "$target_triplet" &&
      "$BUILD_TRIPLET" != "$host_triplet" ]]; then
    configure_env_vars+=(CC_FOR_TARGET="${target_triplet}-gcc")
fi

That is a good example of the general rule: in a Canadian build, never let a default choose a compiler. Set CC, CC_FOR_BUILD and CC_FOR_TARGET by name.

6. Freestanding Targets

For x86_64-elf, arm-none-eabi or wasm32-unknown-unknown, there is no libc step. The compiler is configured without a hosted C++ library and without target headers:

$SRC/gcc/configure \
  --build=$BUILD --host=$BUILD --target=$TARGET \
  --prefix=$PREFIX \
  --without-headers --disable-hosted-libstdcxx \
  --disable-threads --disable-shared \
  --disable-libssp --disable-libquadmath --disable-libatomic \
  --disable-libsanitizer \
  --enable-languages=c,c++ --disable-multilib
make -j"$(nproc)" all-gcc all-target-libgcc
make install-strip-gcc install-strip-target-libgcc

libgcc still exists: it contains the integer helpers, the unwinder that the compiler may need, and the soft-float routines. What does not exist is a C library, a crt1.o, a dynamic loader, or a hosted libstdc++. The program supplies its own entry point and its own memory and I/O primitives, which is what Ch20 is about. The same toolchain can also be built for wasm32-wasip1 when you provide a WASI sysroot; that is the configuration used in Ch21.3.

7. Checking the Result

$FINAL/bin/gcc -v
# Build:  x86_64-linux-gnu
# Host:   aarch64-linux-gnu
# Target: aarch64-linux-gnu

$FINAL/bin/gcc -dumpmachine
# aarch64-linux-gnu

$FINAL/bin/gcc -print-sysroot
# empty or $FINAL/$TARGET, depending on how GCC was configured
$FINAL/bin/gcc -print-search-dirs
$FINAL/bin/gcc -v -E -x c /dev/null
# the include entries must be under $FINAL/$TARGET

Then compile a trivial program and inspect the link line with -Wl,--verbose. It should find crt1.o and libc.so inside $FINAL/$TARGET. If it finds the build machine's /usr/lib/x86_64-linux-gnu, the sysroot is wrong, and the resulting binary is not an AArch64 program.

The full automation, including the phase files, the sys-include workaround, crossback handling, freestanding targets and packaging, is in trcrsired/toolchainbuildscripts. Read gcc/build_common.sh and gcc/triplet-*.sh after this page; the script is dense, but every branch now has a name.