LLVM-Clang插件开发

3,789 阅读8分钟

LLVM-Clang插件开发

LLVM概述

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。 LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博士主持开展。2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。 目前LLVM已经被苹果IOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用。

传统编译器设计

传统编译器分三个阶段:

前端(Frontend)-- 优化器(Optimizer)-- 后端(Backend)

前端负责分析源代码,可以检查语法级错误,并构建针对语言的抽象语法树(AST);抽象语法树可以进一步转换为优化,最终转为新的表示方式,然后再交给让优化器和后端处理;

最终由后端生成可执行的机器码。

LLVM设计

前端可以使用不同的编译工具对代码文件做词法分析以形成抽象语法树AST,然后将分析好的代码转换成LLVM的中间表示IR(intermediate representation);中间部分的优化器只对中间表示IR操作,通过一系列的pass对IR做优化;后端负责将优化好的IR解释成对应平台的机器码。LLVM的优点在于,使用通用的代码表示形式(IR),不同的前端语言都将转换成统一代码形式。

Clang

ClangLLVM项目中的一个子项目,它是基于LLVM架构的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度。它是负责编译C、C++、Objecte-C语言的编译器,属于整个LLVM架构中的前端,对于开发者而言,研究Clang可以给我们带来很多好处。

编译流程

在列出完整步骤之前可以先看个简单例子。看看是如何完成一次编译的。

通过命令可以打印源码的编译阶段执行步骤:

 clang -ccc-print-phases main.m

 0: input, "main.m", objective-c 
 1: preprocessor, {0}, objective-c-cpp-output
 2: compiler, {1}, ir
 3: backend, {2}, assembler
 4: assembler, {3}, object
 5: linker, {4}, image
 6: bind-arch, "x86_64", {5}, image
 
 0: 输入文件,找到源文件
 1: 预处理阶段,该阶段会进行宏的替换、头文件的导入
 2: 编译阶段,进行词法分析、语法分析、检测语法是否正确,生成IR
 3: 后端,LLVM会通过一个一个Pass去优化,最终生成汇编代码
 4: 生成目标文件
 5: 链接动态库和静态库,生成可执行文件
 6: 通过不同的架构生成对应的可执行文件

预处理阶段

通过输入以下命令,可以查看在Clang在预编译处理阶段做了什么

 clang -E main.m

亦可以通过输出到指定文件,方便查看。

clang -E main.m -o main1.m

这个过程的处理包括宏的替换,头文件的导入。下面这些代码也会在这步处理。

  • “#define”
  • “#include”
  • “#indef”
  • “#pragma”。

编译阶段

词法分析

预处理完成以后就会进行词法分析,这里会把代码切割成一个一个Token,比如括号、字符串等。

 clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

通过上面的命令可以查看,

annot_module_include '#import <Foundation/Foundation.h>

#define PJHeight 10

