前言
计算机是只能直接理解机器语言,而不能直接理解高级语言的,所以计算机要执行高级语言编写的程序,就必须要把高级语言翻译成机器语言。这种翻译有两种方式,一是编译,一是解释。所以不能被计算机直接识别的编程语言也可以分为解释型语言和编译型语言。
解释型语言
程序不需要提前编译,只需要在运行时使用专门的解释器对源程序逐行解释成特定平台的机器码并执行,代码在执行时才被解释器一行行动态翻译和执行。而且每执行一次都要翻译一次,因此效率比较低。由于其跨平台性比较好,一般用于脚本、辅助开发等。常见的解释型语言有Python
、JavaScript
、VBScript
、Perl
、Ruby
、MATLAB
等等。
其特点如下:
- 每次执行都需要将源代码逐行进行解释,效率较低
- 需要一个专门的解释器
- 跨平台性好,只要平台提供相应的解释器,就可以运行源代码,方便源程序移植
编译型语言
程序在执行之前需要一个专门的编译过程,使用专门的编译器,针对特定的平台,将源代码一次性的编译成可被该平台硬件执行的代码,并包装成该平台所能识别的可执行性程序的格式。在运行时不需要重新翻译,直接使用编译的结果即可。程序执行效率高,依赖编译器,跨平台性差些。一般用于开发操作系统、大型应用程序、数据库系统等。常见的编译型语言C
、C++
、Objective-C
等等。
其特点如下:
- 在执行前有一个专门的编译过程,将编码编译为可执行文件
- 运行时不需要编译,执行效率高
- 依赖编译器,跨平台性差
iOS
指令架构
Objective-C
作为一门编译型语言,在编译完成之后,其执行还需要依赖相关的指令集架构。苹果平台的iPhone
的处理器的指令集有armv7
、armv7s
、arm64
,而Mac
处理器的指令集i386
、x86_64
。i386
是针对intel
通用微处理器32位处理器,x86_64
是针对x86
架构的64位处理器。
不同机型的指令集
真机的指令集:
-
iPhone5S
及以上机型使用都是arm64
指令集,64位处理器 -
iPhone5
、iPhone5C
、iPad4
使用的则是armv7s
指令集,32位处理器 - 更低版本的手机和
iPad
则是armv7
指令集,32位处理器
模拟器指令集:
- 模拟器32位处理器测试需要
i386
架构, - 模拟器64位处理器测试需要
x86_64
架构,
Xcode
中指令集相关Build Setting
Architectures
:指定工程被编译成可支持哪些指令集类型,而支持的指令集越多,就会编译出包含多个指令集代码的数据包,对应生成二进制包就越大,也就是ipa
包会变大。Valid Architectures
:限制可能被支持的指令集的范围,也就是Xcode
编译出来的二进制包类型最终从这些类型产生,而编译出哪种指令集的包,则由Architectures
与Valid Architectures
的交集来确定。因此这个选项不能为空。Build Active Architecture Only
:指定是否只对当前连接设备所支持的指令集编译。当其值设置为YES
,是为了debug
的时候编译速度更快,它只编译Architecture
版本,而设置为NO
时,会编译所有的版本。所以,一般debug
的时候设置为YES
,release
的时候设置为NO
。
编译器相关
简单的说,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。其主要工作流程:源代码(source code)
→ 预处理器(preprocessor)
→ 编译器(compiler)
→ 目标代码(object code)
→ 链接器(Linker)
→ 可执行程序(executable)
。
总结一下,如图所示:
-
Frontend
:编译器前端,负责解析源代码。用于词法分析、语法分析、语义分析、检查代码的语法错误、构建抽象语法树AST(Abstract Syntax Tree)
。 -
Optimizer
:优化器,负责优化代码。如消除冗余等。 -
Backend
:后端,负责生成机器代码。将代码映射到对应的指令集,并负责生成相关的代码,并且优化。
Objective-C
作为一门编译型语言,其编译器就是clang
,而clang
又是LLVM
的一部分。
LLVM
概述
LLVM
命名最早源自于底层虚拟机(Low Level Virtual Machine)
的缩写,由于命名带来的混乱,目前LLVM
就是该项目的全称。
LLVM
以C++
编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)
、链接时间(link-time)
、运行时间(run-time)
以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
当编译器需要支持多种语言或者多种硬件架构的时候,LLVM
的强大之处就体现出来了。普通的编译器都是作为整体应用程序来编写的,只能对单语言进行支持。而LLVM
最核心之处就是使用通用的代码表示形式IR(intermediate representation)
,即不同的前端语言最终都转换成同一种IR
,让其可以为任何语言编写前端、为任何硬件编写后端。
Clang
是一个C++
编写、基于LLVM
、发布于LLVM BSD
许可证下的C/C++/Objective-C/Objective-C++
编译器。Clang
的诞生是为了代替GCC
,它是一个高度模块化开发的轻量级编译器,编译速度快、占用内存小、非常方便进行二次开发。
编译流程
首先我们在main.m
中实现如下代码:
通过clang
命令可以打印代码的编译流程:
clang -ccc-print-phases main.m
-
- input, "main.m", objective-c 输入文件:找到源文件
-
- preprocessor, {0}, objective-c-cpp-output 预处理阶段:包含导入头文件、替换宏定义等
-
- compiler, {1}, ir 编译阶段:进行词法分析、语法分析、检查语法的正确性,最终生成
IR
- compiler, {1}, ir 编译阶段:进行词法分析、语法分析、检查语法的正确性,最终生成
-
- backend, {2}, assembler 后端优化,生成汇编代码
-
- assembler, {3}, object 生成目标文件
-
- linker, {4}, image 链接需要的动态库和静态库,生成可执行文件
-
- bind-arch, "x86_64", {5}, image 通过不同的架构,生成相应的镜像文件
预处理阶段
使用如下命令,就可以查看预处理阶段做了什么:
clang -E main.m >> mian2.m
打开main2.m
:
可以看出头文件相关的信息都被导入,而且宏定义也被替换了。需要注意的是typedef
不是预处理阶段处理的。
编译阶段
词法分析
预处理阶段完成之后就会进入词法分析,这里会把代码切成一个个Token
,比如大小括号、字符串等等。使用如下命令,就可以查看词法分析做了什么:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
从结果可以看出,词法分析是把代码一个词一个词的分开,标记了具体的位置,如行号和第一个字符的位置。
语法分析
词法分析之后就是语法分析,语法分析所做的事情就是检查语法是否正确。它是在词法分析的基础上将拆分的单词组合成短语,然后将所有节点组合成抽象语法树(AST abstract syntax tree)
。使用如下命令,就可以查看语法分析做了什么:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
结合原来的代码,可以看出FunctionDecl
这就是main
函数的标识,说明从13行第1个字符到18行第1个字符就是main
函数。DeclStmt
就是参数节点。CallExpr
是调用函数的返回值,ImplicitCastExpr
是函数的参数指针,BinaryOperator
是运算符。ReturnStmt
则是返回值。
生成中间代码(IR)
这是LLVM
最独特的一点。语法分析之后,代码生成器就会根据语法树生成IR
。
IR
的语法:
-
@
全局标识 -
%
局部标识 -
alloca
开辟空间 -
align
内存对齐 -
i32
4字节 -
store
写入内存 -
load
读取数据 -
call
调用数据 -
ret
返回
使用如下命令,就可以查看生成的IR
:
clang -S -fobjc-arc -emit-llvm main.m
IR
的优化级别:O0
、O1
、O2
、O3
、Os
,第一个字符是英文大写字母的o
(哦)。其中O0
表示None
,O1
表示Fast
、O2
表示Faster
、O3
表示Fastest
、Os
表示Fastest Samllest
。
使用如下命令可以查看不同优化程度的IR
代码:
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
可以看出代码进行了很大幅度的优化,大部分冗余代码都被去掉了。
还可以使用如下命令将IR
代码生成bitCode
:
clang -emit-llvm -c main.ll -o main.bc
生成汇编代码
我们可以将IR
代码、bitCode
、或者是源码生成汇编代码,这里就不一一展示了。
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
生成目标文件
使用如下命令将汇编代码生成可执行文件:
clang -fmodules -c main.s -o main.o
我们可以通过如下命令查看main.o
文件的内容:
xcrun nm -nm main.o
结果如下:
xcrun nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _main
其中external
表示外部可以访问符号,undefined
表示当前文件未找到符号。
生成可执行文件
链接器把.o
文件和动态库、静态库进行链接,生成可执行文件。
通过如下命令即可:
clang main.o -o main
另外我们还可以通过如下命令,查看生成的可执行文件的符号。
xcrun nm -nm main
结果如下:
xcrun nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f40 (__TEXT,__text) external _main
0000000100002008 (__DATA,__data) non-external __dyld_private
(undefined) external _printf (from libSystem)
表示_printf
这个符号来自于libSystem
。
LLVM
下载及编译
相关文件下载
由于国内网络的显示,需要借助镜像下载LLVM
地址:https://mirror.tuna.tsinghua.edu.cn/help/llvm/
- 下载地址:
https://mirror.tuna.tsinghua.edu.cn/help/llvm/
- 在
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.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
- 在
clang
的tools
目录下安装clang-tools-extra
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
- 安装
cmake
brew install cmake
clang
编译
在我们下载的llvm
的同级文件中创建一个文件夹,使用cmake
编译成xcode
项目:
mkdir build_llvm_xcode
cd build_llvm_xcode
cmake -G Xcode ../llvm
打开工程,先不要编译(直接编译会非常的非常的非常的耗时。。。),选择手动创建Scheme
,添加clang
和libclang
。
实现clang
插件
前面我们大概熟悉了LLVM
工程,现在就来简单实现一个Xcode
的插件用以检测NSString *
属性未使用copy
修饰词并给予提示,更进一步的了解一下其原理。
创建插件文件
-
- 在
/llvm/tools/clang/tools
目录下创建一个文件夹TPlugin
- 在
-
- 修改
/llvm/tools/clang/tools
目录下的CMakeLists.txt
文件,添加一行add_clang_subdirectory(TPlugin)
- 修改
-
- 在
TPlugin
目录下创建两个文件TPlugin.cpp
、CMakeLists.txt
;在CMakeLists.txt
中输入如下代码:
- 在
add_llvm_library( TPlugin MODULE BUILDTREE_ONLY
TPlugin.cpp
)
-
- 在
build_llvm_xcode
文件夹中,重新执行如下代码:
- 在
cmake -G Xcode ../llvm
这样插件文件就创建成功了,我们可以从LLVM
的这个Xcode
工程的Loadable modules
目录下就可以看见自己创建的目录了。
编写插件代码
我们知道编译器检查语法错误是在语法分析生成抽象语法树的时候,所以这个插件的核心就是重写抽象语法树的相关功能。
我们实现如下代码,然后对其进行编译,查看语法树:
#import "TASTProperty.h"
@interface TASTProperty()
@property (nonatomic, copy) NSString *mString;
@property (nonatomic, copy) NSDictionary *mDict;
@property (nonatomic, strong) NSString *str01;
@property (nonatomic, strong) NSString *str02;
@end
@implementation TASTProperty
@end
可以清楚的看到,ObjCPropertyDecl
就是我们需要的语法树的节点,而且定义的属性都能看到其对应的修饰词。所以我们可以读取到语法树的节点,根据节点信息来做判断。
- 导入相关的库
#include
#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;
- 创建自定义的
ASTAction
,注册插件
namespace TPlugin {
// 继承PluginASTAction实现我们自定义的 Action
class TASTAction: public PluginASTAction {
public:
bool ParseArgs(const CompilerInstance &CI,const vector &arg){
return true;
}
unique_ptr CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr (new TConsumer(CI));
}
};
}
// 注册插件
static FrontendPluginRegistry:: Add
X("TPlugin", "This is the description of the plugin");
- 自定义
TConsumer
继承自ASTConsumer
,重写解析语法树的节点的方法
class TConsumer: public ASTConsumer {
private:
MatchFinder matcher;
TMatchCallback callback;
public:
TConsumer(CompilerInstance &CI):callback(CI) {
// 1. 添加一个MatchFinder去匹objcPropertyDecl节点
// 2. 回调在TMatchCallback的run方法里面
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"),&callback);
}
// 在整个文件都解析完后被调用
void HandleTranslationUnit(ASTContext &context) {
cout<<"语法树解析完毕了!"<
- 自定义
TMatchCallback
继承自MatchFinder
用来匹配节点 也就是选中特殊的节点,然后回调给TConsumer
class TMatchCallback: 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;
}
// 暂时就判断NSString才应该用copy修饰。
bool isNeedCopy(const string typeStr) {
return typeStr.find("NSString") != string::npos;
}
public:
TMatchCallback(CompilerInstance &CI):CI(CI) { }
void run(const MatchFinder::MatchResult &Result) {
//通过结果获取到节点。
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs("objcPropertyDecl");
//获取文件名称
string filename = CI.getSourceManager().getFilename(propertyDecl->getSourceRange().getBegin()).str();
if (propertyDecl && isUserSourceCode(filename)) {//如果节点有值,并且是用户文件
//拿到属性的类型
string typeStr = propertyDecl->getType().getAsString();
//拿到节点的描述信息
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
//判断是不是应该用Copy
if (isNeedCopy(typeStr) && !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
cout<getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "%0这个地方推荐用Copy"))<
这样插件的代码基本上就实现了,下面我们将插件继承到Xcode
上。
Xcode
集成插件
打开需要的测试的工程,修改Build Settings
相关设置。
- 在
Other C Flags
中添加如下内容:
-Xclang -load -Xclang TPlugin生成的动态库地址() -Xclang -add-plugin -Xclang TPlugin
- 将
Enable Index-Wihle-Building Functionality
的value
改成NO
。 - 在
Build Settings
目录栏点击加号,选择Add User-Defined Setting
,加入两项,一项key
是CC
,value
是自己编译的clang
的路径,一项key
是CXX
,value
是自己编译的clang++
的路径(.../build_llvm_xcode/Debug/bin/clang++)
。
编译程序,就可以看到会有提示:
总结
clang
编译程序的流程如下:
-
- 输入文件:找到源文件
-
- 预处理阶段:包含导入头文件、替换宏定义等
-
- 编译阶段:进行词法分析、语法分析、检查语法的正确性,最终生成
IR
- 编译阶段:进行词法分析、语法分析、检查语法的正确性,最终生成
-
- 后端优化,生成汇编代码
-
- 生成目标文件
-
- 链接需要的动态库和静态库,生成可执行文件
-
- 通过不同的架构,生成相应的镜像文件