This page looks best with JavaScript enabled

Windows下优雅使用LLVMPass

 ·  ☕ 6 min read · 👀... views

LLVM16-有bug不建议折腾,究极复杂;16及以上的版本可以玩

本文第一次公布的时候是LLVM12版本,所以最初也是以这个版本的配置来写的,当时因为LLVM在windows上有诸多bug,所以做了很多在高版本下不必要的工作。后续随着版本迭代也做过一些修改,但很多不必要的工作没有删去。大家在高版本下玩的时候可以省去很多步骤

准备工作

如果想要在Windows下编译LLVM Pass需要Build好的LLVM完整项目bin与lib,和build后的include,同时需要源码的include目录。所以前面部分还是要按照
LLVM 编译与First Pass
编译得到LLVM。这里大家切记cmake要开启 LLVM_ENABLE_PLUGINS 选项,之前我没开这选项怎么都编译不上(甚至逆了两天opt 像傻逼一样

opt加载插件

LLVM已经逐步开始普及New Pass了,并在17+逐步淘汰Legacy Pass,故准备集成New Pass的Plugin。

这里暂时不对NewPass的加载流程以及与LegacyPass的区别做赘述,有兴趣的同学可以自行根据这些资料学习:

  1. LLVM New pass
  2. LLVM NewPassManager
  3. LLVM PassManager对C++程序设计的思考
  4. llvm NewPassManager API分析及适配方案
  5. llvm PassManager的变更及动态注册Pass的加载过程

仍先写一个HelloWorld Pass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

#include <iostream>

namespace llvm{

    struct NewPassHelloWorld : public PassInfoMixin<NewPassHelloWorld> {

        PreservedAnalyses run(Module &F, ModuleAnalysisManager &AM) {
            std::cout << "NewPassHelloWorld Loaded" << std::endl;
            errs() << "MyPass:";
            errs() << F.getName() << "\n";
            return PreservedAnalyses::all();
        }
        bool isRequird(){ return true; }

    };

}
// This part is the new way of registering your pass
extern "C" ::llvm::PassPluginLibraryInfo
    LLVM_ATTRIBUTE_WEAK llvmGetPassPluginInfo() {
  return {
    LLVM_PLUGIN_API_VERSION, "NewPassHelloWorld", "v0.1",
    [](PassBuilder &PB) {
      PB.registerPipelineParsingCallback(   // 该回调注册的pass会在opt加载pass后调用
        [](StringRef PassName, ModulePassManager &FPM, ...) {
          if(PassName == "NewPassHelloWorld"){
            FPM.addPass(NewPassHelloWorld());
            return true;
          }
          return false;
        }
      );
    }
  };
}

在clang中,PassPlugin.cpp中的PassPlugin::Load函数会遍历 “-Xclang -fpass-plugin” 传入的pass模块,我们必须要导出我们pass插件模块llvmGetPassPluginInfo函数以便clang去调用从而加载插件中的pass。但是我们不可以使用 __declspec(dllexport) 导出函数,会报链接类型错误。
export_llvmGetPassPluginInfo

要导出这个函数,只能通过.def模块定义文件指定导出该函数。新建一个 export.def 文件

LIBRARY QVMProtect
EXPORTS
llvmGetPassPluginInfo

接下来,设计CMakeLists.txt,三个路径大家自行替换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
cmake_minimum_required(VERSION 3.4)

project(QVMProtect)

# 设置编译模式
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD")           #/MD
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd")              #/MDd

# 添加源码目录
aux_source_directory(./  src)
set(srcs ${src})

# 生成动态链接库  同时指定def模块文件
add_library(QVMProtect SHARED ${srcs} export.def)


# 添加头文件,需要编译后的以及源码头文件
# 不想写绝对路径可以配置环境变量LLVM_DIR并使用 find_package(LLVM REQUIRED CONFIG)
include_directories("D:/OLLVM/OLLVM-build-14/Debug/include")              # build后的include
include_directories("D:/OLLVM/ollvm-project-14.x/llvm/include")           # 源码的include

# 添加编译后的lib
set(mylibdir "D:/OLLVM/OLLVM-build-14/Debug/lib")

set(VTKLIBS LLVMCore LLVMSupport LLVMBinaryFormat LLVMRemarks LLVMBitstreamReader)
foreach(libname ${VTKLIBS})
        SET(FOUND_LIB "FOUND_LIB-NOTFOUND")
        find_library(FOUND_LIB NAMES ${libname} HINTS ${mylibdir} NO_DEFAULT_PATH)
                IF (FOUND_LIB)
                        message("found lib: ${FOUND_LIB}")
                        LIST(APPEND mylibs ${FOUND_LIB})
                ELSE()
                        MESSAGE("not lib found: ${libname}")
                ENDIF ()
endforeach(libname)
#message(${mylibs})

#message(${CPPUNIT_LIBRARY})
target_link_libraries(QVMProtect PUBLIC ${mylibs})

然后建立一个工程文件夹,执行 cmake .. 即可生成.sln工程文件。用vs打开该工程,使用Debug x64模式即可编译得到Pass的.dll模块文件

然后,将.c文件编译成.bc文件,然后使用opt去加载

PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QVMProtect\Debug> clang++ -emit-llvm -c target.cpp -o target.bc
PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QVMProtect\Debug> opt --load-pass-plugin=./NewPassHelloWorld.dll --passes='NewPassHelloWorld' target.bc -o target.bc
NewPassHelloWorld add Pass
NewPassHelloWorld add Pass
NewPassHelloWorld Loaded
MyPass:target.bc
PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QVMProtect\Debug> clang .\target.bc -o target.exe
PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QVMProtect\Debug> .\target.exe
hello world

opt加载插件加载成功。

clang加载插件

但是我们知道,有些场景下就不可能允许你使用opt加载插件中转(如使用vs编译程序甚至编译驱动。所以需要clang直接加载opt插件。这里感谢 @Chord 告诉我可以使用clang -Xclang -fpass-plugin="<pass path>“进行加载。我们只需要在插件的导出函数 llvmGetPassPluginInfo 中注册clang pass的 registerPipelineStartEPCallback 就可以了,相当方便。

1
2
3
PS C:\\Users\\Qfrost\\Desktop> clang++ -Xclang -fpass-plugin="./QVMProtect.dll" test.cpp -o test.exe
NewPassHelloWorld Loaded
MyPass:test.cpp

但是使用这一一种方法有一个缺陷就是无法使用-mllvm进行传参。也就是-mllvm后面带的参数没有办法传递到pass,原因可能是加载时机的问题。参数不能传递就使控制哪些Pass加载哪些不加载变得十分复杂,每次都得重新编译模块文件。

还有一个问题需要注意,在LLVM16-的版本,这个方法在windows下有bug,插件模块加载不了,这也是我之前说非常不建议在LLVM16以下的版本的Windows上折腾的原因

魔改Clang代码自动加载Pass DLL

用新版LLVM其实就不需要这个方法了,当时记录这个方法主要是因为旧版LLVM无法加载clang插件,而每次重新编译LLVM又太慢了,故采用了这样一个这种的办法。

核心思想就是,修改clang代码,在clang编译程序生成IR后链接前,插入一段代码,加载一个固定的.dll文件,并调用该dll的一个固定的导出函数,加载到PassManager中。故可以剥离出.dll的设计与编译过程,实现出固定的接口,这样每次只需要将所有的Pass编译入该dll中并替换即可。

简单说下步骤

  1. 对前面的dll添加一个固定导出接口函数
1
2
3
4
5
extern "C" __declspec(dllexport) void __stdcall clangAddPass(
    ModulePassManager &MPM) {
  // 将Pass添加到PassManager
  MPM.addPass(NewPassHelloWorld());
}
  1. 修改 clang/lib/CodeGen/BackendUtil.cpp 在EmitAssemblyHelper::EmitAssemblyWithNewPassManager函数内的找到 if (!CodeGenOpts.DisableLLVMPasses) 在判断体末尾加上下方代码使其固定加载你的.dll Plugin的clangAddPass导出函数,同时在该文件头部 #include<windows.h>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#if _WINDOWS
    using myPassType = void(__stdcall *)(ModulePassManager & MPM);
    auto hModule = ::LoadLibraryA("QVMProtect.dll");
    if (!hModule) {
    
      errs() << "[QVMProtect] : QVMProtect.dll not found\n";
    } else {
    
      auto pfn =
          (myPassType)::GetProcAddress(hModule, "clangAddPass");
      if (pfn != NULL) {
    
        errs() << "[QVMProtect] : QVMProtect.dll Load Successfully\n";
        pfn(MPM);

      } else {
    
        errs() << "[QVMProtect] : clangAddPass not found\n";
      }
    }
#endif // _WINDOWS

使用MSBuild增量编译,并将生成好的.dll Plugin文件放置到Build后的bin文件夹下。使用clang编译程序,可以看到Pass加载成功

PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QVMProtect\Debug> clang .\target.cpp -o target.exe
[QVMProtect] : QVMProtect.dll Load Successfully
NewPassHelloWorld Loaded
MyPass:.\target.cpp

编译驱动

希望用clang来编译Windows驱动并加载混淆Pass模块进行混淆。用LLVM编译驱动最麻烦的地方在于需要指定编译器为clang但是链接器是MSVC(链接器不用MSVC也行但巨麻烦)。之前有参考看雪论坛厂子哥大表哥的方法,但配出来有点问题。

gmh大佬为了让LLVM兼容MSVC的一些特性,开发了 llvm-msvc ,这个项目实际上就是魔改了LLVM使其支持一些参数并为Visual Studio配置工具集使其编译时走它的clang和lld。因为现在新版LLVM已经可以直接加载clang pass模块了,所以其实都可以不需要自己去编译clang(如果不考虑开发的话

  1. 直接安装 llvm-msvc 项目,(如果向换用自己的clang可以修改注册表 HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\LLVM\LLVM-MSVC 指定 llvm-msvc 使用的clang为我们自己的clang)

  2. 然后随便打开一个项目 项目->属性->常规->平台工具集 设置为 LLVM-MSVC_v14X_KernelMode
    llvm-msvc-kernelmode

  3. 项目属性的命令行中添加加载自己的混淆Pass DLL

编译,即可看到成功输出了Pass加载的提示并生成出驱动文件
llvm-msvc-kernelmode-result

参考资料

  1. LLVM IR 的第二个 Pass:上手官方文档 New Pass Manager HelloWorld Pass

  2. can’t build HelloWorld on windows (llvm 12.0.1)

  3. 模块定义 (.Def) 文件

  4. LLVM 13.1 new Pass插件形式 [for win]

Share on

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