在Xcode中,当咱们按下command + B进行build操做后发生了那些事情,这是一个将代码编译的过程。Xcode如今使用的编译器是LLVM,Xcode 早期使用的是GCC编译器,因为一些历史缘由,从Xcode5开始正式过渡到使用LLVM编译器。下文将着重介绍LLVM。html
LLVM项目是模块化、可重用的编译器以及工具链技术的集合。前端
美国计算机协会(ACM)将其 2012 年软件系统奖颁给了LLVM,以前曾得到此奖项的软件和技术包括:Java、Apache、Mosaic、the World Wide Web、SmallTalk、UNIX、Eclipse等等。c++
LLVM项目的发展起源于2000年伊利诺伊大学厄巴纳-香槟分校维克拉姆·艾夫(Vikram Adve)与克里斯·拉特纳(Chris Lattner)的研究,他们想要为全部静态及动态语言创造出动态的编译技术。LLVM是以BSD受权来发展的开源软件。2005年,苹果电脑雇用了克里斯·拉特纳及他的团队为苹果电脑开发应用程序系统,LLVM为现今Mac OS X及iOS开发工具的一部分。git
LLVM的命名最先源自于底层虚拟机(Low Level Virtual Machine)的首字母缩写,因为这个项目的范围并不局限于建立一个虚拟机,这个缩写致使了普遍的疑惑。官方描述以下:The name “LLVM” itself is not an acronym;it is the full name of the project。LLVM这个名称并非首字母缩略词,它是项目的全名。github
LLVM开始成长以后,成为众多编译工具及低级工具技术的统称,使得这个名字变得更不贴切,开发者于是决定放弃这个缩写的意涵,现今LLVM已单纯成为一个品牌,适用于LLVM下的全部项目,包含LLVM中介码(LLVM IR)、LLVM除错工具、LLVM C++标准库等。编程
目前NDK/Xcode均采用LLVM做为默认的编译器。swift
前端将各类类型的源代码编译为中间代码,也就是bitcode,在LLVM体系内,不一样的语言有不一样的编译器前端,常见的如clang负责 c/c++/oc的编译,flang负责fortran的编译,swiftc负责swift的编译等等。后端
不一样的先后端使用统一的中间代码LLVM Intermediate Representation(LLVM IR)。xcode
优化阶段是一个通用的阶段,针对的是统一的LLVM IR,不管是新的编程语言,仍是支持新的硬件设备,都不须要对优化阶段作修改,具体是对bitcode进行各类类型的优化,将bitcode代码进行一些逻辑的转换,使得代码效率更高,体积更小,好比DeadStrip/SimplifyCFG。bash
后端,也叫CodeGenerator,负责把优化后的bitcode编译为指定目标架构的机器码,好比 X86Backend负责把bitcode编译为x86指令集的机器码。
GCC相比之下,先后端耦合在了一块儿。因此,GCC支持一门新的语言,或是为了支持一个新的平台,就变得异常困难。
LLVM如今被做为实现各类静态和运行时编译语言通用基础架构(GCC 家族、Java、.Net、Python、Ruby、Scheme、Haskell、D等)。
LLVM体系中,不一样语言源代码将会被转化为统一的bitcode格式,三个模块相互独立,能够充分复用。好比,若是开发一门新的语言,只要制造一个该语言的前端,将源码编译为bitcode,优化和后端不用管。同理,若是新的芯片架构问世,只需基于LLVM从新编写一套目标平台的后端便可。
LLVM项目的一个子项目。
基于LLVM架构的C/C++/Objective-C/Objective-C++编译器前端。
相比于 GCC,Clang具备以下优势:
编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug 模式下编译 OC 速度比 GCC 快 3 倍);
占用内存小:Clang生成的AST所占用的内存是GCC的五分之一左右;
模块化设计:Clang采用基于库的模块化设计,易于IDE集成及其余用途的重用;
诊断信息可读性强:在编译过程当中,Clang建立并保留了大量详细的元数据(metadata),有利于调试和错误报告;
设计清晰简单,容易理解,易于扩展加强。
客观的说GCC也有不少优势:例如支持多平台,基于C无需 C++编译器便可编译。这个优势到苹果那里反而成了缺点,苹果须要的是快。
clang -ccc-print-phases main.m
复制代码
clang -E main.m -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks
复制代码
词法分析,生成Token
int sum(int a,int b){
int c = a + b;
return c;
}
复制代码
clang -fmodules -E -Xclang -dump-tokens main.m
复制代码
这个命令的做用是,显示每一个Token的类型、值,以及位置。参考该连接,能够看到Clang定义的全部Token类型。 能够分为下面这4类:
利用上面输出的Token先按照语法组合成语义,生成相似VarDecl这样的节点,而后将这些节点按照层级关系构成抽象语法树(AST)。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
复制代码
TranslationUnitDecl是根节点,表示一个编译单元;Decl表示一个声明;Expr表示的是表达式;Literal表示字面量,是一个特殊的Expr;Stmt表示陈述。
除此以外,Clang还有众多种类的节点类型。Clang里,节点主要分红Type类型、Decl声明、Stmt陈述这三种,其余的都是这三种的派生。经过扩展这三类节点,就可以将无限的代码形态用有限的形式来表现出来。
LLVM IR有3种表示形式,但本质上是等价的。
clang -S -emit-llvm main.m
复制代码
clang -c -emit-llvm main.m
复制代码
.ll 文件部份内容,以下:
基于LLVM 、Clang能够作不少实践,以下:
官方参考:
应用:语法树分析、语言转换等
OCLint、Clang静态分析器(Clang Static Analyzer)
Clang插件开发
官方参考:
应用:代码检查(命名规范、代码规范)等
官方参考:
应用:中间代码优化、代码混淆等
llvm-tutorial-cn.readthedocs.io/en/latest/i…
kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/lates…
Clang使用模块化设计,能够将自身功能以库的方式来供上层应用来调用。好比,编码规范检查、IDE 中的语法高亮、语法检查等上层应用,都是使用Clang库的接口开发出来的。Clang有三个接口库能够供上层应用调用,分别是LibClang、Clang Plugin、LibTooling。
LibClang为了兼容更多Clang版本,相比Clang少了不少功能;Clang Plugin和LibTooling具有Clang 的全量能力。Clang Plugin编写代码的方式,和LibTooling几乎同样,不一样的是Clang Plugin还可以控制编译过程,能够加warning或者直接中断编译提示错误。另外,编写好的LibTooling可以很是方便地转成Clang Plugin。 所以,Clang Plugin在功能上是最全的。
下载 LLVM Project
git clone https://github.com/llvm/llvm-project.git
复制代码
上图中,clang目录就是类C语言编译器的代码目录;llvm目录的代码包含两部分,一部分是对源码进行平台 无关优化的优化器代码,另外一部分是生成平台相关汇编代码的生成器代码;lldb目录里是调试器的代码;lld里是连接器代码。
macOS属于类UNIX平台,所以既能够生成Makefile文件来编译,也能够生成Xcode工程来编译。
进入llvm-project文件目录,生成Makefile文件:
cmake -DLLVM_ENABLE_PROJECTS=clang -G "Unix Makefiles" ../llvm
复制代码
生成Xcode工程,可使用以下命令
cmake -G Xcode -DLLVM_ENABLE_PROJECTS=clang ../llvm
复制代码
想要更多地了解CMake的语法和功能,你能够查看官方文档。
执行cmake命令时,你可能会遇到下面的提示:
-- The C compiler identification is unknown -- The CXX compiler identification is unknown CMake Error at CMakeLists.txt:39 (project):
No CMAKE_C_COMPILER could be found.
CMake Error at CMakeLists.txt:39 (project):
No CMAKE_CXX_COMPILER could be found.
复制代码
这代表cmake没有找到代码编译器的命令行工具。分两种状况处理:
xcode-select --install
复制代码
sudo xcode-select --reset
复制代码
生成Xcode工程后,打开生成的LLVM.xcodeproj文件,选择Automatically Create Schemes。
生成Xcode项目后再利用Xcode进行编译,可是速度很慢
add_clang_subdirectory(mskj-plugin)
复制代码
add_llvm_library(MSKJPlugin MODULE MSKJPlugin.cpp PLUGIN_TOOL clang)
复制代码
MSKJPlugin是插件名,MSKJPlugin.cpp是源代码文件,这段代码是指,要将Clang插件代码集成到LLVM的Xcode工程中,并做为一个模块进行编写调试。添加了Clang插件的目录和文件后,再次用cmake命令生成Xcode工程,里面就可以集成MSKJPlugin.cpp文件。
① 编写PluginASTAction代码
因为Clang插件是没有main函数的,入口是PluginASTAction的ParseArgs函数。因此,编写Clang插件还要实现ParseArgs来处理入口参数。代码以下所示:
class MSKJASTAction: public PluginASTAction {
public:
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {
return unique_ptr<MSKJASTConsumer> (new MSKJASTConsumer(ci));
}
bool ParseArgs(const CompilerInstance &ci, const vector<string> &args) {
return true;
}
};
复制代码
② 编写ASTConsumer
FrontActions是编写Clang插件的入口,也是一个接口,是基于ASTFrontendAction的抽象基类。FrontActions为接下来基于AST操做的函数提供了一个入口和工做环境。
经过这个接口,你能够编写在编译过程当中自定义的操做,具体方式是:经过ASTFrontendAction在 AST上自定义操做,重载CreateASTConsumer函数返回你本身的Consumer,以获取AST上的 ASTConsumer单元。ASTConsumer能够提供不少入口,是一个能够访问AST的抽象基类,能够重载 HandleTopLevelDecl()和 HandleTranslationUnit()两个函数,以接收访问AST时的回调。其中,HandleTopLevelDecl()函数是在访问到全局变量、函数定义这样最上层声明时进行回调,HandleTranslationUnit()函数会在接收每一个节点访问时的回调。
class MSKJASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
MSKJHandler handler;
public:
MSKJASTConsumer(CompilerInstance &ci) :handler(ci) {
matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
}
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
复制代码
③ 处理节点
class MSKJHandler : public MatchFinder::MatchCallback {
private:
CompilerInstance &ci;
public:
MSKJHandler(CompilerInstance &ci) :ci(ci) {}
void run(const MatchFinder::MatchResult &Result) {
if (const ObjCInterfaceDecl *decl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
size_t pos = decl->getName().find('_');
if (pos != StringRef::npos) {
DiagnosticsEngine &D = ci.getDiagnostics();
SourceLocation loc = decl->getLocation().getLocWithOffset(pos);
D.Report(loc, D.getCustomDiagID(DiagnosticsEngine::Error, "MSKJ:类名中不能带有下划线"));
}
}
}
};
复制代码
在Clang插件源码中编写注册代码。编译器会在编译过程当中从动态库加载Clang插件。使用FrontendPluginRegistry::Add<>在库中注册插件。注册Clang插件的代码以下:
static FrontendPluginRegistry::Add<MSKJPlugin::MSKJASTAction> X("MSKJPlugin", "The MSKJPlugin is my first clang-plugin.");
复制代码
在Clang插件代码的最下面,定义的MSKJPlugin字符串是命令行字符串,供之后调用时使用,The MSKJPlugin is my first clang-plugin是对Clang插件的描述。
利用CMake命令从新生成Xcode工程,可在Loadable modules下看到MSKJPlugin:
选择MSKJPlugin这个target进行编译,编译完会生成一个动态库文件。
LLVM官方有一个完整可用的Clang插件示例,能够帮咱们打印出最上层函数的名字。
经过学习这个插件示例,看看如何使用Clang插件。
使用Clang插件能够经过-load命令行选项加载包含插件注册表的动态库,-load命令行会加载已经注册了的全部Clang插件。使用-plugin选项选择要运行的Clang插件。Clang插件的其余参数经过-plugin-arg-来传递。
cc1进程相似一种预处理,这种预处理会发生在编译以前。cc1和Clang driver是两个单独的实体,cc1负责前端预处理,Clang driver则主要负责管理编译任务调度,每一个编译任务都会接受cc1前端预处理的参数,而后进行调整。
有两个方法可让-load 和-plugin等选项到Clang的cc1进程中:
下面是一个编译Clang插件,而后使用-Xclang加载使用Clang插件的例子:
$ export BD=/path/to/build/directory
$ (cd $BD && make PrintFunctionNames )
$ clang++ -D_GNU_SOURCE -D_DEBUG -D__STDC_CONSTANT_MACROS \
-D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS -D_GNU_SOURCE \
-I$BD/tools/clang/include -Itools/clang/include -I$BD/include -Iinclude \ tools/clang/tools/clang-check/ClangCheck.cpp -fsyntax-only \
-Xclang -load -Xclang $BD/lib/PrintFunctionNames.so -Xclang \
-plugin -Xclang print-fns
复制代码
上面命令中,先设置构建的路径,再经过make命令进行编译生成PrintFunctionNames.so,最后使用clang命令配合-Xclang参数加载使用Clang插件。
你也能够直接使用-cc1参数,可是就须要按照下面的方式来指定完整的文件路径:
$ clang -cc1 -load ../../Debug+Asserts/lib/libPrintFunctionNames.dylib -plugin print-fns some-input-file.c
复制代码
实现更复杂的插件功能,能够利用clang的API对语法树进行相应的分析与处理。
关于AST的资料:
Clang插件自己的编写和使用并不复杂,关键是如何更好地应用到工做中,经过Clang插件不光可以检查代 码规范,还可以进行无用代码分析、自动埋点打桩、线下测试分析、方法名混淆等。
理解iOS的编译原理,有利于咱们更加深层次的理解程序,让咱们从底层的角度去看待问题和思考问题的解决方案。
范冲冲,民生科技有限公司 用户体验技术部 移动金融开发平台开发工程师
Thanks!