引言
本文主要探索dyld的加载流程,了解应用程序在main函数之前都做了什么准备工作,了解dyld是什么,我们所编写的代码、framework等是如何加载到内存里变活起来的。
dyld
dyld(The dynamic link editor )是苹果的动态链接器,是苹果操作系统的重要组成部分,在我们的代码被编译打包成可执行文件的 Mach-O 文件之后 ,交由 dyld 负责链接 , 加载程序 。
dyld源码
本文用的是dyld-852版本的源码。
探索1
main -> start符号断点,调用栈
我们新建一个iOS工程,在main.m中打个断点,运行项目,查看调用堆栈,如图所示:

查看左边,发现在
main函数调用前,系统已经执行了start函数。根据以往经验,我们可以选择的操作方式是查看汇编和添加符号断点。
图中我们看到了,
start是在libdyld.dylib这个库中调用的,dyld是开源库,我们可以下载下来后,阅读源码。本文用的是dyld-852版本的源码。
在添加
start符号断点,运行后,发现符号断点并未停住,而是直接来到了main.m的断点上。因此,start并不是我们所需要的符号断点。仍需努力。
load方法和__attribute__
在ViewController.m中添加load方法
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
+(void)load {
NSLog(@"---%s---",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
}
@end
在main.m中添加__attribute__
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
NSLog(@"main 函数");
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
//确保此函数在 在main函数被调用之前调用
__attribute__ ((constructor))void before_main(){
printf("main 前 :%s\n",__func__);
}
这三者打印顺序如何呢?如图所示:

由图可知,三者执行的先后顺序为
load方法-- __attribute__--main函数。因此在main函数执行前的操作,可以从load方法中添加断点开始。补充:
__attribute__ ((constructor))void before_main(){}是确保此函数在 在main函数被调用之前调用。具体请查看这篇文章OC中的 _ attribute _。
load方法断点
在ViewController.m中的+load方法添加断点,运行后如图所示:

由图中我们可以看到左边的调用栈中,在
load之前调用了load_images和_dyld_start。我们在控制台中输入bt 打印详细调用栈,如图所示:
由此可见,我们的app最开始,是由
_dyld_start开始的。
dyld源码
下载好dyld-852后,打开dyld源码工程,由于工程底部所依赖的系统库太多(libdispatch,libsystem),运行不起来无法调试。
dyldbootstrap::start
全局搜索_dyld_start

在
dyldStartup.s中(.s是汇编文件后缀),可以看到有多个重复的.global _dyld_start,分别点击后,可以看到是右边是由于架构判断,导致的重复,因此,我们挑arm64架构的源码来阅读。阅读汇编代码,阅读注释很重要,我们在
第240行看到了call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue),call的意思是呼叫,调用。这里我们全局搜索dyldbootstrap::start,发现搜不到我们想要的。我们全局搜索dyldbootstrap,发现在dyldInitialization.cpp的dyldbootstrap是一个命名空间。C++语法中,有一个词叫namespace命名空间,可当做类来阅读。namespace内部成员和函数,相当于类的成员和方法。我们接着搜索start,如图所示:
rebaseDyld():dyld重定位。这是苹果用来保证应用安全的技术,其中包括:ASLR和 Code Sign。ASLR: Address Space Layout Randomization(地址空间布局随机化)的简称。App在被启动的时候,程序会被映射到逻辑地址空间,这个逻辑地址空间有一个起始地址,ASLR技术让这个起始地址是随机的。这个地址如果是固定的,攻击者很容易就用起始地址+函数偏移地址找到对应的函数地址。Code Sign:代码加密签名机制,但是在 Code Sign操作的时候,加密的哈希不是针对整个文件,而是针对每一个 Page的。这个就保证了 dyld在加载的时候,可以对每个 page进行独立的验证。正是因为 ASLR使得地址随机化,导致起始地址不固定,以及 Code Sign,导致不能直接修改 Images。所以需要 rebase来处理符号引用问题,Rebase的时候只需要通过增加对应偏移量就行了。Rebase主要的作用就是修正内部(指向当前 Mach-O文件)的指针指向,也就是基地址复位功能。
dyld::_main
dyld::_main函数是我们此次研究的重头戏,点进去之后,发现它是一个800多行代码的一个函数。

查看大量源码,分析流程的思路,需要整体把握,不需要一头扎进细节里,否则,将切身体会到“入门到放弃”的完整过程,迷失在未知的世界里。对于此类源码,我们可以根据
返回值来倒推逻辑。接下来,我们将逐步解析其内容。1、通过
return返回的是一个result;
2、通过
result的赋值,找到result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();以及其他有关sMainExecutable的代码位置,我们由此可知,result与sMainExecutable有很大的关联和关系;
3、通过
sMainExecutable =,找到sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);,官方注释为// instantiate ImageLoader for main executable
4、点击进入
instantiateFromLoadedImage,发现内部添加了image镜像,并返回此image镜像。5、进入
instantiateMainExecutable查看如何获得image镜像的,内部的segCount,libCount,data_command,info_command等等内容,可进入sniffLoadCommands配合烂苹果MachOView进行对照查看,可以事半功倍。

dyld::_main内部总结(借助注释来查看)
1.【第一步:条件准备】:环境配置、平台、版本、路径、主机信息等

