1. LLVM 编译

LLVM 对 CMake版本有限制,可以在官网查看

  • Ubuntu 20.04

  • LLVM 12.0.1

  • CMake 3.16.3

第一步:下载LLVM-Core 和 Clang源码

在llvm-project 仓库的 Release界面,下载LLVM-Core和Clang源码

  • llvm-12.0.1.src.tar.xz

  • clang-12.0.1.src.tar.xz

在/home/llvm/Programs文件夹下创建llvm-project文件夹,将下载好的原代码解压缩到对应目录。创建build文件夹

/home/llvm/Programs
  /llvm-project # llvm 项目文件夹
    /llvm  # 解压缩后的llvm代码
    /clang # 解压缩后的clang代码
    /build # 存放编译后的LLVM

第二步:编译LLVM

在/home/llvm/Programs/llvm-project文件夹下创建build.sh文件。执行build.sh开始编译,编译需要一段时间,记得执行build.sh时以root权限执行!!!

  • cmake 参数解释:

    • -G “Unix Makefiles”:生成Unix下的Makefile

    • -DLLVM_ENABLE_PROJECTS=“clang”:除了 LLVM Core 外,还需要编译的子项目。

    • -DLLVM_BUILD_TYPE=Release:在cmake里,有四种编译模式:Debug, Release, RelWithDebInfo, 和MinSizeRel。使用 Release 模式编译会节省很多空间。

    • -DLLVM_TARGETS_TO_BUILD=“X86”:默认是ALL,选择X86可节约很多编译时间。

    • -DBUILD_SHARED_LIBS=On:指定动态链接 LLVM 的库,可以节省空间。

  • make install 指令:

    • 是将编译好的二进制文件和头文件等安装到本机的 /usr/local/bin 和 /usr/local/include 目录,方便后续使用。

cd build
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS="clang" \
-DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="x86" \
-DBUILD_SHARED_LIBS=On ../llvm
make
make install

2. CMake LLVM Pass 项目创建

在/home/llvm/Programs/llvm-project文件夹下创建一个项目文件夹,叫什么随意,这里用OLLVMPP。

整个项目目录结构如下:

  • OLLVMPP:OLLVM文件夹,项目根目录

    • Build:Build文件夹,存放编译后的LLVM Pass

    • Test:Test文件夹,存放编译生成的测试文件和需要优化的代码文件

      • Bin:Bin文件夹,编译生成的二进制文件

      • IR:IR文件夹,编译生成的IR文件

      • TestProgram.cpp:需要优化的代码文件

    • Transforms

      • include:include文件夹,存放LLVM Pass头文件

      • src:src文件夹,存放LLVM Pass源码

        • Demo.cpp:自己写的LLVM Pass代码

      • CMakeList.txt:整个 CMake 项目的配置文件

    • test.sh:编译LLVM Pass并对Test文件夹中的代码文件进行测试

CMakeLists.txt文件内容如下:

# 参考官方文档:https://llvm.org/docs/CMake.html#developing-llvm-passes-out-ofsource
project(OLLVMPP)
cmake_minimum_required(VERSION 3.13.4)
find_package(LLVM REQUIRED CONFIG)
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(AddLLVM)
include_directories("./include") # 包含 ./include 文件夹中的头文件
separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})
add_definitions(${LLVM_DEFINITIONS_LIST})
include_directories(${LLVM_INCLUDE_DIRS})
add_llvm_library( LLVMObfuscator MODULE # 编译后的LLVM Pass模块名称 LLVMObfuscator.so
  # 在此处添加cpp代码源文件,例如src下有一个源文件Demo.cpp
  /src/Demo.cpp
)

test,sh文件内容如下:

cd ./Build
cmake ../Transforms
make
cd ../Test
# 将源代码转换成LLVM IR
clang -S -emit-llvm TestProgram.cpp -o IR/TestProgram.ll
# 优化LLVM IR,这里的 -test 参数时Pass中自己注册的
opt -load ../Build/LLVMObfuscator.so -test -S IR/TestProgram.ll -o IR/TestProgram_test.ll
# 将llvm IR 编译成二进制可执行文件
clang IR/TestProgram_test.ll -o Bin/TestProgram_test

LLVM Pass编写步骤如下:

  • 创建一个类,继承FunctionPass

  • 实现runOnFunction(Funtion &F)函数

  • 向LLVM 注册自己的Pass类

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

namespace{
    // 创建一个类,继承FunctionPass
    class Demo : public FunctionPass{
        public:
            static char ID;
            Demo() : FunctionPass(ID){}
            // 实现runOnFunction(Funtion &F)函数
            bool runOnFunction(Function &F);
    };
}

// runOnFunction 函数实现,获取函数名称
bool Demo::runOnFunction(Function &F){
// do something
    outs() << "function name: " << F.getName() << "\n";
}
char Demo::ID = 0;
// 注册该 Demo Pass,这里写的test就是上面 test.sh 中 优化部分 -test参数
static RegisterPass<Demo> X("test", "LLVM Pass Demo");

3. LLVM Pass:基本块分割

基本块分割,也就是将一个基本块分割成若干个等价的基本块,在分割后的基本块之间加上无条件跳转。在许多基于基本块的代码混淆中,基本块数量越多,代码混淆后的复杂度就越大。

实现思路:

  • 遍历函数中每个基本块,对每个基本块进行分割即可。

  • 注意:有PHI指令的基本块我们选择跳过,因为PHI指令的值依赖于前驱,如果我们分割时改变了前驱,那么执行结果就错误了。

主要API:

  • 可选参数:接收命令行中可选参数作为一个基本块块分割成多少个,使用cl::opt模板类来实现,这里的opt是option,不是优化器的意思。

  • splitBasicBlock:BasicBlock类中的一个成员函数,作用是分割基本块。

  • isa<> :一个模板函数,用于判断一个指针志昂的数据类型是不是给定类型,这里我们用来判断PHI块。

核心代码:

// 命令行参数split_num,用于指定一个基本块分割次数,默认为2
static cl::opt<int> splitNum("split_num", cl::init(2), cl::desc("Split <split_num> time(s) each BB"));

// 判断基本块中是否包含PHI指令
bool SplitBasicBlock::containsPHI(BasicBlock *BB){
    for(Instruction &I : *BB){
        if(isa<PHINode>(&I)){
            return true;
        }
    }
    return false;
}

// 基本块分割,
void SplitBasicBlock::split(BasicBlock *BB){
    // 计算分裂后每个基本块的大小
    // 原基本块的大小 / 分裂数目(向上取整)
    int splitSize = (BB->size() + splitNum - 1) / splitNum;
    for(int i = 1;i < splitNum;i ++){
        BasicBlock *curBB = BB;
        int cnt = 0;
        for(Instruction &I : *curBB){
            if(cnt++ == splitSize){
                // 在 I 指令处对基本块进行分割
                curBB = curBB->splitBasicBlock(&I);
                break;
            }
        }
    }
}

// runOnFunction 函数实现
bool SplitBasicBlock::runOnFunction(Function &F){
    // 第一步:保存原先的所有基本块
    vector<BasicBlock*> origBB;
    for(BasicBlock &BB : F){
        origBB.push_back(&BB);
    }
    // 第二步:对每个不包含 PHI 指令的基本块执行分裂操作
    for(BasicBlock *BB : origBB){
        if(!containsPHI(BB)){
            split(BB);
        }
    }
    return true;
}

// 注册
char SplitBasicBlock::ID = 0;
static RegisterPass<SplitBasicBlock> X("split", "Split a basic block into multiple basic blocks.");