安卓开发及安全交流QQ群:838650234,感兴趣的可以加群。
最近开始搞LLVM,感受到自己自学能力还是不够,话不多说,下面我将从以下五个方面来介绍LLVM。分别是:(1)LLVM是什么?(2)LLVM的组成部分;(3)LLVM+Clang环境搭建;(4)LLVM的运行过程;(5)LLVM Pass的构建运行过程;期间参考了以下优质的帖子(PS:都是大佬啊),会在每一章都有列出说明。
(一)LLVM是什么??
LLVM(low level virtual machine)从本质上来说,是一个开源编译器框架,能够提供程序语言的编译期优化、链接优化、在线编译优化、代码生成。LLVM有两个特点:
(1)LLVM有一个特定指令格式的IR语言,我们可以通过书写Pass来对其IR进行优化。
(2)可以作为多种语言的后端,提供与编程语言无关的优化和针对多种CPU的代码生成功能。
(二)LLVM的组成部分:
LLVM主要由Clang前端、IR优化器(Pass)和LLVM后端构成。其功能分别是:
clang前端:将平台相关的源码生成与平台无关的IR(llvm Bitcode)。
IR优化器:主要对IR进行优化。
llvm后端:将优化后的IR转换为与平台相关的汇编代码或者机器码。
2.1 Clang前端:
Clang前端以.c文件为输入,经语法词法分析后解析为抽象语法数,最后通过LLVM内联API变为LLVM IR。其功能为:词法分析器:把输入的程序代码切成token;语法分析器:接收token流解析为AST。
2.2 IR优化器:
LLVM IR包含三种格式:一种是在内存中的编译中间语言;一种是硬盘上存储的二进制中间语言(以.bc结尾),最后一种是可读的中间格式(以.ll结尾)。这三种中间格式是完全相等的。LLVM IR是LLVM优化和进行代码生成的关键。根据可读的IR,我们可以知道再最终生成目标代码之前,我们已经生成了什么样的代码。我们通过Pass来对IR进行相应的优化。
2.3 LLVM后端:
Llvm clang编译器主要是将各平台源代码编译成与平台无关的IR指令集,这将支撑对IR的优化及转换操作,而llvm后端的主要工作是优化IR指令,并将这些与平台无关的IR指令转换成目标设备相关的指令。
由上图所示,LLVM IR进入后端要经过pass优化,指令选择,指令调度,寄存器分配,代码布局优化以及汇编发行等过程。上述各过程都是pass优化的过程,普通(白色)pass可由用户自定义,内置(灰色)pass由一系列小的pass构成,换句话说我们可以对每一个阶段都可以进行不同程度的优化。同时无须为每个目标平台编写重复的代码。
2.3.1 指令选择
指令选择将内存中三地址结构的IR指令转换成设备相关的DAG节点,如上图所示,指令选择的主要过程有:
(1) 构建初始DAG:该过程只是将LLVM IR指令简单的转换成不合法的SelectionDAG,利用SelectDAGISe类调用visit()函数遍历IR创建DAGNode节点
(2)优化 SelectionDAG:识别目标平台支持的元指令
(3)类型合法化:消除不支持的类型,利用TargetLowering接口
(4)优化SelectionDAG:清除类型合法化后的冗余
(5)操作合法化:将操作进行合法化
(6)优化 SelectionDAG:消除效率低下的操作数
(7)转换设备无关的DAG到目标DAG(DAGNode转换为MachineSDNode(目标平台机器指令))
注:机器指令由.td文件描述。Tablegen就是用于记录这些信息的描述性语言。经过tablegen工具批量生成C++源文件。它的好处就是减少我们描述的工作量。Tablegen主要由Class(类)和Definition(定义)组成。其中Class是用于描述构建其它记录的抽象记录,可以理解成模板。描述目标的共同特点以便批量生成记录。Definition是具体的描述实例,不包含任何未定义的变量。
2.3.2 指令调度
对DAG进行拓扑排序转换为线性指令集尽可能的利用指令级的并行操作,提高运行效率.
2.3.3 寄存器分配
由于IR拥有多个虚拟寄存器,因此需要将输入的任意数目的寄存器重新分配为符合硬件要求的有限个数寄存器。
2.3.4 代码发行
输出汇编代码或二进制代码
(三)LLVM+Clang环境搭建:
参考了这篇博客:http://www.cnblogs.com/codemood/p/3142848.html
环境:Ubuntu16.04 32位
gcc版本号:5.3.1
Cmake版本号:3.5.1
3.1新建LLVM文件夹:
mkdir LLVM
3.2下载以下五个包:llvm-3.3.src、cfe-3.3.src、clang-tools-extra-3.3.src、compiler-rt-3.3.src、libcxx-3.3.src将其解压至LLVM文件夹下。可以在这个链接或网页找资源。下载链接:https://download.csdn.net/download/weixin_38244174/10667601
3.3然后按下面的步骤组织,这样可以将clang,clang-tools-extra和compiler-rt就可以和llvm一起编译了。
mv cfe-3.3.src clang
mv clang/ llvm-3.3.src/tools/
mv clang-tools-extra-3.3.src extra
mv extra/ llvm-3.3.src/tools/clang/
mv compiler-rt-3.3.src compiler-rt
mv compiler-rt llvm-3.3.src/projects/
3.4 在LLVM文件夹下新建build目录:
mkdir buid
cd build
3.5 配置并编译,时间可能比较长,20min以上。
../llvm-3.3.src/configure --enable-optimized --enable-targets=host-only
make -j4
sudo make install
3.6 验证成功与否:
clang -help
若显示这样则成功:
(四)LLVM运行过程:
(一)理论基础:
1.如上图所示,以c文件为例,首先使用-emit-llvm命令告诉clang前端将.C文件生成llvm的IR(LLVM IR主要有三种格式:一种是在内存中的编译中间语言;一种是硬盘上存储的二进制中间语言(以.bc结尾),最后一种是可读的中间格式(以.ll结尾)。这三种中间格式是完全相等的)。
2.生成可执行文件有两种方式:
a)使用llc命令将IR文件(.bc文件)生成目标文件(.o文件),通过系统链接器链接多个目标文件,生成可执行文件。(针对单个IR)
b)使用llvm-link链接多个IR,使用llc命令将IR文件生成目标文件,之后生成可执行文件。(针对多个IR)
(二)具体实践:
参考链接:https://blog.csdn.net/earbao/article/details/53421319
对源码的编译。
1.创建简单的c语言源码文件test.c
#include <stdio.h>
int main() {
printf("hello llvm\n");
return 0;
}
2.编译可执行文件
clang test.c -o test
3.生成LLVM字节码文件
clang -O3 -emit-llvm test.c -c -o test.bc
4.生成LLVM可视化字节码文件
clang -O3 -emit-llvm test.c -S -o test.ll
5.运行可执行文件
./test
6.运行字节码文件
lli test.bc
7.反汇编字节码文件
llvm-dis < test.bc | less
8.编译字节码为汇编文件
llc test.bc -o test.s
(五)LLVM Pass的构建运行过程:
参考博客:https://blog.csdn.net/ZCMUCZX/article/details/80856655
该Pass功能是:opt命令行工具会动态的去加载动态链接库,以运行Pass,之后Pass会遍历每一个函数,输出其函数名,但未对IR做任何改动。
步骤如下:
5.1 创建一个FuncBlockCount.cpp文件,内容如下:
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
//引入llvm命名空间,可以让其实用LLVM当中的函数
using namespace llvm;
//创建一个匿名的命名空间
namespace {
//声明Pass
struct FuncBlockCount : public FunctionPass
{
//声明Pass的标识符,会被LLVM用作识别Pass
static char ID;
//对父类进行初始化
FuncBlockCount() : FunctionPass(ID){}
//其实就是FunctionPass的一个虚函数,这里对它进行了实现。一个FunctionPass的子类要想做一些实际的工作,就必须对这个虚函数进行实现。
bool runOnFunction(Function &F) override {
//errs()是一个LLVM提供的C++输出流,我们可以用它来输出到控制台
errs() << "Function "<<F.getName()<<'\n';
//函数返回false说明它没有改动函数F。之后,如果我们真的变换了程序,我们需要返回一个true。
return false;
}
};
}
//初始化Pass ID
char FuncBlockCount::ID = 0;
//需要注册Pass、填写名称和命令行参数
static RegisterPass<FuncBlockCount> X("funcblockcount","Function Block Count",false,false);//,最后两个参数描述了它的行为:如果一个pass不修改CFG,那么第三个参数将被设置为true;如果pass是一个分析传递,例如dominator树(支配树)传递,则true作为第四个参数提供。
5.2 使用以下命令编译so文件
g++ FuncBlockCount.cpp -fPIC -g -Wall -Wextra -std=c++11 `llvm-config --cxxflags ` -shared -o FuncBlockCount.so
5.3 测试文件为(4)中的test.ll文件;
5.4使用命令运行新Pass.
opt -load /home/kyriehe/Desktop/testpass/FuncBlockCount.so -funcblockcount test.ll
5.5若显示下图则说明运行正确。
5.6 将C语言测试代码编译为LLVM IR的形式
clang -O0 -S -emit-llvm test.c -o test.ll