iOS-底层原理 15:dyld发展史

iOS 底层原理 文章汇总

dyld简介

  • dyld全名 The dynamic link editor

  • 是苹果的动态链接器

  • 是苹果操作系统的一个重要组成部分;

  • 在应用被编译打包成可执行文件之后(即Mach-O),将其交由dyld负责链接,加载程序

  • dyld贯穿了App启动的过程,包含加载依赖库、主程序,如果我们需要进行性能优化、启动优化等,不可避免的需要和dyld打交道

  • 且dyld是开源的,我们可以在官网下载它的源码来阅读理解

dyld 1.0(1996-2004)

  • dyld 1包含在NeXTStep 3.3中,在此之前的NeXT使用静态二进制数据。作用并不是很大,

  • dyld 1是在系统广泛使用C++动态库之前编写的,由于C++有许多特性,例如其初始化器的工作,在静态环境工作良好,但是在动态环境中可能会降低性能。因此大型的C++动态库会导致dyld需要完成大量的工作,速度变慢

  • 在发布macOS 10.0Cheetah前,还增加了一个特性,即Prebinding预绑定。我们可以使用Prebinding技术为系统中的所有dylib和应用程序找到固定的地址。dyld将会加载这些地址的所有内容。如果加载成功,将会编辑所有dylib和程序的二进制数据,来获得所有预计算。当下次需要将所有数据放入相同地址时就不需要进行额外操作了,将大大的提高速度。但是这也意味着每次启动都需要编辑这些二进制数据,至少从安全性来说,这种方式并不友好。

dyld 2(2004-2017)

dyld 2从2004年发布至今,已经经过了多个版本迭代,我们现在常见的一些特性,例如ASLRCode Signshare cache等技术,都是在dyld 2中引入的

dyld 2.0(2004-2007)

  • 2004年在macOS Tiger中推出了dyld 2

  • dyld 2dyld 1完全重写的版本,可以正确支持C++初始化器语义,同时扩展了mach-o格式并更新dyld。从而获得了高效率C++库的支持。

  • dyld 2具有完成的dlopendlsym(主要用于动态加载库和调用函数)实现,且具有正确的语义,因此弃用了旧版的API

    • dlopen:打开一个库,获取句柄

    • dlsym:在打开的库中查找符号的值

    • dlclose:关闭句柄。

    • dlerror:返回一个描述最后一次调用dlopen、dlsym,或 dlclose 的错误信息的字符串。

  • dyld设计目标提升启动速度。因此仅进行有限的健全性检查。主要是因为以前的恶意程序比较少

  • 同时dyld也有一些安全问题,因此对一些功能进行了改进,来提高dyld在平台上的安全性

  • 由于启动速度的大幅提升,因此我们可以减少Prebinding的工作量。与编辑程序数据的区别在于,在这里我们仅编辑系统库,且可以仅在软件更新时做这些事情。因此在软件更新过程中,可能会看到“优化系统性能”类似的文字。这就是在更新时进行Prebinding。现在dyld用于所有优化,其用途就是优化。因此后面有了dyld 2

dyld 2.x(2007-2017)

  • 在2004-20017这几年间进行了大量改进,dyld 2的性能显著提高
  • 首先,增加了大量的基础架构平台
    • 自从dyld 2在PowerPC发布之后,增加了x86x86_64armarm64和许多的衍生平台。
    • 还推出了iOStvOSwatchOS,这些都需要新的dyld功能
  • 通过多种方式增加安全性
    • 增加 codeSigning代码签名、
    • ASLR(Address space layout randomization)地址空间配置随机加载:每次加载库时,可能位于不同的地址
    • bound checking边界检查:mach-o文件中增加了Header的边界检查功能,从而避免恶意二进制数据的注入
  • 增强了性能
    • 可以消除Prebinding,用share cache共享代码代替

ASLR

  • ASLR是一种防范内存损坏漏洞被利用的计算机安全技术,ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者跳转到内存特定位置来利用函数

  • Linux已在内核版本2.6.12中添加ASLR

  • Apple在Mac OS X Leopard 10.5(2007年十月发行)中某些库导入了随机地址偏移,但其实现并没有提供ASLR所定义的完整保护能力。而Mac OS X Lion 10.7则对所有的应用程序均提供了ASLR支持。

  • Apple在iOS 4.3内导入了ASLR

