本文主要从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
在一开始它工作得很好,但存在一些问题。
- 编译器标志:
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版本。 -
链接器的flag。
llvm-config --ldflags
告诉你在哪里查找库并调整一些其他链接器设置。llvm-config --libs <components>
打印出你需要链接的库集合以使用指定的组件(你可以通过llvm-config --components
查看所有组件列表)。然而,如果你的系统上安装了多个版本的 LLVM,并且他们带有动态库的时候,你可能在成功链接后遇到运行时错误。 - 链接顺序。
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_DIRS
和LLVM_LIBRARY_DIRS
。
find_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")