2.【第二步:共享缓存配置】:
checkSharedRegionDisable检查是否可用,通过mapSharedCache映射共享缓存到贡献缓存区域
3.【第三步:实例化主程序】:通过
instantiateFromLoadedImage实例化主程序,得到一个sMainExecutable。将images加入到dyld_all_image_info表中。
4.【第四步:插入动态库】:
for循环 DYLD_INSERT_LIBRARIES,调用loadInsertedDylib(*lib);插入动态库。
5.【第五步:link主程序和动态库】:直接调用
link方法,将sMainExecutable链接,再for循环将第四步插入的动态库链接。
6.【第六步:weakBind弱引用绑定主程序】:
for循环sImageRoots.size(),插入镜像符号;sMainExecutable->recursiveBindWithAccounting绑定主程序;image->recursiveBind绑定插入的动态库,并通知已经注册完毕。sMainExecutable->weakBind弱引用绑定主程序。
7.【第七步:初始化主程序】:调用
initializeMainExecutable初始化主程序。
8.【第八步:寻找main函数入口】:寻找
LC_MAIN入口地址并执行,如果找不到,则寻找LC_UNIXTHREAD,dyld将start设置成main()。
【第七步:初始化主程序】initializeMainExecutable的分析
1.进入initializeMainExecutable后,for循环将插入的动态库初始化。然后执行runInitializers初始化主程序。

2.进入
sMainExecutable->runInitializers,找到processInitializers函数,进入下一步。
3.进入
processInitializers,主线为递归初始化images list镜像列表里的所有镜像,并创建一个list列表,用于存储为初始化的向上的依赖关系。ups > 0的判断,是如果仍有依赖关系,将他们初始化。
4.全局搜索
recursiveInitialization,找到ImageLoader里的recursiveInitialization 函数。查看try{}内部的实现:1)
for循环初始化更底层的库2)
terminationRecorder记录终止命令3)
notifySingle通知objc记得要初始化这个image镜像。4)
doInitialization初始化这个image镜像
doInitialization
进入到doInitialization函数,有两个函数调用

1.进入
doImageInit,发现是 获取mach-o的init方法的地址并调用:
2.进入
doModInitFunctions,解析并执行DATA,mod_init_func这个section中保存的函数(这里保存的是全局C++对象的构造函数以及所有带__attribute((constructor)的C函数),也就是我们之前在main.m中写的before_main()函数。并将libSystem库中的libSystem_initializer执行。
**陷入迷茫,无法继续下一步,返回到ImageLoader::recursiveInitialization函数,探索notifySingle
notifySingle
1.点击进入notifySingle,发现来到的是函数声明的地方,void (*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*);
2.全局搜索notifySingle(,找到它的实现:

3.全局搜索
sNotifyObjCInit,无实现函数,在registerObjCNotifiers函数中找到 = init赋值。
4.全局搜索
registerObjCNotifiers,在dyldAPIs.cpp中的_dyld_objc_notify_register函数中,找到调用位置。
5.全局搜索
_dyld_objc_notify_register,发现没有调用该函数的代码。细看类名dyldAPIs.cpp,该类为dyld对外的接口类。由于上面提到的时候,对通知objc记得初始化该镜像,因此,我们将打开libobjc源码。
6.打开objc4_818_2工程,全局搜索_dyld_objc_notify_register。在_objc_init函数中,找到其调用位置,其中load_images为dyld中registerObjCNotifiers为sNotifyObjCInit赋值的参数。而load_images,是一个函数,由此可知,dyld通知objc的方式,是通过load_images函数进行回调的。也就是说,dyld中的notifySingle是一个回调函数。

load_images
进入load_images,首先查找发现所有+load方法,然后执行所有+load方法

进入
call_load_methods中,内部为创建一个autoreleasepool,并在autoreleasepool中do-while循环执行所有的+load方法。
call_class_loads()和call_category_loads()都是最终调用(*load_method)(cls, @selector(load));函数。 
到此为止,
+load的加载流程已经梳理通顺。对应上了我们前面的ViewController.m中+load方法。
+load流程
调用流程如图所示:

→
_dyld_start (dyld)→
dyldbootstrap::start (dyld)→
dyld::_main (dyld)→
initializeMainExecutable(dyld)→
ImageLoader::runInitializers (dyld)→
ImageLoader::processInitializers (dyld)→
ImageLoader::recursiveInitialization (dyld)→
dyld::notifySingle (dyld,回调函数,sNotifyObjCInit = load_images函数)→
libobjc.A.dylib load_images (objc,接收回调,调所有+load方法)→
VC +load。(开发者代码)
doInitialization未完成的流程
前面进入doInitialization内部后,发现无法继续深入,此时,我们回到objc调试工程,添加符号断点_objc_init。如图所示

从调用栈中可以看到,
doModInitFunctions到_objc_init中缺失了两个流程。
_os_object_init
_os_object_init位于libdispatch.dylib库中,我们将下载libdispatch-1271.40.12源码
,解压后打开工程,全局搜索_os_object_init

发现
_os_object_init是在libdispatch_init调起的。
在之前的调用栈中,libdispatch_init是由libSystem_initializer调起的,此函数位于libSystem.B.dylib库中。我们将下载Libsystem-1292.60.1源码解压后打开工程,全局搜索libdispatch_init,确实位于libSystem_initializer中调用了。

_objc_init流程
→_dyld_start(dyld)
→dyldbootstrap::start(dyld)
→dyld::_main(dyld)
→dyld::initializeMainExecutable(dyld)
→ImageLoader::runInitializers(dyld)
→ImageLoader::processInitializers(dyld)
→ImageLoader::recursiveInitialization(dyld)
→ImageLoaderMachO::doInitialization(dyld)
→ImageLoaderMachO::doModInitFunctions(dyld)
→libSystem_initializer(libSystem)
→libdispatch_init(libdispatch)
→_os_object_init(libdispatch)
→_objc_init(libobjc)
总结
本文Demo
dyld主线加载流程流程图如下:
