背景
大家都知道iOS在加载app的时候本质其实是加载app中的MachO文件,那么MachO文件又是谁来进行加载与执行的呢?其中的过程又是如何的?我们就来初探一下MachO文件的加载顺序。
一、什么是Mach-O文件?
1.1、初识Mach-O
要想了解MachO文件的加载顺序,首先我们要先了解一下什么是MachO文件。MachO属于一种文件格式,其中包含了可执行文件、静态库、动态库、dyld等;其中包含的可执行文件是集合了多种架构的,例如包含了 armv7、arm64等;
1.2、MachO的结构:
Header:用于快速确定该文件的CPU类型、文件类型;
Load Commands:指示加载器如何设置并且加载二进制数据;
Text:存放代码。
Data:存放数据。例如:数据、字符串常量、类、方法等;
1.3、如何找到Mach-O文件?
我们创建一个新项目然后编译,在Products下会生成一个项目名.app文件,我们右键 show in finder 然后再右键显示包内容,会看到一个黑框的文件,该文件就是MachO文件。
Mach-O是Machobject文件格式的缩写,它是一种用于可执行文件、目标代码、动态库的文件格式。作为a.out格式的替代,与a.out格式比较Mach-O提供了更强的扩展性。
1.4、如何查看Mach-O文件?
通过终端命令 otool -l +文件名 进行查看,但是命令显示的内容太多了我们不好看,可以通过终端命令 otool -l +文件名 > 输出路径 将内容输出成文件,但是打开文件还是不太好看,这时我们就该利用工具了。例如:
既然MachO属于一种文件格式,那么就一定有解析这种格式的方法与程序,那么对MachO文件进行解析执行的就是DYLD。
二、DYLD
DYLD(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。官网地址:https://opensource.apple.com/
2.1、新建一个项目
1、首先我们要先了解一下App启动时候的运行顺序,那么一个App入口就是mian.m文件里面的mian函数,我们现在在main函数中打印一句话标记一下。
2、App通过了main函数的之后会调用AppDelegate,然后最终会引导到ViewController界面上,那我们就在ViewController中增加一个load函数,这里问什么要加load函数是因为load函数一定是最先被加载的,我们的目的是为了查看App的启动顺序,所以load函数最合适,接下来我们还会验证为什么load函数是最先加载的。
4、我们现在运行看一下打印结果是什么,建议最好用真机进行测试。
2.2、如何找到DYLD的加载入口?
-
首先我们现在知道的就是App入口都是通过mian.m文件里面的mian函数开始的,但根据上面的测试打印结果我们发现了其实framework的load是最先被调用的,那么我们就在framework的load函数中增加一个断点再运行一下看看。
然后我们在右侧查看调用流程,我们就看到了在App的main函数之前的调用流程了。
根据上面的分析,我们已经对DYLD的加载顺序有了一个大致的了解,那么大家有没有兴趣跟着我,把DYLD详细的加载流程走一遍呢?Let‘s go !
三、DYLD加载探究
3.1、前期准备资料:
我要分析DYLD的加载流程就必须要下载DYLD的源代码,下面给出下载地址https://opensource.apple.com;
注意!要下载DYLD源码点击macOS,这里我们选择11.2这个版本,点进去后搜索dyld 会找到dyld-832.7.3 ;除了dyld之外还需要下载objc4-818.2
3.2、分析过程:
3.2.1、start函数:
start函数的内容不多,我们简要的分析一下;首先我们打开dyld源码,然后Command+shift+O 搜索start,然后Command+shift+J 定位文件。
执行__guard_setup(apple); 这一步是为了进行栈溢出保护;
执行_subsystem_init(apple); 这一步是初始化相关数据;
最后执行一个返回return dyld::_main((macho_header)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue); 返回调用的是dyld的_main函数,是具体加载步骤。这里的返回值也是一个main函数,而这个返回的main函数就是就是咱们App项目中main.m中的main函数,所以这个dyld的main函数就是一个寻找、加载、初始化这个App的main函数的过程,那么所有的加载逻辑都是由这个函数来执行的,我们继续跟进下去。
3.2.2、main函数:
(1)、初期配置:
我们继续跟踪dyld::_main函数,刚一进来就看到这个函数的很大,将近1千多行的代码。我们用不着每行都看一遍,只要抓住几个重点步骤就行了。
-
大概再6473行左右会出现一个setContext(mainExecutableMH, argc, argv, envp, apple);函数,从_mian函数开始到这里都是在做一些配置的信息,把配置好的信息保存起来,当前这步的含义是用过调用setContext上下文,将括号内的参数信息保存起来,我们可以再看一下setContext里面的内容,就会发现其实上下文都是通过一个叫gLinkContext的来进行保存的。到这里只是初步设置,如果下面的信息发生了改变还会进行更新。
接下来往下50行左右,configureProcessRestrictions; 函数开始再到s是对进程进行了受限配置,进程是受AMFI(Apple Mobile File Integrity苹果移动文件保护)内核模块,用来检查一些参数的存在性。最后我们可以看到又执行了setContext,只是因为上面的进行保护可能会引起一些环境变量发送改版,所以需要再一次重新进行保存。
再往下走到defaultUninitializedFallbackPaths,目前到这里,都是配置和初始化环境,还没有加载程序。
继续往下我们会看到两个环境变量sEnv.DYLD_PRINT_OPTS 和 sEnv.DYLD_PRINT_ENV,通过代码我们发现如果配置了这两个变量就会执行下面的pint方法,我们可以通过在项目中的target里设置,让他们进行打印。
![image](https://upload-images.jianshu.io/upload_images/26320104-5bc616925e83e990.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![image](https://upload-images.jianshu.io/upload_images/26320104-e9d0318e6faeb0b8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
(2)、加载共享缓存:
再往下几行,我们发现了checkSharedRegionDisable这个函数,从这往下到mapSharedCache都是在加载共享缓存,例如:UIKit,Foundation框架。这里要再提一下iOS的共享缓存的意义。
(3)、DYLD加载方式:
经过了以上的配置、加载共享缓存的步骤之后,DYLD现在要正式开始,目前DYLD的执行方式分为2种;Dyld2 和 Dyld3。
(3-1)、DYLD3方式:
iOS11 之后增加了Dyld3 通过使用Closure闭包方式进行加载,这种方式比之前Dyld2的效率更高效,但本质的流程还是与Dyld2一致的,我们可以快速的看一下。先从共享缓存中查找闭包(Closure);(3-2)、DYLD2方式:
通过了解Dyld3闭包模式我们对dyld的执行有了一个大概的认知,但是从分析Dyld3的加载过程我们并没有发现我们的framework、vc、main函数是如何加载的,load函数是如何被调用执行的,这些都需要我们通过了解Dyld2来进行验证(DYLD3的方式更加优化,流程更加便捷)。
DYLD_INSERT_LIBRARIES 环境变量,作用是在dyld加载时允许插入动态库,这个环境变量可以通过在root环境(越狱设备)下把自己的类库加入到三方应用中,从而实现代码注入;这块我先埋个伏笔,后续我会对iOS 防HOOK方面进行详细的介绍。
link()链接主程序;
从 sAllImages 中一次链接动态库,sAllImages[i+1] +1是因为上面已经加载了dyld的image程序,所以下标从+1开始;
如果加载失败了,需要再次回到 reloadAllImages 继续执行;
image->recursiveBind() 绑定插入的动态库;
下面就来到最重要的 initializeMainExecutable() 初始化方法;虽然这么看只是一句简单函数调用,其实这个函数涉及的步骤很多,我们根据刚才debug获得信息大致能猜到,这个函数应该是对Image(镜像)进行处理,我们继续前进。
1、跟进initializeMainExecutable() 我们看到的是一个循环,内容是将所以的Image执行初始化runInitializers函数。
4、继续跟进recursiveInitialization(),我们把焦点放到notifySingle()上,
5、到这步我们已经距离结果越来越近了,继续跟进notifySingle(),上面的内容我们直接忽略,先关注这个函数 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) sNotifyObjCInit是一个回调指针,我们搜索一下sNotifyObjCInit看看他是在什么时候被初始化的。通过搜索我们发现了sNotifyObjCInit是在registerObjCNotifiers函数下将第二个参数赋值给了它,因为是回调指针,我们如果找到了init这个函数就知道了具体是实现。
5-1、接下来我们就看是谁调用了registerObjCNotifiers(),通过搜索registerObjCNotifiers发现是由_dyld_objc_notify_register()这个函数调用它的,init参数都透传过来的,我们还需要继续追踪是谁调用了dyld_objc_notify_register()?。
5-2、追踪到dyld_objc_notify_register()函数这里,我们已无法从源码中得到结果了,怎么办呢?不要慌。我们需要插上真机,增加符号断点进行调试,看看是否有结果。 顺利进入了debug后我们查看右侧栏的调用顺序,发现在dyld_objc_notify_register之前是调用的是_objc_init,那么我们就可以再去查看_objc_init函数。
5-3、打开objc源码后我们command+shift+O搜索_objc_init,我马上就能发现确实是_objc_init调用了_dyld_objc_notify_register,这时我们查看第二个参数load_images,这个就是init的真实实现,我们继续跟踪进去。
5-4、load_images()最后一句话调用了call_load_methods()函数,从名字我们就知道了这个是开始调用load方法了。
5-5、继续 call_load_methods() 函数,发现是通过循环将每个类的load函数进行了调用。
5-6、到这里notifySingle()函数全部执行完毕,我们继续往下看。
6、回到ImageLoader的recursiveInitialization函数中,this->doInitialization(context);会调用全局C++对象的构造函数attribute((constructor))的C函数
7、doInitialization() 内部执行 doModInitFunctions() 加载构造函数。
8、以上执行完毕之后就会回到我们最初的dyld的main函数了。
(4)、返回app主函数:
(uintptr_t)sMainExecutable->getEntryFromLC_MAIN() 找到主程序main函数;
最后一步返回result;
三、总结
3.1、简要总结分析
- 始开 从行执序程
- 进入dyld:main函数
- 加载共享缓存
- DYLD2 / DYLD3 (闭包模式)
- 实例化主程序
- 加载动态库
- 链接主程序、绑定符号(优先加载的是 非懒加载、弱符号)等等
- 最关键的初始化方法initializeMainExecutable
- dyld:ImageLoader::runInitializers
- dyld:ImageLoader::processInitializers:
- dyld:ImageLoader::recursiveInitialization:
- dyld:dyld::notifySingle:函数
- 此函数执行一个回调
- 通过断点调试:此回调是_objc_init初始化时赋值一个函数Load_images
- Load_images里面执行class_load_methods函数
- call_class_loads函数:循环调用各类的load方法
- doModInitFunction函数
- 内部会调用全局C++对象的构造函数attribute((constructor))的C函数
- dyld:dyld::notifySingle:函数
- dyld:ImageLoader::recursiveInitialization:
- dyld:ImageLoader::processInitializers:
- dyld:ImageLoader::runInitializers
- 返回主程序的入口函数。开始进入主程序的main函数!