前言
在我们的应用程序里面,有过一个词,是启动时间。通常我们更熟悉的是程序启动之后所发生的事情,因为这些事情本身就是有我们自己所构造的。但是启动之前的事情,却是系统所构造的。说到这里,就不得不说一个iOS很核心的东西----dyld。这个就和启动时间息息相关,而且从dyld到现在的dyld3,不断的在缩短这个启动时间,优化系统的性能。
启动时间:意思是main函数启动之前所用的时间;启动收尾:意思是启动程序所需要的全部信息。比如:程序启动使用了dyld里面的哪些内容?他们的哪些偏移的位置用于不同的符号?代码签名是什么?。。。等等;
资源准备
正文
缩短启动时间---减少代码
因为代码越少,运行时间越短。所以要使用更少的dylib库,减少嵌入的dylib库。当然使用系统库效果会更加好。同样的,声明较少的库和方法、减少初始化函数也会有缩短运行时间的效果。
缩短启动时间---多使用Swift编写
多使用Swift代码编写工程,因为这会避免很多C、C++和Objective-C才有的陷阱。
-
Swift没有初始化器; -
Swift不允许特定类型的未对齐数据结构,因为我们的cpu读取不同长度的数据结构内存时,需要切换与之对应的长度才行,当内存里面有很多长度不一的数据结构,那么读取起来,就要不停切换,这样十分消耗性能,所以苹果系统才会出现内存对齐这样的操作。而Swift就避免的这样的坑。这也是能提高性能,缩短启动时间的原因; -
Swift代码更加精简;
dyld
dyld包含在NeXTStep 3.3中,而NeXTStep可以追溯到1996年,此前NeXT使用静态二进制数据。NeXTStep具有不同的专用扩展,所以苹果开发者在macOS 10的早起版本上编写第三方包装器,以支持标准Unix软件。但是还是不能完全的匹配,经常有边界例子不能正常工作。因此造成运行缓慢。还有就是这个第三方库大规模的使用C++库。也造成了运行缓慢。
C++的特性:
C++初始化器排序方式,他们在静态环境中工作良好,但是在动态环境中,就可能降低性能。因此大型C++代码库导致动态链接器需要完成大量的工作,从而速度会变慢。
为了解决这一系问题,苹果系统的开发者,也想出来很多办法来解决。方法的其中之一 ----- 预绑定技术
系统使用预绑定技术为系统中的所有dylib和我们的工程,找到固定地址。那么动态加载器将会加载这些地址的所有内容。加载成功之后,将编译地址里面多有对应的二进制数据。以此来获取所有预计算地址,然后下次调用的时候,就把真正的数据放入到所对应的地址里面。
也就是说,当程序第一次加载时,系统会分配相应的地址,而这些地址里面存储的内容,就是脏数据,用来进行占位的。而再次进行调用的时候,就会把真正的数据填入到对应的地址里面。这样就避免重复的操作,大大的提高了程序的运行速度。
但是这样也存在缺陷,就是每次启动的时候,都会编译工程里面的所有的二进制数据。这样还是有很大的工作量。而且在安全上也有问题。
这样就有了dyld 2的推出。
dyld 2
dyld 2更好的支持C++
dyld 2是macOS Tiger的组成部分,是dyld的完全重写版本;他正确支持C++初始化器语义,系统扩展了mach-O格式,并且更新dyld。从而获得高效率的C++库支持;dyld 2还具有完整的本机dlopen和dlsymd实现。同时还改进一些功能,提高平台的安全性。
dyld 2减少了预绑定内容
以前dyld的预绑定是处理系统中的所有dylib和我们的工程,而dyld 2仅仅编辑系统库,这就减少预绑定的工作量。
dyld 2增加了基础结构和平台
dyld 2增加了大量的基础结构和平台,如x86、x86_64、arm、arm64和许多的衍生平台。同时还有iOS、tvOS、watchOS,都是需要匹配dyld 2的。
这样做的好处就是,真机只需要调用arm64架构的逻辑执行,而不需要加载其他架构里面的内容。能够针对不同架构来分别对待执行。
dyld 2增强安全性
首先是增加代码签名和ASLR,也就是地址空间配置随机加载,每次运行得到的地址,都是不同的,所以也不容易被hook。
dyld 2增加了mach-O文件头部中的项目
这个是重要的边界检查,可以避免恶意的二进制数据的加入。
dyld 2增强使用Share Cache
就是消除预绑定,转而使用Share Cache(共享缓存代码)。
Share Cache是一个单个文件,含有大多数系统dylib,因此可以进行优化。
Share Cache还调整所有文本段(TEXT)和所有数据段(DATA),重写整个符号表,目的是减小内容大小,使得每个进程只需要挂载少量的区域。
Share Cache允许打包二进制数据段,从而节省大量的RAM,运行时可以节约500M-1GB内存空间。
Share Cache实际上是dylib预链接器;
Share Cache他还是预生产数据结构供dyld和Objective-C,在运行时使用。所以在程序启动时,不再做那么多大量的加载工作量。提高了系统的性能
dyld 2的启动流程
- 先分析工程的
mach-O文件,弄清楚需要哪些库(递归处理); - 然后映射到所有mach-O文件,将之放入地址空间;
- 再执行符号表查询,将需要的内容复制到对应的地址指针上;
- 再进行绑定和基址重置,基础地址再与上一个随机地址,这一步是增加安全性;
- 最后,运行工程所有初始化器,准备执行main函数。
dyld 3
dyld 3的一个最突出的变革,就是动态链接器,是现在macOS system的默认配置,将全面取代dyld 2。
dyld 3提升了性能;dyld 3增强安全性;dyld 3更容易被测试。
dyld 3提升性能问题
为了提高程序启动和运行的速度,系统将大多数的dyld移出了进程,所以现在它只是普通的后台程序,可以使用标准的测试工具进行测试。也为后面进一步提高性能和运行速度打下了基础。
另外,也允许少量部分dyld驻留在进程中,从而减少程序的受攻击面积。代码少了,代码的运行速度就提高了,因此,也提高了启动速度。
在dyld 3中,将mach-O文件处理和符号表的查询,都写入磁盘。
dyld 3包含三个部分:
- 它是一个进程外
mach-O分析器和编译器; - 也是一个进程内引擎执行启动收尾处理;
- 也是一个启动收尾缓存服务;
大多数程序启动都会使用缓存,但始终不需要调用进程外mach-O分析器或编译器;启动收尾比mach-O更简单,他们是内存映射文件,不需要用复杂的方法进行分析,因此,提高了运行速度。
dyld 3增强安全性
在dyld 2中,增加了一些安全特性,但是很难跟随现实情形来增强安全性。
以前可以通过分析mach-O文件头和查找依赖关系,只需要使用撰改过的mach-O文件头进行攻击,修改相应的路径或者插入库到适当的位置,就能破坏原有的程序。
因此,现在是在后台的进程之外,完成了mach-O文件的一系列工作。
符号表的查找是十分耗时的操作,占用了大量的缓冲部分。因为在给定的库中,除非进行软件更新或者在磁盘上更改库,符号将始终位于库中的相同偏移位置。
dyld 3不再需要分析mach-O头文件,或者查找符号表,这些都是十分耗时的操作,这样也就提高了运行速度。
dyld 3更容易被测试
在以前,比较依赖于动态链接器的底层功能,将它们的库插入进程,因而不能用于测试现有的dyld代码,所以对dyld的安全性和性能水平难以测试。
dyld 3还是一个启动收尾缓存服务
苹果系统直接将程序收尾直接加入到共享缓存中,使启动收尾映射到缓存中,所有dylib都使用这个缓存来启动,就更加快捷了。也是提高程序的启动速度。
对于第三方程序,在使用或更新的时候,就会生成对应的收尾处理。
dyld 3完全兼容dyld 2
dyld 3对符号的解析
dyld必须加载所有符号,这需要占用大量资源,因此应该使用缓存,直接运行现有程序,会占用很多资源,将会花费很长的时间,为了解决这一问题,苹果系统就使用了一种机制----懒加载符号解析。
- 懒加载符号解析:默认情况下,库中的函数指针,如
printf并不是指向printf,默认情况下,它是指向dyld中的一个函数,此函数返回一个指针指向printf,因此启动时,调用printf将会进入dyld,返回printf进行首次调用,然后第二次就直接调用printf。
dyld 2懒加载符号解析:
- 符号的查询太繁琐;
- 每个符号在第一次调用时都会被查找;
- 缺少符号会在第一次调用它们时导致崩溃;
dyld 3懒加载符号解析:
- 所有的符号查找都是缓存的,由于已经缓存并计算所有符号,因此在程序启动时,不会产生额外开销来绑定它们,所以速度非常快;
- 可以检查是否存在所有符号,预先解析所有符号,如果缺失将进行奔溃,而不是在使用程序时再奔溃;
dyld 3的运行流程
- 解析所有路径、环境;
- 先分析工程的
mach-O二进制数据; - 再执行符号表查询,将需要的内容复制到对应的地址指针上;
- 利用这些结果创建收尾处理,向磁盘写入收尾处理;
- 检查启动收尾处理是否正确;
- 然后映射到
dylib之中 - 再跳转到main函数里面
推荐:结合iOS底层探究-----dyld程序启动加载流程可能收获更多。