1. 前言
Evently,我来到了这一步,which I can 决定使用 what’s like 环境。Before 由于项目需要学习了如何利用 LLVM Pass 来处理源码进行混淆操作,但对于环境的配置却不知道如何进行。那么这篇 Blog 就是对于 如何在 Windows 上搭建 LLVM 环境的探索。
之前我看了前人的智慧 如何优雅的在 Windows 上使用 LLVM Pass 插件进行代码混淆,就承接了这套环境进行了代码设计。但里面有些内容让我感觉在 Windows 上感觉很不方便,所以 want 探索新的环境搭建方式,时隔 1 年我终于开始了。使用 Pass 时遇到重大困难,还好有 Windows下优雅使用LLVMPass 这位前人的智慧。
这种不方便是由于使用的是 GCC 标准,而非 MSVC,使得你编译出来的代码找不到动态链接库,需要自带 MinGW 环境 or 使用静态链接。不同点在于:
- 符号名称粉碎方式不同。GCC 和 MSVC 有着各自的链接符号标准。
- 动态链接库不同。MSVC 更适配 WIndows 平台。
2. Problem
首先我们要明确问题是什么,这样才能有的放矢。具体的我们 hope 有这样一个 clang which can 使用命令加载我们编译好的 LLVM Pass on Windows,然而却没有预编译好的版本,即都是不能加载 Pass clang,所以我们要自己进行编译。
一个关键字段 LLVM_BUILD_LLVM_DYLIB:BOOL 需要是 ON 这样才能够加载 LLVM Pass。
看到这里你的心是不是已经凉了一半了,官方文档定性了 not available on Windows。但是我们需要明白的是,这里的 not available 指的是 MSVC 编译器。
这个产生的原因好像是 MSVC 无法处理巨量的导出符号。In LLVM 的论坛中 How to create pass independently on Windows? 中有相关描述和解决方案 which I try 了 and 失败了。
那么来看看 山重水尽疑无路,柳暗花明又一村 是什么样的。 Clang: a C language family frontend for LLVM 中有这样的描述:clang 与 GCC 和 MSVC 都有兼容的。
Depend on clang 的兼容特点,可以设计一个这样的方案:使用 pre-build 的 clang which 不能加载 Pass on Windows 去编译 LLVM 源码,将 LLVM_BUILD_LLVM_DYLIB 设置为 ON 这样就能够得到我们期望的 eastwest 了。
3. clang
这里我们主要关注的与 VS 配套的 clang 版本 。
Visual Studio 2022 | clang 18.1.8 |
---|---|
Visual Studio 2019 | clang 12.0.0 |
这些是可以通过 Visual Studio Install 进行查看的:


