This page looks best with JavaScript enabled

LLVM 编译与First Pass

 ·  ☕ 7 min read · 👀... views

要开始正儿八经认真学LLVM了。先吐槽一下,这东西是真的麻烦,编译起来一堆坑,项目还贼大,一编译就是十几分钟至一个小时。还吃电脑各种环境,各种小版本,前前后后至少编译了好几天才整出一个跨平台、可集成多种工具链的成品。下面做个记录。

编译

编译这一步应该是最麻烦的,编译时间长,小版本多,github各种仓库,鱼龙混杂。这里有个很大的坑,就是LLVM的版本。如果就是随便用用,那版本无所谓,但是如果要集成到工具链里去,那就必须要新版的LLVM。比如想要集成到VS2019的v142生成工具上,就要求LLVM的版本大于等于11,不然就用不了C++语法。

这边是之前找的别人移植好的高版本OLLVM仓库:

  1. https://github.com/0x3f97/ollvm-12.x
  2. https://gitee.com/qingyu_yyy/ollvm-project/tree/14.x

然后就是编译,这里我觉得必须要分情况讨论,Windows下的编译实在是比Linux下编译复杂太多了

Linux

Linux下的编译比较简单,我用的是Ubuntu18的WSL,要手动升级一下cmake(项目要求cmake版本大于3.13)

1
cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86;Mips;ARM;BPF" -DCMAKE_BUILD_TYPE=Debug -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_RTTI=ON ../llvm_project/llvm

然后说几个坑点:

  1. 要加上 -DLLVM_ENABLE_PROJECTS 指定编译的项目,如果不加上这个参数,只会编译基本的llvm项目内容,也就是不含clang的,那是不能用的
  2. 对于 -DCMAKE_BUILD_TYPE,如果是想编译一次然后用的那就用Release,但是如果是要自己调试学习的,一定要编译成Debug类型,不然没法调试。(PS:Debug模式编译更吃电脑,10代i7 16G内存开9线程编译了四十分钟,到最后单进程内存峰值都要到10G,8G内存的电脑估计吃不消编译;生成的文件也达五十几个G,注意留好硬盘空间
  3. 一些情况下可能会报一个错 config.guess: 71: Syntax error: word unexpected (expecting “in”) 查了一圈资料发现是因为行尾的问题。我是在Windows下拉取仓库进WSL编译Linux的Bin时遇到的,在Linux编译时对行尾的识别错误导致失败。做法是要么在Linux(WSL)下拉取仓库,或者用dos2unix转化一下行尾

Windows

(不建议在Windows下折腾LLVM 本节完(×

Windows下编译我遇到过诸多问题,这里就不细说踩坑过程了,记录一下一些坑点吧:

  1. 直接用官方的命令生成.sln项目并直接用VS图形化编译工具编译,电脑会直接卡爆,反正我16G内存到link阶段直接BSOD
  2. 使用 -G “MinGW Makefiles” 参数并用MinGW工具链编译可能会报错 Using std::regex with exceptions disabled is not fully supported,这是由于默认的MinGW的GCC版本是8.1.0(截止至我写此博客时),这个版本太低了。
  3. 使用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控制并发数避免卡死。

1
2
cmake -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_PLUGINS=On -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS_DEBUG="/Ox" -DCMAKE_CXX_FLAGS_DEBUG="/Ox" -DLLVM_INCLUDE_TESTS=On -DLLVM_ENABLE_RTTI=ON -DCMAKE_INSTALL_PREFIX="../Windows" ../
# MinSizeRel模式编译不会生成.ink文件(大概有五十几个G)

这里对几个我踩过坑的选项做一些解释:

  1. 插件加载:在Windows下,为了加载插件(即导出插件的符号),在编译时需要开一个选项,LLVM12即之前的版本应该是LLVM_EXPORT_SYMOBLS_FOR_PLUGINS,后续版本中应为LLVM_ENABLE_PLUGINS
  2. libc编译:在新版LLVM中已经没有Project:libcxx和libcxxabi了,如果要编译运行库需要指定 -DLLVM_ENABLE_RUNTIMES=“libcxx;libcxxabi;libunwind” 参数。
  3. 架构指定:有的版本的LLVM需要手动指定架构,即,要加上 -DCMAKE_CXX_FLAGS="-DENDIAN_LITTLE"
  4. BUILD_TYPE:这个就很迷了,如果要完整符号并且可以挂调试器调试这种,是要编译Debug的,同时Debug版本下,当clang或者加载的插件发生任何崩溃时会告诉你崩溃的位置和栈回溯。然后我发现,其他的好像都差不多,发生崩溃的时候就一句话 clang-cl : error : clang frontend command failed due to signal (use -v to see invocation),包括RelWithDebInfo发生崩溃后也是不会有任何报错提示的。 总结一下,要调试、或者说有做开发需求的,就用Debug编译,否则根本不知道错在哪里,如果单纯使用,就用MinSizeRel
  5. 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

1
2
3
4
5
MSBuild LLVM.sln -p:configuration=Debug -p:Platform="x64" -m:1
MSBuild LLVM.sln -p:configuration=MinSizeRel -p:Platform="x64" -m:1

MSBuild install.vcxproj -p:configuration=Debug -p:Platform="x64" -m 
MSBuild install.vcxproj -p:configuration=MinSizeRel -p:Platform="x64" -m 

接下来,电脑完全卡死,本章结束(×

执行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。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// MyPass
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace {
  struct MyPass : public FunctionPass {
    static char ID;
    MyPass() : FunctionPass(ID) {}

    bool runOnFunction(Function &F) override {
      errs() << "MyPass:";
      errs().write_escaped(F.getName()) << '\n';
      return false;
    }
  };
}

char MyPass::ID = 0;
static RegisterPass<MyPass> X("myPass", "Hello MyPass", false, false);

然后就需要编译这个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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// test.cpp
#include <stdio.h>
#include <iostream>
void hello() {
    printf("Hello");
}
void world() {
    printf("World");
}
int main(int argc, char* argv[]) {
  std::cout << "Main" << std::endl;
  hello();
  world();
  return 0;
}

将其编译成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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
root@Qfrost:/mnt/c/Users/Qfrost/Desktop/code/LLVM/ollvm-12.x-main/llvm/lib/Transforms/MyPass# opt -load ./MyPass.so -mypass test.bc
WARNING: You're attempting to print out a bitcode file.
This is inadvisable as it may cause display problems. If
you REALLY want to taste LLVM bitcode first-hand, you
can force output with the `-f' option.

MyPass:__cxx_global_var_init
MyPass:_Z5hellov
MyPass:_Z5worldv
MyPass:main
MyPass:_GLOBAL__sub_I_test.cpp

参考资料

  1. https://www.leadroyal.cn/p/647/

  2. https://zhuanlan.zhihu.com/p/68113486

  3. https://www.jianshu.com/p/9f450969121b

  4. https://zhuanlan.zhihu.com/p/122522485

  5. https://llvm.org/docs/WritingAnLLVMPass.html

  6. https://blog.csdn.net/qq_41923691/article/details/123258565

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer