LLVM的编译以及cmake构建

本文主要从wsl的安装,到LLVM的编译,以及通过cmake构建llvm的tutorial,Kaleidoscope。

1 wsl的安装

参考如下的文章:windows WSL2避坑指南

2 LLVM的编译

2.1 获取LLVM

git clone git@github.com:llvm/llvm-project.git

2.2 编译LLVM

由于llvm的编译过大。因此我给它分配了30G的swap内存,如下的cmake命令能有效减少编译以及链接时间,例如我这边只选择编译了x86的架构,并使用动态库,通过lld来加快链接速度。

cmake命令

$ cd llvm-project/
$ cmake -S llvm -B build -G Ninja 
-DCMAKE_BUILD_TYPE=Debug 
-DCMAKE_INSTALL_PREFIX=install 
-DLLVM_TARGETS_TO_BUILD=X86 
-DBUILD_SHARED_LIBS=ON -DLLVM_USE_SPLIT_DWARF=ON 
-DLLVM_OPTIMIZED_TABLEGEN=ON 
-DLLVM_ENABLE_PROJECTS="clang" 
-DLLVM_USE_LINKER=lld 
-DCMAKE_C_COMPILER_LAUNCHER=ccache 
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache

各个命令解释

cmake -S llvm -B build -G Ninja
  • cmake: 调用 CMake。
  • -S llvm: 指定源代码路径。-S 表示 source,llvm 是源代码目录。如果你在当前目录下运行该命令,llvm 目录应该包含 LLVM 项目的 CMakeLists.txt 文件。
  • -B build: 指定构建目录。-B 表示 build,build 是构建的输出目录,所有的中间文件和最终编译输出都会放在这个目录下。
  • -G Ninja: 指定生成器为 Ninja。Ninja 是一个小巧快速的构建系统,用于替代 Make,特别适合于处理大量文件的项目。
-DCMAKE_BUILD_TYPE=Debug
  • -DCMAKE_BUILD_TYPE=Debug: 设置构建类型为 Debug。这意味着编译器会生成带有调试信息的可执行文件,并且不会启用优化,以便更容易进行调试。可选值包括 Release、RelWithDebInfo、MinSizeRel 等。
    -DCMAKE_INSTALL_PREFIX=install
    
  • -DCMAKE_INSTALL_PREFIX=install: 指定安装路径。编译完成后,通过 make install 或 ninja install 安装时,所有的可执行文件、库和头文件会被安装到 install 目录。
    -DLLVM_TARGETS_TO_BUILD=X86
    
  • -DLLVM_TARGETS_TO_BUILD=X86: 指定编译的目标架构为 X86。LLVM 支持多种目标架构(如 ARM、MIPS 等),这里限制只编译 X86 目标架构,减少编译时间和生成的文件大小。如果需要支持多种架构,可以用 ; 分隔多个架构名称。
    -DBUILD_SHARED_LIBS=ON
    
  • -DBUILD_SHARED_LIBS=ON: 指定构建为共享库(动态库)。设置为 ON 时,LLVM 会生成 .so(在 Unix 系统)或 .dll(在 Windows 系统)文件,而不是静态库(.a 或 .lib)。
    -DLLVM_USE_SPLIT_DWARF=ON
    
  • -DLLVM_USE_SPLIT_DWARF=ON: 启用 Split DWARF 机制。在使用 DWARF 格式生成调试信息时,Split DWARF 会将调试信息分离到独立的 .dwo 文件中,从而减小可执行文件的大小,加快链接速度。通常与 Debug 构建类型结合使用。
-DLLVM_OPTIMIZED_TABLEGEN=ON
  • -DLLVM_OPTIMIZED_TABLEGEN=ON: 使用优化后的 TableGen 工具来生成 LLVM 中的表。TableGen 是 LLVM 用来生成各种表格驱动的代码的工具。开启此选项会在编译前优化 TableGen 的生成过程,减少编译时间。
-DLLVM_ENABLE_PROJECTS="clang"
  • -DLLVM_ENABLE_PROJECTS=”clang”: 指定要编译的子项目,这里是 clang。LLVM 项目包含多个子项目,如 clang、lld、compiler-rt 等。这个选项让 CMake 只编译和构建 clang 项目。如果要编译多个项目,可以用 ; 分隔它们,例如 “clang;lld”。
-DLLVM_USE_LINKER=lld
  • -DLLVM_USE_LINKER=lld: 使用 LLVM 的 lld 链接器代替系统默认的链接器。lld 是一个快速的链接器,可以加速链接时间,尤其在处理大型项目时。
