DYLD 加载Mach-O的流程

背景

大家都知道iOS在加载app的时候本质其实是加载app中的MachO文件,那么MachO文件又是谁来进行加载与执行的呢?其中的过程又是如何的?我们就来初探一下MachO文件的加载顺序。

一、什么是Mach-O文件?

1.1、初识Mach-O

要想了解MachO文件的加载顺序,首先我们要先了解一下什么是MachO文件。MachO属于一种文件格式,其中包含了可执行文件、静态库、动态库、dyld等;其中包含的可执行文件是集合了多种架构的,例如包含了 armv7、arm64等;

1.2、MachO的结构:
图片.png

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提供了更强的扩展性。


图片.png

图片.png

这里要注意,Mach-O是一种文件类型,我们常见的.o文件、.a库、.Framework等都属于这个类型;我们可通过file命令查看文件类型。例如:
image
1.4、如何查看Mach-O文件?

通过终端命令 otool -l +文件名 进行查看,但是命令显示的内容太多了我们不好看,可以通过终端命令 otool -l +文件名 > 输出路径 将内容输出成文件,但是打开文件还是不太好看,这时我们就该利用工具了。例如:

image
我们需要下载一个叫MachOView的工具,直接将MachO文件拖到工具图标上就可以了。效果跟MachO的结构图一样。 工具下载地址:链接: https://pan.baidu.com/s/112A7mZ0ssPdJHSvPNl5OGg 密码: t5ab
既然MachO属于一种文件格式,那么就一定有解析这种格式的方法与程序,那么对MachO文件进行解析执行的就是DYLD。

二、DYLD

DYLD(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。官网地址:https://opensource.apple.com/

2.1、新建一个项目

1、首先我们要先了解一下App启动时候的运行顺序,那么一个App入口就是mian.m文件里面的mian函数,我们现在在main函数中打印一句话标记一下。


图片.png

2、App通过了main函数的之后会调用AppDelegate,然后最终会引导到ViewController界面上,那我们就在ViewController中增加一个load函数,这里问什么要加load函数是因为load函数一定是最先被加载的,我们的目的是为了查看App的启动顺序,所以load函数最合适,接下来我们还会验证为什么load函数是最先加载的。


图片.png
3、除了上面的main函数和VC的load函数之外,我们的App一般还会引用一些framework,所以我们不妨也看看framework是什么是后被加载运行的。我们可以自己创建一个framework,然后再framework中新建一个Main.m的文件,同样的参考ViewController我们也增加一个load函数。
图片.png

图片.png

4、我们现在运行看一下打印结果是什么,建议最好用真机进行测试。


图片.png
2.2、如何找到DYLD的加载入口?
  • 首先我们现在知道的就是App入口都是通过mian.m文件里面的mian函数开始的,但根据上面的测试打印结果我们发现了其实framework的load是最先被调用的,那么我们就在framework的load函数中增加一个断点再运行一下看看。

    然后我们在右侧查看调用流程,我们就看到了在App的main函数之前的调用流程了。
    image
    通过调用流程我们发现,最开始是调用了了一个叫 _dyld_start的函数,然后通过dyld的main函数继续进行调用,然后通过调用ImageLoader 的函数调用notifySingle 后再load_images,最终调用到了我们framework中的load函数。

    根据上面的分析,我们已经对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

image

3.2、分析过程:
3.2.1、start函数:

start函数的内容不多,我们简要的分析一下;首先我们打开dyld源码,然后Command+shift+O 搜索start,然后Command+shift+J 定位文件。

image
image
第一个关键步骤就是rebaseDyld(dyldsMachHeader);方法,他的目的是重定位,其中参数dyldsMachHeader就是MachO文件里的Header内容。这里牵扯到一个概念就是ASLR大家可以百度一下,我这里简要解释一下,在iOS系统中打开一个App的时候是会将App的二进制数据从硬盘copy到内存里,那么这时候二进制数据就会对应一个内存地址,由于考虑安全等因素的问题,内存的地址都是由虚拟缓存地址替代,而且地址的起始位置都是动态的,每次启动的时候都会不一样,这个技术就是ASLR。所以当DYLD加载MachO的时候最先一步要做的就是对数据进行重定位。

执行__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的来进行保存的。到这里只是初步设置,如果下面的信息发生了改变还会进行更新。

    image

接下来往下50行左右,configureProcessRestrictions; 函数开始再到s是对进程进行了受限配置,进程是受AMFI(Apple Mobile File Integrity苹果移动文件保护)内核模块,用来检查一些参数的存在性。最后我们可以看到又执行了setContext,只是因为上面的进行保护可能会引起一些环境变量发送改版,所以需要再一次重新进行保存。

image

再往下走到defaultUninitializedFallbackPaths,目前到这里,都是配置和初始化环境,还没有加载程序。

image

继续往下我们会看到两个环境变量sEnv.DYLD_PRINT_OPTSsEnv.DYLD_PRINT_ENV,通过代码我们发现如果配置了这两个变量就会执行下面的pint方法,我们可以通过在项目中的target里设置,让他们进行打印。

image

![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)、加载共享缓存:
image

再往下几行,我们发现了checkSharedRegionDisable这个函数,从这往下到mapSharedCache都是在加载共享缓存,例如:UIKit,Foundation框架。这里要再提一下iOS的共享缓存的意义。

我们知道iOS在加载的时候最终会将MachO加载到内存中,其中一些通用的框架不只一个App会使用到,所以为了节省空间提升效率,苹果采用了共享缓存的机制,如下图:
图片.png
(3)、DYLD加载方式:

经过了以上的配置、加载共享缓存的步骤之后,DYLD现在要正式开始,目前DYLD的执行方式分为2种;Dyld2 和 Dyld3。

(3-1)、DYLD3方式:

iOS11 之后增加了Dyld3 通过使用Closure闭包方式进行加载,这种方式比之前Dyld2的效率更高效,但本质的流程还是与Dyld2一致的,我们可以快速的看一下。先从共享缓存中查找闭包(Closure);
image

如果mainClosure是空,并且有失效了,则加载方式也会发生改变;
image
没有从缓存中找到有效的Closure,就新建一个;尝试启动Closure,验证知否过期,如果过期了则再创建一个,让后再次启动Closure;图
image
启动成功返回result;
(3-2)、DYLD2方式:

通过了解Dyld3闭包模式我们对dyld的执行有了一个大概的认知,但是从分析Dyld3的加载过程我们并没有发现我们的framework、vc、main函数是如何加载的,load函数是如何被调用执行的,这些都需要我们通过了解Dyld2来进行验证(DYLD3的方式更加优化,流程更加便捷)。

image
reloadAllImages: 往下就开始进行Dyld2的流程;实例化主程序sMainExecutable,这是dyld第一个加载的image;我们看一下instantiateFromLoadedImage() 跟踪到最后我们发现了mach-O中加载segmentdylib的数量是有上限的,如果超过上限就会报错。
image
image
image
image
image
checkVersionedPaths()检查动态库的版本;
image

DYLD_INSERT_LIBRARIES 环境变量,作用是在dyld加载时允许插入动态库,这个环境变量可以通过在root环境(越狱设备)下把自己的类库加入到三方应用中,从而实现代码注入;这块我先埋个伏笔,后续我会对iOS 防HOOK方面进行详细的介绍。
link()链接主程序;
image

sAllImages 中一次链接动态库,sAllImages[i+1] +1是因为上面已经加载了dyld的image程序,所以下标从+1开始;
如果加载失败了,需要再次回到 reloadAllImages 继续执行;
image

image->recursiveBind() 绑定插入的动态库;
image

下面就来到最重要的 initializeMainExecutable() 初始化方法;虽然这么看只是一句简单函数调用,其实这个函数涉及的步骤很多,我们根据刚才debug获得信息大致能猜到,这个函数应该是对Image(镜像)进行处理,我们继续前进。

图片.png

1、跟进initializeMainExecutable() 我们看到的是一个循环,内容是将所以的Image执行初始化runInitializers函数。
image

2、继续跟进runInitializers(command+shift+O)然后继续调用了processInitializers()函数。这里可能我们通过command+左键 无法追踪,我们使用command+shift+O 然后选择即可。
image
image

3、继续跟进processInitializers() 然后继续调用了recursiveInitialization()函数。
image

4、继续跟进recursiveInitialization(),我们把焦点放到notifySingle()上,

image

image

5、到这步我们已经距离结果越来越近了,继续跟进notifySingle(),上面的内容我们直接忽略,先关注这个函数 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) sNotifyObjCInit是一个回调指针,我们搜索一下sNotifyObjCInit看看他是在什么时候被初始化的。通过搜索我们发现了sNotifyObjCInit是在registerObjCNotifiers函数下将第二个参数赋值给了它,因为是回调指针,我们如果找到了init这个函数就知道了具体是实现。
图片.png

