010-iOS底层原理-dyld加载流程

引言

本文主要探索dyld的加载流程,了解应用程序在main函数之前都做了什么准备工作,了解dyld是什么,我们所编写的代码、framework等是如何加载到内存里变活起来的。

dyld

dyld(The dynamic link editor )是苹果的动态链接器,是苹果操作系统的重要组成部分,在我们的代码被编译打包成可执行文件Mach-O 文件之后 ,交由 dyld 负责链接 , 加载程序 。
dyld源码
本文用的是dyld-852版本的源码。

探索1

main -> start符号断点,调用栈

我们新建一个iOS工程,在main.m中打个断点,运行项目,查看调用堆栈,如图所示:

main调用堆栈

查看左边,发现在main函数调用前,系统已经执行了start函数。根据以往经验,我们可以选择的操作方式是查看汇编添加符号断点
调用栈汇编

图中我们看到了,start是在libdyld.dylib这个库中调用的,dyld是开源库,我们可以下载下来后,阅读源码。本文用的是dyld-852版本的源码。
添加start符号断点

在添加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之前调用了load_images_dyld_start。我们在控制台中输入bt 打印详细调用栈,如图所示:
bt打印调用堆栈

由此可见,我们的app最开始,是由_dyld_start开始的。

dyld源码

下载好dyld-852后,打开dyld源码工程,由于工程底部所依赖的系统库太多(libdispatch,libsystem),运行不起来无法调试。

dyldbootstrap::start

全局搜索_dyld_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.cppdyldbootstrap是一个命名空间C++语法中,有一个词叫namespace命名空间,可当做类来阅读。namespace内部成员和函数,相当于类的成员和方法。我们接着搜索start,如图所示:
dyldbootstrap::start函数内部

rebaseDyld():dyld重定位。这是苹果用来保证应用安全的技术,其中包括:ASLRCode Sign
ASLRAddress 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多行代码的一个函数。

dyld::_main

查看大量源码,分析流程的思路,需要整体把握,不需要一头扎进细节里,否则,将切身体会到“入门到放弃”的完整过程,迷失在未知的世界里。对于此类源码,我们可以根据返回值倒推逻辑。接下来,我们将逐步解析其内容。
1、通过return返回的是一个result
return result

2、通过result的赋值,找到result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();以及其他有关sMainExecutable的代码位置,我们由此可知,resultsMainExecutable有很大的关联和关系;
result找到sMainExecutable

3、通过sMainExecutable =,找到sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);,官方注释为// instantiate ImageLoader for main executable
sMainExecutable的赋值

4、点击进入instantiateFromLoadedImage,发现内部添加了image镜像,并返回此image镜像
5、进入instantiateMainExecutable查看如何获得image镜像的,内部的segCountlibCountdata_commandinfo_command等等内容,可进入sniffLoadCommands配合烂苹果MachOView进行对照查看,可以事半功倍。
instantiateMainExecutable
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循环将第四步插入的动态库链接。
link主程序和动态库

6.【第六步:weakBind弱引用绑定主程序】:for循环sImageRoots.size(),插入镜像符号;sMainExecutable->recursiveBindWithAccounting绑定主程序;image->recursiveBind绑定插入的动态库,并通知已经注册完毕。sMainExecutable->weakBind弱引用绑定主程序。
weakBind弱引用绑定主程序

7.【第七步:初始化主程序】:调用initializeMainExecutable初始化主程序。
运行所有初始化

8.【第八步:寻找main函数入口】:寻找LC_MAIN入口地址并执行,如果找不到,则寻找LC_UNIXTHREADdyldstart设置成main()
寻找main函数

【第七步:初始化主程序】initializeMainExecutable的分析

1.进入initializeMainExecutable后,for循环将插入的动态库初始化。然后执行runInitializers初始化主程序。

initializeMainExecutable

2.进入sMainExecutable->runInitializers,找到processInitializers函数,进入下一步。
sMainExecutable->runInitializers

3.进入processInitializers,主线为递归初始化images list镜像列表里的所有镜像,并创建一个list列表,用于存储为初始化的向上的依赖关系。ups > 0的判断,是如果仍有依赖关系,将他们初始化。
processInitializers

4.全局搜索recursiveInitialization,找到ImageLoader里的recursiveInitialization 函数。查看try{}内部的实现:
1)for循环初始化更底层的库
2)terminationRecorder记录终止命令
3)notifySingle通知objc记得要初始化这个image镜像。
4)doInitialization初始化这个image镜像

doInitialization

进入到doInitialization函数,有两个函数调用

doInitialization函数

1.进入doImageInit,发现是 获取mach-o的init方法的地址并调用:
doImageInit

2.进入doModInitFunctions,解析并执行DATA,mod_init_func这个section中保存的函数(这里保存的是全局C++对象的构造函数以及所有带__attribute((constructor)的C函数),也就是我们之前在main.m中写的before_main()函数。并将libSystem库中的libSystem_initializer执行。
before_main调用栈

**陷入迷茫,无法继续下一步,返回到ImageLoader::recursiveInitialization函数,探索notifySingle

notifySingle

1.点击进入notifySingle,发现来到的是函数声明的地方,void (*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*);
2.全局搜索notifySingle(,找到它的实现:

notifySingle内部实现

3.全局搜索sNotifyObjCInit,无实现函数,在registerObjCNotifiers函数中找到 = init赋值。
sNotifyObjCInit赋值

4.全局搜索registerObjCNotifiers,在dyldAPIs.cpp中的_dyld_objc_notify_register函数中,找到调用位置。
image.png

5.全局搜索_dyld_objc_notify_register,发现没有调用该函数的代码。细看类名dyldAPIs.cpp,该类为dyld对外的接口类。由于上面提到的时候,对通知objc记得初始化该镜像,因此,我们将打开libobjc源码。

6.打开objc4_818_2工程,全局搜索_dyld_objc_notify_register。在_objc_init函数中,找到其调用位置,其中load_imagesdyldregisterObjCNotifierssNotifyObjCInit赋值的参数。而load_images,是一个函数,由此可知,dyld通知objc的方式,是通过load_images函数进行回调的。也就是说,dyld中的notifySingle是一个回调函数。

objc -> _dyld_objc_notify_register

load_images

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

load_images

进入call_load_methods中,内部为创建一个autoreleasepool,并在autoreleasepooldo-while循环执行所有的+load方法。
call_load_methods
内部继续调用的call_class_loads()call_category_loads()都是最终调用(*load_method)(cls, @selector(load));函数。
call_class_loads()

到此为止,+load的加载流程已经梳理通顺。对应上了我们前面的ViewController.m+load方法。

+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。如图所示

_objc_init符号断点调用栈

从调用栈中可以看到,doModInitFunctions_objc_init中缺失了两个流程。

_os_object_init

_os_object_init位于libdispatch.dylib库中,我们将下载libdispatch-1271.40.12源码
,解压后打开工程,全局搜索_os_object_init

_os_object_init

发现_os_object_init是在libdispatch_init调起的。
libdispatch_init

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

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主线加载流程流程图如下:

dyld加载流程.png

你可能感兴趣的:(010-iOS底层原理-dyld加载流程)