[iOS] dyld加载流程

2021-01-12  本文已影响0人  沉江小鱼

本文的目的主要是分析 dyld的加载流程,了解一下在main 函数之前,底层还做了哪些事情。

0. 引子

load ->C++方法 -> main函数

问题:为什么是这么一个顺序呢?main 不是入口函数吗?为什么不是 main最先执行?下面我们就探索一下main函数之前,系统还做了哪些事情。

1. 编译过程 & 库的概念

在分析 app启动之前,我们需要先了解一下iOS编译过程以及动态库静态库

1.1 编译过程

编译过程如下图所示:


image.jpeg

主要分为以下几步:

1.2 静态库 & 动态库
1.2.1 静态库

在链接阶段,会将汇编生成的目标程序与引用的静态库一起链接打包到可执行文件当中。此时的静态库就不会再改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的。

1.2.2 动态库

程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时才被载入。

1.3 静态库 & 动态库 图示
image.png

2. dyld

根据dyld源码,以及libobjclibSystemlibdispatch 源码协同分析。

2.1 什么是 dyld?

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的重要组成部分,在 app 被编译打包成可执行文件格式的 Mach-O文件后,交由 dyld负责链接,加载程序。

共享缓存机制:在iOS系统中,每个程序依赖的动态库都需要通过 dyld 一个一个加载到内存,然而,很多系统库几乎都是每个程序都会用到的,如果每个程序运行的时候都重复的去加载一次,肯定会运行缓慢,所以为了优化启动速度,提高程序性能,就有了共享缓存机制。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着。

所以App 的启动流程图如下:

image.png
2.2 App启动的起始点

从上图中可以看出,最开始是从dyld中的_dyld_start开始的,所以需要下载 dyld 源码来进行分析。

3. dyld流程分析

3.1 _dyld_start

dyld-750.6源码中搜索_dyld_start,发现其在dyldStartUp.s文件中,查找arm64架构,如下:

image.png

发现其调用了dyldbootstrap 命名空间下的start方法:

    // call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)

3.2 dyldbootstrap::start

源码中搜索找到dyldbootstrap命名空间,在这个文件中查找start方法,源码如下:

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    rebaseDyld(dyldsMachHeader);

    // kernel sets up env pointer to be just past end of agv array
    const char** envp = &argv[argc+1];
    
    // kernel sets up apple pointer to be just past end of envp array
    const char** apple = envp;
    while(*apple != NULL) { ++apple; }
    ++apple;

    // set up random value for stack canary
    __guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(argc, argv, envp, apple);
#endif

    // now that we are done bootstrapping dyld, call dyld's main
    uintptr_t appsSlide = appsMachHeader->getSlide();
    return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

这个方法的核心是在返回值那调用了 dyld::_main 函数,同时做了很多 dyld 初始化相关的工作:

ASLR:是Address Space Layout Randomization(地址空间布局随机化)的简称。App在被启动的时候,程序会被映射到逻辑地址空间,这个逻辑地址空间有一个起始地址,ASLR技术让这个起始地址是随机的。这个地址如果是固定的,黑客很容易就用起始地址+函数偏移地址找到对应的函数地址。

Code Sign:就是苹果代码加密签名机制,但是在Code Sign操作的时候,加密的哈希不是针对整个文件,而是针对每一个Page的。这个就保证了dyld在加载的时候,可以对每个page进行独立的验证。

正是因为ASLR使得地址随机化,导致起始地址不固定,以及Code Sign,导致不能直接修改Image。所以需要rebase来处理符号引用问题,Rebase的时候只需要通过增加对应偏移量就行了。Rebase主要的作用就是修正内部(指向当前Mach-O文件)的指针指向,也就是基地址复位功能。

补充:
macho_headerMach-O文件的头部,而 dyld 加载的文件就是 Mach-O文件

3.3 dyld::_main

dyld::_main的源码实现很长,如果对 dyld加载流程不太了解,可以根据_main函数的返回值进行反推,这里分部分进行介绍。

