构建LLVM交叉编译工具链
编译前准备
编译前需要在 llvm 的源码根目录下新建一个 build 目录,然后进入这个目录进行 make,这个主要原因是 LLVM 目前不支持在 llvm-project 目录下直接编译,否则会失败,官方的说法是 in-tree build is not supported
。这个 build 目录官方的正式定义叫 OBJ_ROOT
,所有 cmake 生成的项目配置文件,以及编译过程中生成的 .o 文件和最终的 bin 文件都会存放在这个目录下。
$ mkdir build |
使用 cmake 的基本命令模板如下:
$ cd OBJ_ROOT |
其中:
generator
表示用于最终驱动gcc执行编译生成llvm的工具,是用双引号括起来的字符串,cmake支持跨平台开发,有以下四种选项:Unix Makefiles
: 即采用Unix上传统的Make,指定该选项后cmake负责生成用于Make的makefile文件。Ninja
: 采用Ninja,指定该选项后 cmake 负责生成用于 Ninja 的 build.ninja 文件。这是LLVM 的开发社区推荐采用的方式,因为对于像LLVM这样的大型软件来说,采用 Ninja 会大大加速编译的速度。Visual Studio
: 指示 cmake 产生用于 Visual Studio 的项目构造文件。Xcode
: 指示 cmake 产生用于 Xcode 的项目构造文件, Xcode 是运行在操作系统 MacOS X 上的集成开发工具(IDE)。
SRC_ROOT
: LLVM 的官方定义是the top level directory of the LLVM source tree
, 在这里指的就是我们下载的仓库根目录llvm-project
下的llvm
子目录,在 LLVM 项目中 llvm 子目录存放的是这个项目的主框架代码,是必须要编译的对象。options
,以-D
开头定义的选项宏,如果超过一个则用空格分隔。这些选项会影响 cmake 生成的构造配置文件并进而影响整个编译构造过程,针对 LLVM 常用的有以下这些,更多选项请参阅官网:CMAKE_BUILD_TYPE=type
:指定生成的应用程序(这里当然指的是 LLVM)的类型,type
包括Debug
、Release
、RelWithDebInfo
或者MinSizeRel
。如果不指定缺省为Debug
。CMAKE_INSTALL_PREFIX=directory
:用于指定编译完后安装LLVM工具和库的路径,如果不指定,默认安装在/usr/local
。LLVM_TARGETS_TO_BUILD
:用于指定生成的LLVM可以支持的体系架构(这里称为 target),LLVM 和 GCC 有个很大的不同点是, GCC 需要为每个特定的体系架构,譬如 arm/x86 独立生成一套交叉工具链套件,而 LLVM 是在一个工具链套件中就可以支持多个体系架构。如果不指定,默认会编译所有的 targets,具体的 targets 有哪些,可以看源码llvm-project/llvm/CMakeLists.txt
中LLVM_ALL_TARGETS
的定义。具体制作时可以自己指定需要的 targets,通过以分号分隔方式给出,譬如-DLLVM_TARGETS_TO_BUILD="ARM;PowerPC;X86"
。LLVM_DEFAULT_TARGET_TRIPLE
: 可以通过该选项修改默认的 target 的 triple 组合,不指定默认是x86_64-unknown-linux-gnu
。LLVM_ENABLE_PROJECTS='...'
: LLVM 是整个工具链套件的总称,LLVM 下包括了很多个子项目,譬如 clang, clang-tools-extra, libcxx, libcxxabi, libunwind, lldb, compiler-rt, lld, polly, or debuginfo-tests 等。如果不指定该选项,默认只编译 llvm 这个主框架。如果要选择并指定编译哪些子项目,可以通过分号分隔方式给出,譬如我们在编译 llvm 之外还想编译 Clang, libcxx, 和 libcxxabi, 那么可以写成这样:-DLLVM_ENABLE_PROJECTS="clang;libcxx;libcxxabi"
。
基于以上理解执行如下命令:
cmake -S llvm -B build -G "Ninja" \ |
简单解释一下以上命令的效果就是:
- 采用ninja方式编译LLVM;
- 编译Release版本(这里只是使用llvm工具链,不涉及开发,所以采用Release方式可以缩短编译时间和减少对硬盘的消耗,生成的可执行程序执行速度也快);
- 编译完成后如果要安装将安装在/opt/BinaryTranslation/llvm-13.0.1目录下;
-DLLVM_TARGETS_TO_BUILD=all
表示编译所有目标平台;- 修改默认的triple组合为除了llvm外还会生成clang、libcxx和libcxxabi。
什么是Target Triple
目标三元组(Target Triple)是GNU构建系统中的核心概念,描述了一个代码运行的平台。它们包含三个字段:CPU家族/型号的名称、供应商、操作系统名称。你可以通过运行gcc来查看当前系统的默认目标三元组:
gcc -dumpmachine目标三元组的结构很简单:
>machine-vendor-operatingsystem比如在FreeBSD系统中:
>x86_64-unknown-freebsd请注意,供应商字段通常是无关紧要的。在X86的系统上通常为
pc
或unknown
,在其他操作系统中有时也为none
。由于供应商字段大多情况下并不使用,因此GNU构建系统允许忽略供应商字段。例如:
>x86_64-freebsd如果构建系统希望知道明确的目标三元组,那么它将自动推断供应商是默认的(未知的)。解析目标三元组要复杂一些,因为有时操作系统可以是两个字段:
>x86_64-unknown-linux-gnu因此当忽略供应商名称时可能会产生一些混淆:
>x86_64-linux-gnu显然此时三元组的含义是模棱两可的。大多数自动配置的软件包都带有一个shell脚本,名为config.sub,其功能是使用已知的CPU和已知操作系统的列表来消除歧义。
目标三元组旨在成为系统的无歧义平台名称(在消除歧义之后)。它们让构建系统准确地理解代码将在哪个系统上运行,并允许自动启用特定于平台的特性。在编译领域中,通常涉及三个平台(可能是相同的三个平台):
- Build Platform:编译工具运行的平台;
- Host Platform:编译得到的软件最终将要运行的平台;
- Target Platform:如果编译的软件是一个编译器,那么目标平台就是编译器所产生的机器指令的平台。
这意味着最多可以使用三个不同目标的编译器(如果你在平台A上构建GCC,它将运行在平台B上,为平台C生成可执行程序)。这个问题可以通过简单地在编译工具前面加上目标三元组来解决。在构建交叉编译器时,安装的可执行程序将以指定的目标三元组作为前缀:
>i686-elf-gcc如果构建系统为所有编译工具加上目标前缀,就可以防止使用错误的编译器。
执行编译和安装
ninja -j $(nproc) |
简单检查一下安装的结果
ls /opt/BinaryTranslation/llvm-13.0.1 -l |
检查一下生成的 clang 的版本:
clang version 13.0.1 (https://github.com/llvm/llvm-project.git 75e33f71c2dae584b13a7d1186ae0a038ba98838) |
为了后面直接在命令行中输入 clang 运行编译器, 将安装 clang 工具所在路径添加到 PATH 环境变量中,这里不啰嗦了。
验证一下工具链是否可以工作
编辑一个简单的 test.c 文件
|
受限于 LLVM 自身链接器和 C 库的不完善,clang 目前需要使用 GNU 的链接器和 C 库来生成 RISC-V 的可执行程序。运行 clang 编译程序,通过 --sysroot
选项来指定 gnu 工具链的 sysroot,通过 --gcc-toolchain
来指定 gcc 工具链的位置。
/opt/BinaryTranslation/llvm/bin/clang --gcc-toolchain="/opt/riscv-toolchain-bin-rv64gc/" --sysroot="/opt/riscv-toolchain-bin-rv64gc/riscv64-unknown-elf/" -v --target=riscv64 -march=rv64imafd -o test.out test.c |
编译通过,那么说明LLVM就安装好了。