iOS底层之dyld应用程序链接加载

950 阅读8分钟

前言

应用程序是如何加载的,代码是如何写入到内存中,动静态库是如何加载到内存的,并且是怎么和APP连接到一起的呢。一起来探究一下。

应用程序的加载

一个APP会依赖很多基础的库,比如UIKit、AVFoundation、CoreFoundation等等。
所谓库,简单说来就是一些可执行的二进制文件,能够被操作系统加载到内存中。分为动态库和静态库。

image.png

上图是一个编译过程。
源文件,也就是我们的代码会通过预编译,编译成汇编语言,然后再和库文件进行链接,生成可执行文件。
静态链接和动态链接又有什么区别呢?

image.png

从上图可以看出静态库的静态链接就是在需要的APP中都要写入一份到内存中,比如一个静态库内存是1M,那么100个APP使用了,就占用了100M的内存,这样就大大的浪费了内存空间。而动态库就是为了解决这个问题,动态链接就是在共享内存中只存在一份,需要的时候就链接这个动态库,这样就减小了包的体积大小。
静态链接是在编译期,动态链接是在运行期。

加载过程

image.png

APP启动阶段,主要是由动态链接器(DYLD)进行链接,在一系列的加载、链接之后再调用main函数,开始执行我们写入的代码。这里的image不是图片,而是库的镜像文件。

DYLD

什么是dyld呢,dyld全称是the dynamic link editor,它是苹果的动态链接器,是苹果操作系统一个重要的组成部分,它的主要功能包含一些load、bind、link之类的对库进行的操作。
dyld分为几个版本,dyld、dyld2、dyld3,目前主要使用的是dyld2和dyld3。

引入

要如何知道dyld是怎么加载的呢?从上一个图可以看到dyld在一系列就在后会调用main函数,那么在main函数前面打一个断点,看看在这之前调用了什么。

image.png

image.png

可以看到在main之前有个start,点击能看到是在libdyld.dylib的动态库里。要想看这个动态库,可以到苹果开源代码里去找。搜索找到dyld,下载一份到本地,然后全局搜索start,可是搜索出来有三千多个结果,并且没有找到libdyld.dylib下的start,说明这个start并不是我们要找的,或者说dyld里面真正调用的并不是start。
那么要怎么找呢,能不能找到还有比main调用更早的。在ViewController.m里实现+(void)load方法,并且在这里和main函数前都打一个断点,看看哪个更早。

image.png 从上图可以看到load方法比main函数更早调用。而且最先执行的是_dyld_start,去dyld搜索一下。

image.png

找到了_dyld_start的实现,它有几种情况,比如i386、x86、arm64等等,这里就研究x86情况下的代码。可以明显看到在这里先call了dyldbootstrap::start,那么就去看看这个是什么?
全局搜索dyldbootstrap,并且在其内找到start的实现。

image.png

看源码,不知道怎么看,可以看这个函数有没有返回值,如果有,那么就直接找重点找关键,这个返回值就是重点,可以看到这里调用了_main函数,并且传入了一些参数。那么进入这个_main()函数看看。

dyld流程

image.png

可以看到这个函数大概是有800多行,一个800多行的函数,一个合格的程序员肯定是不会这样写的,既然这样写了,肯定也是因为需要。但是看这么一长串的代码就是很费心的事,所以这里就主要看一些流程上的东西,就不用去抠细节,因为看细节的东西,相关联的可能会有成千上万行代码,短时间肯定很难看懂。
大概看了一下源码,前面部分大多是些路径的获取和拼接,看不到重点。这种情况就去看看它有没有返回值,有的话又是return了什么,利用反推法,来推到出整个流程,并且在推导的过程中主要就留意load、bind、link等字样。

image.png

可以看到这个函数返回了一个result,并且赋值就这几个地方,并且看到有sMainExecutable进行赋值,那么看看sMainExecutable是不是我们要找的。

image.png

可以看到sMainExecutable就是做了一些bind、link的操作,大概率就是它了,就跟着它继续查找。先找到它是在哪里初始化或者赋值的。

image.png

找到初始化的地方,看看内部如何实现的。

sMainExecutable的初始化

image.png

这段代码上面有一段注释,大概意思是在dyld获得控制之前,内核映射到主可执行文件中。我们需要为已经映射在主可执行文件中的文件创建一个ImageLoader。这一步通过mh(macho_header)和路径创建一个imageLoader,并且添加。看看是addImage()如何添加的