-DCMAKE_C_COMPILER_LAUNCHER=ccache 
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
  • -DCMAKE_C_COMPILER_LAUNCHER=ccache 和 -DCMAKE_CXX_COMPILER_LAUNCHER=ccache: 使用 ccache 来缓存编译结果。这两个选项指定 C 和 C++ 编译器的启动器为 ccache,它是一个编译缓存工具,可以显著减少重复编译的时间。每次编译时,如果 ccache 检测到相同的源代码已经编译过,它会直接返回之前的结果,而不再重新编译。

make命令

我们通过ninja来进行编译

$ cd build/
$  ninja -j10 install

3 集成LLVM tool

Building an LLVM-based tool. Lessons learned

3.1 编译系统

3.1.1 llvm-config

使用 LLVM 作为库的最著名方法是使用 llvm-config,如下所示

> clang -c `llvm-config --cxxflags` foo.cpp -o foo.o
> clang -c `llvm-config --cxxflags` bar.cpp -o bar.o
> clang `llvm-config --ldflags` `llvm-config --libs core support` bar.o foo.o -o foobar.bin

在一开始它工作得很好,但存在一些问题。

  1. 编译器标志:llvm-config --cxxflags 会给出 LLVM 编译时使用的flag,这些flag不一定是你项目所需要的。我们来看一个例子:
    -I/opt/llvm/6.0.0/include
    -Werror=unguarded-availability-new
    -O3 -DNDEBUG
    

    第一个flag是正确的,你需要它。第二个flag是 Clang 特有的:它可能不适用于 gcc,也可能不适用于较旧版本的 Clang。剩下的flag(-O3 -NDEBUG)会强制你以release模式编译你的项目。但是有可能你仅仅只是想要一个debug版本。

  2. 链接器的flag。llvm-config --ldflags 告诉你在哪里查找库并调整一些其他链接器设置。llvm-config --libs <components> 打印出你需要链接的库集合以使用指定的组件(你可以通过 llvm-config --components 查看所有组件列表)。然而,如果你的系统上安装了多个版本的 LLVM,并且他们带有动态库的时候,你可能在成功链接后遇到运行时错误。

  3. 链接顺序。llvm-config --libs 仅适用于 LLVM 库。如果我们还想使用 Clang 库与 llvm-config,那么你会遇到麻烦:库应该按正确的顺序排列。它可能会工作,也可能不会。这个问题只在 Linux 上出现。你要么手动重新排序 Clang 库直到编译通过,要么将库列表包裹在 --start-group/--end-group 中。这是一个合理的解决方案,但在 macOS 上不起作用。如果非要使用llvm-config的话,可以使用下面类似这样的解决方案:
    if macOS 
    LDFLAGS=-lLLVM -lclangEdit 
    else 
    LDFLAGS=-Wl,--start-group -lLLVM -lclangEdit -Wl,--end-group 
    endif 
    clang foo.o bar.o $LDFLAGS -o foobar.bin
    

3.1.2 Cmake

LLVM 本身使用 CMake 作为主要的构建系统。LLVM 工程师在使其对 LLVM 用户非常友好方面付出了大量工作。 通过 CMake 添加 LLVM 和 Clang 作为依赖关系是相当简单的:

find_package(LLVM REQUIRED CONFIG
             PATHS ${search_paths}
             NO_DEFAULT_PATH)
find_package(Clang REQUIRED CONFIG
             PATHS ${search_paths}
             NO_DEFAULT_PATH)

请注意 ${search_paths} 和 NO_DEFAULT_PATH。

在我们的例子中,${search_paths} 是:

set (search_paths
  ${PATH_TO_LLVM}
  ${PATH_TO_LLVM}/lib/cmake
  ${PATH_TO_LLVM}/lib/cmake/llvm
  ${PATH_TO_LLVM}/lib/cmake/clang
  ${PATH_TO_LLVM}/share/clang/cmake/
  ${PATH_TO_LLVM}/share/llvm/cmake/
)

PATH_TO_LLVM 是用户通过外部方式提供给 CMake 的。我会以kaleidoscope的例子来说明如何使用CMake的命令集成llvm的lib。

3.2 llvm的kaleidoscope tutorial的构建

Kaleidoscope 其整体结构如下,其中llvm这个目录就是我们通过上面的编译得到的llvm的install目录。

├── llvm
├── CMakeLists.txt
├── README.md
├── lexer
│   ├── CMakeLists.txt
│   ├── lexer.cpp
│   └── lexer.h
├── main.cpp
└── parser
    ├── CMakeLists.txt
    ├── ast.cpp
    ├── ast.h
    ├── logger.cpp
    ├── logger.h
    ├── parser.cpp
    └── parser.h

