一、动态链接
本文只是简单说一下iOS中的装载下半部 — 动态链接。关于动态库的实现细节,比如:如何做到被多个进程共享(地址无关代码PIC、全局偏移表GOT等)没有细说。
1.1 流程概述
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有的程序模块都链接成一个个单独的可执行文件。
动态链接涉及运行时的链接及多个文件的装载,必需要有操作系统的支持,因为动态链接的情况下,进程的虚拟地址空间的分布会比静态链接情况下更为复杂,还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙的变化。目前主流的操作系统几乎都支持动态链接这种方式。
iOS中,动态链接库的加载步骤大概可以分为5步:(今日头条 iOS 客户端启动速度优化)
第一步:load dylibs image 读取库镜像文件。在每个动态库的加载过程中, dyld需要:
- 分析所依赖的动态库
- 找到动态库的mach-o文件
- 打开文件
- 验证文件
- 在系统核心注册文件签名
- 对动态库的每一个segment调用mmap()
第二步:Rebase image
第三步:Bind image
- 发生在 link 这个过程(下文中的第五步),就是将加载进来的二进制变为可用状态的过程。简单来说就是:
rebase => binding
。 - 由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址。
- rebase修复的是指向当前镜像内部资源的指针。rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO。
- bind修复的是指向镜像外部资源的指针。bind步骤在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算。
第四步:Objc setup
- 注册Objc类 (class registration)
- 把category的定义插入方法列表 (category registration)
- 保证每一个selector唯一 (selctor uniquing)
第五步:initializers
以上三步属于静态调整(fix-up),都是在修改__DATA
segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。在这里的工作有:
- Objc的+load()函数
- C++的构造函数属性函数 形如
__attribute__((constructor))
void DoSomeInitializationWork() - 非基本类型的C++静态全局变量的创建(通常是类或结构体)(non-trivial initializer) 比如一个全局静态结构体的构建,如果在构造函数中有繁重的工作,那么会拖慢启动速度
Objc的load函数和C++的静态构造函数采用由底向上的方式执行,来保证每个执行的方法,都可以找到所依赖的动态库。
上图是在自定义的类XXViewController的+load方法断点的调用堆栈,清楚的看到整个调用栈和顺序:
- dyld 开始将程序二进制文件初始化
- 交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
- 由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
- runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法
至此,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)。
整个事件由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。
如果程序刚刚被运行过,那么程序的代码会被dyld缓存,因此即使杀掉进程再次重启加载时间也会相对快一点,如果长时间没有启动或者当前dyld的缓存已经被其他应用占据,那么这次启动所花费的时间就要长一点,这就分别是热启动和冷启动的概念。
下面来介绍动态链接中的这几个概念:
1.2 rebase
rebase就是指针修正的过程。
一个mach-o的二进制文件中,包含了text段和data段。而data段中的数据也会存在引用关系。 我们知道在代码中,我们可以用指针来引用,那么在一个文件中怎么代表引用呢,那就是偏移(相对于text段开始的偏移)。
当二进制加载到内存中的时候,起始地址就是申请的内存的起始地址(slide),不会是0,那么如何再能够找到这些引用的正确内存位置呢? 把偏移加上(slide)就好了。 这个过程就是rebase的过程。
1.3 bind
“决议”更倾向于静态链接,而“绑定”更倾向于动态链接,即它们所使用的范围不一样。
bind就是符号绑定的过程。
为什么要bind? 因为符号在不同的库里面。
举个简单的例子,我们代码里面调用了 NSClassFromString
. 但是NSClassFromString
的代码和符号都是在 Foundation.framework
这个动态库里面。而在程序未加载之前,我们的代码是不知道NSLog
在哪里的,于是编译器就编译了一个 stub 来调用 NSClassFromString
:
可以看到,我们的代码里面直接从 pc + 0x3701c的地方取出来一个值,然后直接br, 也就是认为这个值就是 NSClassFromString
的真实地址了。我们再看看这个位置的值是啥:
也就是说,这块地址的8个字节会在bind之后存入的就是 NSClassFromString
的代码地址, 那么就实现了真正调用 NSClassFromString
的过程。
上面我们知道了为啥要bind. 那是如何bind的呢? bind又分为哪些呢?
1.2.1 怎么bind
首先 mach-o 的 LoadCommand里面的会有一个cmd来描述 dynamic loader info,数据结构与示例如下:
1 | //以下的偏移量是相对于目标文件/可执行文件的起始地址,注意后者的起始地址一般不会是0,寻址时要加上 |
解析出来会得到这样的信息:
rebase
:就是针对 “mach-o在加载到虚拟内存中不是固定的首地址” 这一现象做数据修正的过程。一般可执行文件在没有ASLR造成的首地址不固定的情况下,装载进虚拟地址中的首地址都是固定的,比如:Linux下一般都是0x08040000
,Windows下一般都是0x0040000
,Mach-O的TEXT地址在__PageZero之后的0x100000000
地址.binding
:就是将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject,即符号_OBJC_CLASS_$_NSObject
,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起。lazyBinding
:就是在加载动态库的时候不会立即binding,当时当第一次调用这个方法的时候再实施binding。 做到的方法也很简单: 通过dyld_stub_binder
这个符号来做。 lazy binding的方法第一次会调用到dyld_stub_binder, 然后dyld_stub_binder负责找到真实的方法,并且将地址bind到桩上,下一次就不用再bind了。weakBinding
:OC的代码貌似不会编译出Weak Bind
. 目前遇到的Weak Bind
都是C++的template
的方法。特点就是:Weak bind的符号每加载进来二进制都会bind到最新的符号上。比如2个动态库里面都有同样的weak bind
符号,那么所有的的符号引用都会bind到后加载进来的那个符号上。
可以看到,这里面记录了二进制data段里面哪些是 rebase信息,哪些是binding信息:
可以看到binding info的数据结构,bind的过程根据不同的opcode解析出不同的信息,在opcode为BIND_OPCODE_DO_BIND
的时候,会执行bindLocation
来进行bind。
截取了 bindLocation 的代码:
1 | uintptr_t ImageLoaderMachO::bindLocation(const LinkContext& context,...){ |
可以看出, bind过程也不是单纯的就是把符号地址填过来就好了, 还有type和addend的逻辑。不过一般不多见,大部分都是BIND_TYPE_POINTER
.
addend 一般用于要bind某个数组中的某个子元素时,记录这个子元素在数组的偏移。
1.2.2 Lazy Bind
延迟加载是为了启动速度。上面看到bind的过程,发现bind的过程需要查到对应的符号再进行bind. 如果在启动的时候,所有的符号都立即bind成功,那么势必拖慢启动速度。
其实很多符号都是LazyBind的。就是第一次调用到才会真正的bind.
其实刚才截图的 imp___la_symbol_ptr__objc_getClass
就是一个 LazyBind 的符号。 图中的 0x10d6e8 指向了 stub_helper
这个section中的代码。
如上图中
- 先取了
0x10d6f0
的 4个字节数据存入 w16. 这个数据其实是 lazy bind info段的偏移 - 然后走到 0x10d6d0, 取出 ImageLoader cache, 存入 x17
- 把 lazy bind info offset 和 ImageLoaderCache 存入栈上。
- 然后取出 dyld_stub_binder的地址,存入x16. 跳转 dyld_stub_binder
- dyld_stub_binder 会根据传入的 lazy bind info的 offset来执行真正的bind. bind结束后,刚才看到的
0x10d6e8
这个地址就变成了NSClassFromString
。就完成了LazyBind的过程。
dyld_stub_binder
的源码此处不再展示。
1.2.3 Weak Bind
OC的代码貌似不会编译出Weak Bind
. 目前遇到的Weak Bind
都是C++的 template
的方法。特点就是:Weak bind的符号每加载进来二进制都会bind到最新的符号上。比如2个动态库里面都有同样的weak bind
符号,那么所有的的符号引用都会bind到后加载进来的那个符号上。
二、库: 静态库和动态库
库(Library),是我们在开发中的重要角色,库的作用在于代码共享、模块分割以及提升良好的工程管理实践。说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用。
为什么要用库?一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件(静态库和动态库的共同点就是不会暴露内部具体的代码信息)。另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。
根据库在使用的时候 Link 时机或者说方式(静态链接、动态链接),库分为静态库和动态库。
2.1 静态库
静态库即静态链接库(Windows 下的 .lib,linux 下的.a,Mac 下的 .a .framework)。之所以叫做静态,是因为静态库在链接时
会被完整地拷贝一份到可执行文件中(会使最终的可执行文件体积增大)。被多个程序使用就会有多份冗余拷贝。如果更新静态库,需要重新编译一次可执行文件,重新链接新的静态库。
2.2 动态库
动态库即动态链接库。与静态库相反,动态库在编译时并不会被拷贝到可执行文件中,可执行文件中只会存储指向动态库的引用(使用了动态库的符号、及对应库的路径等)。等到程序运行时
,动态库才会被真正加载进来,此时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。
动态库的优点是:
- 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去(不需要拷贝到每个可执行文件中),所以可执行文件的体积要小很多。
- 代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份(因为这个原因,动态库也被称作共享库)。
- 易于维护:使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
常见的可执行文件的形式:
- Linux系统中,ELF动态链接文件被称为动态共享对象(
DSO,Dynamic SharedObjects
),简称共享对象,一般都是以.so
为扩展名的一些文件; - Windows系统中,动态链接文件被称为动态链接库(
Dynamical Linking Library
),通常就是我们平时很常见的以.dll
为扩展名的文件; - OS X 和其他 UN*X 不同,它的库不是“共享对象(.so)”,因为 OS X 和 ELF 不兼容,而且这个概念在 Mach-O 中不存在。OS 中的动态链接文件一般称为动态库文件,带有
.dylib
、.framework
及链接符号.tbd
。- 库文件可以在
/usr/lib
目录下找到(这一点和其他所有的 UN*X 一样,不同的是在OS X 和 iOS 中没有/lib目录),这些库已被设置全局可用。 - 我们在使用系统的.dylib动态库时,经常发现没有头文件,其实这些库的头文件都位于一个已知位置,如
/usr/local/include
、/usr/include
等 (后者文件夹在新系统中由SDK附带了,见 /usr/include missing on macOS Catalina (with Xcode 11) )。
- 库文件可以在
- OS X 与其他 UN*X 另一点不同是:没有
libc
。开发者可能熟悉其他 UN*X 上的C运行时库(或Windows上的MSVCRT) 。但是在 OS X 上对应的库/usr/lib/libc.dylib
只不过是指向libSystem.B.dylib
的符号链接。 - 以C语言运行库为例,补充一下运行库的概念:任何一个C程序,它的背后都有一套庞大的代码来进行支撑,以使得该程序能够正常运行。这套代码至少包括入口函数,及其所依赖的函数所构成的函数集合。当然,它还理应包括各种标准库函数的实现。这样的一个代码集合称之为运行时库(Runtime Library)。而C语言的运行库,即被称为C运行库(CRT)。运行库顾名思义是让程序能正常运行的一个库。
2.3 两个非常重要的库 LibSystem、libobjc
libSystem 提供了 LibC(运行库) 的功能,还包含了在其他 UN*X 上原本由其他一些库提供的功能,列几个熟知的:
- GCD libdispatch
- C语言库 libsystem_c
- Block libsystem_blocks
- 加密库(比如常见的md5函数) libcommonCrypto
还有些库(如数学库 libm、线程库 libpthread)虽然在/usr/lib中看到虽然有这些库的文件,但都是libSystem.B.dylib的替身/快捷方式,即都是指向libSystem的符号链接。
libSystem 库是系统上所有二进制代码的绝对先决条件,即所有的二进制文件都依赖这个库,不论是C、C++还是Objective-C的程序。这是因为这个库是对底层系统调用和内核服务的接口,如果没有这些接口就什么事也干不了。这个库还是/usr/ib/system目录下一些库的保护伞库(通过LC_REEXPORT_LIB
加载命令重新导出了符号) 。
总结来说:libSystem在运行库的基础上,增加了一些对底层系统调用和内核服务的抽象接口。所以在下面的流程中,会发现libSystem是先于其他动态库初始化的。
libobjc与libsystem一样,都是默认添加的lib,包含iOS开发天天接触的objc runtime.
2.4 补充两个概念: 模块与image
程序模块
:从本质上讲,普通可执行程序和动态库中都包含指令和数据,这一点没有区别。在使用动态库的情况下,程序本身被分为了程序主要模块(Program1
)和动态链接文件(Lib.so
Lib.dylib
Lib.dll
),但实际上它们都可以看作是整个程序的一个模块,所以当我们提到程序模块时可以指程序主模块也可以指动态链接库。映像(image)
,通常也是指这两者。可执行文件/动态链接文件,在装载时被直接映射到进程的虚拟地址空间中运行,它是进程的虚拟空间的映像,所以很多时候,也被叫做映像/镜像文件(Image File)。
2.5 .a/.dylib与.framework的区别
前者是纯二进制文件,文件不能直接使用,需要有.h文件的配合,后者除了二进制文件、头文件还有资源文件,代码可以直接导入使用(.a + .h + sourceFile = .framework
)。
Framework 是苹果公司的 Cocoa/Cocoa Touch 程序中使用的一种资源打包方式,可以将代码文件、头文件、资源文件(nib/xib、图片、国际化文本)、说明文档等集中在一起,方便开发者使用。Framework 其实是资源打包的方式,和静态库动态库的本质是没有什么关系(所以framework文件可以是静态库也可以是动态库,iOS 中用到的所有系统 framework 都是动态链接的)。
在其它大部分平台上,动态库都可以用于不同应用间共享, 共享可执行文件,这就大大节省了内存。但是iOS平台在 iOS 8 之前,苹果不允许第三方框架使用动态方式加载,开发者可以使用的动态 Framework 只有苹果系统提供的 UIKit.Framework,Foundation.Framework 等。开发者要进行模块化,只能打包成静态库文件:.a + 头文件
、.framework
(这时候的 Framework 只支持打包成静态库的 Framework),前种方式打包不够方便,使用时也比较麻烦,没有后者的便捷性。
iOS 8/Xcode 6 推出之后,允许开发者有条件地创建和使用动态库,支持了动态 Framework。开发者打包的动态 Framework 和系统的 UIKit.Framework 还是有很大区别。后者不需要拷贝到目标程序中,是一个链接。而前者在打包和提交 app 时会被放到 app main bundle 的根目录中,运行在沙盒里,而不是系统中。也就是说,不同的 app 就算使用了同样的 framework,但还是会有多份的框架被分别签名,打包和加载,因此苹果又把这种 Framework 称为 Embedded Framework(可植入性 Framework)。
不过 iOS8 上开放了 App Extension 功能,可以为一个应用创建插件,这样主app和插件之间共享动态库还是可行的。
数量上,苹果公司建议最多使用6个非系统动态库。
然后就是,在上传App Store打包的时候,苹果会对我们的代码进行一次 Code Singing,包括 app 可执行文件和所有Embedded 的动态库,所以如果是动态从服务器更新的动态库,是签名不了的,sandbox验证动态库的签名非法时,就会造成crash。因此应用插件化、软件版本实时模块升级等功能在iOS上无法实现。不过在 in house(企业发布) 包和develop 包中可以使用。
三、Mach-O 文件的动态链接 — dyld
3.1 dyld2与dyld3
dyld 是 the dynamic link editor 的缩写,它是苹果的动态链接器。在系统内核做好程序准备工作之后,交由 dyld 负责余下的工作。
(除了动态链接编辑器(dynamic link editor),有的场景也翻译为动态加载器(dynamic loader))
在2017WWDC,Apple推出了Dyld3。在iOS 13系统中,iOS全面采用新的dyld 3以替代之前版本的dyld 2。dyld 3带来了可观的性能提升,减少了APP的启动时间。
Dyld2是从程序开始时才开始执行的,而Dyld3则将Dyld2的一些过程进行了分解。
Dyld3最大的特点是部分进程外的,分为out-of-process,和in-process。即操作系统在当前app进程之外完成了一部分dyld2在进程内的工作。以达到提升app启动性能和增强安全的目的。
out-process会做:
- 分析Mach-O Headers
- 分析以来的动态库
- 查找需要的Rebase和Bind的符号
- 将上面的分析结果写入缓存。
in-process会做:
- 读取缓存的分析结果
- 验证分析结果
- 加载Mach-O文件
- Rebase&Bind
- Initializers
使用了Dyld3后,App的启动速度会进一步提高。
而WWDC2019 苹果宣布针对Dyld3做了以下优化:
- 避免链接无用的framework;
- 避免在app启动时链接动态库;
- 硬链接所有依赖项
3.2 dyld的工作机制
在Mach-O 文件的装载完成,即内核加载器做完相关的工作后,对于需要动态链接(使用了动态库)的可执行文件(大部分可执行文件都是动态链接的)来说,控制权会转交给链接器,链接器进而接着处理文件头中的其他加载命令。真正的库加载和符号解析的工作都是通过LC_LOAD_DYLINKER
加载命令指定的动态链接器在用户态完成的。通常情况下,使用的是 /usr/lib/dyld
作为动态链接器,不过这条加载命令可以指定任何程序作为参数。
链接器接管刚创建的进程的控制权,因为内核将进程的入口点设置为链接器的入口点。
dyld是一个用户态的进程。dyld不属于内核的一部分,而是作为一个单独的开源项目由苹果进行维护的(当然也属于Darwin的一部分) ,点击查看项目网址。从内核的角度看,dyld是一个可插入的组件,可以替换为第三方的链接器。dyld对应的二进制文件有两个,分别是
/usr/lib/dyld
、/urs/lib/system/libdyld.dylib
,前者通用二进制格式(FAT)
,filetype为MH_DYLINKER
,后者是普通的动态链接库格式(Mach-O)。
从调用堆栈上看dyld、libdyld.dylib的作用:
前者dyld
是一段可执行的程序,内核将其映射至进程地址空间,将控制权交给它进行执行,递归加载所需的动态库,其中也会将动态链接器的另一种形式的libdyld.dylib
加载,因为动态链接器dyld其不但在应用的装载阶段起作用,在主程序运行的时候,其充当一个库的角色,还提供了dlopen
、dlsym
等api,可以让主程序显式运行时链接(见下文)。(关于这一点,没有找到明确的文档说明。如果有人有正确的理解,请一定要评论区告诉我一下,感激不尽)
Linux中,动态链接库的存在形式稍有不同,Linux动态链接器本身是一个共享对象(动态库),它的路径是/lib/ld-linux.so.2,这实际上是个软链接,它指向/lib/ld-x.y.z.so, 这个才是真正的动态连接器文件。共享对象其实也是ELF文件,它也有跟可执行文件一样的ELF文件头(包括e_entry、段表等)。动态链接器是个非常特殊的共享对象,它不仅是个共享对象,还是个可执行的程序,可以直接在命令行下面运行。因为ld.so是共享对象,又是动态链接器,所以本来应由动态链接器进行的共享对象的重定位,就要靠自己来,又称“自举”。自举完成后ld.so以一个共享对象的角色,来实现动态链接库的功能。
我们需要了解一下LC_LOAD_DYLIB
这个加载命令,这个命令会告诉链接器在哪里可以找到这些符号,即动态库的相关信息(ID、时间戳、版本号、兼容版本号等)。
1 | struct dylib { |
链接器要加载每一个指定的库,并且搜寻匹配的符号。每个被链接的库(Mach-O格式)都有一个符号表,符号表将符号名称和地址关联起来。符号表在Mach-O目标文件中的地址可以通过LC_SYMTAB
加载命令指定的 symoff 找到。对应的符号名称在 stroff, 总共有 nsyms 条符号信息。
下面是LC_SYMTAB
的load_command:
1 | //定义在<mach-o/loader.h>中 |
在 <mach-o/dyld.h> 动态库头文件中,也为我们提供了查询所有动态库 image 的方法(也可以使用otool -L 文件路径
命令来查看,但看着没代码全):
1 |
|
四、dyld工作流程详解
通过源码来看一下dyld的工作流程,只是部分片段,详细的可以下载源码。
4.1 __dyld_start
下面的汇编代码很简单,如果不清楚,可以看一下这篇汇编入门文章iOS需要了解的ARM64汇编。
1 | #if __arm64__ |
4.2 dyldbootstrap::start()
1 | // This is code to bootstrap dyld. This work in normally done for a program by dyld and crt. |
4.3 dyld::_main()
dyld也是Mach-O文件格式的,文件头中的 filetype 字段为MH_DYLINKER
,区别与可执行文件的 MH_EXECUTE
,所以dyld也是有main()函数的(默认名称是mian(),也可以自己修改入口地址的)。
因为这个函数太长,写在一起不好阅读,所以按照流程功能点,自上而下分为一个个代码片段。关键的函数会在代码中注释说明
方法名及说明
1 | // dyld的入口指针,内核加载dyld,跳转到__dyld_start函数:进行了一些寄存器设置,然后就调用了该函数。Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which sets up some registers and call this function. |
第一步 配置上下文信息,设置运行环境,处理环境变量
1 |
|
第二步 加载共享缓存
在iOS系统中,UIKit,Foundation等基础库是每个程序都依赖的,需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。iOS的dyld采用了一个共享库预链接缓存,苹果从iOS 3.0开始将所有的基础库都移到了这个缓存中,合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下(OS X中是在/private/var/db/dyld目录),按不同的架构保存分别保存着,如dyld_shared_cache_armv7。而且在OS X中还有一个辅助的.map文件,而iOS中没有。
如果在iOS上搜索大部分常见的库,比如所有二进制文件都依赖的libSystem,是搜索不到的,这个库的文件不在文件系统中,而是被缓存文件包含。关于如何从共享缓存中提取我们想看的库,可以参考链接dyld详解第一部分
1 |
|
第三步 实例化主程序image
1. 源码解读
ImageLoader:前面已经提到image(映像文件)常见的有可执行文件、动态链接库。ImageLoader 作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。
从下面可以看到大概的顺序:先将动态链接的 image 递归加载,再依次进行可执行文件的链接。
1 |
|
2. instantiateFromLoadedImage
1 | // The kernel maps in main executable before dyld gets control. We need to |
从这个方法中,我们大致可以看到加载有三步:
isCompatibleMachO
是检查mach-o的subtype是否是当前cpu可以支持;instantiateMainExecutable
就是实例化可执行文件, 这个期间会解析LoadCommand, 这个之后会发送 dyld_image_state_mapped 通知;addImage
添加到 allImages中。
第四步 加载插入的动态库
通过遍历 DYLD_INSERT_LIBRARIES 环境变量,调用 loadInsertedDylib 加载。
在三方App的Mach-O文件中通过修改DYLD_INSERT_LIBRARIES的值来加入我们自己的动态库,从而注入代码,hook别人的App。
1 |
|
第五步 链接主程序(重点link())
1. 源码解读
1 |
|
2. ImageLoader::link()
加载二进制的过程: instantiate(实例化) –> addImage –> link –> runInitializers
其中link就是动态链接的过程
1 | void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath) |
3. 反向依赖
每个库之间的符号并非只能单向依赖。即库与库之间是可以相互依赖符号的。
单向依赖:即 A.dylib 依赖 B.dylib。那么B中就不能依赖A中的符号。
一次dyld加载进来的二进制之间可以相互依赖符号。
原因很简单,就是因为上面看到动态链接过程中,并不是完全加载完一个被依赖的动态库,再加载下一个的。而是 recursiveLoadLibraies,recursiveRebase,recursiveBind。 所有的单步操作都会等待前一步所有的库完成。因此当 recursiveBind的时候,所有的动态库二进制已经加载进来了,符号就可以互相找了。
一次dyld的过程只会一次动态link,这次link的过程中的库符号可以互相依赖的,但是如果你通过dlopen
、-[NSBundle loadBundle]
的方式来延迟加载的动态库就不能反向依赖了,必须单向依赖,因为这是另外一次dyld的过程了。
反向依赖还要有个条件,条件就是符号必须存在,如果因为编译优化把符号给strip了,那就没法bind了,还是会加载失败的。
第六步 链接插入的动态库
1 |
|
第七步 弱符号绑定weakBind
1 | // <rdar://problem/12186933> do weak binding only after all inserted images linked |
第八步 执行初始化方法initialize
1. 源码解读
dyld会优先初始化动态库,然后初始化主程序。
1 |
|
2. initializeMainExecutable()
1 | void initializeMainExecutable() |
在上面的doImageInit
、doModInitFunctions
函数中,会发现都有判断libSystem
库是否已加载的代码,即libSystem要首先加载、初始化。在上文中,我们已经强调了这个库的重要性。之所以在这里又提到,是因为这个库也起到了将dyld与objc关联起来的作用:
2. dyld到objc的流程(详细见下篇)
可以从上面的调用堆栈中看到,从dyld到objc的流程:
libSystem
库的初始化libdispatch
库的初始化:libdispatch
是实现 GCD 的核心用户空间库。在void libdispatch_init(void)
方法中会调用void _os_object_init(void)
1 |
|
- 然后就是 objc的源码
objc-os.mm
中的_objc_init
函数了:
1 | /** |
_dyld_objc_notify_register
这个方法在苹果开源的dyld里面可以找到,然后看到调用了dyld::registerObjCNotifiers
这个方法:
1 | void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped) |
这三个函数就很熟悉了,位于objc-runtime-new.mm
中,objc运行时老生常谈的几个方法(关于OBJC的部分,内容太多,这里简单介绍,下篇细谈),每次有新的镜像加载时都会在指定时机触发这几个方法:
- map_images : 每当 dyld 将一个 image 加载进内存时 , 会触发该函数进行image的一些处理:如果是首次,初始化执行环境等,之后
_read_images
进行读取,进行类、元类、方法、协议、分类的一些加载。 - load_images : 每当 dyld 初始化一个 image 会触发该方法,会对该 image 进行+load的调用
- unmap_image : 每当 dyld 将一个 image 移除时 , 会触发该函数
值得说明的是,这个初始化的过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像 GCD、XPC 等重头的系统库初始化分支没有提及(当然,有缓存机制在,也不会重复初始化),总结起来就是 main 函数执行之前,系统做了非常多的加载和初始化工作,但都被很好的隐藏了,我们无需关心。
然后,从上面最后的代码(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL); 以及注释register cxa_atexit() handler to run static terminators in all loaded images when this process exits
可以看出注册了cxa_atexit()
函数,当此进程退出时,该处理程序会运行所有加载的image中的静态终止程序(static terminators)。
第九步 查找主程序入口点并返回,__dyld_start会跳转进入
1 |
|
4.4 小结
引自iOS 程序 main 函数之前发生了什么一文中的片段,《 Mike Ash 这篇 blog 》对 dyld 作用顺序的概括:
- 从 kernel 留下的原始调用栈引导和启动自己
- 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
- non-lazy 符号立即 link 到可执行文件,lazy 的存表里
- Runs static initializers for the executable
- 找到可执行文件的 main 函数,准备参数并调用
- 程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
- 程序main函数 return 后执行 static terminator
- 某些场景下 main 函数结束后调 libSystem 的 _exit 函数
然后,使用调用堆栈,来看下dyld的工作流程,只注释了认为重要的部分。
1 |
|
关于更多的理论知识,可以阅读下iOS程序员的自我修养-MachO文件动态链接(四)、实践篇—fishhook原理(:程序运行期间通过修改符号表(nl_symbol_ptr和la_symbol_ptr),来替换要hook的符号对应的地址),将《程序员的自我修养》中的理论结合iOS系统中的实现机制做了个对比介绍。
五、加载动态库的其他方式: dlopen
加载动态库的另一种方式:显式运行时链接dlopen
上面的这种动态链接,其实还可以称为装载时链接,与静态链接相比,其实都是属于在程序运行之前进行的链接。还有另一种动态链接称为显式运行时链接(Explicit Runtime Linking)。
装载时链接:是在程序开始运行时(前)通过dyld动态加载。通过dyld加载的动态库需要在编译时进行链接,链接时会做标记,绑定的地址在加载后再决定。
显式运行时链接:即在运行时通过动态链接器dyld提供的API dlopen 和 dlsym 来加载。这种方式,在编译时是不需要参与链接的。
- dlopen会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。
- dlopen也可以选择是立刻解析所有引用还是滞后去做。
- dlopen打开动态库后返回的是模块的指针(句柄/文件描述符(FD))
- dlsym的作用就是通过dlopen返回的动态库指针和函数的符号,得到函数的地址然后使用。
不过,通过这种运行时加载远程动态库的 App,苹果公司是不允许上线 App Store 的,所以只能用于线下调试环节。(有人说加签过的动态库可以使用dlopen,签名是在App打包的时候完成,如果从其他途径(如网络下载)获取的动态库是无法完成验签的。未验证)
除此之外,还有另一种 NSBundle load/loadAndReturnError 的方式,其底层也是使用dlopen实现,只是增加了验签,也是在App打包的时候完成签名。
1 | NSString *path = [[NSBundle mainBundle] pathForResource:@"TestLib" ofType:@"framework" inDirectory:@"Frameworks"]; |
适用场景:当动态库是二方、三方库,无法修改为静态库,此时为了启动优化,可以选择使用上面的方法进行延迟加载动态库。