int sum(int a){
    return a + 1;
}
int main(int argc, const char * argv[]) {
  '		Loc=<main.m:9:1>
int 'int'	 [StartOfLine]	Loc=<main.m:13:1>
identifier 'sum'	 [LeadingSpace]	Loc=<main.m:13:5>
l_paren '('		Loc=<main.m:13:8>
int 'int'		Loc=<main.m:13:9>
identifier 'a'	 [LeadingSpace]	Loc=<main.m:13:13>
r_paren ')'		Loc=<main.m:13:14>
l_brace '{'		Loc=<main.m:13:15>
return 'return'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:14:5>
identifier 'a'	 [LeadingSpace]	Loc=<main.m:14:12>
plus '+'	 [LeadingSpace]	Loc=<main.m:14:14>
numeric_constant '1'	 [LeadingSpace]	Loc=<main.m:14:16>
semi ';'		Loc=<main.m:14:17>

通过词法分析可以获得每个 token 的类型,值还有类似 StartOfLine 的位置类型和 Loc=main.m:13:1 这个样的具体位置。

语法分析

词法分析完成后,然后是语法分析,会验证语法是否正确,然后将所有的节点组成抽象语法树AST,

 clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

IR代码

构建抽象语法树后,CodeGen会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,作为后端的输入,通过下面的命令可以生成.ll的文本文件,查看IR代码。

  clang -S -fobjc-arc -emit-llvm main.m

IR语法关键字:

  • @ - 代表全局变量
  • % - 代表局部变量
  • alloca - 指令在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存。
  • i32:- i 是几这个整数就会占几位,i32就是32位4字节
  • align - 对齐,比如一个 int,一个 char 和一个 int。单个 int 占4个字节,为了对齐只占一个字节的 char需要向4对齐占用4字节空间。
  • load - 读出,store 写入
  • icmp - 两个整数值比较,返回布尔值
  • br - 选择分支,根据 cond 来转向 label,不根据条件跳转的话类似 goto indirectbr - 根据条件间接跳转到一个 label,而这个 label 一般是在一个数组里,所以跳转目标是可变的,由运行时决定的
  • label - 代码标签

这里 LLVM 会去做些优化工作,在 Xcode 的编译设置(BuildSettings->Code Generation->Optimization Level)里也可以设置优化级别-O1,-O3,-Os,还可以写些自己的 Pass,官方有比较完整的 Pass 教程:Writing an LLVM Pass — LLVM 5 documentation

Pass 是 LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM 完整的优化和转化。

BitCode

如果开启了 bitcode 苹果会做进一步的优化,通过优化后的IR代码生成.bc代码

 clang -emit-llvm -c main.ll -o main.bc

亦可

 clang -emit-llvm -c main.m -o main.bc

直接源文件进行优化,两者对比下优化差异。

汇编代码

然后我们通过最终生成的.bc或者.ll代码生成汇编代码,

clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s

目标文件

生成目标文件

clang -fmodules -c main.m -o main.o

通过nm命令,查看下main.o中的符号

生成可执行文件

链接器把编译产生的.o文件和(.dylb .a)文件,生成一个mach-o文件。

clang main.o -o main

再用nm命令查看链接后的符号,

可以清楚查看到_printf符号来自于libSystem。

下面是完整步骤:

  • 编译信息写入辅助文件,创建文件架构 .app 文件
  • 处理文件打包信息
  • 执行 CocoaPod 编译前脚本,checkPods Manifest.lock
  • 编译.m文件,使用 CompileC 和 clang 命令
  • 链接需要的 Framework
  • 编译 xib
  • 拷贝 xib ,资源文件
  • 编译 ImageAssets
  • 处理 info.plist
  • 执行 CocoaPod 脚本
  • 拷贝标准库
  • 创建 .app 文件和签名

LLVM下载

可以借助镜像下载LLVM的源码:mirror.tuna.tsinghua.edu.cn/help/llvm/

下载LLVM项目

git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git

下载Clang

cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git

下载compiler-rt,libcxx,libcxxabi

cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git

安装extra工具

cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e xtra.git

LLVM编译

安装cmake

  • 查看brew是否安装过cmake 如果有则跳过下面步骤

      brew list
    
  • 通过brew 安装cmake

      brew install cmake
    

通过xcode编译

  • cmake编译成xcode项目

     mkdir llvm_build
     cd llvm_build
     cmake -G Xcode ../llvm
    

llvm_build存放的是LLVM编译后的文件,注意llvm_build目录的位置,笔者在这存放的是与LLVM源文件同级目录,cmake编译指令是在llvm_build目录下,查找LLVM源文件位置../llvm。

使用Xcode编译Clang

  • 选择自动创建Schemes

  • 编译,选择ALL_BUILD Schemes进行编译,预计1+小时,编译时间会比较长。

另外需要注意的是,电脑预留足够的空间存放,大概20G左右,否则编译会报错。

Clang插件

创建插件

cd /llvm/tools/clang/tools

在该目录下新建插件目录PJPlugin.

配置插件

  • 在当前tools目录下,打开CMakeLists.txt文件,新增add_clang_subdirectory(PJPlugin)

  • cd PJPlugin

创建CMakeLists.txt和PJPlugin.cpp文件,然后打开CMakeLists.txt进行相关配置,这里我们简单的添加部分主要核心代码,

add_llvm_library( PJPlugin MODULE BUILDTREE_ONLY
 PJPlugin.cpp
)

camke重新编译

回到llvm_build目录,执行cmake -G Xcode ../llvm

编译完成后可在项目Loadable modules目录下查看到刚刚生成的插件目录,然后我们可以在里面编写插件代码了。

LLVM 官方有一个完整可用的 Clang 插件示例,可以帮我们打印出最上层函数的名字,你可以点击链接查看使用。

Clang 插件本身的编写和使用并不复杂,关键是如何更好地应用到工作中,通过 Clang 插件不光能够检查代码规范,还能够进行无用代码分析、自动埋点打桩、线下测试分析、方法名混淆等,努力学习实践~

使用插件

以下.../llvm_build_xcode 都是绝对路径,为了去除电脑用户名隐私,所以在这...代替,^_^。

  • 一种是,直接使用 -cc1 选项,缺点是要在命令行上指定完整的系统路径配置;
.../llvm_build_xcode/Debug/bin/clang -cc1 -load .../llvm_build_xcode/Debug/lib/PJPlugin.dylib -plugin PJPlugin hello.m
  • 一种是,使用 -Xclang 来为 cc1 进程添加这些选项。-Xclang 参数只运行预处理器,直接将后面参数传递给 cc1 进程,而不影响 clang driver 的工作。
.../llvm_build_xcode/Debug/bin/clang -isysroot Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.2.sdk/ -Xclang -load -Xclang .../llvm_build_xcode/Debug/lib/PJPlugin.dylib  -Xclang -add-plugin -Xclang PJPlugin -c hello.m

Xcode 集成插件

加载插件

打开项目,在Build Settings -> Other C Flags 添加如下内容,

 -Xclang -load -Xclang (.dylib)动态库路径 -Xclang -add-plugin -Xclang PJPlugin

设置Xcode

  • 在Build Settings中新增自定义设置,

  • 添加CC和CXX

CC对应的是自己编译的clang的绝对路径。

CXX对应的是自己编译的clang++的绝对路径。

  • 接下来在Build Setting栏目中搜索index,将Enable Index-Wihle-Building Functionality 修改为NO。

参考文献: