WWDC关于dyld 3优化与应用启动优化分析

2,136 阅读17分钟

一、程序启动过程

1、启动时间

  • 定义

main函数执行之前要做的事情

2、启动加速?

1、减少代码,代码越少,启动就越快

2、应该使用更少的dylib,减少嵌入的dylib,从时间的角度来看,最好使用系统库效果会更好

3、应该声明较少的库和方法,减少初始化函数

4、可以更多的使用Swift,Swift会避免很多在C/C++和Objective-C可能遇到的陷阱,使用Swift可以获得更快的启动速度

3、启动收尾

  • 定义

应用程序所需要的全部信息(比如使用什么dyld,他们在哪些偏移位置使用什么样的符号)

4、启动时长优化(影响因素)

1、 网络请求

2、dylib动态库的加载

3、复杂的nib文件过多

5、关于Swift的一些扩展

Swift的一些特性:

1、Swift没有初始化器

2、不允许特定类型的未对齐数据结构(这样的结构会延长启动时间)

3、代码更精简,开发速度更快

二、Dyld的发展史

1、dyld1(NeXTStep3.3 发布于1996年):

特点:

1、使用静态二进制数据,标准化POSIX, 早于dlopen的调用,效率低下缓慢

2、C++动态库的系统之前编写,C++的很多特性(初始化器排序方式等在静态环境工作良好,但是动态环境降低性能),大型的C++项目,导致动态链接器需要完成大量的工作,速度变慢

3、预绑定技术,为系统中所有的dylib和应用程序找到固定的地址,动态加载器将会加载这些地址的所有内容,如果加载成功,就编辑所有这些二进制数据,以获得所有预计算地址,然后下次当它将所有数据放入相同地址时,不必进行其他额外的工作,这将会极大的提升速度,但是这也意味着,每次启动时会编辑你的二进制数据,所以并不是最好的办法,至少安全上来说如此。

2、dyld2 (dyld的完全重写版本)MacOS Tiger

特点

1、正确支持C++初始化器语义

2、扩展MachO格式,并且更新dyld,从而获得高效率的C++库支持

3、它具有完整的本机dlopen和dlsym实现

4、具有正确的语义,弃用旧版的API(旧版API仍然使用在macOS,没有加入其他的平台使用)

5、dyld设计是提升运行速度,因此仅进行有限的健全性检查。


3、dyld 的安全和效率问题

由于存在一些安全问题,因此需要一些改进以提升现有平台的安全性

由于速度大幅提升,因此可以减少预绑定的数量(不同于编辑应用程序数据,这里只编辑系统库,可以只在软件更新的时候做这些事情,所以在安装应用程序时,会有优化系统性能之类的提示的文字,这时就是在更新预绑定,现在dyld用于所有优化,其用途就是优化)

于是出现了dyld2,增加了大量的基础结构(架构)和平台,dyld2在PowerPC发布之后,增加了x86,x86x64 arm arm64和许多的衍生平台,出现了各大平台的操作系统,他们的更新都需要dyld。

通过使用多种方式来增强安全性,增加了签名和ASLR,也就是地址空间配置随机加载,意味着每次加载库,可能位于不同的地址。其他的请参考2016年WWDC的nick关于如何启动程序的视频。

最后,增加了mach-o文件头中的项目,这是重要的边界检查功能,可以避免恶意二进制数据的加入。

增强了性能,因此可以取消预绑定,转而使用了共享代码。

4、共享代码:

共享代码最早被引入是在iOS3.1和macOS Snow,并且完全取代预绑定,它是一个单文件,含有大多数操作系统dylib,由于合并成一个文件,因此可以进行优化,我们重新调整所有文本段和所有数据段,重写整个符号表,以减小大小,从而在每个进程中仅挂载少量的区域,它允许我们打包二进制数据段以节省大量的RAM,它实际上是dylib预链接器。

这里不会讨论特定的优化效果,但是它的RAM节约是很明显的,在普通的iOS系统上,运行时可以节约500M-1GB内存。

它还预生成数据结构供dyld和objc在运行时使用,让我们不必在程序启动时做这些事情,这也会节约更多的RAM和时间。

共享代码在macOS上本地生成运行dyld共享代码,将会大幅度的优化系统性能,并且带来其他的好处。在其他的平台上,在Apple生成共享代码,然后共享给开发者使用。

三、dyld3(全新的动态链接器)

1、基本介绍

它完全改变动态链接概念,将成为大多数macOS系统程序的默认设置。 2017 Apple OS平台上的所有系统程序都会默认使用它。

在未来的AppleOS平台和第三方程序中,它将会全面取代dyld2。

2、我们为什么要再次使用动态链接器呢?

首先是为了性能。性能是一个永恒的主题,我们要尽量提高启动速度。我们认为,它可以帮助我们获得更快的程序启动和运行速度。


其次是安全性。前面再dyld2中增加了一些安全的特性,但是很难跟随现实情形增强安全性,苹果对此做了很多工作,但是难以实现这个目标。

3、那么我们是否能够进行更积极的安全检查?并且从设计上提高安全性?

最后是可测试性和可靠性。我们能否让dyld变得易于测试?为此Apple发布了很多不错的框架,比如XCTest,可以使用他们进行测试。但是他们依赖于动态链接器的底层功能,将他们的库插入进程,因此他们不能用于测试现有的dyld代码,这让我们难以测试安全性和性能水平。


4、我们遇到这种情况该如何做呢?


我们将大多数dyld移除进程,现在它只是普通的后台程序,可以使用标准测试工具进行测试。这让我们未来进一步提高速度和性能。
另外也允许部分的dyld驻留在进程之中,但是驻留部分尽可能的小,从而减少程序的受攻击面积,因为性能的提升,还会提高启动速度。代码运行速度是前所未有的,为了明白这一点,接下来简要的演示一线dyld 2 如何启动程序。

5、dyld 2的启动流程介绍

我们使用 dyld 2,你的程序开始启动:

1、需要分析你的mach-o文件,弄清楚你需要哪些库,还可能需要其他的库,然后进行了递归分析,直到获得所有dylib的完整图。 普通iOS程序需要300到600个dylib,数据庞大,需要进行大量的处理。

2.然后我们映射到所有mach-o文件,将他们放入地址空间。

3、然后执行符号查找。若你的程序使用printf函数,将会查找printf是否在库系统中,然后找到它的地址,将它复制到应用程序中的函数指针,然后我们进行绑定和基址重置,复制这些指针。由于使用随机地址,所有指针必须使用基址。

4、最后,我们可以运行所有初始化器。这时,我们开始准备执行main函数。

Pasted Graphic.png

6、我们中间经历了大量的工作,我们如何加快其速度将这些步骤移出进程呢?

首先确定安全敏感性组件。从苹果的角度来看,这是最大的安全隐患之一。

分析mach-o文件头和查找依赖关系,因此人们可以使用撰改过的mach-o文件头进行攻击。而且你的程序可能使用@rpath,他们是搜索路径,通过撰改这些路径或者将库插到适当的位置,可以破坏程序。因此苹果在后台程序的进程之外完成所有这些工作。

然后确定大量占用资源的部分,也就是占用缓冲的部分,他们是符号查找。因为在给定的库中,除非进行软件更新或者在磁盘上更改库,符号将始终位于库中的相同偏移位置。

7、我们已经确定这些内容,那就看他们再dyld3中是如何工作的。

Pasted Graphic 3.png

我们将这些部分移到上层,然后向磁盘写入收尾处理,前面讲过,启动收尾处理是启动程序的重要环节。稍后可以在进程中使用dyld3包含这三个部分。

他是一个进程外mach-o分析器和编译器,也是一个进程内引擎,执行启动收尾处理。也是一个启动收尾缓存服务。大多数程序启动会使用缓存,但始终不需要调用进程外mach-o分析器或编译器。启动收尾比mach-o更简单,他们是内存映射文件,不需要用复杂的方法进行分析。我们可以简单的验证他们,其作用是为了提高速度。

下面详细的看每个部分:

因此,dyld3是进程外mach-o分析器,它解析所有搜索路径、所有rpaths、所有环境变量,它们会影响启动的速度。

然后分析mach-o二进制数据,执行所有符号查找,利用这些结果创建收尾处理,它是普通的后台进程,让我们提高测试基础架构的性能。

