前言
应用程序是如何加载的,代码是如何写入到内存中,动静态库是如何加载到内存的,并且是怎么和APP连接到一起的呢。一起来探究一下。
应用程序的加载
一个APP会依赖很多基础的库,比如UIKit、AVFoundation、CoreFoundation等等。
所谓库,简单说来就是一些可执行的二进制文件,能够被操作系统加载到内存中。分为动态库和静态库。
上图是一个编译过程。
源文件,也就是我们的代码会通过预编译,编译成汇编语言,然后再和库文件进行链接,生成可执行文件。
静态链接和动态链接又有什么区别呢?
从上图可以看出静态库的静态链接就是在需要的APP中都要写入一份到内存中,比如一个静态库内存是1M,那么100个APP使用了,就占用了100M的内存,这样就大大的浪费了内存空间。而动态库就是为了解决这个问题,动态链接就是在共享内存中只存在一份,需要的时候就链接这个动态库,这样就减小了包的体积大小。
静态链接是在编译期,动态链接是在运行期。
加载过程
APP启动阶段,主要是由动态链接器(DYLD)进行链接,在一系列的加载、链接之后再调用main函数,开始执行我们写入的代码。这里的image不是图片,而是库的镜像文件。
DYLD
什么是dyld呢,dyld全称是the dynamic link editor
,它是苹果的动态链接器,是苹果操作系统一个重要的组成部分,它的主要功能包含一些load、bind、link之类的对库进行的操作。
dyld分为几个版本,dyld、dyld2、dyld3,目前主要使用的是dyld2和dyld3。
引入
要如何知道dyld是怎么加载的呢?从上一个图可以看到dyld在一系列就在后会调用main函数,那么在main函数前面打一个断点,看看在这之前调用了什么。
可以看到在main之前有个start
,点击能看到是在libdyld.dylib
的动态库里。要想看这个动态库,可以到苹果开源代码里去找。搜索找到dyld
,下载一份到本地,然后全局搜索start
,可是搜索出来有三千多个结果,并且没有找到libdyld.dylib
下的start,说明这个start并不是我们要找的,或者说dyld里面真正调用的并不是start。
那么要怎么找呢,能不能找到还有比main调用更早的。在ViewController.m里实现+(void)load方法,并且在这里和main函数前都打一个断点,看看哪个更早。
从上图可以看到load方法比main函数更早调用。而且最先执行的是_dyld_start
,去dyld搜索一下。
找到了_dyld_start
的实现,它有几种情况,比如i386、x86、arm64等等,这里就研究x86情况下的代码。可以明显看到在这里先call了dyldbootstrap::start
,那么就去看看这个是什么?
全局搜索dyldbootstrap,并且在其内找到start的实现。
看源码,不知道怎么看,可以看这个函数有没有返回值,如果有,那么就直接找重点找关键,这个返回值就是重点,可以看到这里调用了_main
函数,并且传入了一些参数。那么进入这个_main()
函数看看。
dyld流程
可以看到这个函数大概是有800多行,一个800多行的函数,一个合格的程序员肯定是不会这样写的,既然这样写了,肯定也是因为需要。但是看这么一长串的代码就是很费心的事,所以这里就主要看一些流程上的东西
,就不用去抠细节,因为看细节的东西,相关联的可能会有成千上万行代码,短时间肯定很难看懂。
大概看了一下源码,前面部分大多是些路径的获取和拼接,看不到重点。这种情况就去看看它有没有返回值,有的话又是return了什么,利用反推法
,来推到出整个流程,并且在推导的过程中主要就留意load、bind、link等字样。
可以看到这个函数返回了一个result,并且赋值就这几个地方,并且看到有sMainExecutable
进行赋值,那么看看sMainExecutable
是不是我们要找的。
可以看到sMainExecutable
就是做了一些bind、link的操作,大概率就是它了,就跟着它继续查找。先找到它是在哪里初始化或者赋值的。
找到初始化的地方,看看内部如何实现的。
sMainExecutable的初始化
这段代码上面有一段注释,大概意思是在dyld获得控制之前,内核映射到主可执行文件中。我们需要为已经映射在主可执行文件中的文件创建一个ImageLoader
。这一步通过mh(macho_header)
和路径创建一个imageLoader
,并且添加。看看是addImage()
如何添加的
这一步获取了image的start和end地址,然后调用addMappedRange()
。
这里直接进行映射赋值,并且是个链表结构。start、end和image就是外面传入进来的。
macho_header是什么,可以参考这篇文章MachO详解以及使用
主要流程
sMainExecutable初始化之前,就是做一些准备工作,到了这一步就开了dyld流程,大概在第7000行代码的样子,最后还剩300行代码,从这里可以开始看大致流程是怎样的。
loadInsertedDylib()
一直到第7121行,在这之前也是做了一些准备,到这一步就开始加载插入的动态库。点击进入看看做了什么。
这里主要掉能用load()
方法,根据外面出入的path,这个path就是sEnv.DYLD_INSERT_LIBRARIES
,而sEnv就是static EnvironmentVariables sEnv;
,看字面意思就是环境变量,path就是环境变量里的动态插入的库的地址
。
再看看load()
如何实现。
可以看到这里直接调用loadPhase0()
,而loadPhase0()
里面又根据各种情况再调用loadPhase1()
,loadPhase1()
又调用loadPhase2()或者loadPhase3()
,就这样层层递进,最后加载完所有的库。
Link 主程序和插入的动态库
看看link()函数的实现。
该函数里检查了当前image是否已添加,然后再调用ImageLoader的link()函数
,上面这个link方法是dyld的函数。
这里是ImageLoader的link()函数
,主要是递归加载了一些库,和批量通知处理。在notifyBatch()
就只调用了notifyBatchPartial()
,在这个方法里就是将ImageLoader
里的一些成员赋值给dyld_image_info
里的成员,进行了一个link。
Bind
这两步是主程序对linkContext的一个绑定。
第一个是递归绑定,把这些库和主程序进行绑定,先绑定下层的库,再绑定当前库。
第二个是弱绑定,这一步是在所有的传入的镜像文件都link好了之后才调用的(这个函数里代码有300多行,实在是晦涩难明)。
initializeMainExecutable()
这一步就是初始化主程序,其实应该是run主程序,因为主程序在前面初始化赋值过了。
notifyMonitoringDyldMain()
这一步是通知dyld可以进入main函数了,在这里就是发送了一个消息。
initializeMainExecutable()
接下来的主要研究对象就是initializeMainExecutable()
,之前的操作就是一些加载、链接和绑定,这一步就是主程序跑起来了,看看这里面做了什么操作。这里就直接看流程是怎样的,直接找代码里的重点。
进入runInitializers()
函数。
先看processInitializers()
,找到它的实现。
这段代码可以看到下面是一个递归,那么重点就是中间那句代码,也就是recursiveInitialization()
,接着看这个函数的实现。
第一个红框里可以看到这里先初始化了一些底层的依赖库,并进行递归操作,保证底层的库都初始化到。
接着看notifySingle()
。
点击进入看到这是一个函数的声明,我们要看它是在哪里赋值的,就全局搜索。
可以看到这里进行赋值,点击红框里的值,就可以看到它的实现在哪里。
可以看到sNotifyObjCInit
是一个关键点,这里获取了realPath和machHeader
,找到它是在哪里赋值的。
接着找到在哪里调用的registerObjCNotifiers()
。
最终找到_dyld_objc_notify_register()
,这个是在哪里调用呢?在dyld里全局搜索,没有找到具体的调用位置,看这个函数的名字有个objc
,会不会是在libobjc
里呢。到libobjc去搜索。
最终成功在libobjc里找到,这个函数是在_objc_init
函数里调用的。
到了这一步就成功的把dyld和objc连在一起了。因为我们是反推过来的,所以这个时候可以直接在源码环境打断点运行,看看调用_objc_init之前做了什么(也可以在非源码环境打上一个_objc_init的符号断点)。
大致流程
从上图可以看出在_objc_init调用之前做了哪些操作。有一些函数在之前的探索中已经看到过了。做一个流程图。
总结
这篇文章就大致了解了一下dyld加载的主要流程,下一篇文章再继续探索_dyld_objc_notify_register()
之后又做了些什么。