Dyld的加载流程分析

引言:

众所周知,我们的iOS应用是通过Dyld进行加载的,那么Dyld是如何加载我们的应用的,它的流程是怎样的,下面我们把dyld的加载分为几个步骤做个简短的分析。

1 dyld的start启动

首先我们创建一个Demo工程,在我们的AppDelegate.m文件里加入+(load)方法并断点,如下图所示:

1

运行Demo App后,可以得到所下图


2

从图2中我们可以看到,我们的App是从_dyld_start开始的,我们点击dyld_start,看到汇编的第18行代码,这里调用了dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*)这行代码,我们再使用bt命令看下详细的堆栈,如下图


3

从这里可以看出一切的开始是从_dyld_start开始的,这个dyld_start在可以在dyld的源码里找(注:这里使用的dyld的源码的版本是dyld-832.7.3),下面我们打开dyld的源码进行分析

我们在dyld的源码里找 dyldbootstrap这个命名空间,按住shift+comand+j进入文件,所下图所示


4

从这里可以看到是在dyldInitialization.cpp文件里,然后我们在文件里搜索start,找到uintptr_tstart(constdyld3::MachOLoaded* appsMachHeader,intargc,constchar* argv[],

constdyld3::MachOLoaded* dyldsMachHeader,uintptr_t* startGlue)函数,如图所示:



5

我们从start函数逐步往下分析整个流程,我们先看下参数,appsMachHeader是我们App的MachHeader,dyldsMachHeader是dyld的MachHeader。

第121行代码是告诉我们的degbugServer,我的dyld开始起动了。

第125行代码rebaseDyld(dyldsMachHeader) 重定位我们的dyld。

第136,143行是栈溢出保护和初始化dyld,之后就是调用dyld的main函数(这是最核心的),我们着重分析下dyld的main函数流程。


6

main函数的前几行代码都是代码检测相关的,不是核心内容

我们下面来看主程序的配置相关,如图


7

这些是配置主程序的MachHeader(就是Macho的头),主程序的Slide(就是主程序的ASLR的偏移值,每次启动都是不一样的)

下面调用setContext(mainExecutableMH, argc, argv, envp, apple);保存我们配置的信息


8

然后通过configureProcessRestrictions(mainExecutableMH, envp)这个函数配置进程受限制(AMFI相关(Apple Mobile File Integrity苹果移动文件保护)),下图都是进程受限相关的配置,比如是否强制使用dyld3(dyld是在iOS11推出来的,加载高效)

9

下图打印我们的环境变量,这个环境变理可通过 Environment Variables配置

10

以下都是dyld的启动,配置,以及主程序的相关配置和一些代码检测的流程,下面我们来分析共享缓存

2 dyld加载共享缓存

11

点击进去checkSharedRegionDisable发现有一个“iOS cannot run without shared region”说明,这是表明我们的iOS是一定有共享缓存的。

接着调用mapSharedCache传进去主程序的Slide,这个函数调用了loadDyldCache加载我们的dyld库存,如下图所示


12

满足options.forcePrivate 这个条件的话,只加载当前进程

reuseExistingCache如果缓存已经加载不再处理,如果第一次加载执行mapCacheSystemWide这个函数

通过以上分析,可以得出结论,动态库的共享缓存是最先被加载的(我们自己开发的动态库不可以)。从iOS11引入了dyld3的ClosureMode(闭包模式加载更快),下面我们来分析一下

3 dyld3的闭包模式


13

这里先判断闭包模式是否打开,如果没有的话将会走dyld2的流程,打开走dyld3的流程(dyld2,dyld3的加载流程一致),下面我们来分析dyld3的闭包模式


14

先从共享缓存中查找这个实例,如果拿到就先验证


15

这里判断是否查找成功,并且验证闭包的有效性,如果失效,sLaunchModeUsed设置为NULL


16

这里如果没找到,再去缓存中查一次,如果mainClosure为空,这里就调用buildLaunchClosure创建闭包实例

最终拿到这个mainClosure实例启动这个实例,如下图所示


17

如果启动失败或者闭包过期,这里就再重新调用buildLaunchClosure创建并调用launchWithClosure重新启动一次。

启动成功后设置gLinkContext.startedInitializingMainExecutable = true;这个主程序加载成功了。

同时返回结果result(即主程序的main),如下图所示:

