目前市面上的许多安全公司都会在保护IOS应用程序或安卓APP时都会用到ollvm技术。ollvm是基于LLVM编译器基础设施的混淆工具。它的主要目的是通过对程序的代码结构进行转换,使得逆向工程分析变得更加困难。由于llvm是ollvm的基础,所以想要了解ollvm就需要对llvm有所理解。下文将对llvm编译,基本概念,pass编写三个方面就行介绍分析。
(1)llvm编译
环境准备
网上流传的ubuntu18.04虚拟机: https://pan.baidu.com/s/1HVnuQd0OQ59Y8XzTDHBXJw?pwd=2vbq
(一下所有指令尽量切换到root执行)
刚打开这台ubuntu,发现缺少ifconfig,ssh,vim这些基本命令,于是先安装
su root
apt update
apt install net-tools
apt install openssh-server
apt-get update
apt-get install vim
ssh配置好后(用service ssh status命令查看),后续我们可以使用xftp或者MobaXterm等连接工具连接该机器来离线下载llvm-project压缩包
接着要按照Cmake,Ninja和build-essential(一系列基本的构建工具,包括c++编译器)
CMake是一个跨平台的构建工具。一个软件项目,尤其是比较复杂的项目,里面可能有很多源文件(比如.cpp文件)、头文件(比如.h文件),还有各种库文件。CMake的作用就是帮助管理这些文件如何编译成可执行程序或者库,它可以根据规则(这些规则写在CMakeLists.txt文件中),自动地找到所有需要的文件,确定它们的编译顺序,还能处理不同操作系统和编译器的差异
Ninja是一个专注于快速构建的构建系统。它就像是一个超级高效的 “建筑工人团队”。它的主要目标是尽可能快地执行构建任务。当一个大型项目需要频繁地构建(比如开发过程中,每次修改代码后都要重新编译),Ninja可以利用它高效的算法和简单的设计,快速地完成构建工作。它的构建速度比一些传统的构建工具要快很多。
在等下的llvm编译过程中,CMake可以生成Ninja能理解的构建文件。在使用CMake构建项目时,可以指定生成Ninja格式的构建文件。这样,CMake就像是一个 “设计师”,把项目构建的蓝图(CMakeLists.txt中的规则转化成Ninja能看懂的 “施工图纸”。然后Ninja就可以根据这些由CMake生成的文件,快速地进行实际的构建工作,把项目的各种文件 “组装” 成最终的软件。它们俩一起合作,能够高效地完成复杂软件项目的构建过程
安装命令
apt install cmake
apt install ninja-build
apt-get install build-essential
调整内存,磁盘大小以及cpu核数,以便后续构建的稳定核速度。
这里由于物理机性能限制,只调节了这么多,有条件可以适量增加内存和核数
扩容
apt-get install gparted
gparted
点勾后成功
补充
1.一开始使用apt apt-get时会出现一下类似报错:
E: Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), is another process using it?
解决方案参考
https://blog.csdn.net/xvrixingkong/article/details/129604200
注意:尽量采用杀死相关冲突apt进程的办法,而不是删除所有lock file即以下文件夹(可能导致崩溃)
2.网卡丢失
解决方案参考
https://blog.csdn.net/yanchenyu365/article/details/124492414
已经设置了一遍之后。如果在某次开启虚拟机后网卡突然又消失了,运行命令service NetworkManager restart即可
编译llvm
https://github.com/llvm/llvm-project
本文选择llvm10.0.1
下载然后上传至虚拟机,笔者选择在/home/wuya下创建目录LLVM,然后把tar包放入LLVM目录中
接着执行一系列命令进行解压和编译
tar -Jxvf llvm-project-10.0.1.tar.xz
cd llvm-project-10.0.1
mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE="Release" -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" ../llvm
ninja && ninja install -j4
build文件夹里面会生成一系列用于构建项目的文件。这些文件包含了很多重要的信息,比如告诉构建工具(这里用的是Ninja)怎么去编译 LLVM 的各个部分。在后续执行ninja以及ninja install这些构建和安装操作的时候,都是在这个build文件夹里进行的。所有的中间结果、最终生成的可执行文件、库文件等等,也都会放在这个文件夹或者它里面的子文件夹里
cmake -G Ninja -DCMAKE_BUILD_TYPE="Release" -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" ../llvm 解析:
-G Ninja:告诉cmake,要生成适合Ninja这个构建系统的构建文件
-DCMAKE_BUILD_TYPE="Release":设置构建的类型为Release,即希望构建出来的 LLVM 项目是适合在正式的生产环境里使用的
-DLLVM_ENABLE_PROJECTS="clang":告诉cmake,在构建 LLVM 项目的时候,要把其中的clang子项目也一起构建
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi":启用 LLVM 项目里的libcxx和libcxxabi这两个运行时库
../llvm:告诉cmake,LLVM 项目的源代码在当前目录的上一级目录里的llvm文件夹下
注意:执行"ninja && ninja install -j4"这一步,由于虚拟机性能不足,花了将近两个小时,而且中间断了几次,需要重新运行命令(会接着之前编译好的继续进行)。
编译好后的结果
(2)LLVM基本概念
LLVM是一个编译器(确切的说是一套框架+基于框架的一些编译器实现,如clang),是当下很先进的一套编译系统
架构
编译器的三段式设计:
- Frontend(前端):主要工作是解析源代码:词法分析、语法分析、语义分析、检查错误,并构建抽象语法树(Abstract Syntax Tree AST)来表示输入代码。AST 可以选择性地转换为中间表达式,以便用于优化器。
- Optimizer(优化器):负责优化代码,使代码更高效。例如消除冗余
- Backend(后端):负责将优化器优化后的中间代码转化为目标机器指令集,最大化利用目标机器的特殊指令,提高代码的性能。例如:指令选择、寄存器分配和指令调度
而llvm的架构图如下:
IR
IR(Intermediate Representation)是LLVM项目中的中间表示形式。它主要是在编译器的前端(把高级编程语言,如 C、C++ 等转换的部分)和后端(生成机器码的部分)之间起一个"桥梁"的作用。就像是一个翻译的中间步骤,把不同的高级语言先统一成一种中间形式,然后再根据目标硬件生成机器码。
IR(中间表示)作为一种低级形式的表述语言,具备用于呈现高级语言的能力,其特性使得能够实现高效的代码优化。在关于 IR 的相关资料中给出了这样的表述:RISC-like instruction set。这里的 RISC-like instruction set 意味着 IR 在一定程度上具有类似于精简指令集计算机(RISC)指令集的特征或风格。IR实际上有两种表示方式:bc和ll,这两者之间可以相互转换
- ll:ll具有人类可读的文本格式。它的语法比较接近汇编语言,有明确的操作码、操作数和指令格式,主要用于调试和开发阶段。
- bc:bc一种二进制格式。它是一种更加紧凑、高效的存储方式,主要用于在 LLVM 工具链内部进行传递和存储。
llvm的编译好后的目录如下:
下面介绍一下tools
llvm-tools
llvm-tools是一组与LLVM相关的工具集合
- bugpoint:bugpoint 用于通过将给定的测试用例缩小到仍会导致问题(无论是崩溃还是错误编译)的最少的优化遍数和 / 或指令数量,来调试优化遍数或代码生成后端。有关使用 bugpoint 的更多信息,请参阅 HowToSubmitABug.html。
- llvm-ar:归档器会生成一个包含给定LLVM位码文件的归档文件,并且可选择性地带有一个索引以便更快地查找。
- llvm-as:汇编器将人类可读的LLVM汇编代码转换为LLVM位码。
- llvm-dis:反汇编器将LLVM位码转换为人类可读的LLVM汇编代码。
- llvm-link:llvm-link用于将多个LLVM模块链接成一个单一的程序
- lli:lli是LLVM解释器,它可以直接执行LLVM位码(尽管速度非常慢……)。对于支持它的架构(目前有 x86、Sparc 和PowerPC),默认情况下,lli将充当即时编译器(如果该功能已编译进去),并且执行代码的速度会比解释器快得多
- llc:llc是LLVM后端编译器,它将LLVM位码转换为本地代码汇编文件。
- opt:opt读取LLVM位码,应用一系列LLVM到LLVM的转换(这些转换在命令行上指定),并输出转换后的位码。"opt -help"是获取LLVM中可用的程序转换列表的一种好方法。
详情可以阅读官方文档
https://llvm.org/docs/GettingStarted.html#llvm-tools
体验llvm编译
来一个最简单的helloworld hello.c文件
流程总结
LLVM-PASS介绍
LLVM-PASS是什么:
LLVM-Pass就像是一个"小工具",它可以对LLVM中间表示(IR)进行一些特定的操作。中间表示(IR)是编译器把高级语言(像 C、C++)转换成的一种中间形式的代码,介于高级语言和机器码之间。
这些"小工具"可以用来优化代码,比如让代码运行得更快,或者让生成的可执行文件更小。也可以用来做代码分析,看看代码有没有潜在的问题或者有没有符合某种规则
工作方式:
想象有一条"传送带",上面放着IR代码。LLVM-Pass就是在这条"传送带"旁边的"小机器"。当IR代码经过这个"小机器"时,它就会按照自己的功能对IR代码进行处理。
(3)LLVM-PASS编写
vscode安装
为了更好操作llvm-project,先安装vscode
从VSCode官网下载的deb安装包
本文这里安装1.83.1
https://update.code.visualstudio.com/1.83.1/linux-deb-x64/stable
将压缩包上传到虚拟机
在放置.deb安装包的文件夹下运行命令
dpkg -i code_1.83.1-1696982868_amd64.deb
-i后面跟的是自己的.deb安装包的名字
运行好后就可以找到vscode图标了
llvm项目内编写
在llvm/lib/Transforms下创建pass文件夹,里面创建CMakeLists.txt和cpp文件
CMakeLists.txt:
add_llvm_library( LLVMPrintName MODULE
PrintName.cpp
PLUGIN_TOOL
opt
)
cpp源文件:
编写官方参考:
https://releases.llvm.org/9.0.1/docs/WritingAnLLVMPass.html
#include "llvm/IR/Function.h"
#include "llvm/IR/Module.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {
struct PrintFunctionNamePass : public ModulePass {
static char ID;
PrintFunctionNamePass() : ModulePass(ID) {}
bool runOnModule(Module &M) override {
// 遍历模块中的每个函数
for (auto &F : M) {
// 打印函数名
errs() << "函数名: " << F.getName() << "\n";
}
return false;
}
};
char PrintFunctionNamePass::ID = 0;
static RegisterPass<PrintFunctionNamePass> X("printname", "打印函数名LLVM Pass",
false, // 这个Pass不修改IR
false); // 这个Pass不是分析Pass
}
- "printname":
这是为所注册的LLVM-Pass指定的一个命令行标识符(Command Line Identifier)。当你在使用LLVM的工具(比如opt)并想要应用这个Pass时,就可以在命令行中通过这个标识符来指定要使用的Pass。例如,在opt工具中,要输入 opt -printname <input_llvm_ir_file>的命令,其中-printname就是通过这个参数指定的标识符来调用 </input_llvm_ir_file> - "打印函数名LLVM Pass":
这是对所注册的LLVM-Pass的一个简短描述。它主要用于在一些工具的输出或者文档中,当提到这个Pass时,能够让用户快速了解这个Pass的主要功能,也就是打印函数名的功能。
然后再Transform目录下的构造文件添加
add_subdirectory(PrintName)
add_subdirectory 语句主要用于将一个指定的子目录纳入到当前的构建过程中
ninja LLVMPrintName
编译好后,显示了路径
进入lib后查看
opt -load /home/wuya/LLVM/llvm-project-10.0.1/build/lib/LLVMPrintName.so -printname hello.ll -o hello.bc
llvm项目外
目录结构
项目根目录的CMakeLists.txt是整体项目配置:
- 这里通常会设置项目的最低CMake版本要求,如:cmake_minimum_required(VERSION...)。
- 调用find_package来查找外部依赖项,比如查找LLVM库,这对于确保项目能够正确链接到LLVM至关重要。
- 使用add_subdirectory命令将子目录(如 llvmpass)纳入整个项目的构建过程中,这样CMake会进入子目录并处理其中的构建配置。
cmake_minimum_required(VERSION 3.10)
SET(CMAKE_CXX_FLAGS "-Wall -fno-rtti")
find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_subdirectory(llvmpass)
llvmpass是源代码文件夹,里面的CMakeLists.txt是特定子模块配置:
- add_library语句定义了一个库目标。在这种情况下,创建了一个名为LLVMPrintName的库,是LLVM pass的实现。它指定了源文件(如 PrintName.cpp),并可能通过MODULE选项表明这是一个动态库或插件。
- target_link_libraries语句指定了这个库目标需要链接的其他库。在这里,它将LLVMPrintName库与LLVM 库链接起来,确保在编译这个库时能够正确访问 LLVM 的功能。
add_library( LLVMPrintName MODULE
PrintName.cpp
)
target_link_libraries(LLVMPrintName PRIVATE ${LLVM_LIBRARY_DIRS})
创建build目录
然后LLVMPrintName.so就会在build/llvmpass下生成
参考文章
https://blog.csdn.net/xvrixingkong/article/details/129604200
https://blog.csdn.net/yanchenyu365/article/details/124492414
https://zhuanlan.zhihu.com/p/692007432
https://llvm.org/docs/GettingStarted.html#llvm-tools
https://blog.csdn.net/weixin_39450145/article/details/134635674
https://www.cnblogs.com/BobHuang/p/17640378.html
https://releases.llvm.org/9.0.1/docs/WritingAnLLVMPass.html
https://stackoverflow.com/questions/17225956/developing-an-llvm-pass-with-cmake-out-of-llvm-source-directory