iOS 杂谈dyld

哈罗,大家好好久没有更新iOS专栏的内容了,今天想与大家聊一聊iOS的dyld的内容,想用杂谈的形式与大家侃侃分享一下dyld的步骤,让我们愉快的开始吧:

其实当初我是想从头到尾开始写一篇关于dyld内容的帖子的,但是当我查看网络上有很多文章已经写得很好了,自己再写的话已经无法站在别人的高度来再进行创作了,所以今天这一篇我将借用网络上我认为写得比较不错的一篇文章,我再来基于该文来进行下总结,这个也是为什么我们要用杂谈的方式进行的原因了

参考文献如下:
iOS dyld详解

该文把dyld分为了以下10个步骤:

  • 第一步:dyldbootstrap::start()函数
  • 第二步:设置运行环境。
  • 第三步:加载共享缓存。
  • 第四步:实例化主程序。
  • 第五步:加载插入的动态库。
  • 第六步:链接主程序。
  • 第七步:链接插入的动态库。
  • 第八步:执行弱符号绑定
  • 第九步:执行初始化方法。
  • 第十步:查找入口点并返回。

我们在这10个步骤基于一些重要的点再谈谈我自己的看法,也为算是帮助大家在这个基础上提炼一下更重要的过程,大家也可以在面试中简约的作为dyld过程的表达:

首先是第一步运行dyldbootstrap::start()函数:

首先要知道Rebasing是什么:

在过去会把 dylib 加载到指定地址,所有指针和数据对于代码来说都是对的,dyld 就无需做任何 fix-up 了。如今用了 ASLR 后会将 dylib 加载到新的随机地址(actual_address),这个随机的地址跟代码和数据指向的旧地址(preferred_address)会有偏差,dyld 需要修正这个偏差(slide),做法就是将 dylib 内部的指针地址都加上这个偏移量,偏移量的计算方法如下:

Slide = actual_address - preferred_address

为什么要进行dyld Rebasing呢,首先大家要知道的是dyld本身就是一个动态的库,他自己也是需要运行的,所以他就自己也需要Rebasing才能运行

第二步:设置运行环境:

这一步主要是设置运行参数、环境变量等,这一步不作为重点讲述

第三步:加载共享缓存:

首先我们要知道什么是共享缓存:

在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着

也就是说共享缓存是一些动态库的集合,基本上就是系统的动态库,这里做的重要的事情就是一次性把所有系统动态库加载到内存中,如果共享缓存已加载或者被别人加载了(因为动态库本身就是共用的,共享缓存也就是动态库的集合,不需要每个进程都加载一遍),则不做任何处理(我是这么理解的)

第四步:实例化主程序:

这一步将主程序的Mach-O加载进内存,并实例化一个ImageLoader指向了这个主程序的image(镜像),这里又涉及到了一个概念,image也就是镜像指的是什么呢:

其实很简单他就是指可执行的Mach-O文件,并且Mach-O文件不紧紧指的是最后生成的可直接运行App程序,还包括所有的动态,静态库这些都是数据可执行的二进制Mach-O文件,也就是我们说的镜像

第五步:加载插入的动态库:

这一步主要是是加载环境变量DYLD_INSERT_LIBRARIES中配置的动态库,为什么又要加载动态库呢,因为第三部是加载系统的动态库,但是你的程序会使用到一些第三方的动态库的话,那么也是需要load进行内存才能使用的,加载每一个镜像就需要初始化一个ImageLoader指向加载的镜像

第六步:链接主程序:

链接主程序最主要的步骤就是 recursiveRebase,recursiveBind也就是循环的Rebase,Bind(循环就是这个库依赖了别的库,被依赖的库也需要Rebasing,Binding)

  • Rebasing :

上面已经解释过了,就是加上一个偏移

  • Binding :

