02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM、Xcode编译的过程】

5,107 阅读15分钟

前言

之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也 深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

因此我们决定 进一步探究iOS底层原理的任务 ,本文探索的底层原理围绕“编译器LLVM项目ClangSwiftC优化器LLVMXcode编译的过程】”展开

一、概述

工欲善其事必先利其器,我们要探索iOS的底层原理,需要掌握一定的前知识,如:

本文的核心目标就是对LLVM项目进行简单介绍 且 从而方便我们按照脉络一步步去探索编译器个阶段的工作原理

image.png

1.什么是LLVM?

LLVM(全称 "Low Level Virtual Machine" :低级虚拟机),官方对其的描述是:

  • The name "LLVM" itself is not an acronym; it is the full name of the project.(“LLVM”这个名称本身不是首字母缩略词; 它是项目的全名)
  • LLVM项目的创始人是 Chris Lattner(Swift之父)image.png
  • 官网:llvm.org/
  • The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. (LLVM项目是模块化、可重用的编译器以及工具链技术的集合)
  • 美国计算机协会 (ACM) 将其2012 年软件系统奖项颁给了LLVM,之前曾经获得此奖项的软件和技术包括:Java、Apache、 Mosaic、the World Wide Web、Smalltalk、UNIX、Eclipse等等

2.LLVM项目简述

LLVM项目的架构如图: image.png 从上图我们可以清晰看到,整个程序编译链可以划分为三部分:编译器前端(左边部分)、优化器(中间部分)、编译器后端(右边部分)。(从我的这篇文章可以更详细了解编译相关的知识:计算机编译原理)

  • 编译器前端(Frontend):词法分析、语法分析、语义分析、生成中间代码llvm-ir
  • 优化器(Optimizer):对中间代码进行优化、改造,使之变成性能更加高效的中间代码llvm-ir(内存空间、执行效率)
  • 编译器后端(Backend):生成指定硬件架构的可执行文件

对编译器王者LLVM的进一步认识:

  • 使用统一的中间代码: 不同的编译器前端、编译器后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
  • 只需实现一个前端: 如果需要支持一种新的编程语言,那么只需要实现一个新的前端
  • 只需实现一个后端: 如果需要支持一种新的硬件设备,那么只需要实现一个新的后端
  • 通用优化器: 优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改
  • LLVMGCC的比较:
    • 相比之下,GCC的前端和后端没分得太开,前端后端耦合在了一起。所以GCC为了支持一门新的语言,或者为了支持一个新的目标平台,就 变得特别困难
    • LLVM现在被作为实现各种静态和运行时编译语言的通用基础结构(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell等)

3.探索iOS底层,对LLVM项目的关注点

结合LLVM项目的架构图 和 程序编译链 我们可以知道,我们在探索iOS底层原理的时候,需要关注

  • OC编译器前端(Clang)、Swift编译器前端(SwiftC)
  • 优化器(了解即可)
  • 编译器后端(模拟器x86、真机arm架构:以arm64为例,因为现代流行使用和近期未来的iOS设备都会朝arm64架构发展,老架构如armv7、armv7s会被逐渐淘汰)

二、编译器前端Clang

1.什么是Clang?

  • Clang 是 LLVM项目的一个子项目
  • Clang 是 基于LLVM架构的C/C++/Objective-C编译器前端
  • Clang官网:clang.llvm.org/get_started…
  • 苹果开发平台Xcode之前的编译器前端是gcc后来被替换成了Clang
  • 相比于GCC,Clang具有如下优点:
    • 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
    • 占用内存小:Clang生成的AST所占用的内存是GCC的五分之一左右
    • 模块化设计:Clang采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
    • 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
    • 设计清晰简单容易理解易于扩展增强

2.ClangLLVM的关系

  • 广义的LLVM:整个LLVM架构(包括编译器前端(如Clang)、优化器、编译器后端)
  • 狭义的LLVM:LLVM编译器后端(代码优化、目标代码生成等)

3.Clang 工作的主要流程

Clang.png

  • 预处理(Pre-process):include 扩展、标记化处理、去除注释、条件编译、宏删除、宏替换。 对C输出.i, 对C++输出 .ii, 对 OC 输出 .mi, 对Objective-C++ 输出 .mii
  • 词法分析 (Lexical Analysis):将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程;
  • 语法分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成抽象语法树 AST 。由 Clang 中 Parser 和 Sema 配合完成;
  • 静态分析(Static Analysis):使用它来表示用于分析源代码以便自动发现错误;
  • 中间代码生成(Code Generation):开始 IR 中间代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR。

4.Clang命令行指令

// 假设原始文件为main.m

// 预编译命令
clang -E main.m -o main.mi

// 生成AST语法树
clang -Xclang -ast-dump -fsyntax-only main.m

