iOS开发进阶:LLVM及Clang插件初探
一、LLVM概述
LLVM是架构编译器(Compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-timne)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
目前LLVM已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用。
- 编译器前端(Frontend):编译器前端的任务时解析源代码。他会进行词法分析、语法分析、语义分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree, AST)。LLVM的前端还会生成中间代码(Intermediate Representation, IR)。
- 优化器(Optimizer):优化器负责进行各种优化。改善代码的运行时间,例如消除冗余的计算等。
- 后端(Backend)/代码生成器(Code Generator):将代码映射到目标指令集。生成机器语言,并且进行机器语言相关的代码优化。
iOS的编译器架构
LLVM的设计
LLVM使用通用的代码表示形式(IR),他是用来在编译器中表示代码的形式,所以LLVM可以位任意编程语言独立编写前端,摒弃可以位任意硬件架构独立编写后端。Clang
Clang时LLVM项目中的一个子项目。他是基于LLVM架构的轻量级编译器,负责C、C++、Objective语言的编译,属于LLVM架构中的编译器前端。
二、编译流程
案例代码包括一个math-add.h文件,内部定义一个add函数。另一个是explorer.m文件,内部引入math-add.h,定义一个宏,然后func中做测试。
//math-add.h
int add(int x, int y){
return x + y;
}
//explorer.m
#include "math-add.h"
#define KCapacity 200
int func(void){
int a = KCapacity;
int b = 256;
int c = add(a, b);
return c;
}
打印编译阶段
clang -ccc-print-phases main.m
打印结果
//0:输入文件:找到源文件
0: input, "main.m", objective-c
//1:预处理阶段:处理宏的替换、头文件的导入等
1: preprocessor, {0}, objective-c-cpp-output
//2:编译阶段:进行此法分析、语法分析、检查语法是否正确,生成IR
2: compiler, {1}, ir
//3:后端:LLVM会通过一个一个的Pass去优化,每个Pass做一些事情,最终生成汇编代码
3: backend, {2}, assembler
//4:生成目标文件
4: assembler, {3}, object
//5:链接:链接需要的动态库和静态哭,生成客执行文件。
5: linker, {4}, image
//6:通过不同的架构生成对应的可执行文件
6: bind-arch, "x86_64", {5}, image
1.预处理阶段
- 执行命令
clang -E explorer.m
,结果如下图,可以看到,宏定义直接被替换掉了,导入了头文件中的函数。
2.编译阶段
-
词法分析:执行命令
clang -fmodules -fsyntax-only -Xclang -dump-tokens explorer.m
,结果会把代码切成一个个的Token。这里可以看到在int
、func
、(
、)
、{
、int
等,这种粒度上进行了拆分。
-
语法分析:执行命令
clang -fmodules -fsyntax-only -Xclang -ast-dump explorer.m
。FunctionDecl
是函数的声明,标记在explorer.m的第6行第一个字符开始,到11行第1个字符结束,函数名称为func,返回值为int,参数列表为void。CompoundStmt
复合语句,这里表示函数体从第5行第13个字符开始到第7行第一个字符结束。VarDecl
表示变量声明。
3.生成汇编代码
- 执行命令:
clang -S -fobjc-arc -emit-llvm explorer.m
可以生成LLVM
IR文件,结果如下图:
%1 = alloca i32, align 4
:表示开辟一个(int32类型)32位大小的内存空间,4字节对齐,返回值放在%1.
store i32 200, i32* %1, align 4
:表示将int32类型的200,存储到%1。
%4 = load i32, i32* %1, align 4
:表示将%1中的值加载到内存,放在%5。
%6 = call i32 @add(i32 %4, i32 %5)
:表示调用add函数,参数是%4和%5的值,返回值存储在%6.
ret i32 %7
:表示return%7中的值。
IR代码的生成还可以设置优化级别:分别是-O0
、-O1
、-O2
、-O3
、-Os
,使用clang -O1 -S -fobjc-arc -emit-llvm explorer.m
.
如果开启bitcode,会进一步优化,从.ll到.bc中间代码:clang -emit-llvm -c explorer.ll -o explorer.bc
.
通过.ll或者.bc生成汇编代码:clang -S -fobjc-arc explorer.ll -o explorer.s
或clang -S -fobjc-arc explorer.bc -o explorer.s
4.生成目标文件(汇编器生成.o文件)
目标文件的生成,是汇编器以汇编代码作为输入、将汇编代码转换为机器代码,最后输出目标文件object file。命令clang -fmodules -c explorer.s -o explorer.o
5.生成可执行文件(链接,生成mach-o文件)
连接器把编译产生的.o文件和库(.dylib和.a文件)生成一个mach-o文件。指令clang explorer.o -o explorer
。
这里没有需要找到main函数才能执行成功。我们将最初的源码中的func改成main即可。然后走一遍生成.ll->生成.s->生成.o再执行如上代码即可成功生成可执行文件。
查看链接之后的符号命令:xcrun nm -nm explorer
,结果如下,可以看到我们定义的函数。
二、LLVM的编译和Clang插件的编写
LLVM项目下载
//LLVM项目下载:
git clone https://mirrors.tuna.tinghua.edu.cn/git/llvm/llvm.git
//在llvm/tools下下载Clang:
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
//在llvm.projects目录下下载compiler-rt,libcxx, libcxxabi
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
//在Clang下安装extra工具
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-e xtra.git
LLVM编译
//通过brew 安装 cmake
brew install cmake
//在llvm目录同级创建一个xcode项目目录,比如build-xcode,然后进入到这个文件夹执行命令即可
mkdir build-code
cd build-code
came -G Xcode ../llvm
编译到最后如果没有error,就是构建xcode项目成功了。
创建插件
- 创建插件目录:在
/llvm/tools/clang/tools
目录下创建文件夹如NXChecker
- 修改
/llvm/tools/clang/tools
目录下的CMakeLists.txt
文件,末尾新增add_clang_subdirectory(NXChecker)
- 在
NXChecker
目录下新建一个NXChecker.cpp
的文件和CMakeLists.txt
文件。在CMakeLists.txt
中添加如下代码
add_llvm_library(NXChecker MODULE BUILDTREE_ONLY NXChecker.cpp)
- 进入
build-xcode
重新生成一下Xcode
项目,cmake -G Xcode ../llvm
- 最后就可以在
LLVM
的Xcode
项目中看到Loadable modules
目录下有自己的Plugin
目录了,所有的插件代码将在NXChecker
中完成编写。
编写插件
(略)
编写完成插件后,运行一下,切换到当前插件的Target-NXChecker
对应的scheme-NXChecker
,然后运营一下,会发现/build-xcode/Debug/lib/NXChecker.dylib
文件,这个就是我们插件的可执行文件。
测试插件
~/Desktop/LLVM/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/ -Xclang -load -Xclang ~/Desktop/LLVM/build_xcode/Debug/lib/NXChecker.dylib -Xclang -add-plugin -Xclang NXChecker -c ~/Workspace/Microapp/iOSApp-Objective-C/iOSApp-Objective-C/ViewController.m
- clang文件的路径:
~/Desktop/LLVM/build_xcode/Debug/bin/clang
- SDK路径:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/
- 插件路径:
~/Desktop/LLVM/build_xcode/Debug/lib/NXChecker.dylib
- 测试源码文件路径:
~/Workspace/Microapp/iOSApp-Objective-C/iOSApp-Objective-C/ViewController.m
Xcode集成插件
- 在
Build Settings
->Other C Flags
中添加如下内容:-Xclang -load -Xclang ~/Desktop/LLVM/build_xcode/Debug/lib/NXChecker.dylib -Xclang -add-plugin -Xclang NXChecker
- 新增用户自定义设置
Add User-Defined Setting
- 在User-Defined中新增
CC:~/Desktop/LLVM/build-xcode/Debug/bin/clang
;CXX:~/Desktop/LLVM/build-xcode/Debug/bin/clang++
- 将Build Settings中
Enable Index-While-Building Functionality
从Default
改成NO
。