bounds checking 边界检查

  • 对mach-o header中的许多内容添加了重要的边界检查功能,从而可以避免恶意二进制数据的注入

share cache 共享代码

  • share cache最早实在iOS3.1macOS Snow Leopard中被引入,用于完全取代Prebinding

  • share cache是一个单文件,包含大多数系统dylib,由于这些dylib合并成了一个文件,所以可以进行优化。

    • 重新调整所有文本段(_TEXT)数据段(_DATA),并重写整个符号表,以此来减小文件的大小,从而在每个进程中仅挂载少量的区域。允许我们打包二进制数据段,从而节省大量的RAM

    • 本质是一个dylib预链接器,它在RAM上的节约是显著的,在普通的iOS程序中运行可以节约500-1g内存

    • 还可以预生成数据结构,用来供dyld和Ob-C在运行时使用。从而不必在程序启动时做这些事情,这也会节约更多的RAM和时间

  • share cache在macOS上本地生成,运行dyld共享代码,将大幅优化系统性能

dyld 2 工作流程

dyld 2是纯粹的in-process,即在程序进程内执行的,也就意味着只有当应用程序被启动时,dyld 2才能开始执行任务

以下是dyld 2的工作流程图示


dyld 2的工作流程图示
  • 1、dyld的初始化,主要代码在dyldbootstrap::start,接着执行dyld::_maindyld::_main代码较多,是dyld加载的核心部分;
  • 2、检查并准备环境,例如获取二进制路径、检查环境配置,解析主二进制的image header等信息
  • 3、实例化主二进制的image loader,校验主二进制和dyld的版本是否匹配
  • 4、检查share cache是否已经map,如果没有则需要先执行map share cache操作
  • 5、检查DYLD_INSERT_LIBRARIES,如果有则加载插入的动态库(即实例化image loader)
  • 6、执行link操作,会先递归加载依赖的所有动态库(会对依赖库进行排序,被依赖的总是在前面),同时在这阶段将执行符号绑定,以及rebasebinding操作;
  • 7、执行初始化方法,OC的+load和C的constructor方法都会在这个阶段执行;
  • 8、读取Mach-oLC_MAIN段获取程序的入口地址,调用main函数

简化版

  • ① 解析 mach-o 文件,找到其依赖的库,并且递归的找到所有依赖的库,形成一张动态库的依赖图。iOS 上的大部分 app 都依赖几百个动态链接库(大部分是系统的动态库),所以这个步骤包含了较大的工作量。

  • ② 匹配 mach-o 文件到自身的地址空间

  • ③ 进行符号查找(perform symbol lookups)

  • rebasebinding:由于 app 需要让地址空间配置随机加载,所以所有的指针都需要加上一个基地址

  • ⑤ 运行初始化程序,之后运行 main() 函数

dyld 3(2017-至今)

  • dyld 3是2017年WWDC推出的全新的动态链接器,它完全改变了动态链接的概念,且将成为大多数macOS系统程序的默认设置。2017 Apple OS平台上的所有系统程序都会默认使用dyld 3.

  • dyld 3最早是在2017年的iOS 11中引入,主要用来优化系统库。

  • 而在iOS 13系统中,iOS全面采用新的dyld 3来替代之前的dyld 2,因为dyld 3完全兼容dyld 2,其API接口也是一样的,所以,在大部分情况下,开发者并不需要做额外的适配就能平滑过渡。

为什么需要重新设计dyld 2,形成新的dyld 3 ?

重新设计dyld,主要从以下几方面进行考虑

  • 性能:想要尽可能的提高启动速度

  • 安全性:在dyld 2中增加了安全特性,但是很难跟随现实情形,虽然做了很多工作,但是难以实现这个目标

  • 可靠性可测试性:为此Apple发布了很多不错的测试框架,例如XCTest,但是这些测试框架依赖于动态链接器底层功能,然后将测试框架的库插入进程中,所以不能用于测试现有的dyld代码,且难以测试安全性和性能水平

如何将 dyld 2 改进和优化为 dyld 3?

改进和优化建议

