第22.3章:GCC:构建、宿主、目标
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_FOR_BUILD:编译"构建期间运行的程序"的编译器。它必须运行在build上。这是构建机上的本地编译器。 -
CC:把 GCC 自己的源码编译成安装出来的编译器的编译器。它必须 生成host的程序。 -
CC_FOR_TARGET:构建libgcc、libstdc++这类目标库的编译器。它必须生成target的程序,而且在构建期间必须能运行在构建机上。
本地构建里三者是同一个程序。交叉构建里 CC 和 CC_FOR_BUILD 相同,
CC_FOR_TARGET 不同。加拿大构建里三者可以全不同,把它们混为一谈的构建会产出一个
运行在错误机器上的编译器。
2. 在构建一个编译器之前,先要两个编译器
要从 x86_64-linux-gnu 机器产出一个运行在 aarch64-linux-gnu 上的编译器,
你首先需要一个 x86_64-linux-gnu → aarch64-linux-gnu 交叉编译器。这个编译器运行在
构建机上、生成 AArch64 代码,所以它能编译:
- 以后运行在 AArch64 宿主上的编译器二进制,以及
- 新编译器要用的 AArch64
libgcc和libstdc++。
在加拿大本地构建里,宿主等于目标,所以这一个交叉编译器同时干两件事。在加拿大交叉
构建里,宿主和目标不同,你需要两个: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
仔细看三个编译器变量:
-
CC=$HOST-gcc是第四步的交叉编译器。它运行在构建机上、生成 AArch64 代码, 所以它能编译出将来运行在 AArch64 上的 GCC 二进制。 -
CC_FOR_BUILD=$BUILD-gcc是普通的本地编译器。它构建make期间 运行的生成器。 -
CC_FOR_TARGET默认是$TARGET-gcc。因为宿主等于目标,它和CC是同一个交叉编译器;它为目标构建 AArch64 的libgcc和libstdc++。
源码树内的 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;
脚本很密,但现在每个分支都有名字了。