LLVM编译与自定义PASS

1,248 阅读5分钟

LLVM 是什么?

根据官方的描述 ,LLVM项目是一个模块化和可重用的编译器和工具链技术的集合。他主要包含几个模块:

  • LLVM-Core: LLVM核心库可以将源程序语言转化为“LLVM IR”语言, IR语言独立于不同的CPU平台,由于其完善的文档,可以将各种不同的语言都加入到编译链当中。
  • Clang: 基于LLVM的C/C++/Objective-C编译器, 跟C语言相关的语言都可以使用Clang来编译。
  • LLDB: 一个高性能的调试器。
  • libc++: C++标准库, 支持c++11和c++ 14。
  • compiler-rt : 为目标平台提供其硬件不支持的低级功能的优化实现。
  • MLIR: 目的是做一个通用、可复用的编译器框架,减少构建Domain Specific Compiler的开销,目前主要用于机器学习领域

概括的说 LLVM编译器 的工作流程就是 :

  • 前端把源代码翻译成中间表示 (IR)。
  • 后端把IR编译成目标平台的机器码。

编译LLVM源码

下载源码:

llvm源码地址 : github.com/llvm/llvm-p… , 下载各个版本中的llvm-xx.x.x.src.tar.xz 部分。

编译:

llvm分为Debug和 Release 两个版本的,默认编译选项为Debug,编译时间较长,且占用内存较大,

命令为

mkdir build;
cd build; cmake -DLLVM_INCLUDE_TESTS=off ../llvm;
ninja -j8

llvm默认的编译工具是ninja, 除此以外还可以选择make等

LLVM_INCLUDE_TESTS选项为是部分编译Test部分

如需编译Release版本,则添加Build-Type:

cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=off ../llvm

默认编译选项不包含llvm的子模块内容,可以使用 -DLLVM_ENABLE_PROJECTS='...' , 参数来添加 如:

cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=off  -DLLVM_ENABLE_PROJECTS='clang;libc++' ../llvm

LLVM工具介绍

  • llvm-ar:LLVM的静态库打包器,类似Linux系统中的ar。
  • llvm-as:LLVM 汇编器,
  • lli: 可以执行一个IR语言文件
  • llvm-as: LLVM的汇编器,生成一个bc格式的二进制文件,可以被llvm识别
  • llvm-dis: LLVM 反汇编器 ,可以将二进制文件转为 IR文件。
  • llc: 将bc文件转成对应平台的汇编代码。
  • llvm-link: llvm的链接器,用于链接多个文件.
  • opt: llvm重要的指令,可以执行各种PASS。

IR语言示例

这里编写一个简单的HelloWorld代码

#include <stdio.h>
int main( ){
    printf("hello world\n");
    return 0 ;
}

使用Clang进行编译

clang -emit-llvm -S hello.c  -o hello.ll

即可生成IR中间语言的输出文件 hello.ll

; ModuleID = 'hello.c'
source_filename = "hello.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
​
@.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00", align 1
​
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i64 0, i64 0))
  ret i32 0
}
​
declare dso_local i32 @printf(i8*, ...) #1
​
attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
​
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
​
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 10.0.0-4ubuntu1 "}
​

使用上面编译生成的 lli 程序 ,可以直接运行当前的hello.ll 文件

lli ./hello.ll

IR语法简介

根据上文输出的IR语言范例,简单总结一下IR语言的基础语法

target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
e小端序
m:o符号表中使用Mach-O格式的name mangling
i64:64i64类型的变量采用64比特的ABI对齐
f80:128将long double类型的变量采用128比特的ABI对齐
n8:16:32:64目标CPU的原生整型包含8比特、16比特、32比特和64比特
S128栈以128比特自然对齐

以 分号 ; 开头的代码代表注释

; Function Attrs: noinline nounwind optnone uwtable

基本数据类型

  • i32 代表 int
  • i8 代表char

@ 代表全局符号,包括变量和函数 % 代表局部符号,包括局部变量

