阅读 2462

iOS 编译详解 LLVM Clang

前言

语言类型

我们有很多维度可以将计算机语言进行分类,其中以编译/执行方式为维度,可以将计算机语言分为:

  • 编译型语言

    • C++ Objective C Swift Kotlin
    • 先通过编译器生成机器码,机器码可以直接在 CPU 上执行
    • 👍 执行效率较高
    • 👎 调试周期长
  • 直译式语言(脚本语言)

    • JavaScript Python
    • 不需要经过编译,在执行时通过一个中间的解释器将代码解释为 CPU 可以执行的代码
    • 👍 编写调试方便
    • 👎 执行效率低

编译型语言和直译式语言的编译过程如下

从上图我们可以知道,编译型语言需要在运行之前就将代码全部编译好,最终运行的文件是编译后的可执行文件。我们将编译型语言所使用的编译方式称为 AOT (Ahead of time) 预先编译。

而直译式语言则是在运行的过程中,一边编译一边执行,最终运行的文件其实就是一开始写的源代码。我们将直译式语言所使用的编译方式称为 JIT (Just in time)即时编译。

iOS 编译工具

iOS 在 Xcode 5 版本前使用的是 GCC 编译器,在 Xcode 5 中将 GCC 彻底抛弃,替换为了 LLVM 。LLVM 包含了编译器前端、优化器和编译器后端三大模块。其中 Swift 除了在编译器前端和 Objective-C 稍有不同,其他模块都差不多。

Objective-C 采用 Clang 作为编译器前端

Swift 采用 Swift 作为编译器前端

LLVM

作者 Chris Lattner

Chris Lattner 在2000年开发了一个叫作 Low Level Virtual Machine 的编译器开发工具套件,后来涉及范围越来越大,可以用于常规编译器,JIT 编译器,汇编器,调试器,静态分析工具等一系列跟编程语言相关的工作,于是就把简称 LLVM 这个简称作为了正式的名字。Chris Lattner 后来又开发了 Clang ,使得 LLVM 直接挑战 GCC 的地位。

2005年加入苹果,将苹果使用的 GCC 全面转为 LLVM。

2010年开始主导开发 Swift 语言。

2017年离开了 Apple 入职特斯拉负责自动驾驶软件的开发,并于同年下半年入职 Google 加入深度学习与人工智能研发团队。

LLVM 简介

LLVM 是一个开源的,模块化和可重用的编译器和工具链技术的集合,或者说是一个编译器套件。

可以使用 LLVM 来编译 Kotlin,Ruby,Python,Haskell,Java,D,PHP,Pure,Lua 和许多其他语言

LLVM 核心库还提供一个优化器,对流行的 CPU 做代码生成支持。

LLVM 同时支持 AOT 预先编译和 JIT 即时编译

2012年,LLVM 获得美国计算机学会 ACM 的软件系统大奖,和 UNIX,WWW,TCP/IP,Tex,JAVA 等齐名。

LLVM IR

LLVM IR (Intermediate Representation)直译过来是“中间描述”,它是整个编译过程中生成的区别于源码和机器码的一种中间代码。IR 提供了独立于任何特定机器架构的源语,因此它是 LLVM 优化和进行代码生成的关键,也是 LLVM 有别于其他编译器的最大特点。LLVM 的核心功能都是围绕的 IR 建立的,它是 LLVM 编译过程中前端的输出,后端的输入。

在这一点上 IR 和 JVM 的 Java bytecode 很像,两者都是用于表述计算的模型,但两者所处的抽象层次不同。Java bytecode 更高层(更抽象),包含了大量类 Java 的面向对象语言的操作。LLVM IR 则更底层(更接近机器)。IR 的存在意味着它可以作为多种语言的后端,这样 LLVM 就能够提供和语言无关的优化,同时还能够方便的针对多种 CPU 代码生成。

为什么需要 IR

编译器的架构分为前端、优化器和后端。传统编译器(如 CGG )的前端和后端没有完全分离,耦合在了一起,因而如果要支持一门新的语言或硬件平台,需要做大量的工作。而 LLVM 和传统编译器最大的不同点在于,前端输入的任何语言,在经过编译器前端处理后,生成的中间码都是 IR 格式的。