dyld也是一个小型进程内引擎,这部分驻留在进程中,是你通常会看到的部分,它所做的事情是检查启动收尾处理是否正确。

然后映射到dylib之中,再跳转到main函数。

Pasted Graphic 4.png

你可能注意到:dyld3不需要分析mach-o文件头或执行符号查找,不需要做这些就可以启动你的应用程序。由于这些是花费时间的部分,因此可以极大的提高程序启动速度。

最后,dyld 3 还是一个启动收尾缓存服务。这是什么意思呢? 我们将系统收尾直接加入到共享缓存,我们已使用这个工具在系统中运行和分析每个mach-o文件,我们可以直接将他们放入共享缓存,使它们映射到缓存中,所有的dylib都使用它来启动,我们甚至不需要打开其它文件。对于第三方程序,我们在程序安装或系统更新的时候,生成你的收尾处理,因为那时系统库已经发生更改。默认情况下,甚至在程序运行之前,就在iOS、tvOS和watchOS 上生成了收尾处理。

在macOS上,由于可以侧向加载程序,如果需要,进程内引擎可以在首次启动时RPC到后台程序,在此之后,能够使用缓存的收尾处理。但是在其他的平台并不需要这么做。

可能存在的问题:

首先,完全兼容dyld 2.x,所以现有的一些API可能会导致你的程序运行变慢,或者会在dyld 3中使用回退模式。尽量避免这个问题,或者稍后再讨论。

所做的一些优化,现在可能已经不再需要,不要在这方面多话费过多的力气。

将会使用更严格的链接语义。很多的语义现在还无法使用,现在甚至是错误的,在加入新动态链接器时,发现有很多这样的情况,目的是为了发现所有的边界例子。苹果所做的事情,是放入一个工作区以支持旧二进制数据,但是并不想更进一步,然后会进行链接或后续检查,查看你使用了哪些SDK,然后将禁用新二进制数据的工作区,让你能够解决这些问题。所以新二进制数据将会造成链接器问题。

接下来是讨论,数据段的未对齐指针。假设你有一个全局性结构体指向一个函数或指向另外一个全局性函数,在你的程序启动之前,我们必须修复这个指针。在我们的系统上,指针必须自然对齐以获得最佳性能,然而,修复未对齐指针非常复杂,它们可能覆盖多个内存页,造成更多的内存页错误和其他问题,这可能会产生与多处理器相关的细微问题。 Pasted Graphic 5.png 静态链接器已经忽略这个警告,ld警告 指针未对齐。如果你能消除这个警告,那么问题已经得到解决。

本周提供的源代码存在一些Swift键径问题,但是他们将会被修复,你可以忽略这些问题,也可以修复这这些问题。如果想知道怎么做,接下来将演示,这需要大量的工作,但是你不能在Swift中做这些事情:

Pasted Graphic 7.png 标注的部分是强指针指向,默认情况下,编译器将会为你进行正确的对齐。但是有些时候,你可能需要特殊的对齐。在本例子中要求默认对齐规则进行对齐。

解决方法:设置一个结构体,在这个结构体中设置指针,这将会强制动态链接器在程序启动时修复指针,所以看到这样的代码,可以清除所有对齐,重新调整结构,将指针放在前面,这样更有利于对齐。希望不要写这样的处理,如果编写Swift代码,你肯定不必这样做。

符号解析 dyld 2执行懒符号解析,dyld必须加载所有的符号,这需要占用大量的资源,因此应该使用缓存。直接运行现有程序,确实会占用很多资源,将会占用很多时间。为此我们使用一种机制,名称为懒符号解析。

默认情况下,库中的函数指针,比如printf并不指向printf,默认情况下,它指向dyld中的一个函数,此函数返回一个指向printf的函数指针。因此启动时,调用printf将会进入dyld,返回printf进行首次调用。然后,第二次,直接调用printf。

由于我们已经缓存并且计算所有的符号,因此在程序启动时不会产生额外的开销来绑定他们,我们将会这样做。

当你这样做时,缺失符号的行为将会有所不同。在现有懒符号机制中,如果缺失一个符号,首次调用,将会正确启动,首次调用该符号程序将会崩溃。如果使用强符号,将会立即崩溃。

