第22.3章:GCC:构建、宿主、目标

English

1. 三个参数,一套构建系统

GCC 顶层 configure 接受 --build、--host 和 --target。它们不是同义词,也没有一个是"平台":

参数 含义 谁运行产物
--build= 编译 GCC 的机器 make 期间产生的构建工具
--host= 安装后的 GCC 运行的机器 安装出来的 gcc、cc1plus、collect2
--target= GCC 生成代码所面向的机器 你以后编译出来的程序

默认值:--host 默认等于 --build,--target 默认等于 --host。所以普通本地构建不需要参数;交叉编译器需要 --target; 加拿大构建还需要 --host。通常还是会显式写 --build,因为 config.guess 猜出来的规范名可能和你其他工具链用的名字不一样。

构建系统内部区分三个编译器,这才是关键:

本地构建里三者是同一个程序。交叉构建里 CC 和 CC_FOR_BUILD 相同, CC_FOR_TARGET 不同。加拿大构建里三者可以全不同,把它们混为一谈的构建会产出一个 运行在错误机器上的编译器。

2. 在构建一个编译器之前,先要两个编译器

要从 x86_64-linux-gnu 机器产出一个运行在 aarch64-linux-gnu 上的编译器, 你首先需要一个 x86_64-linux-gnu → aarch64-linux-gnu 交叉编译器。这个编译器运行在 构建机上、生成 AArch64 代码,所以它能编译:

在加拿大本地构建里,宿主等于目标,所以这一个交叉编译器同时干两件事。在加拿大交叉 构建里,宿主和目标不同,你需要两个:build→host 用于编译器二进制, build→target 用于目标库。对反向编译(build = target),第二个是 本地编译器;对普通加拿大交叉编译,它是另一个交叉编译器。原因不只是目标运行时:GCC 本身就是 C++ 程序,它将要运行的宿主需要完整的 C++ 系统根。build→host 交叉编译器负责产出链接到该宿主 C++ 运行时的编译器二进制。因此构建顺序是:

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)

toolchainbuildscripts 里的参考脚本正是这个顺序。三元组列表就是一组 (build, host, target) 行, build_common.sh 会先构建前置项:

# 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 用 --build=$BUILD_TRIPLET --host=H --target=T 配置, 安装到 $ROOT/H/T。最后一次调用安装到 $ROOT/HOST/TARGET。 这个 prefix 约定让各部件可以被找到:交叉编译器在 $ROOT/$BUILD/$HOST/bin/$HOST-gcc,最终产物在 $ROOT/$HOST/$TARGET/bin。

3. 加拿大本地构建的完整流程

下面给出 build=x86_64-linux-gnu、host=target=aarch64-linux-gnu 的具体步骤。它和脚本自动化的顺序一致,只是把记账部分展开。

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

第一步:交叉编译器用的 binutils

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

这会产出 $TARGET-as、$TARGET-ld、$TARGET-ar、 $TARGET-strip 等,全部运行在构建机上。不要跳过这一步:没有匹配的汇编器和链接器的 编译器安装,不叫工具链。

第二步:第一阶段 GCC,还没有 libc

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

这个 GCC 能编译 C,也能构建 libgcc,但它没有目标头文件,也没有目标 C 库。 这正是重点:libc 需要编译器,编译器又需要 libc。第一阶段编译器打破了循环。

第三步:内核头文件和 libc 装进系统根

# 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 的 libc.so 往往是一个包含绝对路径的链接脚本。如果你装到临时 prefix 再复制, 脚本里的路径仍然指向临时目录。要么直接用 --prefix=$CROSS/$TARGET 安装,要么复制后 重写那几个小的链接脚本(libc.so、libm.so)。参考脚本采用第二种, 并把构建 prefix 从脚本里删掉。

musl 的流程更短;Android、Windows、macOS 通常直接从 SDK 的系统根开始,根本不需要构建 libc。 不变量是一样的:头文件在 $CROSS/$TARGET/include,库在 $CROSS/$TARGET/lib,crt1.o 之类也在这个系统根里。