3.1 clang18
这有一个 good message that 你可以直接下载能够加载 Pass 版本的 clang18 on LLVM 18.1.8。
下载上图中划线的压缩包,解压后的 clang 是能够加载 Pass 的,所以如果你重新开始编写项目,建议直接从 clang18 的标准来写代码。
LLVM x86_64-pc-windows-msvc 帖子中有关于这方面的讨论。
3.2 clang12
由于之前我的项目使用 llvm 12.0.0 编写的,相关代码在 clang18 下无法成功通过编译,所以不得不进行新的探索。但是有一点可以确定的是,探索一定会成功,因为 clang18 已经有了相应的 pre-build 的 eastwest 了。
这里先来句 grass your mum,在 LLVM 12.0.0 的下载页面没有期待的 eastwest,这 your horse 给 Linux 配套了一吨也不给 Windows 留一个。
4. 步骤
4.1 下载
- 你需要下载与 VS 版本对应的 clang,这样才能够利用 MSVC 的编译环境来进行编译。
- 下载 Ninja。
- 下载 cmake。一般如果你下载了 VS 的话可以在 Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin 文件夹下找到 cmake。
- 下载 LLVM 12 源码。
4.2 cmake
下面是 cmake 的一些参数选项,这里主要 say 一下 -G 选项,which 是用来控制生成器的。由于 VS 的 cl 编译器,所以不能选择 Visual Studio 这套系统,比较好用的其它方案就是使用 Ninja 较为小巧。
Options
-S <path-to-source> = Explicitly specify a source directory.
-B <path-to-build> = Explicitly specify a build directory.
-G <generator-name> = Specify a build system generator.
Generators
The following generators are available on this platform (* marks default):
* Visual Studio 17 2022 = Generates Visual Studio 2022 project files.
Use -A option to specify architecture.
Visual Studio 16 2019 = Generates Visual Studio 2019 project files.
Use -A option to specify architecture.
Ninja = Generates build.ninja files.
- 解压 LLVM 12.0.0 的源码,切换到其目录下创建 build 文件夹。然后创建如下 .bat 文件, 你需要自行设置其中的 CC、CXX 和 INSTALL 的值。然后运行得到相关内容。
这里需要注意对于 clang 版本的选择,要与 3.clang 中描述的对应,即如果是 VS 2022 则要下载 clang18.1.8,而 VS 2019 则要下载 clang12.0.0。这里建议 clang12 的源码用 clang12 编译,不要使用 clang18,会导致后面编译 Pass 时出现链接错误。
set CC=E:/Tools/tmp/bin/clang.exe
set CXX=E:/Tools/tmp/bin/clang++.exe
set INSTALL=E:\Tools\clang12
cmake -G "Ninja" -DLLVM_HOST_TRIPLE=x86_64-pc-windows-msvc -DCMAKE_C_COMPILER="%CC%" -DCMAKE_CXX_COMPILER="%CXX%" -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_RTTI=ON -DCMAKE_INSTALL_PREFIX="%INSTALL%" -DLLVM_ENABLE_PROJECTS="clang;lld" -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_TERMINFO=OFF -DLLVM_BUILD_LLVM_DYLIB=ON -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_WERROR=OFF -DCMAKE_CXX_FLAGS="-Wno-error -Wno-language-extension-token -Wno-deprecated-declarations -Wno-microsoft-enum-value -Wno-unused-parameter -Wno-deprecated-copy -Wno-overlength-strings -Wno-invalid-offsetof" ../llvm
下面是对上述长 cmake 命令的逐个解释:
cmake -G "Ninja" ::generate
-DLLVM_HOST_TRIPLE=x86_64-pc-windows-msvc ::target platform
-DCMAKE_C_COMPILER="%CC%" ::C compiler path
-DCMAKE_CXX_COMPILER="%CXX%" ::C++ compiler path
-DCMAKE_BUILD_TYPE=Release
-DLLVM_ENABLE_RTTI=ON ::open Runtime Type
-DCMAKE_INSTALL_PREFIX="%INSTALL%" ::install position
-DLLVM_ENABLE_PROJECTS="clang;lld"
-DLLVM_TARGETS_TO_BUILD="X86"
-DLLVM_ENABLE_TERMINFO=OFF
-DLLVM_BUILD_LLVM_DYLIB=ON ::important, it must be on
-DLLVM_INCLUDE_TESTS=OFF
-DLLVM_ENABLE_WERROR=OFF
-DCMAKE_CXX_FLAGS="-Wno-error -Wno-language-extension-token -Wno-deprecated-declarations -Wno-microsoft-enum-value -Wno-unused-parameter -Wno-deprecated-copy -Wno-overlength-strings -Wno-invalid-offsetof"
../llvm ::path
你需要在 VS 控制台中运行上面的 .bat 脚本,因为需要有相应的 VS 编译环境。
- 运行完成之后会生成一个 build.ninja 文件需要对这个文件进行一点小小的修改,不然会后续进行链接时出现问题。
使用这样的关键字进行搜索 .def -fuse-ld=lld-link,在其行中添加 -Xlinker /DEF:,一般不会太多个,我的里面只有 9 处需要手动添加的。
下图就是不进行上述修改产生的报错信息:
- 运行下面命令进行编译,-j 后面的参数是线程数量,根据你 CPU 的性能进行调整。
cmake --build . --target install -j 16
经过上面的过程之后你就可以得到一份可以加载 Pass 的 clang 了。
4.3 编译 Pass
创建一个如下的 CMakeLists.txt 文件在你想要编译 Pass 的位置。特别我我要指出 -frtti 选项,之前是 -fno-rtti 在 MinGW 环境中,但切换到 MSVC 中必须设置。然后就是add_library中添加 Pass 源码所在文件。
- CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(design)
# Enable C++17 (required for LLVM 14+)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT DEFINED ENV{LLVM_HOME})
# User must define the LLVM_HOME environment that point to the root installation dir of llvm
message(FATAL_ERROR "Environment variable $LLVM_HOME is not defined!")
endif()
message(STATUS "LLVM_HOME = [$ENV{LLVM_HOME}]")
if(NOT DEFINED ENV{LLVM_DIR})
# Default llvm config file path
set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib/cmake/llvm)
endif()
# Check the path
if (NOT EXISTS $ENV{LLVM_DIR})
message(STATUS "Path ($ENV{LLVM_DIR}) not found!")
# If default llvm config path not found, try this one,
# which is config with [-DLLVM_LIBDIR_SUFFIX=64] before building llvm
set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib64/cmake/llvm)
if (NOT EXISTS $ENV{LLVM_DIR})
message(FATAL_ERROR "Path ($ENV{LLVM_DIR}) not found!")
else()
message(STATUS "Path ($ENV{LLVM_DIR}) found!")
endif()
else()
message(STATUS "Path ($ENV{LLVM_DIR}) found!")
endif()
find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_library(design SHARED design.cpp)
include_directories(./)
# LLVM is (typically) built with no C++ RTTI. We need to match that;
# otherwise, we'll get linker errors about missing RTTI data.
set_target_properties(design PROPERTIES COMPILE_FLAGS "-frtti")
# Get proper shared-library behavior (where symbols are not necessarily
# resolved when the shared library is linked) on OS X.
if(APPLE)
set_target_properties(design PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
endif(APPLE)
target_link_libraries(design
PRIVATE LLVMCore LLVMSupport
)
接着创建一个 cmake_build.bat 文件,在 VS 控制台中运行该脚本即可完成 Pass 编译。
我的是 Visual Studio 2022,但 clang12 的配套环境是 2019,但 2022 是可以使用 2019 的工具链的,去 install 中下载相应的工具链。
下载完成后 Microsoft Visual Studio\2022\Community\VC\Tools\MSVC 中就会多出现一个文件夹记住 2019 的版本号。
为了配置相应的 MSVC 环境,可以打开一个 x86_64 的控制台,然后输入 echo %path%
。
将 echo 输出的结果全部复制出来,填入 cmake_build.bat 的 path。同时将其中的 14.42.34433 替换为 14.29.30133 这样就配置好了 VS 2019 的环境变量而不用去单独下载 2019 了。
- cmake_build.bat
set PATH=you copy in x86_64
set LLVM_HOME=E:\Tools\clang12
set CC=E:\Tools\clang12\bin\clang.exe
set CXX=E:\Tools\clang12\bin\clang++.exe
:: Remove build dir
rd /Q /S .\build
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_RTTI=ON -S ./ -B ./build
cmake --build ./build --config Release
pause
双击 cmake_build.bat 即可完成对于 Pass 的编译。
4.4 使用 Pass
下面是一个失败的例子,我使用这样的命令之后没有得出想要的结果。探究其原因可能是使用的 LegacyPass 不能被在 Windows 很好的支持相关吧。不 want 进行深入探索了。
clang -emit-llvm test.cpp -o test.bc
opt --load-pass-plugin="me.dll" -passes=hello test.bc -o test.bc
Failed to load passes from 'me.dll'. Request ignored.
opt.exe: unknown pass name 'design'
#include <llvm/Pass.h>
#include <llvm/IR/Function.h>
#include <llvm/Support/raw_ostream.h>
using namespace llvm;
namespace {
struct HelloPass : public FunctionPass {
static char ID;
HelloPass() : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {
outs() << "Hello, Function: " << F.getName() << "!\n";
return false; // No modification to the IR
}
};
}
char HelloPass::ID = 0;
static RegisterPass<HelloPass> X("hello", "Hello World Pass");
就在这时我找了这篇文章 Windows下优雅使用LLVMPass ,再次感谢这位大佬的分享。我需要使用 New Pass 去进行注册才能够被成功加载,那么新 Pass 的例子如下:
#include <llvm/Support/raw_ostream.h>
#include <llvm/IR/PassManager.h>
#include <llvm/Passes/PassPlugin.h>
#include <llvm/Passes/PassBuilder.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( //this callback register the function will be called after opt load the pass
[](StringRef PassName, ModulePassManager &FPM, ...) {
if(PassName == "NewPassHelloWorld"){
FPM.addPass(NewPassHelloWorld());
return true;
}
return false;
}
);
}
};
}
这里使用新的类模板 PassInfoMixin 进行注册。在注册函数 PassPluginLibraryInfo 中使用了 lambda 表达式构造了临时函数。
最后运行下面的指令,成功的看到了测试 Pass 被成功调用。至此 fishwheel 的环境搭建 on Windows 算是大功告成了。
opt --load-pass-plugin=NewPassHelloWorld.dll --passes=NewPassHelloWorld aes.bc -o aes_d.bc
NewPassHelloWorld Loaded
MyPass:aes.bc
5. Bugs
这里记录一下一些 Bug 的实际截图:
- clang18 编译的 clang12 编译 Pass