为此,提供了一个兼容模式,我们要做的是,将导致自动崩溃的符号,放入dyld 3,如果不能找到你的符号,我们将会绑定该符号,首次调用将崩溃,这是现在的SDK的工作模式。

未来的SDK中,我们将强制预先进行所有符号解析,如果你缺失一个符号,将会崩溃。在开发过程中,你应该能够发现这些崩溃现象。而不是用户在程序运行时发现它们。

现在可以模拟它,有一个特殊的链接器标注,即 bind at load。如果你将它添加到你的调试程序,将会变得很慢,因此只应该放入调试版本。但是将它放入调试版本,你将获得更加可靠的行为。这让你能够更好的使用dyld3。另外只应该在测试版本中使用!

image.png

dlopen、dlsym和dladdr 这些仅仅应该在十分必要时才使用它们,它们具有一些十分出错的语义,但是一些情况下,仍然需要使用他们,特别是使用dlsym找到的符号,我们需要在运行时找到它们,我们不会提前知道这些符号,不能使用prefetch和presearching。当你使用dlopen或dlsym时,我们会读入以前未接触过的所有符号表页,这会占用大量的资源,此外,我们可能必须RPC到后台程序,这取决于其复杂度,我们正在开发更好的替代方法,目前还没有完成。我们还需要了解你们的用例,以确保我们开发出的方案适合你们的需求。这些方案即将会发布,希望得到你们的反馈意见。

dlclose dlclose是一个误用词,它是一个Unix API,如果在我们的系统上编写它,我们会将它命名为dlrelease,实际上它并不关闭dylib,它减少refcount计数,如果refcount变为0,将会关闭它。它的重要性是什么?它并不利于资源管理,如果你有一个库用于特定硬件,你不应该关闭硬件来响应dlclose,因为程序中的其他代码可能会在后台打开硬件,因此你的硬件不会关闭,应该使用显式资源管理。

我们的平台还有很多特性防止dylib被卸载,来介绍几个例子,因为你们可能会去这样做。你的dylib中可以有Objective-C类,这将导致dylib不可卸载,你可以具有Swift类,这也会导致dylib不可卸载。你可以具有C底层线程或C++线程本地变量,这些都会导致dylib不可卸载。因此在具有一些现成Unix程序的macOS上,我们会保持这个特性,但是我们所有其它平台几乎每个dylib都会这样做,并不能在这些平台上有效的工作,因此我们可以将它视为无操作指令,不会在任何平台上进行操作。如果这会导致问题,请告诉我们。

dyld all image infos 这是进程中的内在dylib的接口,它来自最初的dyld1,但是它只是内存中的一个结构而不是API,当我们有5或10个dylib时并没有问题,但是如果有300、400、500个dylib,其设计方式将导致浪费大量内存,我们需要回收那些内存。我们需要高性能,而且节省内存,所以在未来的版本中,我们将会取消它。但是,会提供一个替代性的API,因此,它很少被用到,如果你要使用它,我希望你知道为什么要使用它,如何使用它,确保我们设计的API适合你的用例,有很多功能已经不再适用,不符合你的预期,如果你不需要它们,可以忽略它们,我们希望获得这方面的信息,请让我们知道你将如何使用它。

最佳实践 Pasted Graphic 9.png 首先,应确保将bind at load 添加到LD FLAGS,应该仅在调试版本中这样做。 应修复数据段中的任何未对齐指针 Pasted Graphic 10.png 这里有个警告,应该使用新Swift键径功能,消除所有错误警告,你也可以忽略这个警告,因为苹果将会解决这个问题

当你调用dlclose时,应该确保不依赖于任何正运行的终止函数 我们想知道你们为什么使用dlopen、dlsym、dladdr 和 all image infos 结构,以确保我们的替代性API能够满足你们的需求。

如果它们是POSIX的一部分,将会被保留,这只会造成性能降低,对于all image infos,它将会被取消以节省内存。

请使用DYLD USAGE标题向我们报告漏洞,让我们能够支持你们的所有用例,更多信息请访问:developer.apple.com/videos/play…