// 生成IR中间代码
clang -S -emit-llvm main.m -o main.ll

// 生成IR中间代码并优化,
clang -o3 -S -emit-llvm main.m -o main.ll

// 如果开启bitcode,生成.bc文件,这也是中间码的一种形式
clang -emit-llvm -c main.m -o main.bc

// 产生汇编命令
clang -S main.m -o main.s

// 生成目标.o文件
clang -c main.m -o main.o

image.png

三、编译器前端SwiftC

1.什么是 SwiftC?

image.png

  • SwiftC 是 Swift 语言的编译器前端

2. SwiftCLLVM的关系

Swiftc.png

  • SwiftC 作为 Swift语言的编译器前端,最终输出中间代码: LLVM-IR ,提供给编译器后端使用
  • LLVM 作为 编译器后端,输出指定硬件架构环境的 可执行文件(软件包)

3.SwiftC的主要工作流程

Swiftc.png image.png

  • Parse: 词法分析组件,生成 AST语法树;
  • Sema(Semantic Analysis):对 AST语法树 进行类型检查,转换为格式正确且类型检查完备的 AST;
  • Clang Importer: 负责导入 Clang 模块,并将导出的 C 或 Objective-C API 映射到相应的 Swift API 中。最终导入的 AST 可以被语义分析引用。(因为iOS Native环境下 支持Swift、OC、C、CPP混编)
  • SIL Gen:由 AST 生成 Raw SIL(原生 SIL,代码量很大,不会进行类型检查);
  • SIL 保证转换:SIL 保证转换阶段负责执行额外且影响程序正确性的数据流诊断,转换后的最终结果是规范的 SIL;
  • SIL 优化:该阶段负责对程序执行额外的高级且专用于 Swift 的优化,包括(例如)自动引用计数优化、去虚拟化、以及通用的专业化;
  • Swift 编译过程引入 SIL 有几个优点:
    • 完成的变数程序的语义(Fully represents program semantics );
    • 既能进行代码的生成,又能进行代码分析(Designed for both code generation and analysis );
    • 处在编译管线的主通道(Sits on the hot path of the compiler pipeline );
    • 架起桥梁连接源码与 LLVM,减少源码与 LLVM 之间的抽象鸿沟(Bridges the abstraction gap between source and LLVM)
  1. Swift Code : 开发者自己编写的代码
  2. Swift AST : 根据swiftc生成语法树
  3. Raw Swift IL : Swift特有的中间代码
  4. Canonical Swift IL : 更加简洁的中间代码版本
  5. LLVM IR : 编译器前端处理完后转交给LLVM生成后端中间代码
  6. Assembly : 后端对代码进行优化转变成汇编代码
  7. Executable : 汇编代码转换成可执行的二进制代码

4. SwiftC 命令行指令

// 假设原始文件为main.swift

// 分析输出AST
swiftc main.swift -dump-parse

// 分析并且检查类型输出AST
swiftc main.swift -dump-ast

// 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen -o main.sil 

// 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil -o main.sil 

// 生成优化后的中间体语言(SIL),并将结果导入到main.sil文件中
swiftc main.swift -emit-sil  -o main.sil 

// 生成优化后的中间体语言(SIL),并将sil文件中的乱码字符串进行还原,并将结果导入到main.sil文件中
swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil

// 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir  -o main.ir

// 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc -o main.bc

// 生成汇编
swiftc main.swift -emit-assembly -o main.s

// 编译生成可执行.out文件
swiftc main.swift -o main.o 
 

image.png

四、编译器后端

主要流程

LLVM后端.png

  • 优化(Optimize):LLVM 会去做些优化工作;
    • 在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s;
    • 优化级参数位于参数位于Build Settings -> Apple Clang - Code Generation ->Optimization Level
    • 优化级参数 是利用 LLVM 的 Pass 去处理的,我们可以自己去自定义 Pass。
  • 生成目标文件(Assemble):生成 Target 相关 Object(Mach-o);
  • 链接(Link):生成 Executable 可执行文件。

五、Xcode 编译过程

LLVM

LLVM

如上图所示,在Xcode按下CMD+B之后的工作流程。

  • 预处理(Pre-process):他的主要工作就是将宏替换,删除注释展开头文件,生成.i文件。

  • 词法分析(Lexical Analysis):将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。

  • 语法分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成抽象语法树 AST 。由 Clang 中 Parser 和 Sema 配合完成。

  • 静态分析(Static Analysis):使用它来表示用于分析源代码以便自动发现错误。

  • 中间代码生成(Code Generation):生成中间代码 IR,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,后端的输入。

  • 优化(Optimize):LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-O1-O3-Os...还可以写些自己的 Pass,官方有比较完整的 Pass 教程: Writing an LLVM Pass 。如果开启了Bitcode苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的Bitcode去生成。

  • 生成目标文件(Assemble):生成Target相关Object(Mach-o文件)。

  • 链接(Link):生成Executable可执行文件。

