第22.4章:LLVM:构建与宿主
1. Clang 没有"目标的构建"
GCC 针对一个目标配置。用 --target=aarch64-linux-gnu 构建出来的
gcc 只生成 AArch64,别的都不行。LLVM 是另一种设计:一个代码生成器带多个后端,
全部编进一个 clang 二进制。目标在调用时选择:
clang++ --target=aarch64-linux-gnu --sysroot=$SYSROOT ...
clang++ --target=x86_64-windows-gnu --sysroot=$MINGW ...
clang++ --target=wasm32-wasip1 --sysroot=$WASI ...
所以 GCC 意义上的"目标的 clang"不存在。存在的是一个运行在宿主上、包含
LLVM_TARGETS_TO_BUILD 所启用后端的 clang。只要 AArch64 后端在,同一个二进制就能
在 x86_64 机器上、AArch64 机器上或者 Windows 机器上生成 AArch64 代码。交叉编译不是另一次构建,
而是同一个本地驱动换一个 --target。
但编译器总得在某个地方运行。那就是宿主。因此 LLVM 的构建只有两个三元组,不是三个:
| 概念 | 在 LLVM/CMake 里 | 含义 |
|---|---|---|
| 构建 | 运行 CMake/Ninja 的机器 | cmake 和 ninja 在哪里运行 |
| 宿主 | CMAKE_C_COMPILER_TARGET、LLVM_HOST_TRIPLE |
产出的 clang 二进制在哪里运行 |
| 默认目标 | LLVM_DEFAULT_TARGET_TRIPLE |
用户不传 --target 时 clang 生成什么 |
| 启用的后端 | LLVM_TARGETS_TO_BUILD |
这个二进制能为哪些指令集生成代码 |
最容易弄错的是 CMAKE_C_COMPILER_TARGET 和
LLVM_DEFAULT_TARGET_TRIPLE 的区别。前者告诉"构建 clang 的那个编译器"为某个三元组
生成代码,所以 clang 二进制本身就是那个三元组的。后者只改 clang 的默认值,二进制仍然
运行在构建机上。想要后者却设了前者,得到的是一个在你机器上不能执行的编译器。
因为目标是调用参数,clang 只有两个构建类别,二者都由构建和宿主决定:
| 类别 | 关系 | 含义 |
|---|---|---|
| 本地(Native) | build = host | clang 二进制在哪里构建就在哪里运行。交叉编译属于这一类:同一个本地 clang
加 --target= 和 --sysroot= 调用。 |
| 加拿大(Canadian) | build ≠ host | clang 二进制在另一台机器上生产,运行在宿主上。 |
clang 没有交叉构建。22.1 里 GCC 的交叉行存在,是因为 GCC 在配置期固定目标;clang 不固定。交叉是本地 clang 的一种用法, 所以是本地编译的特例。加拿大本地/加拿大交叉/反向编译的划分也不适用:目标不是 clang 构建的 一部分,所以加拿大 clang 就是加拿大编译。
2. 什么算一次加拿大 LLVM 构建
假设你在 x86_64-linux-gnu 上,想要一个运行在 aarch64-linux-gnu 上、
能生成 AArch64 的 clang。你像交叉编译任何 C++ 程序一样交叉编译 clang:使用的编译器必须生成
AArch64,系统根必须是 AArch64 的 C/C++ 系统根,因为 clang 要链接 libc 和 C++ 运行时。
这就是加拿大构建:构建是 x86_64,宿主是 aarch64。这是 clang 唯一的非本地类别。目标根本不进入
CMake 的三元组;它是后端和默认值,之后由 --target 选择。
如果你想要一个运行在构建机上、但默认面向 AArch64 的 clang,就不要设
CMAKE_C_COMPILER_TARGET。让二进制保持本地,设
LLVM_DEFAULT_TARGET_TRIPLE=aarch64-linux-gnu,并确保
LLVM_TARGETS_TO_BUILD 包含 AArch64。这是 LLVM 里最接近 GCC 交叉编译器
的东西,它只是一个多带一个后端的本地 clang。当你再传
--target=aarch64-linux-gnu --sysroot=$SYSROOT 时,就是在用这个本地 clang
交叉编译;编译器二进制没有变。
两种情况下,构建都需要同样三样实际的东西:
- 如果宿主不等于构建,需要一个 build-host 的 C++ 系统根。clang 是 C++ 写的; 产出的二进制必须链接宿主的 C++ 运行时。
-
构建期间运行的本地代码生成工具:
llvm-tblgen、clang-tblgen、llvm-config,以及 pseudo/confusable 生成器。 它们运行在构建机上,不能是交叉编译出来的那一份。 -
为任何要构建的运行时库(
compiler-rt、libc++、libc++abi、libunwind)准备带 libc 和头文件的目标系统根。 22.2 的系统根规则原样适用。
3. CMake 配置
用 CMake toolchain 文件表达宿主和系统根是最干净的方式。参考脚本的形态是:
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)
# The produced clang runs on this triple, so clang emits its code for it.
set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_ASM_COMPILER_TARGET aarch64-linux-gnu)
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_CROSSCOMPILING On)
set(CMAKE_SYSROOT /home/you/toolchains/llvm/aarch64-linux-gnu/aarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
运行 CMake 测试程序的编译器是构建机上的 clang,但
CMAKE_C_COMPILER_TARGET 让它产出 AArch64 二进制。CMake 本身运行在构建机上;
它构建出来的目标文件属于宿主。这就是一次加拿大构建,只是没有 --host 参数。
然后用宿主三元组、默认目标和本地工具配置 LLVM:
cmake -G Ninja -S llvm-project/llvm -B build-aarch64 \
-DCMAKE_TOOLCHAIN_FILE=$PWD/aarch64.cmake \
-DCMAKE_INSTALL_PREFIX=/home/you/toolchains/llvm/aarch64-linux-gnu/llvm \
-DLLVM_HOST_TRIPLE=aarch64-linux-gnu \
-DLLVM_DEFAULT_TARGET_TRIPLE=aarch64-linux-gnu \
-DLLVM_TARGETS_TO_BUILD="AArch64;X86" \
-DLLVM_NATIVE_TOOL_DIR=/home/you/toolchains/llvm/native/bin \
-DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra" \
-DLLVM_ENABLE_ZLIB=FORCE_ON \
-DZLIB_LIBRARY=$SYSROOT/usr/lib/libz.a \
-DZLIB_INCLUDE_DIR=$SYSROOT/usr/include \
-DBUILD_SHARED_LIBS=ON \
-DLLVM_ENABLE_LTO=Thin \
-DLLVM_ENABLE_TERMINFO=OFF \
-DLLVM_ENABLE_LIBXML2=OFF \
-DLLVM_INCLUDE_TESTS=OFF \
-DLLVM_INCLUDE_BENCHMARKS=OFF \
-DLLVM_ENABLE_ASSERTIONS=OFF
ninja -C build-aarch64
ninja -C build-aarch64 install/strip
CMake 有时会在第一次配置时忽略新的 toolchain 文件。参考脚本因此把同一条 cmake
命令跑两遍;第二次相同的调用很便宜,可以避免"它用了宿主编译器"这种令人困惑的失败。
4. 本地工具:真正会咬人的地方
LLVM 在构建时用 llvm-tblgen 和 clang-tblgen 生成很大一部分自己的源码。
这些程序必须运行在构建机上。交叉编译出来的 clang 在那里跑不了,所以 CMake 需要一份本地副本。
现代做法是 LLVM_NATIVE_TOOL_DIR,由
D131052 引入,2023 年合入。把它指向本地 LLVM 构建的
bin 目录,CMake 就会使用那些可执行文件,而不是自己再构建:
-DLLVM_NATIVE_TOOL_DIR=/home/you/toolchains/llvm/native/bin
旧的说明会逐个设置工具:
-DLLVM_TABLEGEN=/path/to/native/bin/llvm-tblgen
-DCLANG_TABLEGEN=/path/to/native/bin/clang-tblgen
-DLLVM_CONFIG_PATH=/path/to/native/bin/llvm-config
两种都行。要点是 ninja install 默认不安装 llvm-tblgen 和
clang-tblgen,所以"先安装 clang,再用这个安装去交叉构建"的天真做法会因为找不到
TableGen 而失败。保留本地构建目录,或者把工具拷到一个稳定的位置。
5. 运行时库
"clang 编译器"和"完整的 clang 工具链"是两回事。编译器本身可以使用目标系统根里 GCC 的
libstdc++ 和 libgcc。这是得到可用交叉编译器最快的方式:
$PREFIX/bin/clang++ --target=$TARGET \
--gcc-toolchain=$GCC_PREFIX \
--sysroot=$SYSROOT \
hello.cc -o hello
自包含的 LLVM 工具链还会带上 compiler-rt、libc++、
libc++abi 和 libunwind。把它们作为 runtime 构建,使用刚构建好的 clang
和目标系统根:
cmake -G Ninja -S llvm-project/runtimes -B build-runtimes \
-DCMAKE_C_COMPILER=$PREFIX/bin/clang \
-DCMAKE_CXX_COMPILER=$PREFIX/bin/clang++ \
-DCMAKE_C_COMPILER_TARGET=$TARGET \
-DCMAKE_CXX_COMPILER_TARGET=$TARGET \
-DCMAKE_SYSROOT=$SYSROOT \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" \
-DLLVM_ENABLE_PER_TARGET_RUNTIME_DIR=ON
ninja -C build-runtimes
ninja -C build-runtimes install
构建顺序很重要:compiler-rt 的 builtins 不依赖 C++ 库;libunwind
需要 C++ 头文件但不需要 C++ 库;libc++abi 需要 libunwind;
libc++ 需要 libc++abi。如果你只需要 C,跳过这些,直接用系统根的 libc。
运行时装好之后,显式选择它们:
clang++ --target=$TARGET --sysroot=$SYSROOT \
--rtlib=compiler-rt --stdlib=libc++ \
-nostdinc++ -isystem $PREFIX/include/c++/v1 \
-L$PREFIX/lib -Wl,-rpath,$PREFIX/lib \
hello.cc -o hello
6. 实际会遇到的失败
| 症状 | 原因 | 修复 |
|---|---|---|
构建期间 llvm-tblgen: not found |
没有本地工具;加拿大构建试图在构建机上运行宿主二进制 | LLVM_NATIVE_TOOL_DIR 或 LLVM_TABLEGEN/CLANG_TABLEGEN |
产出的 clang 在构建机上不能执行 |
想要本地 clang,却把 CMAKE_C_COMPILER_TARGET 设成了目标 |
去掉它,或者把结果当作宿主二进制并送到宿主 |
libstdc++.so.6: version GLIBCXX_3.4.xx not found |
宿主的 C++ 运行时比构建 clang 用的更旧 | LLVM_STATIC_LINK_CXX_STDLIB=ON,或随产物带新运行时,或设 LD_LIBRARY_PATH |
cannot find -lz,或者压缩支持被静默关闭 |
没有找到目标的 zlib | LLVM_ENABLE_ZLIB=FORCE_ON 并显式给 ZLIB_LIBRARY/ZLIB_INCLUDE_DIR |
| 宿主库泄漏进构建 | pkg-config 和 CMake 在宿主上搜索 |
CMAKE_FIND_ROOT_PATH、PKG_CONFIG_LIBDIR、PKG_CONFIG_PATH="" |
链接时报缺少 crtbegin.o 或 -lc |
没有目标系统根,或者布局不对 | 读 22.2,检查 -print-search-dirs |
| 构建想要 Python、curses 或 libxml2 | 默认启用了可选项目 | 关掉 lldb,用 LLVM_ENABLE_TERMINFO=OFF、LLVM_ENABLE_LIBXML2=OFF |
BUILD_SHARED_LIBS=ON 是一个有意的取舍。它让 LLVM 库变成动态库,减小安装体积,
让 clang 工具共享它们,但意味着宿主必须有兼容的 libstdc++.so.6 或
libc++.so。如果你要发布到无法控制的机器上,要么用
-DLLVM_STATIC_LINK_CXX_STDLIB=ON 构建,要么把运行时一起打包并设置 rpath。
不要因为宿主的 C 编译器能用,就假设宿主的 C++ 运行时足够新。
7. 验证结果
$PREFIX/bin/clang --version
$PREFIX/bin/clang -print-target-triple
$PREFIX/bin/clang -print-targets # enabled backends
$PREFIX/bin/clang -dumpmachine
$PREFIX/bin/clang --target=$TARGET --sysroot=$SYSROOT \
-v -E -x c /dev/null # include search path
file $PREFIX/bin/clang # must match the host, not the build machine
最后一项检查专门抓走错的加拿大构建。如果 file 说
ELF 64-bit LSB ... x86-64,而你想要 AArch64 宿主,那就是构建用了本地编译器,
而不是面向目标的编译器。编译器仍然能运行,也仍然能编译 hello world,所以这个失败能躲过一次
随意的测试。打包之前,把工具链拷到目标宿主上,在那里运行 clang --version。
这一页的参考实现是
trcrsired/toolchainbuildscripts
里的 canadian/canadian_clang_aarch64-linux-gnu.txt 和
llvm/build_common.sh。那些文件把 CMAKE_C_COMPILER_TARGET、
LLVM_HOST_TRIPLE、LLVM_DEFAULT_TARGET_TRIPLE、系统根、zlib 和本地
TableGen 工具放在一处;读完这一页,这些选项名应该读起来像对构建过程的描述,而不是一串参数。