编译前准备

编译前需要在 llvm 的源码根目录下新建一个 build 目录,然后进入这个目录进行 make,这个主要原因是 LLVM 目前不支持在 llvm-project 目录下直接编译,否则会失败,官方的说法是 in-tree build is not supported。这个 build 目录官方的正式定义叫 OBJ_ROOT,所有 cmake 生成的项目配置文件,以及编译过程中生成的 .o 文件和最终的 bin 文件都会存放在这个目录下。

$ mkdir build
$ cd build

使用 cmake 的基本命令模板如下:

$ cd OBJ_ROOT
$ cmake -G <generator> [options] SRC_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包括DebugReleaseRelWithDebInfo 或者 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.txtLLVM_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" \
-DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld;lldb" \
-DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" \
-DLLVM_TARGETS_TO_BUILD="all" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/opt/BinaryTranslation/llvm-13.0.1 \
-DDEFAULT_SYSROOT=/opt/BinaryTranslation/llvm-13.0.1

简单解释一下以上命令的效果就是:

  • 采用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的系统上通常为pcunknown,在其他操作系统中有时也为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)
$ cmake --build . --target install
$ cmake -DCMAKE_INSTALL_PREFIX=/opt/BinaryTranslation/llvm-13.0.1/ -P cmake_install.cmake
$ ninja install

简单检查一下安装的结果

$ ls /opt/BinaryTranslation/llvm-13.0.1 -l
total 20
drwxrwxr-x 2 u u 4096 10月 9 11:37 bin
drwxrwxr-x 7 u u 4096 10月 9 11:37 include
drwxrwxr-x 4 u u 4096 10月 9 11:37 lib
drwxrwxr-x 2 u u 4096 10月 9 11:37 libexec
drwxrwxr-x 7 u u 4096 10月 9 11:37 share

检查一下生成的 clang 的版本:

clang version 13.0.1 (https://github.com/llvm/llvm-project.git 75e33f71c2dae584b13a7d1186ae0a038ba98838)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /opt/BinaryTranslation/llvm/bin
Found candidate GCC installation: /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
Selected GCC installation: /opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7
Candidate multilib: .;@m64
Candidate multilib: 32;@m32
Selected multilib: .;@m64

为了后面直接在命令行中输入 clang 运行编译器, 将安装 clang 工具所在路径添加到 PATH 环境变量中,这里不啰嗦了。

验证一下工具链是否可以工作

编辑一个简单的 test.c 文件

#include <stdio.h>

int main(int argc, char *argv[])
{
printf("Hello, world!\n");
return 0;
}

受限于 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就安装好了。