网络上关于Binding是如下讲解的: Binding是处理那些指向 dylib 外部的指针,它们实际上被符号(symbol)名称绑定,也就是个字符串。之前提到 __LINKEDIT 段中也存储了需要 bind 的指针,以及指针需要指向的符号。dyld 需要找到 symbol 对应的实现,这需要很多计算,去符号表里查找。找到后会将内容存储到 __DATA 段中的那个指针中。

我觉网络上的讲解太过于笼统了,我可以说一下我的理解,我以前做Linux的时候阅读过一些关于编译,连接相关的书籍,这里我可以用我的知识来帮助大家讲解下:

首先大家要知道动态库应该是代码与地址无关的也就是PIC的(例如我们用GCC -fPIC可以在Linux下面编译出位置无关的.so动态库),但是你的程序或者说一个进程需要用到这些动态库的时候就需要精确到地址(这里的地址指的是虚拟地址,你的进程肯定是运行在一个固定的虚拟地址下的),那么这些这些地址无关的代码肯定是不能直接运行的(很简单,你试想一下如果你调用一个函数或者变量如果调用地址都不确定的话那么肯定运行不了)所以要对这些动态库进行重定位(Linux下重定位这个概念应该符合这个Binding的过程),重定位的大致过程就是对于动态库的got段里面的地址进行修改达到可以找到目标符号的目的

再顺便帮助大家提一点,大家想过一个问题没有,为什么dyld本身不需要Binding呢,大家可以想象一点,dyld虽然是动态库但是本身不是动态链接的,言外之意就是他本身不会再依赖别的动态库了,因为他本身就是用来加载链接其他库的如果他再依赖别的库那么,那么谁来帮他加载链接呢,所以dyld与Linux下的动态链接器ld.so是一样的他们是静态链接的,所以运行的时候不需要链接其他的库重定位(bingding)这个步骤了

第七步:链接插入的动态库:

这一步最重要的内容就是对于动态库进行recursiveRebase,recursiveBind,与上面的类似

第八步:执行弱符号绑定:

首先要知道什么是弱符号:

函数和全局变量编译后需要有唯一的符号名(简单的可以理解为变量名或者函数名之类的),在链接时才不会混淆。程序员所写代码中的变量名会经过修饰后作为符号名,比如 C 中fun会被修饰为_fun,而符号分为弱符号与强符号,对于 C/C++ 来说,编译器默认函数和已初始化的全局变量为强符号(包括显示初始化为0),未初始化的全局变量为弱符号,另外你也可以使用

 __attribute__ ((weak)) 

来定义一个弱符号,编译器决议符号时有如下规则:

不允许强符号被多次定义,否则会报错。
多个符号名重复且只有一个强符号时,选择强符号。
多个符号名重复且都是弱符号时,选择占用空间最大的一个。

值得注意的是这里说的强弱符号只是针对于跨目标文件符号的定义,例如你的本目标文件的局部变量则没有该强弱的区分

第八步这一步做的大概就是确定弱符号的数据偏移与大小,最终决定最合适的弱符号绑定到可运行地址上面(我是这么理解的)

第九步:执行初始化方法:

这一步最重要的就是注册的init回调函数(load_images)回调里面调用了call_load_methods()来执行所有的+ load方法;以及调用了全局C++对象的构造函数以及所有带

__attribute__((constructor)

的C函数,值得一提的就是,这个语法也是GNU C 的一大特色,后面括号里面的内容不是随便加的,这个语法可以定一个段segment,链接器在遇到这种固定的段的时候会有一些特殊的处理,在这里就是这类的函数在某个时间段(main函数之前)统一运行,类似的语法还有

__attribute__((destructor))

这个可以保证这类的函数在main函数调用后调用这个方法

第十步:查找入口点并返回:

这一步就可以跳转到我们自己运行的main函数了

好了,十个步骤我们已经走完了,这里面借用了一篇大师的文章和我的一些个人理解去帮助大家去阅读,如果有任何疑问或者想法可以给我留言,另外希望大家关注我,您的持续关注是我持续写作的动力,那我们下期见了···

你可能感兴趣的:(iOS 杂谈dyld)