什么是LLVM
- The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
- LLVM是一个模块化和可重用的编译器和工具链技术的集合。
作用:用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time).在2000年,Chris Lattner开发了这一套编译器工具库套件.后来随着LLVM的发展,LLVM可以用于常规编译器,JIT编译器,汇编器,调试器,静态分析工具等一系列跟编程语言相关的工作。
2012年,LLVM 获得美国计算机学会 ACM 的软件系统大奖,和 UNIX,WWW,TCP/IP,Apache,JAVA, Eclipse等齐名。
注:LLVM工程包含了一组模块化,可复用的编辑器和工具链。同其名字原意(Low Level Virtual Machine)不同的是,LLVM不是一个首字母缩写,而是工程的名字。
Xcode版本的相对应编译器的变迁
Xcode版本 | 编译器版本 |
---|---|
Xcode3之前 | GCC |
Xcode3 | GCC与 LLVM混合编译器 |
Xcode4 | LLVM-GCC 成为默认编译器 |
Xcode4.2 | LLVM3.0成为默认编译器 |
Xcode5 | LLVM5.0, 完成 GCC到LLVM的过渡 |
GCC -> LLVM 简介
GCC是 Xcode早期使用的一个强大的编译器.这个编译器被移植到各种系统中,其中就是 Mac OSX 操作系统,所以这就反映在 Xcode中,在早期的 Xcode 调试代码的一个工具就是 GDB,它是GNU调试器.
为什么从GCC变迁到LLVM?
Apple(包括中后期的NeXT)一直使用GCC作为官方的编译器。GCC作为开源世界的编译器标准一直做得不错,但Apple对编译工具会提出更高的要求。 一方面,是Apple对Objective-C语言(甚至后来对C语言)新增很多特性,但GCC开发者并不买Apple的帐——不给实现,因此索性后来两者分成两条分支分别开发,这也造成Apple的编译器版本远落后于GCC的官方版本。另一方面,GCC的代码耦合度太高,不好独立,而且越是后期的版本,代码质量越差,但Apple想做的很多功能(比如更好的IDE支持)需要模块化的方式来调用GCC,但GCC一直不给做。甚至最近,《GCC运行环境豁免条款(英文版)》从根本上限制了LLVM-GCC的开发。 所以,这种不和让Apple一直在寻找一个高效的、模块化的、协议更放松的开源替代品.
目前LLVM包含的主要子项目包括:
- LLVM Core:包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。
- Clang:一个C/C++/Objective-C编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具.
- dragonegg: gcc插件,可将GCC的优化和代码生成器替换为LLVM的相应工具。
- LLDB:基于LLVM提供的库和Clang构建的优秀的本地调试器。
- libc++、libc++ ABI: 符合标准的,高性能的C++标准库实现,以及对C++11的完整支持。
- compiler-rt:针对__fixunsdfdi和其他目标机器上没有一个核心IR(intermediate representation)对应的短原生指令序列时,提供高度调优过的底层代码生成支持。
- OpenMP: Clang中对多平台并行编程的runtime支持。
- vmkit:基于LLVM的Java和.NET虚拟机实
- polly: 支持高级别的循环和数据本地化优化支持的LLVM框架。
- libclc: OpenCL标准库的实现
- klee: 基于LLVM编译基础设施的符号化虚拟机
- SAFECode:内存安全的C/C++编译器
- lld: clang/llvm内置的链接器
LLVM编译架构
传统的静态编译器分为三个阶段:前端、优化和后端。
典型例子:GCC编译器, 如何做到解耦?
LLVM Three-Phase 编译器器架构:
架构优点:
不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
如果需要支持一种新的编程语言,那么只需要实现一个新的前端
如果需要支持一种新的硬件设备,那么只需要实现一个新的后端
优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改
LLVM现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
Clang/Swift - LLVM 编译器器架构:
Frontend:前端
- 词法分析、语法分析、语义分析、生成中间代码
Optimizer:优化器
- 中间代码优化
Backend:后端
- 生成机器码
作为LLVM提供的编译器前端,clang可将用户的源代码(C/C++/Objective-C)编译成语言/目标设备无关的IR(Intermediate Representation)实现。其可提供良好的插件支持,容许用户在编译时,运行额外的自定义动作。
Xcode
编译过程
在列出完整步骤之前可以先看个简单例子。看看是如何完成一次编译的。
#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;
}
复制代码
- 查看oc的c实现:
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
复制代码
生成的c++文件如下
int main(){
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int eight = 8;
int six = 6;
NSString* site = ((NSString * _Nullable (*)(id, SEL, const char * _Nonnull))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), (const char *)"starming");
int rank = eight + six;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_c__8jb7vhc96p1bhvf5gl7zw_9sj925cz_T_main_9c278d_mi_0, site, rank);
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
复制代码
Clang 命令
- Clang 在概念上是编译器前端,同时,在命令行中也作为一个“黑盒”的 Driver。
- 封装了了编译管线、前端命令、LLVM 命令、Toolchain 命令等,一 个 Clang ⾛走天下。
- ⽅便从 gcc 迁移过来。
例如上面的查看oc的c语言实现,可以利用clang重写objc:
$ clang -rewrite-objc mian.m
复制代码
利用clang命令查看整个编译过程
$ clang -ccc-print-phases main.m
复制代码
- 可以看到编译源文件需要的几个不同的阶段
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output // 预处理
2: compiler, {1}, ir // 编译生成IR(中间代码)
3: backend, {2}, assembler // 汇编器生成汇编代码
4: assembler, {3}, object // 生成机器码(目标文件)
5: linker, {4}, image // 链接
6: bind-arch, "x86_64", {5}, image // 根据运行平台,生成镜像文件(Image),也就是最后的可执行文件
复制代码
想看清clang前端的全部过程?接下来可以继续通过clang命令查看各阶段都做了哪些处理。
1⃣ Preprocess -预处理
$ clang -E main.m
复制代码
/*
... 头文件
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 185 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2
int main(){
@autoreleasepool {
int eight = 8;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
复制代码
这个过程的处理包括宏的替换,头文件的导入,以及类似#if的处理。
2⃣ Lexical Analysis - 词法分析
- 词法分析,也作 Lex 或者 Tokenization
- 将预处理理过的代码⽂文本转化成 Token 流
- 不校验语义
预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。
$ clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
复制代码
3⃣ Semantic Analysis - 语法分析
- 语法分析,在 Clang 中由 Parser 和 Sema 两个模块配合完 成
- 验证语法是否正确
- 根据当前语⾔言的语法,⽣生成语意节点,并将所有节点组合成 抽象语法树(AST)
如下例子:
int testAST(int a, int b) {
while (b != 0) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
return a;
}
复制代码
验证语法是否正确,然后将所有节点组成抽象语法树 AST 。
$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
复制代码
生成如下语法树:
`-FunctionDecl 0x7ffd8f84d678 <main.m:3:1, line:13:1> line:3:5 test 'int (int, int)'
|-ParmVarDecl 0x7ffd8f84d4f8 <col:10, col:14> col:14 used a 'int'
|-ParmVarDecl 0x7ffd8f84d570 <col:17, col:21> col:21 used b 'int'
`-CompoundStmt 0x7ffd8f84dba8 <col:24, line:13:1>
|-WhileStmt 0x7ffd8f84db30 <line:4:5, line:11:5>
| |-<<<NULL>>>
| |-BinaryOperator 0x7ffd8f84d7d8 <line:4:12, col:17> 'int' '!='
| | |-ImplicitCastExpr 0x7ffd8f84d7c0 <col:12> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7ffd8f84d778 <col:12> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
| | `-IntegerLiteral 0x7ffd8f84d7a0 <col:17> 'int' 0
| `-CompoundStmt 0x7ffd8f84db10 <col:20, line:11:5>
| `-IfStmt 0x7ffd8f84dad8 <line:6:9, line:10:9>
| |-<<<NULL>>>
| |-<<<NULL>>>
| |-BinaryOperator 0x7ffd8f84d880 <line:6:13, col:17> 'int' '>'
| | |-ImplicitCastExpr 0x7ffd8f84d850 <col:13> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7ffd8f84d800 <col:13> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
| | `-ImplicitCastExpr 0x7ffd8f84d868 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7ffd8f84d828 <col:17> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
| |-CompoundStmt 0x7ffd8f84d9a0 <col:20, line:8:9>
| | `-BinaryOperator 0x7ffd8f84d978 <line:7:13, col:21> 'int' '='
| | |-DeclRefExpr 0x7ffd8f84d8a8 <col:13> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
| | `-BinaryOperator 0x7ffd8f84d950 <col:17, col:21> 'int' '-'
| | |-ImplicitCastExpr 0x7ffd8f84d920 <col:17> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7ffd8f84d8d0 <col:17> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
| | `-ImplicitCastExpr 0x7ffd8f84d938 <col:21> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7ffd8f84d8f8 <col:21> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
| `-CompoundStmt 0x7ffd8f84dab8 <line:8:16, line:10:9>
| `-BinaryOperator 0x7ffd8f84da90 <line:9:13, col:21> 'int' '='
| |-DeclRefExpr 0x7ffd8f84d9c0 <col:13> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
| `-BinaryOperator 0x7ffd8f84da68 <col:17, col:21> 'int' '-'
| |-ImplicitCastExpr 0x7ffd8f84da38 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7ffd8f84d9e8 <col:17> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
| `-ImplicitCastExpr 0x7ffd8f84da50 <col:21> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7ffd8f84da10 <col:21> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
`-ReturnStmt 0x7ffd8f84db90 <line:12:5, col:12>
`-ImplicitCastExpr 0x7ffd8f84db78 <col:12> 'int' <LValueToRValue>
`-DeclRefExpr 0x7ffd8f84db50 <col:12> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
复制代码
4⃣ Static Analysis - 静态分析
- 通过语法树进行代码静态分析,找出非语法性错误
- 模拟代码执行路径,分析出 control-flow graph (CFG)【MRC下会分析出引用计数的错误】
- 预置了常用 Checker(检查器)
5⃣ 中间代码生成
- CodeGen 负责将语法树从顶至下遍历,翻译成 LLVM IR
- LLVM IR 是 Frontend 的输出,也是 LLVM Backend 的输 入,前后端的桥接语言
- 与 Objective-C Runtime 桥接
$ clang -S -fobjc-arc -emit-llvm main.m -o main.ll
复制代码
到这一步,LLVM前段编译器clang的工作已经基本做完了。
LLVM IR
- LLVM IR有3种表示形式,但本质是等价的:
- text:便于阅读的文本格式,类似于汇编语言,拓展名.ll
$ clang -S -emit-llvm main.m -o main.ll
复制代码
- memory:内存格式
- bitcode:二进制格式,拓展名.bc,
$ clang -c -emit-llvm main.m -o main.bc
复制代码
Optimizer优化器
SSA(Static Single Assignment)静态单一赋值优化
概念
In compiler design, static single assignment form (often abbreviated as SSA form or simply SSA) is a property of an intermediate representation (IR), which requires that each variable is assigned exactly once, and every variable is defined before it is used.
– From Wikipedia
从上面的描述可以看出,SSA 形式的 IR 主要特征是每个变量只赋值一次。相比而言,非SSA形式的IR里一个变量可以赋值多次。
优点
可以简化很多编译优化方法的过程;
对很多编译优化方法来说,可以获得更好的优化结果,
下面给出一个例子:
int main() {
int x, y;
x = 1;
x = 2;
y = x;
}
复制代码
- 非SSA
y := 1
y := 2
x := y
复制代码
显然,我们一眼就可以看出,上述代码第一行的赋值行为是多余的,第三行使用的 y 值来自于第二行中的赋值。对于采用非 SSA 形式 IR 的编译器来说,它需要做数据流分析(具体来说是到达-定义分析)来确定选取哪一行的 y 值。但是对于 SSA 形式来说,就不存在这个问题了。如下所示:
- SSA
y1 := 1
y2 := 2
x1 := y2
复制代码
我们不需要做数据流分析就可以知道第三行中使用的y来自于第二行的定义,这个例子很好地说明了SSA的优势。除此之外,还有许多其他的优化算法在采用SSA形式之后优化效果得到了极大提高。甚至,有部分优化算法只能在SSA上做。
- 这里 LLVM 会去做些优化工作,在Xcode的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass,官方有比较完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation
优化IR:(级别-03)
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
复制代码
- Pass 是 LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM 完整的优化和转化。
如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。
生成字节码:
clang -emit-llvm -c main.m -o main.bc
复制代码
注:xx.bc文件是位流格式,由于是二进制的,所以直接看就是一堆乱码,查看bitcode最好的方式是用hexdump工具。
6⃣ Assemble -生成Target相关汇编
clang -S -fobjc-arc main.m -o main.s
复制代码
7⃣ Assemble - 生成 Target 相关机器码 Object (Mach-O)
clang -fmodules -c main.m -o main.o
复制代码
8⃣ Link 生成 Executable 可执行文件
clang main.o -o main
执行
./main
输出
starming rank 14
复制代码
总结:Clang-LLVM 下,一个源文件的编译过程:
整个工作流程犹如:
完整的编译过程
1.优先编译cocopods里面的所有依赖文件
2.编译信息写入辅助信息,创建编译后的文件架构
3.处理打包信息。例如development环境下处理xxxx.entitlements的打包信息
4.执行cocopods编译前脚本 checkPods Manifest.lock
5.编译包内所有m文件 (使用Compile和Clang的几个主要命令)
6.链接需要的framework,例如AFNetworking.framework,Masonry.framework等信息
7.编译xib文件
8.copy Xib文件,图片等资源文件放到结果目录
9.编译imageAsserts
10.处理infoplist
11.执行Cocoapods脚本
12.copy标准库
13.创建.app文件和签名
我的深圳编译流程:
clang插件开发
准备工作
- 安装brew
brew是一个软件包管理工具,类似于centos下的yum或者ubuntu下的apt-get,非常方便,免去了自己手动编译安装的不便
brew 安装目录 /usr/local/Cellar
brew 配置目录 /usr/local/etc
brew 命令目录 /usr/local/bin
注:homebrew在安装完成后自动在/usr/local/bin加个软连接,所以平常都是用这个路径
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
复制代码
- 安装cmake
CMake是一个跨平台的编译(Build)工具,可以用简单的语句来描述所有平台的编译过程。
$ brew install cmake
复制代码
- 安装ninja
Ninja 是一个构建系统,与 Make 类似。作为输入,你需要描述将源文件处理为目标文件这一过程所需的命令。 Ninja 使用这些命令保持目标处于最新状态。 Ninja 的主要设计目标是速度。
$ brew install ninja
复制代码
下载编译工具
- 下载LLVM
// 大小648.2M
$ git clone https://git.llvm.org/git/llvm.git/
复制代码
- 下载clang
// 大小240.6M
$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/
复制代码
- 在LLVM源码同级目录下新建一个【llvm_build】目录(最终会在【llvm_build】目录下生成【build.ninja】)
$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安装路径
复制代码
- 依次执行编译、安装指令
$ ninja
复制代码
编译完毕后, 【llvm_build】目录大概 21.05 G(仅供参考)
$ ninja install
复制代码
安装完毕后,安装目录大概 11.92 G(仅供参考)
- 在llvm同级目录下新建一个【llvm_xcode】目录
$ cd llvm_xcode
$ cmake -G Xcode ../llvm
复制代码
开始开发插件
-
在【llvm/tools/clang/tools】源码目录下新建一个插件目录,假设叫做【yb-plugin】
-
在【llvm/tools/clang/tools/CMakeLists.txt】最后加入内容: add_clang_subdirectory(yb-plugin),小括号里是插件目录名
# libclang may require clang-tidy in clang-tools-extra.
add_clang_subdirectory(libclang)
add_clang_subdirectory(yb-plugin)
复制代码
- 在【yb-plugin】目录下新建一个【YBPlugin.cpp】和【CMakeLists.txt】,并在【CMakeLists.txt】文件里面添加如下内容:
add_llvm_loadable_module(YBPlugin YBPlugin.cpp)
复制代码
MJPlugin是插件名,MJPlugin.cpp是源代码文件
编译插件
生成xcode项目
- 利用cmake生成的Xcode项目来编译插件(第一次编写完插件,需要利用cmake重新生成一下Xcode项目)
编写插件
- 插件源代码在【Sources/Loadable modules】目录下可以找到,这样就可以直接在Xcode里编写插件代码
编译插件生成动态库文件
- 选择MJPlugin这个target进行编译,编译完会生成一个动态库文件,将动态库文件存放在桌面。
加载插件
- 在Xcode项目中指定加载插件动态库:BuildSettings > OTHER_CFLAGS
-Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称
复制代码
使用插件
- 首先要对Xcode进行Hack,才能修改默认的编译器
下载【XcodeHacking.zip】,解压,修改【HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec】的内容,设 置一下自己编译好的clang的路径
- 然后在XcodeHacking目录下进行命令行,将XcodeHacking的内容剪切到Xcode内部
$ sudo mv HackedClang.xcplugin `xcode-select-print- path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
$ sudo mv HackedBuildSystem.xcspec `xcode-select-print- path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
复制代码
- 配置项目
插件效果
应用场景
代码混淆
APP包瘦身
开发新的编程语言