本文主要是理解LLVM编译流程以及clang插件的开发。
LLVM
LLVM是架构编译器的框架系统
,以C++编写而成,用于优化
任意程序语言编写的程序的编译时间
(compile-time)、链接时间
(link-time)、运行时间
(run-time)以及空闲时间
(idle-time)。对开发者保持开放,并兼容已有脚本。
传统编译器设计
源码 Source Code + 前端 Frontend + 优化器 Optimizer + 后端 Backend(代码生成器 CodeGenerator)+ 机器码 Machine Code,如下图所示
主要分为三部分:
1. 编译器前端(Frontend)
编译器前端的任务是解析源代码
,它会进行词法分析
,语法分析
,语义分析
,检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree,AST)
,LLVM的前端还会生成中间代码(intermediate representation, IR)
.
2. 优化器(Optimizer)
优化器负责进行各种优化,改善代码的运行时间,例如消除冗余计算。
3. 后端(Backend)/代码生成器(CodeGenerator)
将代码映射到目标指令集。生成指定平台机器语言
,并且进行机器相关的代码优化
。
ios的编译器架构
Objective C/C/C++ 使用的编译器前端是Clang,Swift是Swift,后端都是LLVM。
LLVM 的设计
LLVM最重要的地方就是支持多种源语言或多种硬件架构
,通过通用的代码表示形式IR,类似于桥接模式,实现了前后端分离
。
Clang
LLVM项目中的一个子项目
,负责C,C++,Object-C语言的编译器,在整个LLVM架构中,属于编译器前端。通过clang的学习,可以更好的应用到项目中,比如通过Clang插件,不仅能够检查代码规范
,还能够进行无用代码分析
、自动埋点打桩
,线下测试分析
、方法名混淆
等。
clang插件本身的编写和使用并不复杂,关键是如何更好的应用到工作中,
编译流程
我们通过一个简单例子来看下一下完整过程。 新建一个main.m 文件
#import <Foundation/Foundation.h>
#define DEFINEEight 8
int main(){
@autoreleasepool {
int eight = DEFINEEight;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:”starming”];
int rank = eight + six;
NSLog(@“%@ rank %d”, site, rank);
}
return 0;
}
复制代码
终端输入命令:
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
复制代码
主要有六个阶段
- 输入文件,找到源文件
- 预处理:进行
宏的替换、头文件的导入,条件编译
- 编译:进行
词法分析、语法分析
、检测语法是否正确,生成IR
- 后端:LLVM会通过一个一个
Pass去优化
,最终生成汇编代码 - 汇编:汇编代码生成目标文件
- 链接:
链接动态库和静态库,
生成可执行文件 - 绑定:通过不同的架构生成对应的可执行文件
预处理阶段
执行
clang -E main.m
复制代码
执行完毕后就可以看到头文件的导入和宏的替换。
编译阶段
编译阶段主要是进行词法、语法等的分析和检查,然后生成中间代码IR
1. 词法分析
这里会把代码切成一个个token
,比如大小括号、等于号还有字符串等。
可以通过如下命令查看。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
复制代码
如果头文件找不到,指定sdk.
clang -isysroot (自己SDK路径) -fmodules -fsyntax-only -Xclang -dump-tokens main.m
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.1.sdk/ -fmodules -fsyntax-only -Xclang -dump-tokens main.m
复制代码
2. 语法分析
词法分析完成后就是语法分析
,它的任务是验证语法是否正确
,在词法分析的基础上将单词序列组合成各类此法短语,如程序、语句、表达式 等等,然后将所有节点组成抽象语法树
(Abstract Syntax Tree,AST),语法分析程序判断程序在结构上是否正确
。
可以通过如下命令查看。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
复制代码
生成抽象语法树
3. 生成中间代码IR(intermediate representation)
完成以上步骤后,就开始生成中间代码IR了,代码生成器
(Code Generation)会将语法树自顶向下遍历
逐步翻译成LLVM IR
,
通过下面命令可以生成.ll的文本文件
,查看IR代码。
clang -S -fobjc-arc -emit-llvm main.m
复制代码
OC代码在这一步会进行runtime桥接,:property合成、ARC处理等 IR 的基本语法
@ %局部符号 未命名
- @ 全局符号
- % 局部符号
- alloca 开辟空间
- align 内存对齐
- i32 32个bit 4个字节
- store 写入内存
- load 读取数据
- call 调用函数
- ret 返回 IR的优化 LLVM优化级别分别是-O0 -O1 -O2 -O3 -Os
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
复制代码
IR文件在OC中是可以进行优化的,一般设置是在target - Build Setting - Optimization Level
(优化器等级)中设置。LLVM的优化级别分别是-O0 -O1 -O2 -O3 -Os
(第一个是大写英文字母O),下面是带优化的生成中间代码IR的命令
bitCode
- xcode7以后开启bitcode,苹果会做进一步优化,生成.bc的中间代码,我们通过优化后的IR代码生成.bc代码
clang -emit-llvm -c main.ll -o main.bc
复制代码
生成汇编代码
我们通过最终的.bc或者.ll代码
生成汇编代码
clang -S -fobjc-arc main.bc -o main.s clang -S -fobjc-arc main.ll -o main.s
复制代码
复制代码
另外,生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
复制代码
生成目标文件
目标文件的生成,是汇编器以汇编代码作为插入
,将汇编代码转换为机器代码
,最后输出目标文件(object file)
clang -fmodules -c main.s -o main.o
复制代码
复制代码
可以通过nm
命令,查看下main.o中的符号
$xcrun nm -nm main.o
复制代码
以下是main.o中的符号,其文件格式为 目标文件
undefined
表示在当前文件暂时找不到符号
external
表示这个符号是外部可以访问
的
链接
链接主要是链接需要的动态库和静态库,生成可执行文件,其中
- 静态库会和可执行文件合并
- 动态库是独立的
clang main.o -o main
复制代码
复制代码
查看链接之后的符号
$xcrun nm -nm main
复制代码
复制代码
绑定
绑定主要是通过不同的架构,生成对应的mach-o格式可执行文件
LLVM 编译
我已经整理好了对应的下载通过xcode脚本
#!/bin/bash
LLVMPath=`pwd`
# 1. 下载LLVM项目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm-project.git
# 2. 下载Clang
cd llvm/tools/
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
# 3. 下载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
# 4. 安装extra工具
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
# 5. 安装cmake
if cmake >/dev/null 2>&1
then
echo "cmake已经安装"
else
echo "cmake未安装"
echo "cmake执行安装"
brew install cmake >> /dev/null
if test $? -eq
then
echo "安装cmake成功"
else
echo "安装cmake失败"
fi
fi
# 6. 通过xcode编译
echo "通过xcode编译"
cd $LLVMPath
mkdir llvm_build
cd llvm_build
cmake -G Xcode ../llvm
复制代码
运行成功之后就会进入llvm_build,就是我们通过xcode方式编译好的工程。
Clang插件
创建CLPlugin插件
在这个路径下创建一个CLPlugin文件夹,
创建一个CLPlugin.cpp 文件和 CMakeLists.txt文件
CMakeList.txt文件添加
add_llvm_library( CLPlugin MODULE BUILDTREE_ONLY CLPlugin.cpp)
进入llvm_build路径下重新编译一下
cmake -G Xcode ../llvm
编写插件代码
在CLPlugin
目录下的CLPlugin.cpp
文件中,加入以下代码
#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
namespace CLPlugin {
class CLMatchCallback: public MatchFinder::MatchCallback{
private:
CompilerInstance &CI;
//判断是否是自己的文件
bool isUserSourceCode(const string fileName){
if (fileName.empty()) return false;
//非Xcode中的代码都认为是用户的
if (fileName.find("/Applications/Xcode.app/") == 0) return false;
return true;
}
//判断是否应该用copy修饰
bool isShouldUseCopy(const string typeStr){
if(typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos){
return true;
}
return false;
}
public:
//3.、自定义回调类,继承自MatchCallback扫描完毕的回调函数
CLMatchCallback(CompilerInstance &CI):CI(CI){}
void run(const MatchFinder::MatchResult &Result) {
//通过结果获取到节点对象
const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");
//获取文件名称(包含路径的)
string fileName = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if (propertyDecl && isUserSourceCode(fileName)) {//如果节点有值 && 不是系统文件!
//节点的类型 转成字符串
string typeStr = propertyDecl->getType().getAsString();
//拿到节点的描述信息
ObjCPropertyAttribute::Kind attrKind = propertyDecl->getPropertyAttributes();
// 判断是否应该使用copy,但是没有使用copy
if (isShouldUseCopy(typeStr) && !(attrKind & ObjCPropertyAttribute::kind_copy)) {//应该使用copy但是没有使用Copy
//诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//Report 报告
diag.Report(propertyDecl->getLocation(),diag.getCustomDiagID(DiagnosticsEngine::Error, "这个地方应该用Copy"));
// cout<<typeStr<<"应该使用copy修饰但是没有用!发出警告!!"<<endl;
}
}
}
};
//2. 自定义的CLConsumer,继承自ASTConsumer,用于监听AST节点的信息 -- 过滤器
class CLConsumer:public ASTConsumer{
private:
//MatchFinder AST 节点的过滤器
MatchFinder matcher;
CLMatchCallback callback;
public:
CLConsumer(CompilerInstance &CI):callback(CI){
//添加一个MatchFinder去匹配ObjCPropertyDecl节点
//回调!
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//解析完毕一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D){
// cout<<"正在解析...."<<endl;
return true;
}
//当整个文件都解析完成后回调!!
void HandleTranslationUnit(ASTContext &Ctx) {
cout<<"文件解析完毕!!"<<endl;
matcher.matchAST(Ctx);
}
};
//1. 定义一个类 继承PluginASTAction,实现我们自定义的Action,自定义AST语法书树行为
class CLASTAction:public PluginASTAction{
public:
bool ParseArgs(const CompilerInstance &CI, const vector<string> &arg) {
return true;
}
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<CLConsumer> (new CLConsumer(CI));
}
};
}
//4. 注册插件!
static FrontendPluginRegistry::Add<CLPlugin::CLASTAction> X("CLPlugin","this is the description");
复制代码
简单汇总下编写过程:
- 第一步,编写 PluginASTAction 代码处理入口参数。
- 第二步,通过 ASTConsumer访问所有 AST 节点,获取想要的内容。
- 第三步,编写 MatchCallback回调函数。
- 第四步,注册 Clang 插件,提供外部使用。
测试一下
编译成功后生成clang文件路径,CLPlugin路径在lib下面
//命令格式
自己编译的clang文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator15.1.sdk/ -Xclang -load -Xclang 插件(.dyld)路径 -Xclang -add-plugin -Xclang 插件名 -c 源码路径
复制代码
修改为自己sdk路径
Xcode集成插件
使用 Clang 插件可以通过 -load 命令
行选项加载包含插件注册表的动态库
,
-load 命令行会加载已经注册了的所有 Clang 插件。使用 -plugin 选项选择要运行的 Clang 插件。Clang 插件的其他参数通过 -plugin-arg-来传递。
cc1 进程类似一种预处理,这种预处理会发生在编译之前。cc1 和 Clang driver 是两个单独的实体,cc1 负责前端预处理,Clang driver 则主要负责管理编译任务调度,每个编译任务都会接受 cc1 前端预处理的参数,然后进行调整。
有两个方法可以让 -load 和 -plugin 等选项到 Clang 的 cc1 进程中:
一种是,直接使用 -cc1 选项,缺点是要在命令行上指定完整的系统路径配置;
另一种是,使用 -Xclang 来为 cc1 进程添加这些选项。-Xclang 参数只运行预处理器,直接将后面参数传递给 cc1 进程,而不影响 clang driver 的工作。
- 在
Build Settings
栏目中新增两项用户定义的设置,分别是CC
和CXX
CC
对应的是自己编译的clang
的绝对路径CXX
对应的是自己编译的clang++
的绝对路径- 接下来在
Build Settings
中搜索index
,将Enable Index-Wihle-Building Functionality
的Default
改为NO
最后重新编译项目,我们的插件就开始代码检查了。