传统的静态编译器

LLVM 编译器

这样做的优点是如果需要支持一种新的编程语言,那么我们只需要实现一种新的前端。如果我们需要支持一种新的硬件设备,那我们只需要实现一个新的后端。而优化阶段因为是针对了统一的 LLVM IR ,所以它是一个通用的阶段,不论是支持新的编程语言,还是支持新的硬件设备,这里都不需要对优化阶段做修改。所以从这里可以看出 LLVM IR 的作用。

LLVM IR 的三种格式:
  • 内存中的编译中间语言
  • 硬盘上存储的可读中间格式(以 .ll 结尾)
  • 硬盘上存储的二进制中间语言(以 .bc 结尾)

这三种中间格式是完全等价的。

Bitcode

iOS 开发的小伙伴可能对 IR 不是很了解,但我相信你一定听说过 Bitcode 。Bitcode 说白了其实就是我们前面提到的 LLVM IR 三种格式中的第三种,即存储在磁盘上的二进制文件(以 .bc 结尾)。

之所以要把 Bitcode 拿出来单独说,是因为 Apple 单独对 Bitcode 进行了额外的优化。从 Xcode 7 开始,Apple 支持在提交 App 编译产物的同时提交 App 的 Bitcode (非强制),并且之后对提交了 Bitcode 的 App 都单独进行了云端编译打包。也就是说,即便在提交时已经将本地编译好的 ipa 提交到 App Store,Apple 最终还是会使用 Bitcode 在云端再次打包,并且最终用户下载到手机上的版本也是由 Apple 在云端编译出来的版本,而非开发人员在本地编译的版本。

这里有一篇文章Xcode 7 Bitcode的工作流程及安全性评估,揭示了360团队如何通过一个小 trick 来验证 Apple 审核人员安装的 App 是直接由本地编译出来的版本还是云端通过 Bitcode 编译出来的版本,并由此发现了一个可能绕过审核的漏洞。

为什么需要 Bitcode

Apple 之所以这么做,一是因为 Apple 可以在云端编译过程中做一些额外的针对性优化工作,而这些额外的优化是本地环境所无法实现的。二是 Apple 可以为安装 App 的目标设备进行二进制优化,减少安装包的下载大小。

比如我们在本地编译生成的 ipa 是同时包含了多个CPU架构的(armv7 ,arm64 ),对于 iPhone X 而言 armv7 的架构文件就是无用文件。而 Apple 可以在云端为不同的设备编译出对应 CPU 架构的 ipa ,这样 iPhone X 下载到的 App 就只包含了所需的 arm64 架构文件。

更为黑科技的是,由于 Bitcode 是无关设备架构的,它可以被转化为任何被支持的 CPU 架构,包括现在还没被发明的 CPU 架构。以后如果苹果新出了一款新手机并且 CPU 也是全新设计的,在苹果后台服务器一样可以从这个 App 的 Bitcode 开始编译转化为新 CPU 上的可执行程序,可供新手机用户下载运行这个 App ,而无需开发人员重新在本地编译打包上传。

Clang & Swift

Clang 编译器

Clang 是 LLVM 的子项目,是 C、C++ 和 Objective-C 编译器,目标是替代传统编译器 GCC 。Clang 在整个 Objective-C 编译过程中扮演了编译器前端的角色,同时也参与到了 Swift 编译过程中的 Objective-C API 映射阶段。

Clang 的主要功能是输出代码对应的抽象语法树( AST ),针对用户发生的编译错误准确地给出建议,并将代码编译成 LLVM IR。

Clang 的特点是编译速度快,模块化,代码简单易懂,诊断信息可读性强,占用内存小以及容易扩展和重用等。

我们以 Xcode 为例,Clang 编译 Objective-C 代码的速度是 Xcode 5 版本前使用的 GCC 的3倍,其生成的 AST 所耗用掉的内存仅仅是 GCC 的五分之一左右。

Clang 的主要工作:

  • 预处理: 比如把宏嵌入到对应的位置,头文件的导入,去除注释( clang -E main.m )
  • 词法分析: 这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等
  • 语法分析: 验证语法是否正确
  • 生成 AST : 将所有节点组成抽象语法树 AST
  • 静态分析:分析代码是否存在问题,给出错误信息和修复方案
  • 生成 LLVM IR: CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR

Swift 编译器

和 Clang 一样,Swift 编译器主要负责对 Swift 源代码进行静态分析和纠错,并转换为 LLVM IR 。他是 Swift 编译的前端模块。不过和 Clang 不同,Swift 编译器会多出 SIL optimizer ,它会先将 Swift 文件转换成中间代码 SIL ,然后再根据 SIL 生成 IR 。是不是觉得很复杂,Swift 编译器会在编译其间生成两种不同的中间代码,这是为什么呢?下面会有详细的解释。

Swift 编译器的主要工作:

  • 解析:解析器负责生成没有任何语义或类型信息的抽象语法树( AST ),并针对输入源的语法问题发出警告或错误
  • 词法分析:获取解析的 AST 并将其转换为格式良好,完全类型检查的AST形式,为源代码中的词法问题发出警告或错误
  • Clang 导入器:导入 Clang 模块并将它们导出的 C 或 Objective-C API 映射到相应的 Swift API
  • SIL 生成:将经过类型检查的 AST 降级为 SIL
  • SIL 规范化:执行额外的数据流诊断(例如使用未初始化的变量)
  • SIL 优化:为程序执行额外的高级 Swift 特定优化,包括自动引用计数优化,虚拟化和通用专业化
  • LLVM IR 生成:将 SIL 降级到 LLVM IR

为什么要增加 SIL 层

Swift 中间语言( SWIFT Integration Layer )是一种高级的,特定于 Swift 的中间语言,适用于进一步分析和优化 Swift 代码。SIL 属于 High-Level IR,其相对于LLVM IR 的抽象层级更高,而且是特定于 Swift 语言的。

由于源码和 LLVM IR 之间存在着非常大的抽象鸿沟,IR 不适用对源码进行分析和检查。因此 Clang 使用了 Analysis 通过 CFG (控制流图)来对代码进行分析和检查。但是 CFG 本身不够精准,且不在主流程上(会和 IR 生成过程并行执行),因此 CFG 和 IR 生成中会出现部分重复分析,做无用功。

而在 Swift 的编译过程中,SIL 会在生成 LLVM IR 之前做好所有的分析和规范化,并在 IRGen 的帮助下降级到 LLVM IR ,避免了部分重复任务,也使得整个编译流程更加统一。

而且因为 Swift 在编译时就完成了方法绑定直接通过地址调用属于强类型语言,方法调用不再是像 Objective-C 那样的消息转发,这样编译就可以获得更多的信息用在后面的后端优化上。因此我们可以在 SIL 上对 Swift 做针对性的优化,而这些优化是 LLVM IR 所无法实现的。

这些优化包括:
  • 临界拆分:不支持任意的基础 block 参数通过终端进行临界拆分
  • 泛型优化:分析泛型函数的特定调用,并生成新的特定版本的函数.然后将泛型的特定用法全部重写为对应的特定函数的直接调用
  • witness和虚函数表的去虚拟化优化:通过给定类型去查找关联的类的虚函数表或者类型的 witness 表,并将虚函数调用替换为调用函数映射
  • 内联优化:对于transparent函数进行内联
  • 内存提升:将 alloc_box 结构优化为 alloc_stack
  • 引用计数优化
  • 高级领域特定优化:对基础的 Swift 类型容器(类似 ArrayString )实现了高级优化

通过分析和检查的安全 SIL 会被 IRGen 转换成 LLVM IR,并进一步接受 LLVM 的优化。

##动手实操

  1. 首先,我们写一个简单的程序,只有一个入口函数和简单的逻辑。
#import <Foundation/Foundation.h>
#define DEFINEEight 8

void test(int a, int b) {
    int c = a + b - DEFINEEight;
}
复制代码
  1. 写好代码后,通过以下命令,LLVM 会预处理你的代码,比如把宏嵌入到对应的位置,头文件的导入,去除注释等。
clang -E main.m
复制代码

得到的就是这样的代码

# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 185 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2