image.png

这一步获取了image的start和end地址,然后调用addMappedRange()

image.png

这里直接进行映射赋值,并且是个链表结构。start、end和image就是外面传入进来的。

macho_header是什么,可以参考这篇文章MachO详解以及使用

主要流程

sMainExecutable初始化之前,就是做一些准备工作,到了这一步就开了dyld流程,大概在第7000行代码的样子,最后还剩300行代码,从这里可以开始看大致流程是怎样的。

loadInsertedDylib()

image.png

一直到第7121行,在这之前也是做了一些准备,到这一步就开始加载插入的动态库。点击进入看看做了什么。

image.png

这里主要掉能用load()方法,根据外面出入的path,这个path就是sEnv.DYLD_INSERT_LIBRARIES,而sEnv就是static EnvironmentVariables sEnv;,看字面意思就是环境变量,path就是环境变量里的动态插入的库的地址
再看看load()如何实现。

image.png

可以看到这里直接调用loadPhase0(),而loadPhase0()里面又根据各种情况再调用loadPhase1()loadPhase1()又调用loadPhase2()或者loadPhase3(),就这样层层递进,最后加载完所有的库。

Link 主程序和插入的动态库

image.png image.png

看看link()函数的实现。

image.png

该函数里检查了当前image是否已添加,然后再调用ImageLoader的link()函数,上面这个link方法是dyld的函数。

image.png

这里是ImageLoader的link()函数,主要是递归加载了一些库,和批量通知处理。在notifyBatch()就只调用了notifyBatchPartial(),在这个方法里就是将ImageLoader里的一些成员赋值给dyld_image_info里的成员,进行了一个link。

Bind

image.png image.png

这两步是主程序对linkContext的一个绑定。
第一个是递归绑定,把这些库和主程序进行绑定,先绑定下层的库,再绑定当前库。
第二个是弱绑定,这一步是在所有的传入的镜像文件都link好了之后才调用的(这个函数里代码有300多行,实在是晦涩难明)。

initializeMainExecutable()

image.png

这一步就是初始化主程序,其实应该是run主程序,因为主程序在前面初始化赋值过了。

notifyMonitoringDyldMain()

image.png image.png

这一步是通知dyld可以进入main函数了,在这里就是发送了一个消息。

initializeMainExecutable()

接下来的主要研究对象就是initializeMainExecutable(),之前的操作就是一些加载、链接和绑定,这一步就是主程序跑起来了,看看这里面做了什么操作。这里就直接看流程是怎样的,直接找代码里的重点。

image.png

进入runInitializers()函数。

image.png

先看processInitializers(),找到它的实现。

image.png

这段代码可以看到下面是一个递归,那么重点就是中间那句代码,也就是recursiveInitialization(),接着看这个函数的实现。

image.png

第一个红框里可以看到这里先初始化了一些底层的依赖库,并进行递归操作,保证底层的库都初始化到。
接着看notifySingle()

image.png

点击进入看到这是一个函数的声明,我们要看它是在哪里赋值的,就全局搜索。

image.png

可以看到这里进行赋值,点击红框里的值,就可以看到它的实现在哪里。

image.png

可以看到sNotifyObjCInit是一个关键点,这里获取了realPath和machHeader,找到它是在哪里赋值的。

image.png

接着找到在哪里调用的registerObjCNotifiers()

image.png

最终找到_dyld_objc_notify_register(),这个是在哪里调用呢?在dyld里全局搜索,没有找到具体的调用位置,看这个函数的名字有个objc,会不会是在libobjc里呢。到libobjc去搜索。

image.png

最终成功在libobjc里找到,这个函数是在_objc_init函数里调用的。
到了这一步就成功的把dyld和objc连在一起了。因为我们是反推过来的,所以这个时候可以直接在源码环境打断点运行,看看调用_objc_init之前做了什么(也可以在非源码环境打上一个_objc_init的符号断点)。

大致流程

image.png

从上图可以看出在_objc_init调用之前做了哪些操作。有一些函数在之前的探索中已经看到过了。做一个流程图。

dyld流程.png

总结

这篇文章就大致了解了一下dyld加载的主要流程,下一篇文章再继续探索_dyld_objc_notify_register()之后又做了些什么。