前言
计算机语言分为机器语言:汇编语言,高级语言。
可以将高级语言分为两种:1,编译语言和解释型语言(直译式语言)。
编译型语言(一次性翻译)
编译型语言的程序只要经过编译器编译之后,每次运行程序都可以直接运行如oc,swift等
解释型语言(逐步进行解释执行)
解释语言编写的程序在每次运行时都需要通过解释器对程序进行动态解释和执行,如php,javascript等
编译链接过程:
1,预处理:macro 宏, import 头文件替换及处理其他的预编译指令,产生.i文件。(都是以#号开头)
2,编译:把预处理完的一系列文件进行一系列词法、语法、静态分析,并且优化后生成相应的汇编代码,产生.s文件;
3,汇编:汇编器将汇编代码生成机器指令,输出目标文件,产生.o文件(根据汇编指令和机器指令的对照表一一翻译就可以了);
4,链接:在一个文件中可能会到其他文件,因此,还需要将编译生成的目标文件和系统提供的文件组合到一起,这个过程就是链接。经过链接,最后生成可执行文件。
预处理(预编译)-> 产生.i文件
使用终端到main.m所在文件夹,使用命令:
clang -E main.m -o main.i
处理源代码文件中的以"#"开头的预编译指令:
-
"#define"删除并展开对应宏定义。
-
处理所有的条件预编译指令。如#if/#ifdef/#else/#endif。
-
"#include/#import"包含的文件递归插入到此处。
-
删除所有的注释"//或/**/"。
-
添加行号和文件名标识。如“# 1 “main.m””,编译调试会用到。
编译 -> 产生.s文件
编译器:用来把源文件转换为更低级的语言,xcode使用的clang前端编译器的作用就是把源代码转化为更为低级的LLVM IR,这个LLVM IR是操作系统无关的,然后后端编译器LLVM通过这个中间语言来进行下一步二进制文件的产出。这要得益于LLVM的三层架构,假如你需要增加一种语言,只需要增加一种前端,如过你需要增加一种处理器架构,也只需要增加一种后端。
编译器前端:Clang编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。Clang 是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。
公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。
编译器后端:
clang -S main.i -o main.s
编译过程也分为 词法分析 -> 语法分析 -> 静态分析 最后优化生成相应的汇编代码,得到.s文件
词法分析:源代码的字符序列分割成一个个token(关键字、标识符、字面量、特殊符号),比如把标识符放到符号表(静态链接那篇,重点讲符号表)。
语法分析:把词法分析生成的token生成抽象语法树 AST,此时运算符号的优先级确定了;有些符号具有多重含义也确定了,比如“*”是乘号还是对指针取内容;表达式不合法、括号不匹配等,都会报错。
静态分析:分析类型声明和匹配问题。比如整型和字符串相加,肯定会报错。
一般会把类型分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。
静态类型,是在编译时做检查。当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。
中间语言生成和优化:CodeGen根据AST自顶向下遍历逐步翻译成 LLVM IR,并且在编译期就可以确定的表达式进行优化,比如代码里t1=2+6,可以优化t1=8。(假如开启了bitcode,)
目标代码生成与优化:根据中间语言生成依赖具体机器的汇编语言。并优化汇编语言。这个过程中,假如有变量且定义在同一个编译单元里,那给这个变量分配空间,确定变量的地址。假如变量或者函数不定义在这个编译单元,得链接时候,才能确定地址。
汇编 -> 产生.o文件
clang -c main.s -o main.o
汇编器将上一步生成的可读的汇编代码转化为机器代码。最终产物就是 以 .o 结尾的目标文件。
链接
clang main.o -o main
这一阶段是将上个阶段生成的目标文件和引用的静态库链接起来,最终生成可执行文件(Mach-O),链接器解决了目标文件和库之间的链接。
动态库和静态库
动态库
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)将一些程序升级变得简单。
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
静态库
在程序编译期的链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
-
程序在运行时独立运行,不需要依赖外部库。
-
静态库的调用通常比动态库的调用更快,因为不需要动态链接
-
生成的可执行程序较大。如果多个使用静态链接生成的程序同时运行会占用大量的内存空间静态库一旦被编译链接到可执行文件中,想要更新或替换静态库的代码需要重新编译整个程序。
动态库和静态库的区别
静态库
-
在编译时加载
-
优点:代码装载和执行速度比动态库快。
-
缺点:浪费内存和磁盘空间,模块更新困难。
动态库
-
在运行时加载
-
优点:体积比静态库小很多,更加节省内存。
-
缺点:代码装载和执行速度比静态库慢。
动态链接器
动态链接器的定义
动态链接器通常指的是dyld
。dyld
是它负责处理程序和动态库(dylib,动态链接库)之间的链接操作。dyld
的主要任务是在程序启动时解析和加载所需的动态库,并解决这些库中的符号引用。
- 加载动态库:
dyld
在程序启动时会加载指定的动态库。这包括系统库和应用程序可能依赖的任何第三方库。dyld
能够延迟加载某些库,直到它们被实际使用,这有助于提高启动速度和内存效率。 - 解析符号: 当程序引用了一个动态库中的函数或变量时,
dyld
会查找并绑定这些符号到正确的地址。这个过程称为符号解析,它确保了程序能正确地访问库中的功能。 - 重定位:
dyld
还负责重定位,这意味着它会调整库中的地址,以确保它们在进程的虚拟地址空间中正确放置。这通常涉及修改库内部的指针,使它们指向正确的内存位置。 - 缓存:
dyld
使用缓存机制来存储已加载的库和已解析的符号,这样在后续的程序执行中就不需要重复加载或解析相同的内容,从而加快了程序的运行速度。 - 懒惰链接(Lazy Binding): 为了进一步提高性能,
dyld
可以采用懒惰链接策略,只在程序真正尝试访问一个库中的符号时才解析该符号。这避免了在程序启动时不必要的工作。 - 可执行文件和动态库的集成: 在iOS中,
dyld
不仅处理动态库,还负责加载和执行可执行文件本身。可执行文件中包含了dyld
的启动信息,告诉它需要加载哪些库和如何初始化程序。 - 安全特性:
dyld
还实现了一些安全特性,如ASLR(地址空间布局随机化),通过随机化库的加载地址来增加攻击者预测内存布局的难度,从而提高系统的安全性。
dyld2和dyld3的区别
dyld2
和dyld3
是dyld
的不同版本,它们在功能和性能上有一些关键区别
下图为dyld2执行流程:
下图为dyld3执行流程:
相比于dyld 3,dyld 2的加载过程没有那么优化。它没有将工作分成两个阶段,而是直接在进程中完成所有任务,包括解析、查找依赖、映射文件、查找符号以及绑定和重定位等。这意味着这些操作可能会阻塞主线程,导致应用程序启动速度变慢。此外,由于没有生成和使用闭包,dyld 2也没有提供额外的安全性保障来防止恶意软件尝试修改或注入代码到正在运行的应用程序中。
dyld 3 包含这三个部分:
- 进程外 Mach-O 分析器和编译器 (out-of-process mach-o parser)
由于 dyld 2 存在的问题,dyld 3 中将采用提前写入把结果数据缓存成文件的方式构成一个 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 打开之前准备好。
dyld 3的符号缺失问题
dyld 2 默认采取的是 lazy symbol
的符号加载方式,但在 dyld 3中,在 App 启动之前,符号解析的结果已经在 lauch closure 内了,所以 lazy symbol
就不再需要。这时,如果有符号缺失的情况,APP 的行为会有不同:在 dyld 2 中,首次调用缺失符号时 APP 会 crash;而 dyld 3 中,缺失符号会导致 APP 一启动就会 crash。