什么是 LLVM?
LLVM 是编译器工具链技术的集合。 lld项目是外部链接器。
编译器编译每个文件以产生Mach-O(可执行文件); 链接器将项目中的多个 Mach-O 文件合并为一个。
Xcode运行的过程就是执行一些命令脚本。 下面的截图是Xcode编译main.m的脚本。
找到bin目录下的clang命令,在前面添加一些参数,比如哪种语言,编译哪个框架,添加Xcode中设置的配置参数,最后输出为.o文件。
LLVM编译器架构
编译器分为三个部分,编译器后端、通用优化器和编译器前端。 中间的优化器不会改变
添加语言只需要处理编译器后端。
添加框架只需添加编译器前端框架处理即可
clang代表了C、C++、Objective-C在编译器架构中的后端,同时也充当了命令行中的“黑匣子”驱动,它封装了编译管道、前端命令、LLVM命令、工具链命令、等等。LLVM将执行上面描述的整个编译过程。 大致流程如下: OC源文件编译流程
使用以下命令查看OC源文件的编译过程
clang -ccc-print-phases main.m
0:先找到main.m文件
1:预处理器是替换include、import、宏定义
2:编译成IR中间代码
3:将中间代码交给前端生成汇编代码
4:汇编生成目标代码
5:链接静态库、动态库
6:适合某个框架的代码
预处理
使用以下命令查看预处理阶段完成的工作
clang -E main.m
预处理主要做了以下几件事:
1、删除所有#defines,代码中使用宏定义的地方将被替换
2. 将#include 中包含的文件插入该文件的位置。 插入过程是递归的
3.删除注释符号和注释
4.添加行号和文件标签,方便调试
编译编译的过程就是对预处理后的文件进行词法分析、句法分析、语义分析和优化,形成相应的汇编代码 1、词法分析
此步骤将源文件中的代码转换为特殊的标记流。 源代码被一一分为字符和短语。 行尾Loc标注了对应的源文件以及源代码所在的具体行数,方便报错。 定位问题。
使用以下命令进行词法分析
clang -Xclang -dump-tokens main.m
以下面的代码为例:
这个源代码在第11行
int main(int argc, char * argv[]) {
通过词法分析,会转化为以下特殊token
int 'int' [StartOfLine] Loc=
identifier 'main' [LeadingSpace] Loc=
l_paren '(' Loc=
int 'int' Loc=
identifier 'argc' [LeadingSpace] Loc=
comma ',' Loc=
char 'char' [LeadingSpace] Loc=
star '*' [LeadingSpace] Loc=
identifier 'argv' [LeadingSpace] Loc=
l_square '[' Loc=
r_square ']' Loc=
r_paren ')' Loc=
l_brace '{' [LeadingSpace] Loc=
2. 语法分析
这一步是根据词法分析将token流解析成语法树,由Clang中的Parser和Sema模块完成。
其上的每个节点也标记了其在源代码中的位置
验证句型是否正确,如漏一项; 报告错误信息
根据当前语言的句型,生词语义节点,并将所有节点组合成抽象语法树
使用以下命令进行语法分析
clang -Xclang -ast-dump -fsyntax-only main.m
会被解析成如下语法树
-FunctionDecl 0x7ffe251a8ce0 line:11:5 main 'int (int, char **)'
|-ParmVarDecl 0x7ffe251a8b00 col:14 argc 'int'
|-ParmVarDecl 0x7ffe251a8bc0 col:27 argv 'char **':'char **'
`-CompoundStmt 0x7ffe251a9200
|-ObjCAutoreleasePoolStmt 0x7ffe251a91b8
| `-CompoundStmt 0x7ffe251a9188
| |-DeclStmt 0x7ffe251a8e30
| | `-VarDecl 0x7ffe251a8da8 line:14:13 used eight 'int' cinit
| | `-IntegerLiteral 0x7ffe251a8e10 'int' 8
| |-DeclStmt 0x7ffe251a8ee8
| | `-VarDecl 0x7ffe251a8e60 col:13 used six 'int' cinit
| | `-IntegerLiteral 0x7ffe251a8ec8 'int' 6
| |-DeclStmt 0x7ffe251a9010
| | `-VarDecl 0x7ffe251a8f18 col:13 used rank 'int' cinit
| | `-BinaryOperator 0x7ffe251a8ff0 'int' '+'
| | |-ImplicitCastExpr 0x7ffe251a8fc0 'int'
| | | `-DeclRefExpr 0x7ffe251a8f80 'int' lvalue Var 0x7ffe251a8da8 'eight' 'int'
| | `-ImplicitCastExpr 0x7ffe251a8fd8 'int'
| | `-DeclRefExpr 0x7ffe251a8fa0 'int' lvalue Var 0x7ffe251a8e60 'six' 'int'
| `-CallExpr 0x7ffe251a9128 'void'
| |-ImplicitCastExpr 0x7ffe251a9110 'void (*)(id, ...)'
| | `-DeclRefExpr 0x7ffe251a9028 'void (id, ...)' Function 0x7ffe20b20e88 'NSLog' 'void (id, ...)'
| |-ImplicitCastExpr 0x7ffe251a9158 'id':'id'
| | `-ObjCStringLiteral 0x7ffe251a9068 'NSString *'
| | `-StringLiteral 0x7ffe251a9048 'char [8]' lvalue "rank-%d"
| `-ImplicitCastExpr 0x7ffe251a9170 'int'
| `-DeclRefExpr 0x7ffe251a9088 'int' lvalue Var 0x7ffe251a8f18 'rank' 'int'
`-ReturnStmt 0x7ffe251a91f0
`-IntegerLiteral 0x7ffe251a91d0 'int' 0
3.静态分析(代码静态分析通过语法树查找非语法错误) 1.错误检测
称为出现和未定义、已定义和未使用的变量
2. 类型检测
类型一般分为两类:动态和静态。 动态检测在运行时进行,静态检测在编译时进行。 在编写代码时,您可以向任何对象发送任何消息,并且在运行时,您将检查该对象是否仍然可以响应此类消息。
4. CodeGen - IR 代码生成 1. CodeGen 负责从上到下遍历语法树并将其翻译为 LLVM IR2。 LLVM IR 是 Frontend 的输出和 LLVM Backend 的输入。 前端和Objective-C Runtime的桥接语言 桥接和Objective-C Runtime桥接的应用
1、Objective-C中的Class/Meta Class/Protocol/Category结构体的内存结构是在这一步生成的,并放置在Mach-O指定的Section中(如Class:_DATA,_objc _classrefs),这个DATA段将还存储一些静态变量
2. objct对象发送的消息会被编译成什么? 它将被编译成 objc_msgSend 调用。 发生这一步,语法树中的ObjCMessageExpr被翻译成对应版本的objc_msgSend,对super关键字的调用被翻译成objc_msgSendSuper
3、根据修饰符strong/weak/copy/atomic,合成@property手动实现的getter/setter,@synthesize的过程也是在这一步完成的
4、生成block_layout的数据结构、变量的捕获(__block/和__weak)、生成_block_invoke函数都发生在这一步
5、总说ARC是编译器帮我们插入一些内存管理代码,也是在这一步完成的。
ARC:分析对象的引用关系,插入objc_StoreStrong / Objc_StoreWeak等ARC代码
将 ObjCAutotreleasePoolStmt 转换为 objc_autoreleasePoolPush/Pop
实现手动调用[super dealloc]
用ivar综合每个Class的.cxx_destructor方法来手动释放该类的成员变量,而不是MRC时代的“self.xxx = nil”
LLVM中间产品和优化
使用以下命令生成LLVM中间产物IR(Intermediate Representation),并复制此过程
clang -O3 -S -emit-llvm main.m -o main.ll
使用以下命令,代码将使用 LLVM 进行优化。
//针对全局变量优化、循环优化、尾递归优化等。
//在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass。
clang -emit-llvm -c main.m -o main.bc
生成汇编代码
使用以下命令生成相应的汇编代码。
clang -S -fobjc-arc main.m -o main.s
至此,编译阶段完成,将编写的代码转换为机器可以识别的汇编代码。 汇编器将汇编代码转换成机器可以执行的指令。 每个汇编语句几乎对应一条机器指令。 根据汇编指令和机器指令的对照表一一翻译就够了。
使用以下命令生成对应的目标文件。
clang -fmodules -c main.m -o main.o
后来Xcode新建的工程中没有pch文件,为什么?
pch文件就是用pch文件导入UIKit、Foundation等库,这样就不需要在每个源文件中解析那么多东西了。 现在iOS正在搞乱一些全局变量和它自己的模块中的一些东西。 把它放在上面。
Xcode上有模块的概念,并且各个设置也是开放的。 默认情况下,库被打包为模块,尤其是 UIKit 和 Foundation 等库都是模块。 好处是,我加上这个参数(fmodules)后,我也会手动把#import改成@import,现在的编译会比最早不带pch的快很多,因为pch的时候默认不会出现出现
$clang -E -fmodules main.m //加入fmodules参数生成可执行文件
关联
该阶段是将上一阶段生成的目标文件与引用的静态库进行链接,最终生成可执行文件。 链接器解决了目标文件和库之间的链接。
链接器在编译时做了什么?
1. Mach-O主要包含代码和数据。 代码是函数的定义,数据是全局变量的定义。 代码和数据都通过符号关联起来。
2、对于Mach-O上面的代码,要操作的变量和函数必须绑定到各自的地址。 链接器的作用是完成变量和函数的符号及其地址的绑定。
为什么要进行符号绑定?
1、如果地址和符号没有绑定,为了让机器知道你操作的是哪个地址,需要在写代码的时候设置内存地址。
2.可读性差,修改代码后需要重新维护地址
3、需要针对不同平台编写多段代码,相当于直接写汇编
为什么要将项目中的多个 Mach-O 合并为一个?
1.多个文件之间的变量和套接字是相互依赖的源码编译的过程,因此链接器需要绑定项目中多个Mach-O文件的符号和地址。
2、如果不绑定,单个文件生成的Mach-O将难以运行。 当运行时遇到调用其他文件的函数实现时,将找不到函数地址。
3.链接多个目标文件,创建符号表,记录所有已定义和未定义的符号。 如果出现相同的符号,则会出现错误消息“ld:重复符号”。 如果目标文件中没有符号,如果找到符号,则会提示“未定义符号”的错误信息。
链接器对代码执行的主要操作是什么?
1.进入代码文件查找未定义的变量
2、收集所有符号定义和引用地址,放入全局符号表中
3.计算合并后的宽度和位置,生成相同类型的线段进行合并,并建立绑定
4、项目中不同文件中变量的地址重定位
链接器如何消除无用函数并保证Mach-O的大小? 当链接器安排函数的调用关系时,它会跟踪来自主函数的每个引用,并将其标记为活动状态。 后续完成后,这些没有标记为live的函数都是无用函数。总结:一个源文件的编译过程
代码练习
#import
int main() {
NSLog(@"hello world!");
return 0;
}
1.生成Mach-O可执行文件
clang -fmodules main.m -o main
2.生成抽象语法树
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
3.生成汇编代码
clang -S main.m -o main.s
加载和链接
一个App从可执行文件到实际的启动运行代码,基本上需要经历加载和动态库链接两个步骤。
程序运行时,会有独立的虚拟地址空间,操作系统上会同时运行多个进程,它们之间的虚拟地址空间是隔离的。
加载是将可执行文件映射到虚拟内存的过程。 由于显存资源稀缺,只将程序中最常用的部分保存在显存中,不太常用的数据则放在C盘中。 这也是一个动态加载的过程。
加载过程就是流程构建的过程。 操作系统主要做了三件事:
1.创建独立的虚拟地址
2、读取可执行文件头,建立虚拟空间与可执行文件的映射关系
3、设置CPU的寄存器区域作为可执行文件的入口地址,开始运行
静态库
静态库是在编译时链接的库,需要链接到您的 Mach-O 文件中。 如果需要更新,必须重新编译。 它不能动态加载和更新。
动态库
动态库是在运行时链接的库。 动态加载可以使用dyld来实现。 iOS中的系统库都是动态链接的。
共享缓存
Mach-O是编译后的产品,动态库只能在运行时链接。 Mach-O 中没有动态库的符号定义。
Mach-O中动态库中的符号是未定义的,但记录了它们的名称和对应的库路径。
运行时用dlopen和dlsym导入动态库时源码编译的过程,首先根据记录的库路径找到对应的库,然后通过记录的名称和子符号找到绑定地址。
优势:
代码共享、易于维护、减少可执行文件大小
参考:
iOS底层学习——从编译到启动的奇幻旅程
sunnyxx 的 clang 视频分享