经过这一步步,我们用各种高级语言编写的代码就转换成了机器可以看懂可以执行的目标代码了。

这里只是作了一个Xcode编译过程的一个简单的介绍,需要深入了解的同学可以查看 深入浅出iOS编译

六、通过命令行,了解OC源文件的编译过程

源码:

1.命令行查看编译的过程:

cmd命令:

$ clang -ccc-print-phases main.m

解读:

  • +- 0: input, "main.m", objective-c:+-0 输入名为“main.m ” 的objc 源码文件
  • +- 1: preprocessor, {0}, objective-c-cpp-output: +-1 预处理器preprocessor 进行预处理操作,生成 并输出 main.cpp 的底层C++代码
  • +- 2: compiler, {1}, ir: +-2 通过编译器compiler(此处为编译器前端clang Fronted) 工作(词法分析、语法分析、语义分析、生成中间代码),输出中间代码 ir(全称为 LLVM IR)
  • +- 3: backend, {2}, assembler: +-3 通过编译器compiler(此处为编译器优化器 Optimizer)工作(对编译器前端生成的中间代码ir进行优化,得到优化过后的中间代码ir)
  • +- 4: assembler, {3}, object:+-4 通过编译器compiler(此处为编译器后端LLVM Backend )工作(:加工优化过后的中间代码ir,生成汇编assembler)生成汇编代码
  • +- 5: linker, {4}, image:+-5 通过编译器compiler(此处为编译器后端LLVM Backend )工作(将汇编代码 以及项目开发过程中 绑定了的系统动态库 进行链接)
  • 6: bind-arch, "x86_64", {5}, image: +-6 通过编译器compiler(此处为编译器后端LLVM Backend )工作(把汇编代码生成对应目标执行环境的机器架构的可执行文件二进制机器码(此处为bind-arch, "x86_64 "))

2.查看preprocessor(预处理)的结果

cmd命令:

$ clang -E main.m

3.词法分析,生成Token

cmd命令:

clang -fmodules -E -Xclang -dump-tokens main.m

4.生成语法树-AST(Abstract Syntax Tree)

cmd命令:

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

5.生成中间代码LLVM-IR

LLVM IR有3种表示形式(但本质是等价的,就好比水可以有气体、液体、固体3种形态) 1.text格式:便于阅读的文本格式,类似于汇编语言,拓展名.ll cmd命令:

$ clang -S -emit-llvm main.m

  • IR基本语法
    • 注释以分号 ; 开头
    • 全局标识符以@开头,局部标识符以%开头
    • alloca,在当前函数栈帧中分配内存
    • i32,32bit,4个字节的意思
    • align,内存对齐
    • store,写入数据
    • load,读取数据
  • 官方语法参考 llvm.org/docs/LangRe… 2.memory:内存格式 3.bitcode:二进制格式,拓展名.bc cmd命令:
$ clang -c -emit-llvm main.m

七、通过个人的本地LLVM项目工程进行项目编译

我们寻常开发的时候都是借助官方IDE Xcode 自带的编译器 进行编译的。我们在前面已经基本了解了llvm、编译器前端、编译器后端、优化器等,我们也可以通过自己本地的llvm编译器对自己的工程进行编译,进一步探索llvm编译的过程。

我想,很多朋友都想了解,通过自定义的llvm项目进行编译有什么意义呢?它的应用实践价值是什么呢?

1.应用

2.下载llvm项目的副本到本地

下载LLVM工程:

3.源码编译

完成前面两项步骤之后,我们本地环境就有了两种源码编译的可能:

    1. 通过cmake+ninja,借助我本地的LLVM项目进行编译
    1. 通过Xcode,借助我本地的LLVM项目进行编译

3.1 通过cmake+ninja,借助我本地的LLVM项目进行编译

  • 安装cmake和ninja(先安装brew,brew.sh/

    • $ brew install cmake
    • $ brew install ninja
  • ninja如果安装失败,可以直接从github获取release版放入【/usr/local/bin】中 github.com/ninja-build…

  • LLVM源码同级目录下新建一个【llvm_build】目录(最终会在【llvm_build】目录下生成【build.ninja】)

    • $ cd llvm_build
    • $ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的编译输出路径! 此处输出路径为llvm-release image.png
    • 更多cmake相关选项,可以参考: llvm.org/docs/CMake.…
  • 依次执行编译、安装指令

    • $ ninja 编译完毕后, 【llvm_build】目录大概 21.05 G(仅供参考)
    • $ ninja install 安装完毕后,安装目录大概 11.92 G(仅供参考)

3.2 通过Xcode,借助我本地的LLVM项目进行编译

也可以生成Xcode项目再进行编译,但是速度很慢(可能需要1个多小时) 在llvm同级目录下新建一个【llvm_xcode】目录

$ cd llvm_xcode 
$ cmake -G Xcode ../llvm  

八、编写clang插件,进行语法检查

1.新建插件目录

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

2.编写插件代码

  • 2.clang插件源码为cpp文件,编写一个语法检查插件如下: HPPlugin.cpp

#include <iostream> 
#include "clang/AST/AST.h"  
#include "clang/AST/ASTConsumer.h" 
#include "clang/ASTMatchers/ASTMatchers.h" 
#include "clang/ASTMatchers/ASTMatchFinder.h" 
#include "clang/Frontend/CompilerInstance.h" 
#include "clang/Frontend/FrontendPluginRegistry.h" 

using namespace clang; 
using namespace std; 
using namespace llvm; 
using namespace clang::ast_matchers;
 


namespace HPPlugin {

    class HPHandler : public MatchFinder::MatchCallback {

    private:

        CompilerInstance &ci;

        

    public:

        HPHandler(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, "HP插件:类名中不能带有下划线"));

                }

            }

        }

    };

    

    class HPASTConsumer: public ASTConsumer {

    private:

        MatchFinder matcher;

        HPHandler handler;

        

    public:

        HPASTConsumer(CompilerInstance &ci) :handler(ci) {

            matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);

        }

        

        void HandleTranslationUnit(ASTContext &context) {

            matcher.matchAST(context);

        }

    }; 


    class HPASTAction: public PluginASTAction {

    public:

        unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {

            return unique_ptr<HPASTConsumer> (new HPASTConsumer(ci));

        }
 

        bool ParseArgs(const CompilerInstance &ci, const vector<string> &args) {

            return true;

        }

    };

}
 

static FrontendPluginRegistry::Add<HPPlugin::HPASTAction>

X("HPPlugin", "The HPPlugin is my first clang-plugin.");
 

3.编写插件配置

  • 3.编写CMakeLists.txt
add_llvm_library( HPPlugin MODULE BUILDTREE_ONLY
    HPPlugin.cpp
)

image.png

4.将插件添加至clang编译工具

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

image.png

5.编译刚才新加的插件

利用cmake生成的Xcode项目来编译插件(第一次编写完插件,需要利用cmake重新生成一下Xcode项目):

$ cd llvm_xcode
$ cmake -G Xcode ../llvm

image.png

  • 插件源代码在【Sources/Loadable modules】目录下可以找到,这样就可以直接在Xcode里编写插件代码
  • 选择HPPlugin这个target进行编译,编译完会生成一个动态库文件 image.png

自此,一个Clang插件编写并输出完成!!!

九、自定义Clang插件的使用

Xcode环境是Xcode11:

1.加载自定义插件

在Xcode项目中指定加载刚才编译出来的插件动态库Build Settings > OTHER_CFLAGS

image.png

2.Hack Xcode

我们前面想修改编译器为我们自己本地的LLVM编译器,发现IDE不让我们这么干!我们需要借助一些工具,对Xcode进行Hack:

  • 下载【XcodeHacking.zip】,解压,修改【HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec】的内容,设 置一下自己编译好的clang的路径
  • 然后在XcodeHacking目录下进行命令行,将XcodeHacking的内容剪切到Xcode内部
$ sudo mv HackedClang.xcplugin `xcode-select-printpath`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
 
$ sudo mv HackedBuildSystem.xcspec `xcode-select-printpath`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
 

3.修改Xcode的编译器

4.编译项目

编译项目后,会在编译日志看到HPPlugin插件的打印信息(如果插件更新了,最好先Clean一下项目)

image.png

5.其它插件相关

想要实现更复杂的插件功能,就需要利用clang的API针对语法树(AST)进行相应的分析和处理
关于AST的资料:

6.推荐书籍📚

未深入探索的问题

我们在前面了解到,程序的汇编过程依赖不同的硬件环境,我们本次没有对不同的硬件环境的不同汇编探索。需要了解其它汇编知识的,需要朋友们根据个人需要去探索。

总结

通过通篇介绍,我们了解了: LLVM项目 、iOS的整个编译过程:编译器前端Clang、编译器前端SwiftC、编译器后端、Xcode 编译过程;
并且,在前面的基础上,我们通过命令行工具了解了: OC源文件的编译过程、通过安装和编译本地LLVM工程、编写了clang插件、应用clang插件进行语法检查的实践;
我们接下来,会用其它篇幅,去了解LLDB、汇编,进而为探索OC、Swift语言底层原理做铺垫!

参考文章

推荐文章

专题系列文章

1.前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

其它底层原理专题

1.底层原理相关专题

2.iOS相关专题

3.webApp相关专题

4.跨平台开发方案相关专题

5.阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6.Android、HarmonyOS页面渲染专题

7.小程序页面渲染专题