Bryan Yang

其修远兮,上下求索

嗨,我是杨彪,一名iOS开发者,正在探索iOS求学之道。路漫漫兮修远兮,吾将上下而求索。


LLVM编译器之Clang前端

前沿

瞻仰大佬


Chris Lattner

三大杰作:

  • Clang
  • LLVM
  • Swift

2010年开始编写 Swift语言,而且一个人实现了Swift的大部分基础架构;他也是 LVVM 以及 Clang的主要开发者。


什么是LLVM

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包含的主要子项目包括:

  1. LLVM Core:包含一个现在的源代码/目标设备无关的优化器,一集一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。
  2. Clang:一个C/C++/Objective-C编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具.
  3. dragonegg: gcc插件,可将GCC的优化和代码生成器替换为LLVM的相应工具。
  4. LLDB:基于LLVM提供的库和Clang构建的优秀的本地调试器。
  5. libc++、libc++ ABI: 符合标准的,高性能的C++标准库实现,以及对C++11的完整支持。
  6. compiler-rt:针对__fixunsdfdi和其他目标机器上没有一个核心IR(intermediate representation)对应的短原生指令序列时,提供高度调优过的底层代码生成支持。
  7. OpenMP: Clang中对多平台并行编程的runtime支持。
  8. vmkit:基于LLVM的Java和.NET虚拟机实
  9. polly: 支持高级别的循环和数据本地化优化支持的LLVM框架。
  10. libclc: OpenCL标准库的实现
  11. klee: 基于LLVM编译基础设施的符号化虚拟机
  12. SAFECode:内存安全的C/C++编译器
  13. 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



编译过程

在列出完整步骤之前可以先看个简单例子。看看是如何完成一次编译的。

1
2
3
4
5
6
7
8
9
10
11
12
13
#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实现:
    1
    $ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp

生成的c++文件如下

1
2
3
4
5
6
7
8
9
10
11
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:

1
$ clang -rewrite-objc mian.m

利用clang命令查看整个编译过程

1
$ clang -ccc-print-phases main.m
  • 可以看到编译源文件需要的几个不同的阶段
    1
    2
    3
    4
    5
    6
    7
    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 -预处理

1
$ clang -E main.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
... 头文件

# 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,比如大小括号,等于号还有字符串等。

    1
    $ clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m


3⃣ Semantic Analysis - 语法分析

  • 语法分析,在 Clang 中由 Parser 和 Sema 两个模块配合完
  • 验证语法是否正确
  • 根据当前语⾔言的语法,⽣生成语意节点,并将所有节点组合成 抽象语法树(AST)

如下例子:

1
2
3
4
5
6
7
8
9
10
11
int testAST(int a, int b) {
while (b != 0) {

if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
return a;
}

验证语法是否正确,然后将所有节点组成抽象语法树 AST 。

1
$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

生成如下语法树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
`-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 桥接
    1
    $ clang -S -fobjc-arc -emit-llvm main.m -o main.ll

到这一步,LLVM前段编译器clang的工作已经基本做完了。




LLVM IR

  • LLVM IR有3种表示形式,但本质是等价的:
  • text:便于阅读的文本格式,类似于汇编语言,拓展名.ll

    1
    $ clang -S -emit-llvm main.m -o main.ll
  • memory:内存格式

  • bitcode:二进制格式,拓展名.bc,
    1
    $ 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里一个变量可以赋值多次。

优点

 可以简化很多编译优化方法的过程;

 对很多编译优化方法来说,可以获得更好的优化结果,

下面给出一个例子:

1
2
3
4
5
6
int main() {
int x, y;
x = 1;
x = 2;
y = x;
}

  • 非SSA
    1
    2
    3
    y := 1
    y := 2
    x := y

显然,我们一眼就可以看出,上述代码第一行的赋值行为是多余的,第三行使用的 y 值来自于第二行中的赋值。对于采用非 SSA 形式 IR 的编译器来说,它需要做数据流分析(具体来说是到达-定义分析)来确定选取哪一行的 y 值。但是对于 SSA 形式来说,就不存在这个问题了。如下所示:

  • SSA
    1
    2
    3
    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)

1
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll

  • Pass 是 LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM 完整的优化和转化。

如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。

生成字节码:

1
clang -emit-llvm -c main.m -o main.bc

注:xx.bc文件是位流格式,由于是二进制的,所以直接看就是一堆乱码,查看bitcode最好的方式是用hexdump工具。


6⃣ Assemble -生成Target相关汇编

1
clang -S -fobjc-arc main.m -o main.s

7⃣ Assemble - 生成 Target 相关机器码 Object (Mach-O)

1
clang -fmodules -c main.m -o main.o
1
2
3
4
5
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加个软连接,所以平常都是用这个路径

    1
    $ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • 安装cmake

    CMake是一个跨平台的编译(Build)工具,可以用简单的语句来描述所有平台的编译过程。

    1
    $ brew install cmake
  • 安装ninja

    Ninja 是一个构建系统,与 Make 类似。作为输入,你需要描述将源文件处理为目标文件这一过程所需的命令。 Ninja 使用这些命令保持目标处于最新状态。
    Ninja 的主要设计目标是速度。

    1
    $ brew install ninja


下载编译工具

  • 下载LLVM

    1
    2
    // 大小648.2M
    $ git clone https://git.llvm.org/git/llvm.git/
  • 下载clang

    1
    2
    3
    // 大小240.6M
    $ cd llvm/tools
    $ git clone https://git.llvm.org/git/clang.git/
  • 在LLVM源码同级目录下新建一个【llvm_build】目录(最终会在【llvm_build】目录下生成【build.ninja】)

    1
    2
    $ cd llvm_build
    $ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安装路径
  • 依次执行编译、安装指令

    1
    $ ninja

编译完毕后, 【llvm_build】目录大概 21.05 G(仅供参考)

1
$ ninja install

安装完毕后,安装目录大概 11.92 G(仅供参考)

  • 在llvm同级目录下新建一个【llvm_xcode】目录
    1
    2
    $ cd llvm_xcode
    $ cmake -G Xcode ../llvm


开始开发插件

  • 在【llvm/tools/clang/tools】源码目录下新建一个插件目录,假设叫做【yb-plugin】

  • 在【llvm/tools/clang/tools/CMakeLists.txt】最后加入内容: add_clang_subdirectory(yb-plugin),小括号里是插件目录名

    1
    2
    3
    # 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】文件里面添加如下内容:

    1
    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
    1
    -Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称


使用插件

  • 首先要对Xcode进行Hack,才能修改默认的编译器

    下载【XcodeHacking.zip】,解压,修改【HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec】的内容,设
    置一下自己编译好的clang的路径

  • 然后在XcodeHacking目录下进行命令行,将XcodeHacking的内容剪切到Xcode内部

    1
    2
    $ 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
  • 配置项目


插件效果



应用场景

clang插件编写

代码混淆

APP包瘦身

iOS动态化方案——OCS

开发新的编程语言



推荐书记

最近的文章

CocoaPods建立自己的仓库

前序作为iOS开发,无论你是小白还是远古时代MRC的大神,相信没有人不知道CocoaPods,如果不知道的话,那么你应该去面壁十分钟怀疑自己了~github几乎所有(或者说全部)优秀的iOS开源框架都提供了Cocoapods Installation方式,由此可见,CocoaPods的使用为我们开 …

继续阅读
更早的文章

开发中用到的工具记录

记录一下在开发的过程中使用到的一些小工具,这些小工具给我们带来了极大的便利。 (什么Xcode、Git、CocoaPods等就不写了,基本配置) go2shellGo2Shell 可以在 Finder 中打开当前目录的终端窗口,是一个对开发者来说非常有用的App。特点:软件开发工具、小巧(大概只 …

继续阅读