本篇提纲
1、LLVM简介
2、编译器的一些基础知识
1.LLVM简介
基本介绍
LLVM
是架构编译器的框架系统,以c++编写而成,用于优化以任意程序语言编写的程序的编译时间、链接时间、运行时间以及空闲时间,对开发者保持开放并兼容已有脚本。目前已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司所采用。
LLVM
的命名最早来自于底层虚拟机(Low Level Virtual Machine)的缩写。LLVM
的项目是一个模块化和可重复使用的编译器和工具技术的集合。
LLVM的优点
-
现代化的设计
LLVM
的设计是高度模块化的,使得其他代码更为清晰和便于排查问题所在。当编译器决定支持多种源语言或多种硬件架构时,LLVM设计的优势就体现出来了。像其他编译器
GCC
,它方法非常成功,但是由于它是作为整体应用程序设计的,因此他们的用途受到了很大的限制。
LLVM结构示例LLVM
的设计最重要的方面是,使用通用的代码表示形式(IR),它是用来编译器中表示代码的形式。所以LLVM
可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端。
-
语言无关的中间代码
一方面,这使得透过LLVM能够将不同的语言相互连结起来;也使得LLVM能够紧密地与IDE交互和集成。另一方面,发布中间代码而非目标代码能够在目标系统上更好地发挥其潜能而又不伤害可调试性(i.e. 在目标系统上针对本机的硬件环境产生目标代码,但又能够直接通过中间代码来进行行级调试)
-
作为工具和函数库
使用LLVM提供的工具可以比较容易地实现新的编程语言的优化编译器或VM,或为现有的编程语言引入一些更好的优化/调试特性。 [5]
2.编译器的一些基础知识
传统编译器
传统编译器设计-
编译器前端(Frontend)
编译器的前端的任务是解析源代码。它会进行:词法分析,语法分析,语义分析,检查源代码是否存在错误,然后构建抽象语法树
(Abstract Syntax Tree,AST)。-
词法分析
词法分析(英语:lexical analysis)是计算机科学中将字符序列转换为单词(Token)序列的过程。词法分析是编译的第一阶段。词法分析器的主要任务是:将它们组成词素,生成并输出一个词法单元序列,这个词法单元序列会被输出到语法分析器进行语法分析。主要作用如下:
1、读入源程序的输入字符,将它们组成词素,生成并输出一个词法单元序列;
2、过滤掉程序中的注释和空白;
3、将编译器生成的错误消息与源程序的位置关联起来; -
语法分析
语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确.源程序的结构由上下文无关文法描述。 -
语义分析
语义分析是编译过程的一个逻辑阶段, 语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。语义分析的地位:编译程序最实质性的工作;第一次对源程序的语义作出解释,引起源程序质的变化。
-
-
优化器(Optimizer)
优化器负责进行各种优化。改善代码的运行时间,例如消除冗余计算等。 -
后端(Backend)/代码生成器(CodeGenerator)
将代码映射到目标指令集。生成机器语言,并且进行机器相关的代码优化。
iOS的编译器架构
Objective C、C、C++使用的前端编译器是Clang
,Swift是Swift
,后端都是LLVM
。
Clang简介
Clang
是LLVM项目中的一个子项目。它是基于LLVM架构的轻量编译器,诞生之初是为了替代GCC,提供更快的编译速度。它是负责编译C、C++、OC语言的编译器,属于LLVM中的编译器前端。
- Clang的编译流程
通过输入:
clang -ccc-print-phases main.m
来打印它的编译流程。
image.png
通过截屏看到编译分为六个步骤:
0:输入文件,找到源文件;
1:预处理阶段,这个过程处理包括宏的替换,头文件导入;
2:编译阶段,进行词法分析,语法分析,语义分析,最终生成IR;(一些代码的语法错误,语义错误是在这个阶段发现的,预处理阶段只负责一些宏替换,文件展开)
3:后端,这里LLVM会通过一个一个Pass去优化,每个Pass做一些事情,最终生成汇编代码;
4:生成目标文件;
5:链接,链接需要的动态库和静态库;
6:通过不同的架构,生成对应的可执行文件;
- 0:找到文件源
-
1:预处理阶段
预处理命令包括进行宏替换、头文件导入、条件编译。通过命令:
clang -E main.m>>main1.m
可以在main1.m中看到预处理执行完毕之后的结果。
预处理结果
预处理直接被过滤掉,直接拿到需要的值,main1.m是test=1时,main2.m是test=0的处理结果。
宏
在预编译阶段进行了替换。
注释在预编译期间被过滤掉了。
main3.m是导入了<Foundation/Foundation.h>,内容一下就变多了,变成了十几万行,可见<Foundation/Foundation.h>库内容之多,这里是进行了头文件的展开。
-
2:编译阶段
- 2.1词法分析
预处理完毕后,会进行源码的词法分析,将代码切成一个一个的Token,比如大小括号,等于号还有字符串等。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
通过上述命令可以看到词法分析后的结果
词法分析
就是把源代码拆分成一个一个的Token,遇到关键字,标点符号都会进行拆分,以第一行int main(int argc,const char * argv[]){
为例子
右边的Token对应分别拆分为:int、main、(、int、argc、const、char、*、argv、[、]、)、{,进行一个一个对应行和列的标记,这是第13行整行的标记结果。- 2.2语法分析
词法分析完成之后就是语法分析,它的任务是验证语法是否正确。在词法分析的基础上将单词序列组合成各类短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽象语法树
。语法分析
会判断程序在结构上是否正确。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 2.1词法分析
目前看到的地址都是虚拟地址,运行时才会开辟真实的地址空间。
Mack-O反编译拿到的地址就是这个虚拟地址。
是一个树状的结构。
当语法有问题时,例如删除一个分号,来看语法分析结果:
语法分析报错
精准提示~
-
2.3生成IR(intermediate representation)代码
语法分析,语义分析完毕后就可以生成中间代码IR
了,代码生成器会将语法树自顶向下遍历逐步翻译成LLVM IR。通过命令:clang -S -fobjc-arc -emit-llvm main.m
可以生成.ll的文本文件,查看IR代码。
Objective C代码在这一步会进行runtime的桥接,property合成,ARC处理等。
IR代码 -
2.4 IR基本语法
- @ 全局标识
- % 局部标识
- alloca 开辟空间
- align 内存对齐
- i32 32个bit,4字节
- store 写入内存
- load 读取数据
- call 调用函数
- ret 返回
-
2.5IR的优化
LLVM的优化级别分别是 -O0 -O1 -O2 -O3 -Os(第一个是大写英文字母O)
优化命令:clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
优化后的代码更加的简洁、明了,但是并不是优化的等级越高越好,realase模式下为 -Os,这也是系统最为推荐的优化等级。
可以在Xcode中:target->build->Optimization Level设置编译器的优化等级。
Xcode中的优化设置
-
2.6 bitcode
Xcode7以后开启bitcode
苹果会进一步进行优化。生成中间代码.bc。
我们通过优化过后的IR代码生成.bc代码clang -emit -llvm -c main.ll -o main.bc
-
3:生成汇编代码
我们通过最终的.bc或者.ll代码生成汇编代码clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
我通过命令clang -S -fobjc-arc main.ll -o main.s
生成了对应的汇编代码:
-
4:生成目标文件(汇编器)
目标文件的生成,是汇编以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件。clang -fmodules -c main.s -o main.o
通过
nm
命令,查看main.o
中的符号:xcrun nm -nm main.o
-
5:链接阶段
链接需要的动态库和静态库。链接器把编译产生的.o
文件、需要的动态库.dylib
和静态库.a
链接到一起去,生成可执行文件Mach-O
,命令是:clang main.o -o main
通过命令xcrun nm -nm main
再看下符号。
可以看到此时有标记,prinft
是外部函数,来自于libSystem。
而函数dyld_stub_binder
是负责函数绑定的函数,具体过程是这样:
在第一次执行到符号prinft
时,它的地址是空的,这个时候要从共享缓存中,也就是printf所在的库libSystem中进行动态加载,把printf
的具体内容的地址加载到printf
的映射地址上,而函数dyld_stub_binder
就负责执行这个过程,把libSystem中需要的内容读取处理。那么下一次再读到printf
这个符号,就有了相应的地址,就去执行具体的操作就可以了,已经绑定过了。
-
6:绑定硬件架构
通过不同的硬件架构,生成对应的可执行文件。
总结
本篇文章主要介绍了LLVM
的一些基础的概念,相关知识,以及对编译器和OC的前端编译器clang
的一些简单的语法使用,对clang
的编译步骤进行拆解,来了解一下相关流程。主要是知识介绍。
网友评论