第22章:加拿大编译
为什么需要这一章
前面每一章都默认编译器已经在那儿了。你写一个源文件,调用 g++ 或
clang++,得到一个二进制。这一章把这个假设拿掉。编译器本身也是程序,
它也需要被构建,而且必须为运行它的那台机器构建。在一个新的体系结构上,根本没有编译器
可以用来构建编译器。这就是自举问题,加拿大编译是它的答案。
这个话题听起来像发布流程的杂活,其实不是。同样是三个三元组,决定了二进制能不能在眼前
这台机器上跑、系统根里装的是不是正确的 C 库,以及"交叉编译"到底指哪一件事。如果你曾经
把一套工具链拷到另一台机器上,然后看着它在 crt1.o、找不到的 ld
或者 libstdc++.so.6 版本上失败,这一章就是解释。
这一章很短,也很具体:一页讲命名体系,一页讲系统根,然后 GCC 和 LLVM 各一页。
四页
简短版本
一个平台由三元组命名:指令集、厂商、操作系统和 C 库,按这个顺序。
厂商可以省略,C 库在某些情况下也可以省略。任何一部分不同,平台就不同,二进制不能互相运行。
桌面的 x86_64-linux-gnu 和 Android 的 x86_64-linux-android30
共享指令集和内核;它们仍然是两个平台,因为 glibc 和 bionic 是两个不同的 C 库,而且 Android
还加上了应用沙盒。只差一个 libc 并不比差一个指令集更近。
在 ARM 上,务实的逃生通道是跑在 Wine 下的 Windows PE:同一个 ARM64 .exe 在
Windows on ARM 上原生运行,在 GNU/Linux、Android、Apple Silicon macOS、FreeBSD/ARM64 和
其他 ARM64 宿主上通过 Wine 运行,因为 Wine 提供了 Windows ABI。这就是"Win32 是 Linux 上
唯一稳定的 ABI"这个笑话的来源。
描述一次编译器构建需要三个三元组:构建是构建编译器的那台机器, 宿主是运行编译器的那台机器,目标是编译器生成代码所面向的机器。 本地编译三者相同。交叉编译是构建等于宿主、但不等于目标。加拿大编译是构建不等于宿主。 加拿大编译分为加拿大本地编译(宿主等于目标)和加拿大交叉编译 (宿主不等于目标)。反向编译是加拿大交叉编译中构建等于目标的特例, 也就是你每天从 Linux 测试 Windows 构建时用的那一种。
编译器不提供目标平台的 C 库。系统根 提供它,连同启动对象、内核头文件和 动态加载器。GCC 提供 libgcc 和 libstdc++;LLVM 提供 compiler-rt、libc++、libc++abi 和 libunwind。如果你不提供 libc,目标就是独立的,工具链必须如实说明,而不是假装自己是 Linux。
在这些开始之前,构建机自己也需要同样的东西:一个带 C/C++ 头文件、libc 和 C++ 运行时的 C/C++ 开发环境,因为 GCC 和 Clang 都是 C++ 程序。Python、Go 和 Rust 不定义第二个系统根; 它们的运行时和原生模块都针对 C/C++ 系统根构建。这就是为什么一个新平台首先得到的是 C/C++ 工具链。
GCC 用 --build、--host 和 --target 配置,
它的加拿大构建需要两个编译器:一个运行在构建机上、生成宿主代码,另一个运行在构建机上、
生成目标代码。LLVM 只有构建和宿主。对 clang 来说只有两个构建类别:本地
(build = host)和加拿大(build ≠ host)。
交叉编译不是构建类别;它是本地 clang 加 --target= 和 --sysroot=
的调用方式,所以是本地编译的特例。一个 clang 二进制包含它配置过的所有后端,
--target= 在每次调用时选择一个。
要点
- 三元组命名 ISA、厂商、操作系统和 C 库。
- sysroot 是目标机的根文件系统:
bin、include、lib和share。 - GCC 和 Clang 使用同一套 sysroot 布局。
- build、host 和 target 是三台不同的机器。