我们接下来主要分析一下[第三步] 和 [第八步]。

3.3.1 [第三步] 主程序初始化

sMainExecutable 表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage方法初始化的。

image.jpeg

进入instantiateFromLoadedImage 源码,其中创建一个 ImageLoader 实例对象,通过 instantiateMainExecutable 方法创建:

image.jpeg

进入instantiateMainExecutable源码,其作用是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序。其中sniffLoadCommands函数时获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验:

image.jpeg
3.3.2 [第八步] 执行初始化方法

进入initializeMainExecutable源码,主要是循环遍历,都会执行runInitializers方法

image.jpeg

全局搜索runInitializers(cons,找到如下源码,其核心代码是 processInitializers 函数的调用

截屏2021-01-12 上午9.52.42.png

进入processInitializers函数的源码实现,其中对镜像列表调用 recursiveInitialization函数进行递归实例化:

image.jpeg

全局搜索recursiveInitialization(cons,其源码实现如下:

image.jpeg

在这里,我们分成两部分探索,一部分是notifySingle 函数,一部分是doInitialization函数,首先探索notifySingle 函数

3.3.2.1 notifySingle函数
全局搜索notifySingle(,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());

image.jpeg

全局搜索sNotifyObjCInit,发现没有找到该实现,但是有赋值操作

image.jpeg

搜索registerObjCNotifiers 在哪里调用了,发现在_dyld_objc_notify_register进行了调用:

image.jpeg

注意:_dyld_objc_notify_register的函数需要在objc源码中搜索

objc 源码中搜索_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是 objc中的load_images,而load_images会调用所有的+load方法,所以综上所述,notifySingle是一个回调函数,实质上就是objc 里面的loadImages 这个方法。

load 函数加载
下面我们进入 load_images的源码看看其实现,以此在证明 load_images中调用了所有的load函数。

通过objc 源码中_objc_init源码实现,进入load_images的源码实现:

image.jpeg

接着看call_load_methods源码实现,可以发现其核心是通过 do-while循环调用+load 方法的:

image.jpeg

进入call_class_loads源码实现,看到这里调用的 load方法证实了我们之前提到的类的load方法:

image.jpeg

所以,load_images调用了所有类的 load函数,以上源码分析过程正好对应堆栈的打印信息:

image.jpeg

总结:
load 的源码链为:_dyld_start ->dyldbootstrap::start -> dyld::_main -> dyld::initializeMainExecutable ->ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> dyld::notifySingle(是一个回调处理) -> sNotifyObjCInit ->load_images(libobjc.A.dylib)

那么,_objc_init又是什么时候调用的呢?

3.3.2.2 doInitialization 函数
走到objc_objc_init函数,发现走不通了,我们回退到 recursiveInitialization 递归函数的源码实现,继续查看doInitialization函数

image.jpeg

进入doInitialization 函数的源码实现:

image.jpeg
这里也需要分成两部分,一部分是 doImageInit 函数,另一部分是doModInitFunctions函数

我们在刚才测试程序的 C++方法处加一个断点,看下堆栈信息:

image.jpeg

到了这里还是没有找到_objc_init的调用,我们可以通过_objc_init加一个符号断点来查看调用_objc_init前的堆栈信息:

进入_os_object_init源码实现,其源码实现调用了_objc_init函数:

image.jpeg

结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数 2,即load_images,到sNotifySingle-->sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环。

所以可以简单的理解为 _objc_init中调用_dyld_objc_notify_register是注册一个回调函数,而sNotifySingle 是调用这个函数。

总结:
_objc_init的源码链:_dyld_start--> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

3.3.3 [第九步]寻找主函数入口
3.4 dyld 源码实现:
image.jpeg image.jpeg
3.5 总结

所以,综上所述,最终dyld加载流程,如下图所示,图中也诠释了前文中的问题:为什么是load-->Cxx-->main的调用顺序:

image.png
上一篇下一篇

猜你喜欢

热点阅读