引言:
众所周知,我们的iOS应用是通过Dyld进行加载的,那么Dyld是如何加载我们的应用的,它的流程是怎样的,下面我们把dyld的加载分为几个步骤做个简短的分析。
1 dyld的start启动
首先我们创建一个Demo工程,在我们的AppDelegate.m文件里加入+(load)方法并断点,如下图所示:
运行Demo App后,可以得到所下图
从图2中我们可以看到,我们的App是从_dyld_start开始的,我们点击dyld_start,看到汇编的第18行代码,这里调用了dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*)这行代码,我们再使用bt命令看下详细的堆栈,如下图
从这里可以看出一切的开始是从_dyld_start开始的,这个dyld_start在可以在dyld的源码里找(注:这里使用的dyld的源码的版本是dyld-832.7.3),下面我们打开dyld的源码进行分析
我们在dyld的源码里找 dyldbootstrap这个命名空间,按住shift+comand+j进入文件,所下图所示
从这里可以看到是在dyldInitialization.cpp文件里,然后我们在文件里搜索start,找到uintptr_tstart(constdyld3::MachOLoaded* appsMachHeader,intargc,constchar* argv[],
constdyld3::MachOLoaded* dyldsMachHeader,uintptr_t* startGlue)函数,如图所示:
我们从start函数逐步往下分析整个流程,我们先看下参数,appsMachHeader是我们App的MachHeader,dyldsMachHeader是dyld的MachHeader。
第121行代码是告诉我们的degbugServer,我的dyld开始起动了。
第125行代码rebaseDyld(dyldsMachHeader) 重定位我们的dyld。
第136,143行是栈溢出保护和初始化dyld,之后就是调用dyld的main函数(这是最核心的),我们着重分析下dyld的main函数流程。
main函数的前几行代码都是代码检测相关的,不是核心内容
我们下面来看主程序的配置相关,如图
这些是配置主程序的MachHeader(就是Macho的头),主程序的Slide(就是主程序的ASLR的偏移值,每次启动都是不一样的)
下面调用setContext(mainExecutableMH, argc, argv, envp, apple);保存我们配置的信息
然后通过configureProcessRestrictions(mainExecutableMH, envp)这个函数配置进程受限制(AMFI相关(Apple Mobile File Integrity苹果移动文件保护)),下图都是进程受限相关的配置,比如是否强制使用dyld3(dyld是在iOS11推出来的,加载高效)
下图打印我们的环境变量,这个环境变理可通过 Environment Variables配置
以下都是dyld的启动,配置,以及主程序的相关配置和一些代码检测的流程,下面我们来分析共享缓存
2 dyld加载共享缓存
点击进去checkSharedRegionDisable发现有一个“iOS cannot run without shared region”说明,这是表明我们的iOS是一定有共享缓存的。
接着调用mapSharedCache传进去主程序的Slide,这个函数调用了loadDyldCache加载我们的dyld库存,如下图所示
满足options.forcePrivate 这个条件的话,只加载当前进程
reuseExistingCache如果缓存已经加载不再处理,如果第一次加载执行mapCacheSystemWide这个函数
通过以上分析,可以得出结论,动态库的共享缓存是最先被加载的(我们自己开发的动态库不可以)。从iOS11引入了dyld3的ClosureMode(闭包模式加载更快),下面我们来分析一下
3 dyld3的闭包模式
这里先判断闭包模式是否打开,如果没有的话将会走dyld2的流程,打开走dyld3的流程(dyld2,dyld3的加载流程一致),下面我们来分析dyld3的闭包模式
先从共享缓存中查找这个实例,如果拿到就先验证
这里判断是否查找成功,并且验证闭包的有效性,如果失效,sLaunchModeUsed设置为NULL
这里如果没找到,再去缓存中查一次,如果mainClosure为空,这里就调用buildLaunchClosure创建闭包实例
最终拿到这个mainClosure实例启动这个实例,如下图所示
如果启动失败或者闭包过期,这里就再重新调用buildLaunchClosure创建并调用launchWithClosure重新启动一次。
启动成功后设置gLinkContext.startedInitializingMainExecutable = true;这个主程序加载成功了。
同时返回结果result(即主程序的main),如下图所示:
接着就会实例化我们的主程序了,下面我们来分析是如何加载的。
4 dyld加载主程序
接下看下怎么实例化主程序的,如下图所示
第6862行代码会调用instantiateFromLoadedImage函数实例化我们的主程序,我们来看下instantiateFromLoadedImage这个函数,下图所示:
这里通过ImageLoaderMachO这个函数传image的macho_header,ASLR的偏移值,路径生成ImageLoader对象,然后调用addImage这个函数加入我们的镜像文件,同时返回ImageLoader这个对象。(通过dyld加载的第一个镜像是我们的主程序),我们来看下instantiateMainExecutable的这个函数的流程,如图所示:
这里调用sniffLoadCommands获取loadCommands,如图:
compressed是根据Macho中的LG_DYLD_INFO_ONLY和LG_LOAD_DYLINKER来获取的。
segCount是SEGMENT的数量,最大不能超过255。
libCount是LC_LOAD_DYLIB加载动态库的个数,最大不能超过4095。
*codeSigCmd是代码签名。
*encryptCmd是代码加密信息。
ImageLoaderMachO这个函数调用sniffLoadCommands这个之后会根据compressed这个变量判断调用ImageLoaderMachOCompressed或者ImageLoaderMachOClassic这两个函数实例化。
实例化完毕之后添加到AllImage中。
接着检测当前主程否是当前设备的,如上图所示,到这里我们的主程序实例化结束,接着我们来分析动态库的加载。
5 dyld加载动态库
这里先检查动态库的版本和路径,接着加载动态库,如图所示:
这里先根据环境变量判断动态插入库不为空,接着遍历loadInsertedDylib
这里调用load插入动态库,接着开始链接主程序,
先配置gLinkContext.linkingMainExecutable = true;这个变量为true.
接着调用link函数进行链接,我们来看看是如何链接的:
这里先记录起始时间,在最后在记录结束时间,把加载时间记录下来,这个就是dyld加载应用的时长。
这里链接插入动态库完成了。
之后把这些实例化的镜像文件加入到AllImages中(这里是从i+1开始的,因为主程序已经先加载了),之后再调用link进行链接,这里跟主程序的链接是一样的。
这里的条件不满足的话,将会持续的调用reloadAllImage,这里执行之后,就开始绑定动态库了,如图所示:
这里遍历AllImages绑定插入动态库,之后进行弱符号绑定。
接着调用initializeMainExecutable初始化主程序的Main方法,如图所示:
下面我们来分析主程序Main方法加载的流程。
6 load方法与初始化方法的加载
我们先进入initializeMainExecutable()这个函数,看下它的实现
这里有一个runInitializers函数,我们再进去
这里会调用processInitializers这个函数,我们再跟进去看看
接着我们再跟下recursiveInitialization这个函数
这里调用了notifySingle这个函数,我们需要再跟进去一下,
而这里没有找到loadImge的函数调用,这里到底是怎么回事,我们通过汇编可以看到load_image是在libobjc.dylib中,也就是说在objc的源码中,那它是怎么调用的,我们来看下代码。
在1019行中 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) 这个回调进行关联的。
这里首先判断sNotifyObjCInit这个是否为空,我们在这文件里搜下,发现是在registerObjCNotifiers这里调用的时候赋值的,如下图:
我们搜下registerObjCNotifiers这个函数发现是在dyldAPIS.cpp中的
这个函数调用的,这里有传递init进来,那么又是谁调用的_dyld_objc_notify_register这个函数呢,搜了之后,发现dyld里没用调用的,那么怎么办呢,我们可以在Demo工程中下一个符号断点_dyld_objc_notify_register,结果发现是在libobjc.dylib中的_objc_init调用的,下面打开objc的的源代码,按信shift+command+o找到定义,再按住shift+command+j找到源文件是在objc-os.mm文件中,如图所示:
这里可以看到_dyld_objc_notify_register这个函数是在_objc_init调用的,这里有一个load_images,我们再看下
这里面有一个call_load_methods方法,点进去看下
这里会do while调用call_class_loads方法来加载所有类的+(void)load方法,load方法加载完成后调用了call_category_loads这个方法,加载类加的loads方法,这也是为什么类别的方法与原类的方法重名后,会覆盖原类的方法。
我们回到dyld的源代码找到ImageLoader.cpp文件中的recursiveInitialization函数中调用notifySingle这里走到了objc中,objc把所有的load加载完成后,会调用doInitialization这个函数,进去看下
这里doModInitFunctions调用这个函数,这个函数的作用是什么,我们来看下,
这里就是在加载我们的构造函数,我们在Demo的main.m上面加入构造函数
__attribute__((constructor)) void test1() {
printf("test调用了");
}
经过调试,它比main函数先调用。
我们再回到dyld的main函数,找到这里,如图
这里通过LC_MAIN找到程序入口给result,最后返回主程序的main地址。dyld的加载就结束了。
下面是dyld的initializeMainExecutable初始化主程序的思维导图