第四步:第二阶段 GCC,现在是宿主模式

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

现在 $CROSS/bin/$TARGET-g++ 存在了,libstdc++ 也按 AArch64 构建好了, 交叉工具链完整了。如果你只想要交叉编译器,到这里就结束了。

第五步:宿主编译器用的 binutils

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

这一次工具运行在 AArch64 宿主上。make install-strip 需要一个运行在构建机上的 $HOST-strip;因为这里宿主等于目标,第一步构建的 $TARGET-strip 就是那个程序。在加拿大交叉构建里它就不是了,必须把 STRIP 指向 build→host 工具链的 strip。

第六步:宿主 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

仔细看三个编译器变量:

源码树内的 gmp、mpfr、mpc、isl 是宿主库: 编译器链接它们并在宿主上运行。它们用 CC 构建,不是用 CC_FOR_TARGET。 如果你从外部提供它们,要按宿主构建,并传 --with-gmp、--with-mpfr、 --with-mpc。

4. 为什么系统根出现两次

$CROSS 的交叉工具链需要系统根来构建它自己的 libstdc++; $FINAL 的最终工具链也需要系统根,因为在宿主上,头文件和库必须在那里。它们是两份拷贝: $CROSS/$TARGET 和 $FINAL/$TARGET。参考脚本为每个 prefix 分别安装 libc, 看起来冗余,但两个编译器的生命周期不同。交叉工具链是构建工具,可以删掉;最终工具链是产物, 必须自包含。

加拿大构建期间,GCC 还会想在 $FINAL/$TARGET/sys-include 下找目标头文件。 脚本会创建一个从 $FINAL/$TARGET/sys-include 指向交叉工具链 include 目录的临时符号链接,安装完再删掉。这是对 GCC 默认 include 布局的 构建期兼容处理,不应该随产物发布。

5. 加拿大交叉编译与反向编译的怪癖

如果宿主和目标不同,加上 build→target 编译器,并显式设置 CC_FOR_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 ...

角色现在分开了:CC 产出宿主编译器二进制,CC_FOR_TARGET 产出目标运行时, CC_FOR_BUILD 产出构建期生成器。把 CC 和 CC_FOR_TARGET 弄混是经典失败:构建成功,安装出来的编译器要么在宿主上不能执行,要么为错误的机器生成代码。

反向编译是加拿大交叉编译中 build = target ≠ host 的特例。 GCC 的构建系统在这里 有一个已知怪癖:它可能尝试用 cc 而不是 gcc 作为 CC_FOR_TARGET,而 cc 可能不存在,或者根本是另一个编译器。 参考脚本显式修补了它:

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

这是那条通用规则的好例子:在加拿大构建里,永远不要然默认值去挑编译器。按名字设置 CC、CC_FOR_BUILD 和 CC_FOR_TARGET。

6. 独立目标

对 x86_64-elf、arm-none-eabi 或 wasm32-unknown-unknown, 没有 libc 这一步。编译器按不带宿主 C++ 库、不带目标头文件的方式配置:

$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 仍然存在:它包含整数辅助函数、编译器可能需要的 unwind 代码和软浮点例程。 不存在的是 C 库、crt1.o、动态加载器和宿主版 libstdc++。程序自己提供 入口点、内存和 I/O 原语,这正是第20章的内容。 同一套工具链在提供 WASI 系统根时也可以为 wasm32-wasip1 构建;那就是 第21.3章使用的配置。

7. 检查结果

$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

然后编译一个最简单的程序,用 -Wl,--verbose 看链接行。它应该在 $FINAL/$TARGET 里找到 crt1.o 和 libc.so。如果它找到的是 构建机的 /usr/lib/x86_64-linux-gnu,系统根就是错的,产出的二进制不是 AArch64 程序。

完整自动化——包括阶段文件、sys-include 兼容处理、反向编译、独立目标和打包——在 trcrsired/toolchainbuildscripts。 读完这一页再去看 gcc/build_common.sh 和 gcc/triplet-*.sh; 脚本很密,但现在每个分支都有名字了。