【iOS】—— 编译链接
编译流程
编译流程分为四个步骤:
- 预处理(Prepressing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
预处理
编译的第一步,主要工作是将main.m
文件编译成main.i
文件。指令如下:
clang -E main.m -o main.i
预处理是将main.m文件编译成main.i文件。但是具体的情况是要处理源代码中以#开头的所有预编译指令。如下:
- #define删除,展开对应的宏定义。
- 处理所有的条件预编译指令。如#if,#ifdef,#else,#endif。
- #include&#import包含的文件递归插入到此处。
- 删除所有的注释。
- 添加行号和文件标识。
编译
将main.i
文件编译成main.s
文件,指令如下:
clang -S main.i -o main.s
对上面得到的main.i
文件进行:词法分析,语法分析,静态分析,优化生成相应的汇编代码,最终生成main.s
文件
解释一下:
- **词法分析:**把源代码的字符序列分割成一个个
token
(关键字、表示符、字面量、特殊符号),比如把标识符放到符号表里面。 - **语法分析:**生成抽象语法树AST,确定了符号的优先级,也确定了多重含义符号的意义。
- **静态分析:**分析类型声明和匹配问题。比如整型和字符串相加,肯定会报错。
- 中间语法生成:
CodeGen
根据AST(抽象语法树)
自上向下逐步翻译成LLVM IR
,并且对在编译期就可以确定的表达式进行优化,比如代码里面的a=1+3,可以优化成a=4。 - **目标代码生成与优化:**根据中间代码生成依赖具体机器的汇编语言,并优化汇编语言。个过程中,假如有变量且定义在同一个编译单元里,那么就给这个变量分配空间,确定变量的地址。假如变量或者函数不定义在这个编译单元里面,那就等到链接的时候才能确定地址。
汇编
将main.s
文件编译成main.o
文件(也就是我们常说的目标文件),指令如下:
clang -c main.s -o main.o
这个过程就是把上面得到的main.s
文件里面的汇编指令翻译成机器指令,最终生成等到main.o
。
链接
这个过程就是将main.o
编译成对应的Mach-O
文件,也就是我们常说的可执行文件,指令如下:
clang main.o -o main
链接的本质是将一个或多个目标文件和需要的库(静态库/动态库,如果需要的话)组合成一个Mach-O可执行文件。
动态库和静态库
静态库
在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。这种链接方式称为静态链接。
优缺点
**优点:**在编译链接的时候被整合到可执行文件中,是程序运行的时候独立,不依赖外部库。由于不需要动态链接,因此可以快速的整合到可执行文件中,调用比动态库更快。
**缺点:**生成可执行程序较大,如果多个使用静态链接生成的程序同时运行会占用大量的内存空间。静态库一旦被编译链接到可执行文件中,想要更新或替换静态库的代码需要重新编译整个程序。
动态库
**动态库是动态链接库,是实现共享函数库的一种方式。**在编译的时候动态库不会被拷贝到程序中,只会存储下动态库的引用,等找到真正使用的时候才回去查找-绑定-使用函数。
优缺点
**优点:**节省磁盘空间,且多个用到相同动态库的程序同时运行,库文件会进行共享。动态库的更新和替换比较方便只需要替换动态库文件即可,不需要重新编译整个程序。使得程序文件小,易于分发和部署。
**缺点:**必须依赖动态库,负责无法运行。动态库在程序启动时需要加载,可能会增加程序启动的时间,并且要注意版本兼容问题。
动态库和静态库的区别
- 编译方式
- 静态库是在编译阶段加载,将所用到的库函数的代码打包到生成的可执行文件中,静态库的生成在代码的编译阶段进行。
- 动态库是在运行时动态加载到程序中,可执行文件并不包含函数的实现代码,只是动态库的接口,当用到该函数的时候,从动态库文件中加载供程序使用。
- 内存使用方式
- 静态库是在编译时将库的代码打包到可执行程序中,在程序运行的时候,静态库中的代码一直驻留在内存中,不会占用额外的空间。
- 动态库的代码是在程序运行的时候被加载到内存中,会占用额外的空间,但与静态库相比动态库的内存使用具有更好的空间和性能优势,多个程序可以共享同一个动态库,而不需要重复加载相同的库文件,从而减少了系统的内存占用。
- 更新和维护方式
- 静态库的代码被打包成程序的一部分,静态库在更新和维护的时候需要重新编译和部署,才能更新静态库的代码。
- 动态库可以独立于程序进行更新,作为一个独立的文件,可以被多个程序共享。当需要更新的时候,只要替换掉旧的动态库文件不需要重新编译和部署使用的所有程序。
dyld
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核 XNU 完成 Mach-O 文件的加载,做好程序准备工作之后,交由 dyld 负责余下的工作。在 macOS 系统中,dyld 位于 D/usr/lib/dyld。
dyld2
在iOS13之前,所有的第三方app都是通过dyld2来启动的主要过程如下:
- 解析Mach-O的Head和Load Commands,找到其依赖的库,并递归找到所有依赖的库
- 加载Mach-O文件
- 进行符号查找
- 绑定和变基
- 运行初始化程序
图示如下:
上面的过程法尚在App启动的时候,包含大量的计算和I/O,因此经过优化之后正式提出了都有了dyld3。
dyld3
dyld3 被分为了三个组件:
- 进程外Mach-O分析器和编译器(out-of-process mach-o parser)
由于 dyld 2 存在的问题,dyld3中将采用提前写入把结果数据缓存成文件的方式构成一个 lauch closure(可以理解为缓存文件)。 - 进程内引擎执行launch closure处理(in-process engine)
验证”lauch closures“是否正确,映射dylib,执行main函数。此时,它不再需要分析mach-o header和执行符号查找,节省了不少时间。 - launch closure 缓存服务 (launch closure cache)
系统程序的 lauch closure 直接内置在 shared cache 中,而对于第三方APP,将在APP安装或更新时生成,这样就能保证 launch closure 总是在 APP 打开之前准备好。
图示如下: