第四十节—iOS用到的LLVM(一)
本文为L_Ares个人写作,以任何形式转载请表明原文出处。
LLVM内容较多,本文尽量不废话,看起来会有点刻板,但都是干货,请各位先仔细
并且充分
的理解以下三个问题 :
- 为什么要了解
LLVM
和Clang
?- 什么是
LLVM
?- 什么是
Clang
?注 : 概念性的资料源于百科(
维基百科
或LLVM官网)。
前言 : 为什么要了解LLVM?
因为
Objective-C
和Swift
语言都是编译型语言
,所以需要编译器对其转换成机器语言
才会被系统识别,而它们的编译器都是基于LLVM体系
开发的。对
编译器
、编译型语言
有所遗忘的童鞋看这里。
一、简述什么是LLVM
前言
第一 :
本节的重点虽然是LLVM
,但是主要还是以iOSer
的角度来看的。第二 :
要想深入学习LLVM
的话,本节的这些内容只能算是一个概念的大体简述,尤其主要是iOS
的视角,所以想了解更多LLVM
的同学,还需自行查看LLVM官网。第三 :
如果觉得LLVM官网真的很难懂,又全是英文,翻译无法精准的话,那么可以看一下知乎上的蓝色大大对于LLVM
的讲解,再配合LLVM官网进行学习。
概念
这里的概念全部引自LLVM官网。
LLVM
现在已经不是一个名称的首字母缩写了(最初还真是),而是一个项目名称,是一个模块化的、可重用的编译器
和工具链技术
的集合。简言之,
LLVM
是架构编辑器的框架系统
。
所谓工具链技术
,举个栗子 :
嵌入式
Linux
就提供了一套完整的工具链,它利用GNU
的gcc
做编译器,利用gdb
、xgdb
做调试工具,可以很方便的实现从操作系统的内核态到用户态的应用软件各个级别的调试。
所谓框架系统
:
就是说
LLVM
不仅仅是某一样具体的事物,而是由很多模块组合起来的框架。所谓框架就是你可以基于它提供的功能开发自己的模块,并且集成到
LLVM
系统上,增加LLVM
的功能。或者你也可以利用
LLVM
作为一种底层支持,去完成自己的软件开发。
概述
LLVM
曾是Low Level Virtual Machine
(低等级虚拟机)的缩写,现在它是一个项目体系、是一套框架系统。
LLVM
项目包括了非常多的子项目,包括iOS
开发者常见的Clang
,LLDB
,libc++
和libc++ ABI
,这都是LLVM
的主要子项目。
LLVM
虽然包含了这么多编译器作为子项目,但是它本身并不是编译器,也不是编译器后端。
它提供的是编译器所需的一系列的库。例如程序分析、代码优化、机器代码生成等,并且提供了调用这些库的相关工具。也为这些库提供了方便简单的、具备类型的、与平台无关的统一中间代码语言——LLVM IR
。
LLVM
以C++
编写而成,用于优化以任意程序语言编写的 :
-
编译时间 (
compile-time
) -
链接时间 (
link-time
) -
运行时间 (
run-time
) -
空闲时间 (
idle-time
)
LLVM
对开发者保持开放,并且兼容现有的脚本。
二、编译器的设计
在探索LLVM
的设计之前,先看一下传统的编译器设计,来比较两者的区别,以探索LLVM
的优势。
1. 传统编译器的设计
如图2.1.0所示
图2.1.0传统的编译器整体流程是 :
源码 --> 编译器前端 --> 优化器 --> 编译器后端/代码生成器(CodeGenetator) --> 机器语言
1.1 编译器前端(Frontend)
传统的编译器前端主要任务就是读取源代码,对读取的源代码做词法分析、语法分析、语义分析,通过这些分析来检查源代码是否存在错误,然后构建
语法树 (Abstract Syntax Tree, AST)
。
1.2 优化器(Optimizer)
优化器的责任就是各种优化。改善代码的运行时间,例如消除冗余计算等。
1.3 编译器后端(Backend)/代码生成器(CodeGenerator)
将代码映射到目标指令集,生成机器语言,并进行机器相关的代码优化。
后端或者说代码生成器,最后生成的是对应不同架构的二进制文件,之所以需要生成不同的二进制文件,就是因为不同的架构有不同的指令集,不同的指令集对相同的二进制文件的指令识别是不同的,所以要将代码映射到对应的指令集。
2. iOS的编译器架构
所谓iOS
的编译器架构其实就是LLVM
编译器架构中的一部分。包括Objective-C
、C
、C++
在内,它们使用的编译器前端
都是Clang
。而Swift
的编译器前端
就是Swift
。它们的编译器后端
则全部都是LLVM Code Generator
。
而Clang
也只是整个LLVM
架构中的一个子项目,属于编译器前端
。
来看iOS
的编译器架构图 :
很明显,iOS
的编译器是通过Clang
或者Swift
来完成词法分析、语法分析、语义分析的步骤,来完成对源代码的错误排查,然后生成抽象语法树(AST)
。
这里利用的就是LLVM
的前端,和传统编译器的不同点是,Clang
不止做完了上述的排查,还会生成一个非常重要的东西——IR
。
什么是IR
?
LLVM
的编译器前端不仅会完成分析排查和生成抽象语法树
的工作,还会生成中间代码,这个中间代码在LLVM
中被称作IR (intermediate representation)
。
那么用语言来描述iOS
的编译器架构设计流程就是 :
Clang
/Swift
读取源代码,检查词法、语法、语义的分析,排查错误,生成抽象语法树(AST)
,并生成中间代码IR
。然后将
IR
给到LLVM
的优化器(Optimizer
),改善代码的运行时间,消除冗余计算等。最后将优化过的
IR
再传给后端或者说代码生成器,由后端/代码生成器完成将IR
转换成对应不同架构的二进制文件,完成机器代码的相关优化。
3. LLVM的设计
LLVM
的设计核心 : 中间代码IR
。
LLVM
的设计重点和优势 : 面对多种源语言(编译器前端来做),或者多种硬件架构(编译器后端来做)的情况时,对比传统的编译器例如GCC
,优势就是LLVM
的IR
设计。
由于GCC
是以一个整体应用程序设计的,也就是说,如果你的前端源语言或者后端硬件架构,任一一个发生了变化的话,你需要重新设计一整套前端--优化器--后端
的编译器。
LLVM
的设计图 :
前端读取源码,经过传统步骤后,多增加一步生成中间代码
IR
,然后输出IR
给优化器。优化器读取的是前端传输过来的
IR
,优化的也是IR
,然后输出的还是IR
。所以优化器只需要对传入过来的IR
做优化即可,无论前端是什么样的源语言类型,都会被转成IR
。后端/代码生成器处理的则是优化器传来的
IR
,也是只需要针对IR
做处理,不需要管前端到底是什么类型的源语言。
优势 :
-
如果前端出现了新的源语言类型,
LLVM
只需要针对新语言适配一个IR
转换器即可,优化器和后端都不需要进行大的改变,甚至不用改变。 -
如果后端出现了新的硬件架构,
LLVM
只需要针对新的硬件架构适配一个IR
转换器即可,优化器和前端则不需要进行大的改变,甚至不用改变。
三、关于Clang
1. 什么是Clang
内容引自Clang官网,个人翻译,如有不准确的地方,还请不吝赐教。
Clang
是LLVM原生的
针对C
、C++
、Objective-C
的编译器。它的存在是为了提供一个平台,
Clang
做到了 :
- 非常快的编译速度
- 非常有用的
error
信息和warning
警告- 可以构建非常优秀的源代码
Clang Static Analyzer
和Clang -tidy
都是可以自动发现你代码里bug的工具。它们都是Clang
工具中很好的例子,它们可以将Clang编译器前端
作为一个库去解析C
/C++
的代码。
2. Clang的概述
总的来说,
Clang
是LLVM
的子项目,它是基于LLVM
架构的轻量级编译器,诞生之初是为了替代GCC
,因为GCC
是传统型的编译器,虽然非常好用,但是GCC
的源码太长了,又晦涩难懂,而且编译速度还可提升。于是基于LLVM
架构的Clang
就逐步的扩大了影响力。
3. 一些LLVM或Clang中的名词解释
3.1 词法分析
词法分析是计算机科学中将字符序列转换为单词序列的过程。
词法分析是整个编译流程的第一个阶段,是编译的基础。词法分析要做的是从左到右一个字符一个字符的读入源程序,对构成源程序的字符进行扫描,然后根据构词规则识别单词(也称单词符号或符号,英文叫做
Token
)。所谓单词就是一个字符串,是构成源代码的最小单位。
词法分析一般都是经过词法分析的程序或者专门的函数来完成的,这种程序或者函数叫做词法分析器,也叫扫描器(
Scanner
)。
3.2 语法分析
语法分析是整个编译流程中的一个逻辑阶段。语法分析的任务是在词法分析的基础上,将单词序列的组合成各种语法短语,如程序、语句、表达式等。
语法分析是判断源程序在语法的结构上是否正确。它和词法分析分别像英语中的语法和单词,词法分析标明了单词的含义,语法分析则判断单词组合成的语句是否符合语法规则。
3.3 语义分析
语义分析也是整个编译流程中的一个逻辑阶段。语义分析的任务是对结构上正确的源程序的上下文进行有关性质的检查和类型检查。
语义分析是检查源程序有无语义错误,为代码生成阶段收集类型信息。
语义分析检查的是源程序是否符合规定的语言规范。
语义分析是编译程序最实质性的工作,它是第一次对源代码做出解释的阶段,引起源程序的实质变化。
语义和语法的区别,我举个例子,方便大家理解。比如 : 我学习编程和编程学习我。两者的语法都是没有问题的,符合语法的规范——主谓宾。但是后者并不符合语义规范,后者的语言意义是一种空泛的表达。
3.4 抽象语法树
这是源代码的语法结构的一种抽象表示。所谓抽象语法,是指不会表示出所有细节的语法。
4. iOS中可能遇到的Clang常用指令
这里只说一些iOS中可能常用的Clang
指令,如果想要全部的Clang
指令集,可以在terminal
终端中输入clang --help
查看。
如果认为上面的命令查看不方便的,这里还有github版本,内容就是copy的clang --help
。
或者可以直接在LLVM
官网的Clang Document查询。
Clang命令 | 释义 |
---|---|
-ccc-print-phases | 打印源码的编译阶段,得到的打印结果就是整个源码到机器代码的整体流程步骤。 |
-rewrite-objc | 将OC源码编译成C++源码 |
-E | 查看预处理阶段详细步骤(在terminal 中查看,也可以生成一个新文件 : clang -E main.m >> new_main.m) 。>> 就是文件重定向。 |
-c | 必须在-E 之后才可以使用,例如clang -E main.m -c 。只进行预处理,编译,汇编步骤,不进行链接步骤。执行完成后生成的是.o 链接文件。 |
-S | 只进行预处理和编译步骤,不进行汇编,链接等后续步骤。执行完成后,生成的是.s 汇编代码文件。 |
-o <file> | 完成预编译-->编译-->汇编-->链接 后,生成可执行文件到<file> 文件。 |
-g | 在生成的可执行文件中,包含标准的调试信息。 |
-i <路径>(应该是大写i 最标准) |
在头文件中的搜索路径中添加<路径> 。 |
-fmodules | 启用模块化语言特性。 |
-fsyntax-only | 编译器并不生成代码,后续的操作只是语法级别的修改。 |
-Xclang <arg> | 向clang 编译器传递参数<arg> 。 |
-dump-tokens | 运行预处理器,将源码内部拆分成各种单词(Token)。 |
-ast-dump | 构建抽象语法树AST,然后对其进行拆解和调试。 |
-fobjc-arc | 生成Objective-C指针的retain 和release 的调用。 |
-emit-llvm | 对汇编文件和目标文件生成LLVM IR 代码。 |