Dyld之二: 动态链接过程
动态链接过程是在二进制加载进来之后,main之前的过程。这一过程就是让二进制变为可正常执行状态的过程。 本文从会讲下面几个主要概念:
- rebase
- bind
- 动态链接过程
- 符号反向依赖
rebase
rebase就是指针修正的过程。
一个mach-o的二进制文件中,包含了text段和data段。而data段中的数据也会存在引用关系。 我们知道在代码中,我们可以用指针来引用,那么在一个文件中怎么代表引用呢,那就是偏移(相对于text段开始的偏移)。 而当二进制加载到内存中的时候,起始地址就是申请的内存的起始地址(slide),不会是0,那么如何再能够找到这些引用的正确内存位置呢? 把偏移加上(slide)就好了。 这个过程就是rebase的过程。
下面用个简单的图来说明下原理。
bind
bind就是符号绑定的过程。
为什么要bind? 因为符号在不同的库里面。
举个简单的例子,我们代码里面调用了 NSClassFromString
. 但是NSClassFromString
的代码和符号都是在 Foundation.framework
这个动态库里面。而在程序未加载之前,我们的代码是不知道NSLog
在哪里的,于是编译器就编译了一个 stub 来调用 NSClassFromString
:
可以看到,我们的代码里面直接从 pc + 0x3701c的地方取出来一个值,然后直接br, 也就是认为这个值就是 NSClassFromString
的真实地址了。我们再看看这个位置的值是啥:
也就是说,这块地址的8个字节会在bind之后存入的就是 NSClassFromString
的代码地址, 那么就实现了真正调用 NSClassFromString
的过程。
上面我们知道了为啥要bind. 那是如何bind的呢? bind又分为哪些呢?
怎么bind
首先 mach-o 的 LoadCommand里面的会有一个cmd来描述 dynamic loader info:
可以看到,这里面记录了二进制data段里面哪些是 rebase信息,哪些是binding信息。
可以看到binding info的数据结构,bind的过程根据不同的opcode解析出不同的信息,在opcode为BIND_OPCODE_DO_BIND
的时候,会执行bindLocation
来进行bind.
截取了 bindLocation 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
可以看出, bind过程也不是单纯的就是把符号地址填过来就好了, 还有type和addend的逻辑。不过一般不多见,大部分都是BIND_TYPE_POINTER
.
addend 一般用于要bind某个数组中的某个子元素时,记录这个子元素在数组的偏移。
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
的实现有兴趣的同学可以自己看一看源码。
Weak Bind
OC的代码貌似不会编译出Weak Bind
. 目前遇到的Weak Bind
都是C++的 template
的方法。特点就是:Weak bind的符号每加载进来二进制都会bind到最新的符号上。比如2个动态库里面都有同样的weak bind
符号,那么所有的的符号引用都会bind到后加载进来的那个符号上。
动态链接过程
了解了 rebase
和 bind
是怎么回事之后,我们再来看整个动态链接过程。
在前面文章里面提到了加载二进制的过程: instantiate –> addImage –> link –> runInitializers 其中link就是动态链接的过程。
link的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
- 第一步 recursiveLoadLibraries
这一步就是根据 LoadCommand 中的 LC_LOAD_DYLIB
把依赖的动态库和Framework加载进来。也就是对这些动态库 instantiate
的过程。 只是动态库不会用instantiateMainExecutable
方法来加载了,最终用的是 instantiateFromFile
来加载。
- 第二步 recursiveUpdateDepth
刷新depth, 就是库依赖的层级。层级越深,depth越大。
- 第三步 recursiveRebase
rebase的过程,recursiveRebase
就会把主二进制和依赖进来的动态库全部rebase.
- 第四步 recursiveBind
主二进制和依赖进来的动态库全部执行 bind
- 第五步 weakBind
执行weakBind,这里看到如果是主二进制在link的话,是不会在这个时候执行weak bind
的,在dyld::_main
里面可以看到,是在link完成之后再执行的weakBind
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
- 第六步 registerDOFs
注册DTrace Object Format。 什么是DTrace可以看这个: DTrance
- 第七步 recursiveApplyInterposing
主二进制link时候也不执行
反向依赖
以前在不完全了解动态链接的过程时,以为每个库之间的符号只能单向依赖,即 A.dylib 依赖 B.dylib。那么B中就不能依赖A中的符号。但是某一个我发现主工程依赖的一个动态库中竟然还可以继承来着主工程的类。于是又详细看了下动态链接的过程。原来库与库之间是可以相互依赖符号的。
一次dyld加载进来的二进制之间可以相互依赖符号。
原因很简单,就是因为上面看到静态链接过程中,并不是完全加载完一个被依赖的动态库,再加载下一个的。而是 recursiveLoadLibraies,recursiveRebase, recursiveBind。 所有的单步操作都会等待前一步所有的库完成。因此当 recursiveBind的时候,所有的动态库二进制已经加载进来了,符号就可以互相找了。
一次dyld的过程只会一次动态link, 这次link的过程中的库符号可以互相依赖的,但是如果你通过dlopen
, -[NSBundle loadBundle]
的方式来延迟加载的动态库就不能反向依赖了,必须单向依赖,因为这是另外一次dyld的过程了。
反向依赖还要有个条件,条件就是符号必须存在,如果因为编译优化把符号给strip了,那就没法bind了,还是会加载失败的。