Windows下优雅使用LLVMPass

Posted by Qfrost on 2022-05-28
Estimated Reading Time 8 Minutes
Words 1.9k In Total
Viewed Times

不建议在Windows下折腾LLVM(完

可以说 究极复杂

准备工作

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

以下所有操作均基于 LLVM14.0.0 版本讨论

opt加载插件

吐血折腾之路,麻(

因为考虑到LLVM已经逐步开始普及New Pass了,并有逐步淘汰Legacy Pass的趋势,故准备集成New Pass的Plugin。仍先写一个HelloWorldPass

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
42
#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(
[](StringRef PassName, ModulePassManager &FPM, ...) {
if(PassName == "NewPassHelloWorld"){
FPM.addPass(NewPassHelloWorld());
return true;
}
return false;
}
);
}
};
}

然后这里要注意必须要导出llvmGetPassPluginInfo函数,但是我们不可以使用 __declspec(dllexport) 导出函数,会报链接类型错误。
export_llvmGetPassPluginInfo

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

1
2
3
LIBRARY QObfuscation
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
cmake_minimum_required(VERSION 3.4)

project(QObfuscation)

# 设置编译模式
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(QObfuscation SHARED ${srcs} export.def)


# 添加头文件,需要编译后的以及源码头文件
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(QObfuscation PUBLIC ${mylibs})

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

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

1
2
3
4
5
6
7
8
9
PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QObfuscation\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\QObfuscation\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\QObfuscation\Debug> clang .\target.bc -o target.exe
PS C:\Users\Qfrost\Desktop\code\LLVM\ollvm-12.x-main\llvm\lib\Transforms\MyPass\WindowsPass\QObfuscation\Debug> .\target.exe
hello world

opt加载插件加载成功。

但是我们知道,有些场景下就不可能允许你使用opt加载插件中转(如使用vs编译程序甚至编译驱动。所以需要clang直接加载opt插件。这里感谢 @Chord 告诉我可以使用clang -Xclang -fpass-plugin="<pass path>"进行加载。

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

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

clang加载插件

新版LLVM(LLVM16)可以直接加载clang插件了,正常的导出 llvmGetPassPluginInfo 注册clang pass的 registerPipelineStartEPCallback 就可以了,相当方便(终于修了这个问题 之前白折腾那么久了

1
clang++.exe -Xclang -fpass-plugin="./QObfuscation.dll" .\test.cpp -o test.exe

魔改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("QObfuscation.dll");
if (!hModule) {

errs() << "[QObfuscation] : QObfuscation.dll not found\n";
} else {

auto pfn =
(myPassType)::GetProcAddress(hModule, "clangAddPass");
if (pfn != NULL) {

errs() << "[QObfuscation] : QObfuscation.dll Load Successfully\n";
pfn(MPM);

} else {

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

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

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

编译驱动

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

后面在github上发现了一个神奇项目 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_v142_KernelMode
    llvm-msvc-kernelmode

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

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

参考资料

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

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

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




浙ICP备19044916号-1