要开始正儿八经认真学LLVM了。先吐槽一下,这东西是真的麻烦,编译起来一堆坑,项目还贼大,一编译就是十几分钟至一个小时。还吃电脑各种环境,各种小版本,前前后后至少编译了好几天才整出一个跨平台、可集成多种工具链的成品。下面做个记录。
编译
编译这一步应该是最麻烦的,编译时间长,小版本多,github各种仓库,鱼龙混杂。这里有个很大的坑,就是LLVM的版本。如果就是随便用用,那版本无所谓,但是如果要集成到工具链里去,那就必须要新版的LLVM。比如想要集成到VS2019的v142生成工具上,就要求LLVM的版本大于等于11,不然就用不了C++语法。
这边是之前找的别人移植好的高版本OLLVM仓库:
然后就是编译,这里我觉得必须要分情况讨论,Windows下的编译实在是比Linux下编译复杂太多了
Linux
Linux下的编译比较简单,我用的是Ubuntu18的WSL,要手动升级一下cmake(项目要求cmake版本大于3.13)
|
|
然后说几个坑点:
- 要加上 -DLLVM_ENABLE_PROJECTS 指定编译的项目,如果不加上这个参数,只会编译基本的llvm项目内容,也就是不含clang的,那是不能用的
- 对于 -DCMAKE_BUILD_TYPE,如果是想编译一次然后用的那就用Release,但是如果是要自己调试学习的,一定要编译成Debug类型,不然没法调试。(PS:Debug模式编译更吃电脑,10代i7 16G内存开9线程编译了四十分钟,到最后单进程内存峰值都要到10G,8G内存的电脑估计吃不消编译;生成的文件也达五十几个G,注意留好硬盘空间
- 一些情况下可能会报一个错 config.guess: 71: Syntax error: word unexpected (expecting “in”) 查了一圈资料发现是因为行尾的问题。我是在Windows下拉取仓库进WSL编译Linux的Bin时遇到的,在Linux编译时对行尾的识别错误导致失败。做法是要么在Linux(WSL)下拉取仓库,或者用dos2unix转化一下行尾
Windows
(不建议在Windows下折腾LLVM 本节完(×
Windows下编译我遇到过诸多问题,这里就不细说踩坑过程了,记录一下一些坑点吧:
- 直接用官方的命令生成.sln项目并直接用VS图形化编译工具编译,电脑会直接卡爆,反正我16G内存到link阶段直接BSOD
- 使用 -G “MinGW Makefiles” 参数并用MinGW工具链编译可能会报错 Using std::regex with exceptions disabled is not fully supported,这是由于默认的MinGW的GCC版本是8.1.0(截止至我写此博客时),这个版本太低了。
- 使用MinGW配置文件并下载release的Clang作为编译器,-G “MinGW Makefiles” -D CMAKE_C_COMPILER=clang -D CMAKE_CXX_COMPILER=clang++,可能会报错 Unknown architecture host,这是由于Windows系统的SDK通常是MSVC的,而clang不支持MSVC中的部分语法,比如这个错误就是因为MSVC中使用了typeid而clang不支持引起的
最终我的方法还是使用MSVC工具链来编译。但是需要先设置一下注册表的Windows最大路径长度,因为LLVM会输出一些神奇临时文件最大长度会大于Windows的最大路径长度(260字节)。这个可以在注册表的 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem 项中的LongPathEnabled键上,将0设置为1即可。我选择使用MSVC的命令行进行编译并通过-m控制并发数避免卡死。
|
|
这里对几个我踩过坑的选项做一些解释:
- 插件加载:在Windows下,为了加载插件(即导出插件的符号),在编译时需要开一个选项,LLVM12即之前的版本应该是LLVM_EXPORT_SYMOBLS_FOR_PLUGINS,后续版本中应为LLVM_ENABLE_PLUGINS
- libc编译:在新版LLVM中已经没有Project:libcxx和libcxxabi了,如果要编译运行库需要指定 -DLLVM_ENABLE_RUNTIMES=“libcxx;libcxxabi;libunwind” 参数。
- 架构指定:有的版本的LLVM需要手动指定架构,即,要加上 -DCMAKE_CXX_FLAGS="-DENDIAN_LITTLE"
- BUILD_TYPE:这个就很迷了,如果要完整符号并且可以挂调试器调试这种,是要编译Debug的,同时Debug版本下,当clang或者加载的插件发生任何崩溃时会告诉你崩溃的位置和栈回溯。然后我发现,其他的好像都差不多,发生崩溃的时候就一句话 clang-cl : error : clang frontend command failed due to signal (use -v to see invocation),包括RelWithDebInfo发生崩溃后也是不会有任何报错提示的。 总结一下,要调试、或者说有做开发需求的,就用Debug编译,否则根本不知道错在哪里,如果单纯使用,就用MinSizeRel
- Install:很多情况下,在make完之后会使用make install安装LLVM(将生成的bin和lib复制到安装目录,并配置一些环境变量),故可以使用CMAKE_INSTALL_PREFIX选项指定安装位置
然后其实Windows下编译LLVM的话可以不用编译lld,只编译clang,lld使用MSVC的,这样相当于编译器使用clang连接器使用MSVC,就可以使用clang编译Windows驱动了(Windows sys文件的编译链接器必须要MSVC,或者说不用MSVC的链接器会究极麻烦)
生成好配置文件后,使用VS x64 Native Tools 或 x86_64 Cross Tools工具,用MSBuild命令行编译LLVM
|
|
接下来,电脑完全卡死,本章结束(×
执行Install后大概2G不到空间,然后就可以八十几个G的生成项目删掉了,手动配置Install的目录到LLVM_DIR环境变量里,以后就可以在CMAKEList里用find_package找到LLVM了(这对编译插件很重要)
编写第一个Pass
Linux编译Pass会比较简单,并且官方支持动态加载Pass模块,所以后续若不做特殊说明都是在Linux环境下调试LLVM Pass so模块文件。
首先先将WSL环境变量配上。
vim ~/.bashrc
export LLVM_HOME=/mnt/c/Users/Qfrost/Desktop/code/LLVM/build // 这个是生成的build目录
export PATH=${LLVM_HOME}/bin:$PATH
source ~/.bashrc
然后可以在本机上直接用vscode打开LLVM源码根目录快乐写代码。LLVM Pass的位置是 llvm/lib/Transforms,在这个目录下,可以看到已经有很多Pass了,比如HelloPass和HelloNewPass,这两个Pass的作用仅仅就是对每个函数输出 Hello:%FunctionName%,区别是HelloNewPass会生成独立的模块而HelloPass不会。我们可以仿照他们写一个具有独立模块的Pass。
|
|
然后就需要编译这个Pass,一般有两种编译方法
独立编译成Module
如果要独立编译一个Pass形成so文件,最简单暴力的方法是
clang `llvm-config --cxxflags` `llvm-config --ldflags` -I /LLVM_CODE/llvm/include -I /LLVM_HOME/include -shared -fPIC MyPass.cpp -o MyPass.so
而其中LLVM_CODE是LLVM源码根目录,LLVM_HOME是你的LLVM生成根目录。这里就很离谱,用到的头文件既有源码里的也有生成文件里的。但确实是最简单暴力方便的。
还有就是写一个CMakeLists.txt放在同目录下
cmake_minimum_required(VERSION 3.4)
if(NOT DEFINED ENV{LLVM_HOME})
message(FATAL_ERROR "$LLVM_HOME is not defined")
endif()
if(NOT DEFINED ENV{LLVM_DIR})
set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib/cmake/llvm)
endif()
find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_library(LLVMMyPass MODULE
# List your source files here.
MyPass.cpp
)
然后cmake && make 同样可以在当前目录下生成LLVMMyPass.so文件
源码目录编译
在Pass目录下写一个CMakeLists.txt
add_library(LLVMMyPass MODULE
# List your source files here.
MyPass.cpp
)
然后修改上层CMakeLists.txt,也就是Transforms目录下的CMakeLists.txt,为其添加一行,将自己写的Pass目录添加进去
add_subdirectory(MyPass)
然后就重复第一步的对整个LLVM项目进行build。但这样比完全重新编译还是会快不少的,因为编译过的文件不会重复编译。生成的so在LLVM_HOME/lib下
使用模块Pass
先写一个Hello World
|
|
将其编译成bc文件
- clang -emit-llvm -c test.cpp
这样就有了一个测试代码的bc文件(test.bc)和我们MyPass的so文件(MyPass.so)
然后,用opt加载这个so
- opt -load ./MyPass.so -mypass test.bc
或者 - clang -Xclang -load -Xclang MyPass.so -w test.cpp -o test.bin
|
|