指令:

  • alloca : 给函数分配栈空间,函数返回时自动回收。
  • store : 数据存储到对应的变量中 。i32 0, i32* %1, align 4 带边将0存储到 变量 %1 中, 而%1正是alloca申请的栈空间。
  • ret: 返回指令
  • Add / Sub / Mul / Div / Rems : 加减乘除取余 操作
  • icmp、fcmp : 比较指令
  • call 函数调用

PASS

pass是llvm中非常重要的模块, 可以对整个编译过程添加各种自定义的操作,编译好的release版本中,官方提供了一个默认的pass

opt -load ../build_release/lib/LLVMHello.so  -hello hello.bc

此Pass可以输出hello中定义的函数

Hello: main

自定义pass

官网中介绍了如何自行定义一个pass

releases.llvm.org/10.0.0/docs…

为了方便,我们直接在源码中添加一个Pass, 功能是函数名称混淆

Pass代码编写

  • 改掉Hello模块代码

    • 复制位于llvm中 Hello模块目录的代码( /llvm/lib/Transforms/Hello ),并改名为EncryptFunctionName
    • 将Hello.cpp 改为 Test.cpp
    • Hello.exports 改为 EncryptFuncName.exports
  • 修改Transforms/CMakeList.txt ,追加一行

     add_subdirectory(EncryptFuncName)
    
  • 修改EncryptFunctionName目录中的CMakeLists.txt

    # If we don't need RTTI or EH, there's no reason to export anything
    # from the hello plugin.
    if( NOT LLVM_REQUIRES_RTTI )
      if( NOT LLVM_REQUIRES_EH )
        set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/EncryptFuncName.exports)
      endif()
    endif()
    ​
    if(WIN32 OR CYGWIN)
      set(LLVM_LINK_COMPONENTS Core Support)
    endif()
    ​
    add_llvm_library( EncryptFuncName MODULE BUILDTREE_ONLY
      Test.cpp
      DEPENDS
      intrinsics_gen
      PLUGIN_TOOL
      opt
      )
    
  • 修改Test.cpp

    #include "llvm/ADT/Statistic.h"
    #include "llvm/IR/Function.h"
    #include "llvm/Pass.h"
    #include "llvm/Support/raw_ostream.h"
    #include <iostream>
    using namespace llvm;
    ​
    #define DEBUG_TYPE "encrypt"
    ​
    ​
    int Counter = 0 ;
    ​
    namespace {
      // Hello - The first implementation, without getAnalysisUsage.
      struct EncryptFuncName : public FunctionPass {
        static char ID; // Pass identification, replacement for typeid
        EncryptFuncName() : FunctionPass(ID) { }
    ​
        bool runOnFunction(Function &F) override {
    ​
          if(  F.getName().compare("main") != 0){
    ​
            char buff[30];
             errs().write_escaped(F.getName()) << '\n';
            std::sprintf(buff, "OOO00ooOOO%d" , Counter++); //将除main函数之外的所有函数名称都进行OOO00ooOOO前缀的混淆加密.
            F.setName(buff);
            errs().write_escaped(F.getName()) << '\n';
          }
          return false;
        }
      };
    }
    ​
    char EncryptFuncName::ID = 0;
    static RegisterPass<EncryptFuncName> X("encrypt", "Hello World Pass");
    
  • 编写测试代码, hello.cpp
#include <stdio.h>
int fun1( ){
    return 0;
}
int fun2( ){
    return 1;
}
​
int main( ){
    printf("hello world\n");
    printf("fun1 %d \n" , fun1());
    printf("fun2 %d\n" ,  fun2());
    return 0 ;
}
  • 执行pass并编译

    opt -load ../build_release/lib/EncryptFuncName.so  -encrypt hello.bc;
    clang hello.bc -o hello
    
  • 结果验证

    当编译完成之后,在本地目录生成可执行文件hello,运行命令

     nm hello
    

    可以看到下面的结果:

    分别是将 fun1 =》 OOO00ooOOO0 , fun2 =》 OOO00ooOOO1

1.png

总结

本文档介绍了:

  • llvm源码编译
  • IR语言基础语法
  • 自定义Pass