从上面的dyld 2的工作流程中,我们了解了dyld 2的执行流程,可以从以下两个方面来改进和优化:

  • 确定安全敏感的部分

    • Parse mach-o headers解析mach-o 和 Find dependencies找到依赖库,是安全敏感部分,即最大的安全隐患之一;

    • 恶意撰改mach-o头部,可以进行某些攻击;

    • 如果App使用了 @rpaths搜索路径,可以通过恶意撰改路径或者将一些库插入到特定的位置,来达到破坏程序的目的;

  • 确定大量占用资源的部分(即可缓存部分)

    • Perform symbol lookups符号查找就是其中一个,因为在一个特定的库中,除非进行软件更新或者在磁盘上更改库,不然符号将始终位于库中的相同的偏移位置(即符号偏移量固定);

dyld 2 改进和优化

以下是dyld 2dyld 3 的一些改变,主要是将安全敏感的部分 和 占用大量资源的部分移动到上层,然后将一个closure写入磁盘进行缓存,然后我们在程序进程中使用closure。以下是图示

dyld2 -> dyld 3的改进和优化

dyld 3 组成部分/工作流程

dyld 3的工作流程主要分为3部分,如下所示


dyld 3的工作流程图示

第一部分:out-of-process :mach-o parser

进程外的mach-o分析器和编译器,是普通的后台程序,用于提高测试基础架构的性能。

第一部分主要在App进程之外做以下工作:

  • 解析所有搜索路径@rpath、环境变量,因为它们会影响启动速度

  • 分析 mach-o二进制数据

  • 执行符号查找

  • 利用这些结果创建launch clourse

第二部分:in-process :engine

进程内的引擎,这部分常驻在内存中,且在dyld 3不再需要分析mach-o文件头或者执行符号查找就可以启动应用,因为分析mach-o和执行符号查找都是耗时操作,所以极大的提高了程序启动速度。

第二部分主要在App进程中做以下工作:

  • 检查launch closure是否正确

  • 映射到dylib中,再跳转main函数

第三部分:launch closure :cache

启动闭包launch closure缓存服务。其中大多数程序启动都会使用缓存,而不需要调用进程外 mach-o分析器和编译器。且launch closuremach-o更简单,因为launch closure内存映射文件,不需要用复杂的方法进行分析,我们可以进行简单的校验,目的是为了提高速度

  • 系统应用的launch closure直接加入到共享缓存 share cache

  • 对于第三方应用,我们将在应用安装或者更新期间构建launch closure,因为此时 system library已发生更改

  • 默认情况下,在iOStvOSwatchOS上,这些操作都将在运行之前为您预先构建

  • macOS上,由于可以侧向加载应用程序(这里应该是指非App Store安装的应用),因此如果需要,in-process engine可以在首次启动时RPC(Remote Procedure Call)到out to the daemon,然后,它就可以使用缓存的closure了。

所以综上所述,dyld 3 把很多耗时的查找、计算和 I/O 操作都预先处理好了,使得启动速度有了很大的提升。即dyld 3把很多耗时的操作都提前处理好了,极大提升了启动速度。

启动闭包(launch closure)

这是一个新引入的概念,指的是 app 在启动期间所需要的所有信息。比如这个 app 使用了哪些动态链接库,其中各个符号的偏移量,代码签名在哪里等等。

dyld 3符号缺失问题

  • dyld 2中默认采取的是lazy symbol的符号加载方式

  • dyld 3中,在app启动之前,符号解析的结果已经在launch closure内了,所以lazy symbol就不再需要了。

  • 如果此时,如果有符号缺失的情况,dyld 2 和 dyld 3的表现是不同的

    • dyld 2中,首次调用缺失符号时App会crash

    • dyld 3中,缺失符号会导致App一启动就会crash

总结

  • dyld 2工作流程

    • 解析mach-o头部

    • 查找依赖库

    • 映射mach-o文件,放入地址空间中

    • 执行符号查找

    • 使用ASLR进行rebasebind绑定

    • 运行所有初始化器

    • 执行main函数

  • dyld 3工作流程

    • 进程外:将dyld 2中的mach-o头部解析、符号查找移到了进程外执行,且将其执行结果放入启动闭包,存储到磁盘中

    • 进程内:验证启动闭包正确性,并映射dylib,执行main函数

    • 启动闭包缓存服务

参考链接

推荐阅读更多精彩内容