图片.png

5-1、接下来我们就看是谁调用了registerObjCNotifiers(),通过搜索registerObjCNotifiers发现是由_dyld_objc_notify_register()这个函数调用它的,init参数都透传过来的,我们还需要继续追踪是谁调用了dyld_objc_notify_register()?。
图片.png

5-2、追踪到dyld_objc_notify_register()函数这里,我们已无法从源码中得到结果了,怎么办呢?不要慌。我们需要插上真机,增加符号断点进行调试,看看是否有结果。 顺利进入了debug后我们查看右侧栏的调用顺序,发现在dyld_objc_notify_register之前是调用的是_objc_init,那么我们就可以再去查看_objc_init函数。
图片.png

图片.png

图片.png

5-3、打开objc源码后我们command+shift+O搜索_objc_init,我马上就能发现确实是_objc_init调用了_dyld_objc_notify_register,这时我们查看第二个参数load_images,这个就是init的真实实现,我们继续跟踪进去。
图片.png

图片.png

5-4、load_images()最后一句话调用了call_load_methods()函数,从名字我们就知道了这个是开始调用load方法了。
图片.png

5-5、继续 call_load_methods() 函数,发现是通过循环将每个类的load函数进行了调用。
图片.png

5-6、到这里notifySingle()函数全部执行完毕,我们继续往下看。
6、回到ImageLoaderrecursiveInitialization函数中,this->doInitialization(context);会调用全局C++对象的构造函数attribute((constructor))的C函数
图片.png

7、doInitialization() 内部执行 doModInitFunctions() 加载构造函数。
图片.png

8、以上执行完毕之后就会回到我们最初的dyld的main函数了。

(4)、返回app主函数:

(uintptr_t)sMainExecutable->getEntryFromLC_MAIN() 找到主程序main函数;

图片.png

最后一步返回result;
图片.png

三、总结

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函数
    • 返回主程序的入口函数。开始进入主程序的main函数!

你可能感兴趣的:(DYLD 加载Mach-O的流程)