void test(int a, int b) {
    int c = a + b - 8;
}
复制代码
  1. 预处理完成后就会进行词法分析,这里会把代码切成一个个 Token ,每一个 Token 都代表了一个特征元素。

    Token 的分类
    • 关键字:语法中的关键字,if else while for 等。

    • 标识符:变量名

    • 字面量:值,数字,字符串

    • 特殊符号:加减乘除等符号

    通过以下代码可以得到词法分析后的代码。

    clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
    复制代码

    得到的词法分析代码

    void 'void'      [StartOfLine]  Loc=<main.m:4:1>
    identifier 'test'        [LeadingSpace] Loc=<main.m:4:6>
    l_paren '('             Loc=<main.m:4:10>
    int 'int'               Loc=<main.m:4:11>
    identifier 'a'   [LeadingSpace] Loc=<main.m:4:15>
    comma ','               Loc=<main.m:4:16>
    int 'int'        [LeadingSpace] Loc=<main.m:4:18>
    identifier 'b'   [LeadingSpace] Loc=<main.m:4:22>
    r_paren ')'             Loc=<main.m:4:23>
    l_brace '{'      [LeadingSpace] Loc=<main.m:4:25>
    int 'int'        [StartOfLine] [LeadingSpace]   Loc=<main.m:5:5>
    identifier 'c'   [LeadingSpace] Loc=<main.m:5:9>
    equal '='        [LeadingSpace] Loc=<main.m:5:11>
    identifier 'a'   [LeadingSpace] Loc=<main.m:5:13>
    plus '+'         [LeadingSpace] Loc=<main.m:5:15>
    identifier 'b'   [LeadingSpace] Loc=<main.m:5:17>
    minus '-'        [LeadingSpace] Loc=<main.m:5:19>
    numeric_constant '8'     [LeadingSpace] Loc=<main.m:5:21 <Spelling=main.m:2:21>>
    semi ';'                Loc=<main.m:5:32>
    r_brace '}'      [StartOfLine]  Loc=<main.m:6:1>
    eof ''          Loc=<main.m:6:2>
    复制代码
  2. 然后是语法分析,验证语法是否正确。确认无误后将所有节点组成抽象语法树 AST 。

  3. clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
    复制代码

    得到的 AST

    |-FunctionDecl 0x7f9a2108a0c0 <line:4:1, line:6:1> line:4:6 test 'void (int, int)'
    | |-ParmVarDecl 0x7f9a21089f48 <col:11, col:15> col:15 used a 'int'
    | |-ParmVarDecl 0x7f9a21089fc0 <col:18, col:22> col:22 used b 'int'
    | `-CompoundStmt 0x7f9a2108a348 <col:25, line:6:1>
    |   `-DeclStmt 0x7f9a2108a330 <line:5:5, col:32>
    |     `-VarDecl 0x7f9a2108a1e0 <col:5, line:2:21> line:5:9 c 'int' cinit
    |       `-BinaryOperator 0x7f9a2108a308 <col:13, line:2:21> 'int' '-'
    |         |-BinaryOperator 0x7f9a2108a2c0 <line:5:13, col:17> 'int' '+'
    |         | |-ImplicitCastExpr 0x7f9a2108a290 <col:13> 'int' <LValueToRValue>
    |         | | `-DeclRefExpr 0x7f9a2108a240 <col:13> 'int' lvalue ParmVar 0x7f9a21089f48 'a' 'int'
    |         | `-ImplicitCastExpr 0x7f9a2108a2a8 <col:17> 'int' <LValueToRValue>
    |         |   `-DeclRefExpr 0x7f9a2108a268 <col:17> 'int' lvalue ParmVar 0x7f9a21089fc0 'b' 'int'
    |         `-IntegerLiteral 0x7f9a2108a2e8 <line:2:21> 'int' 8
    `-<undeserialized declarations>
    复制代码

    为了方便查看,我们将 AST 以树状图的形式表示

    节点的分类:

    TranslationUnitDecl:根节点,表示一个源文件

    Decl:声明

    Expr:表达式

    Literal:字面量,是特殊的 Expr

    Stmt:语句

  4. 拿到 AST 后 Clang 静态分析器( Clang static analyzer )会对代码进行静态分析。

    Clang static analyzer 的架构包含了一个 Analyzer core 核心分析引擎和用于检查具体代码的 checkers ,所有的 checkers 都是基于 analyzer core 提供的基础功能来实现具体的代码检查的。

    AST 生成后 Clang static analyzer 会使用 checkers 对代码进行检查,比如是否使用了未声明的变量等等。你也可以编写新的 checkers 来添加自定义检查。这种方式能够方便用户扩展对代码检查规则或者对 bug 类型进行扩展,但是这种架构也有不足,每执行完一条语句后,分析引擎会遍历所有 checker 中的回调函数,所以 checker 越多,速度越慢。

    Clang 的静态分析器不仅能够将出现问题的代码位置暴露出来,还能够提供多个修复代码的方法。

  5. 完成这些步骤后就可以开始 IR 中间代码的生成了,CodeGen 会负责将 AST 自上向下遍历逐步翻译成 LLVM IR。

    clang -S -fobjc-arc -emit-llvm main.m -o main.ll
    复制代码

    生成的代码如下(此处仅截取了 test 方法)

    ; Function Attrs: noinline nounwind optnone ssp uwtable
    define void @test(i32, i32) #0 {
      %3 = alloca i32, align 4
      %4 = alloca i32, align 4
      %5 = alloca i32, align 4
      store i32 %0, i32* %3, align 4
      store i32 %1, i32* %4, align 4
      %6 = load i32, i32* %3, align 4
      %7 = load i32, i32* %4, align 4
      %8 = add nsw i32 %6, %7
      %9 = sub nsw i32 %8, 8
      store i32 %9, i32* %5, align 4
      ret void
    }
    复制代码

    是不是看的头大了。其实 IR 也不是很难,稍微了解一下 IR 的语法就能够读懂其中的逻辑了。

    LLVM IR 语法:

    ; 注释

    @ 全局

    % 局部

    alloca 分配内存空间

    i32 32bit,即4个字节

    align 内存对齐

    Store 写入内存

    load 读取内存

    icmp 两个整数值比较,返回布尔值

    br 选择分支,根据 cond 来转向 label,不根据条件跳转的话类似 goto

    indirectbr 根据条件间接跳转到一个 label,而这个 label 一般是在一个数组里,所以跳转目标是可变的,由运行时决定的

    label 代码标签

    如果有学习过机器码的同学有没有发现,IR代码其实已经很像机器码了。

  6. 这里 LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别 -01 / -03 / -0s ,还可以写些自己的 Pass

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

    我们可以通过在上面一段命令中加入 -O3 / -O2 优化参数来控制优化登记。

    clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
    复制代码

    下面通过一个小例子,来展示 LLVM 对 IR 所做的具体优化。我们先写一个方法,包含一个循环语句。

    int main() {
        int i = DEFINEEight;
        while (i < 10) {
            i++;
            printf("%d",i);
        }
        return 0;
    }
    复制代码

    使用 -O3 参数生成 IR

    ; Function Attrs: nounwind ssp uwtable
    define void @demo() local_unnamed_addr #0 {
      %1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 1), !clang.arc.no_objc_arc_exceptions !9
      %2 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 2), !clang.arc.no_objc_arc_exceptions !9
      %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 3), !clang.arc.no_objc_arc_exceptions !9
      %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 4), !clang.arc.no_objc_arc_exceptions !9
      %5 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 5), !clang.arc.no_objc_arc_exceptions !9
      %6 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 6), !clang.arc.no_objc_arc_exceptions !9
      %7 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 7), !clang.arc.no_objc_arc_exceptions !9
      %8 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 8), !clang.arc.no_objc_arc_exceptions !9
      %9 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 9), !clang.arc.no_objc_arc_exceptions !9
      %10 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 10), !clang.arc.no_objc_arc_exceptions !9
      ret void
    }
    复制代码

    此时我们将循环条件 (i < 10) 修改为 (i < 100)

    ; Function Attrs: nounwind ssp uwtable
    define void @demo() local_unnamed_addr #0 {
      br label %1
    
    ; <label>:1:                                      ; preds = %1, %0
      %2 = phi i32 [ 0, %0 ], [ %3, %1 ]
      %3 = add nuw nsw i32 %2, 1
      %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %3), !clang.arc.no_objc_arc_exceptions !9
      %5 = icmp eq i32 %3, 100
      br i1 %5, label %6, label %1
    
    ; <label>:6:                                      ; preds = %1
      ret void
    }
    复制代码

    可以发现当循环次数较低时,生成的 IR 会直接将所有循环的逻辑都写出来,将执行效率最大化。而当循环次数过大时,则会退而使用更为复杂的逻辑去实现。 除了上面的这种简单优化,LLVM 还提供了其他优化 Pass:

    • 各种类,方法,成员变量等的结构体的生成,并将其放到对应的 Mach-O 的 section 中
    • Non-Fragile ABI 合成 OBJC_IVAR_$_ 偏移值常量
    • ObjCMessageExpr 翻译成相应版本的 objc_msgSendsuper 翻译成 objc_msgSendSuper
    • strongweakcopyatomic 合成@property 自动实现 settergetter
    • @synthesize 的处理
    • 生成 block_layout 数据结构
    • _block__weak
    • _block _invoke
    • ARC 处理,插入 objc_storeStrongobjc_storeWeak 等 ARC 代码
    • ObjCAutoreleasePoolStmt 转 objc_autorealeasePoolPush / Pop,自动添加 [super dealloc],给每个 ivar 的类合成 .cxx_destructor 方法自动释放类的成员变量。
  7. 如果开启了 Bitcode , 苹果会做进一步的优化

  8. clang -emit-llvm -c main.m -o main.bc
    复制代码
  9. 生成汇编

    clang -S -fobjc-arc main.m -o main.s
    复制代码
  10. 生成目标文件

    clang -fmodules -c main.m -o main.o
    复制代码
  11. 生成可执行文件

    clang main.o -o main
    复制代码
  12. 执行

    ./main
    复制代码

总结

LLVM 编译过程

  • 预处理
  • 词法分析
  • 语法分析
  • 生成 AST
  • 静态分析
  • 生成 LLVM IR
  • 编译器优化
  • Bitcode (可选)
  • 生成汇编
  • 生成目标文件
  • 生成可执行文件

LLVM 分工

编译器前端

在 iOS 编译中,Clang 就是整个编译器的前端。它包含了词法分析器、语法分析器、静态分析器、IR 生成器等一系列组件。这些组件共同协作,为 LLVM 提供了预处理,语法分析,语义分析,静态分析、错误处理、生成 IR 等各种各样的功能。

编译器优化

IR 是编译器前端的输出,也是编译器后端的输入,在整个 LLVM 编译器中担任承上启下的角色。可以说,LLVM 的核心功能都是围绕着 IR 而构建的。LLVM 的优化器通过各种各样的 Pass 来直接优化 IR,使得代码优化过程和语言、平台无关,大大提升了开发效率。

编译器后端

在 iOS 编译中,编译器后端其实就是 LLVM 自己提供的一套后端。它包含了 机器码生成器、链接器等工具,会对 IR 进行机器无关的代码优化,生成机器语言。

编译器后端生成的产物

LLVM 机器码生成器会针对不同的架构,生成不同的机器码。

实际应用

深入了解 LLVM 和 Clang ,以及他们提供的开发工具,我们能够实现很多有意思的功能。比如通过 Libclang、libTooling ,我们可以实现语法树分析、语言转化(例如将 Objective-C 转换为 Swift )。我们还可以开发自己的 Clang 插件,对我们的代码做个性化的检查。我们还可以通过写 Pass 实现自定义的代码优化、代码混淆。我们甚至可以开发自己的新语言,你需要做的仅仅是写一个编译器前端,将你的代码转换成 LLVM IR 。

除此之外,了解了编译内部的实现过程和细节,也同样有助于我们在解决问题时找到新的思路。OCEval 是 iOS 的一个动态执行热修复的第三方开源库,和 js 的 eval 函数类似,这个库可以将字符串形式的 Objective-C 代码转换成实际的运行代码并执行,其中非常重要的一个实现细节就是实现了一套简单的词法分析和语法分析,将字符串转成了 AST 并最终获得执行所需要的方法名、参数等。


引用

深入剖析 iOS 编译 Clang LLVM By 戴铭

iOS编译过程的原理和应用 By 🐺面对疾风吧

深入浅出 iOS 编译 By 🐺面对疾风吧

Swift的高级中间语言:SIL By sea_biscute

LLVM

Clang

Swift