18

接着就会实例化我们的主程序了,下面我们来分析是如何加载的。

4 dyld加载主程序

接下看下怎么实例化主程序的,如下图所示


19

第6862行代码会调用instantiateFromLoadedImage函数实例化我们的主程序,我们来看下instantiateFromLoadedImage这个函数,下图所示:


20

这里通过ImageLoaderMachO这个函数传image的macho_header,ASLR的偏移值,路径生成ImageLoader对象,然后调用addImage这个函数加入我们的镜像文件,同时返回ImageLoader这个对象。(通过dyld加载的第一个镜像是我们的主程序),我们来看下instantiateMainExecutable的这个函数的流程,如图所示:


21

这里调用sniffLoadCommands获取loadCommands,如图:


22

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中。


23

接着检测当前主程否是当前设备的,如上图所示,到这里我们的主程序实例化结束,接着我们来分析动态库的加载。

5 dyld加载动态库

24

这里先检查动态库的版本和路径,接着加载动态库,如图所示:


25

这里先根据环境变量判断动态插入库不为空,接着遍历loadInsertedDylib


26

这里调用load插入动态库,接着开始链接主程序,


27

先配置gLinkContext.linkingMainExecutable = true;这个变量为true.

接着调用link函数进行链接,我们来看看是如何链接的:


28


29
30

这里先记录起始时间,在最后在记录结束时间,把加载时间记录下来,这个就是dyld加载应用的时长。

这里链接插入动态库完成了。

31

之后把这些实例化的镜像文件加入到AllImages中(这里是从i+1开始的,因为主程序已经先加载了),之后再调用link进行链接,这里跟主程序的链接是一样的。


32

这里的条件不满足的话,将会持续的调用reloadAllImage,这里执行之后,就开始绑定动态库了,如图所示:


33

这里遍历AllImages绑定插入动态库,之后进行弱符号绑定。

接着调用initializeMainExecutable初始化主程序的Main方法,如图所示:

34

下面我们来分析主程序Main方法加载的流程。

6  load方法与初始化方法的加载

我们先进入initializeMainExecutable()这个函数,看下它的实现

35

这里有一个runInitializers函数,我们再进去


36

这里会调用processInitializers这个函数,我们再跟进去看看


37

接着我们再跟下recursiveInitialization这个函数

38
39

这里调用了notifySingle这个函数,我们需要再跟进去一下,


40
41

而这里没有找到loadImge的函数调用,这里到底是怎么回事,我们通过汇编可以看到load_image是在libobjc.dylib中,也就是说在objc的源码中,那它是怎么调用的,我们来看下代码。

在1019行中 (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()) 这个回调进行关联的。

这里首先判断sNotifyObjCInit这个是否为空,我们在这文件里搜下,发现是在registerObjCNotifiers这里调用的时候赋值的,如下图:


42

我们搜下registerObjCNotifiers这个函数发现是在dyldAPIS.cpp中的

43

这个函数调用的,这里有传递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文件中,如图所示:


44

这里可以看到_dyld_objc_notify_register这个函数是在_objc_init调用的,这里有一个load_images,我们再看下


45

这里面有一个call_load_methods方法,点进去看下


46

这里会do while调用call_class_loads方法来加载所有类的+(void)load方法,load方法加载完成后调用了call_category_loads这个方法,加载类加的loads方法,这也是为什么类别的方法与原类的方法重名后,会覆盖原类的方法。

我们回到dyld的源代码找到ImageLoader.cpp文件中的recursiveInitialization函数中调用notifySingle这里走到了objc中,objc把所有的load加载完成后,会调用doInitialization这个函数,进去看下


47

这里doModInitFunctions调用这个函数,这个函数的作用是什么,我们来看下,


48

这里就是在加载我们的构造函数,我们在Demo的main.m上面加入构造函数

__attribute__((constructor)) void test1() {

 printf("test调用了");

}

经过调试,它比main函数先调用。

我们再回到dyld的main函数,找到这里,如图


49

这里通过LC_MAIN找到程序入口给result,最后返回主程序的main地址。dyld的加载就结束了。

下面是dyld的initializeMainExecutable初始化主程序的思维导图


50

结语:到这里dyld的流程分析就结束了,有所遗漏或者错误的地方,请指正,大家可以相互学习交流,共同进步!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容