因此我们在最外层的CMakeLists.txt中,调用find_package,此时我们就可以使用llvm的cmake中所定义的变量。 注意:我们使用find_package的config模式,因此会找到llvm/lib/cmake/llvm/LLVMConfig.cmake中,这个文件中定义了我们所需要用道路的变量,例如LLVM_INCLUDE_DIRSLLVM_LIBRARY_DIRSfind_package

最外层的CMakeLists.txt文件如下:

...
set (PATH_TO_LLVM ${CMAKE_SOURCE_DIR}/llvm)
set (search_paths
  ${PATH_TO_LLVM}
  ${PATH_TO_LLVM}/lib/cmake
  ${PATH_TO_LLVM}/lib/cmake/llvm
  ${PATH_TO_LLVM}/lib/cmake/clang
  ${PATH_TO_LLVM}/share/clang/cmake/
  ${PATH_TO_LLVM}/share/llvm/cmake/
)
find_package(LLVM REQUIRED CONFIG
             PATHS ${search_paths}
             NO_DEFAULT_PATH)
find_package(Clang REQUIRED CONFIG
             PATHS ${search_paths}
             NO_DEFAULT_PATH)
...
add_executable(main main.cpp)
link_directories(${LLVM_LIBRARY_DIRS})
target_link_libraries(main PUBLIC ${EXTRA_LIBS} LLVMSupport LLVMCore)
# 由于install的时候,rpath会被去除,因此需要设置其install path
set_target_properties(main PROPERTIES LINK_FLAGS "-Wl,--disable-new-dtags" INSTALL_RPATH "${LLVM_LIBRARY_DIRS}")

set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/install)
install(TARGETS main RUNTIME DESTINATION bin)
...

3.3 BUILD_PATH和INSTALL_PATH

CMake中INSTALL_RPATH与BUILD_RPATH问题

3.3.1 查看可执行文件的RPATH

如下所示,通过readelf -d main可以查看rpath。如果我们没有设置rpath,readelf -d main中就不会有rpath这个选项。

$ readelf -d main
victorl@victor:~/github/Kaleidoscope$ readelf -d  install/bin/main

Dynamic section at offset 0x47a60 contains 32 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libLLVMCore.so.20.0git]
 0x0000000000000001 (NEEDED)             Shared library: [libLLVMSupport.so.20.0git]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000f (RPATH)              Library rpath: [/home/victorl/github/Kaleidoscope/llvm/lib]
 0x000000000000000c (INIT)               0x27000
 0x000000000000000d (FINI)               0x391b8
 0x0000000000000019 (INIT_ARRAY)         0x48780
 0x000000000000001b (INIT_ARRAYSZ)       32 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x487a0
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)

3.3.2 RPATH和RUNPATH

RPATH和RUNPATH都可以用来在运行时搜索动态库。Linux搜索库的路径如下:

RPATH: 写在elf文件中
LD_LIBRARY_PATH:环境变量
RUNPATH:写在elf文件中
ldconfig的缓存:配置/etc/ld.conf*可改变
默认的/lib, /usr/lib

所以最好使用RPATH,这样就不用依赖LD_LIBRARY_PATH了。通过链接时使用–enable-new-dtags可以固定生成RUNPATH,使用–disable-new-dtags可以固定生成RPATH。

3.3.3 make install下的CMake

CMake为了方便用户的安装,默认在make install之后会自动remove删除掉相关的RPATH,这个时候你再去查看exe的RPATH,已经发现没有这个字段了。 因此,当每次make install之后,我们进入到安装路径下执行相关exe的时候,就会发现此时的exe已经找不到相关的库路径了,因为它的RPATH已经被CMake给去除了。

3.3.4 让CMake能够在install的过程中写入相关RPATH

CMake在安装的过程会有一个和configure一样的安装路径,CMAKE_INSTALL_PREFIX(configure下是–prefix,当然也可以用shell下的全局变量DESTDIR);这个时候它会把你的安装文件安装到你prefix下的相对路径下,因此当我们希望在make install的时候,比如当前的share_lib在lib目录下,我们希望安装之后的RPATH可以自动找到它,我们就可以这么写:

set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_PREFIX}/lib)

# 需要注意的是,这个变量是全局变量,意味着你所有的target的RPATH都会在install的时候被写成这个(包括myexe和不需要RPATH的share_lib)
set_target_properties(${PROJECT_NAME} PROPERTIES 
      INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib" 
      LINK_FLAGS "-Wl,--disable-new-dtags")
Tags: llvm
Share: X (Twitter) Facebook LinkedIn