第十三节—dyld加载流程

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

想探索dyld的加载流程,还是需要一些比较常识性的东西,我们就从这个东西开始说。

一、关于库

平常我们经常会挂在嘴边的CoreFoundationUIKitOpenGL等等,这些都是我们开发的过程中可能需要依赖的库。

我们会把这些必用或者常用的内容封装成一个库,准备在某个位置,有需要的时候就会直接调取,这样更加的方便。

那么,

1. 库是什么?

库是一份可执行的代码的二进制。它们可以被操作系统载入到内存,并且被识别。

2. 库的常见分类

  • 静态库
    比如.a.lib
  • 动态库(共享库)
    比如.framework.so(安卓的应该很熟吧)、.dll

3. 动态库和静态库的区别

想了解他们的区别,就必须知道一个源文件变成可执行文件的过程。这个其实大家都是知道的,而且在之前的章节说运行时的时候也说过了。

这里再啰嗦一次编译流程 :

源文件--->预编译--->编译--->汇编--->链接--->可执行文件

那么库文件一般都是在链接的时候开始介入和源文件发生一些事情。

来看,

图1.png

图1中是一个项目的两个代码段,1和2分处不同的模块,但是同时都需要库文件B和库文件D

如果是静态库的话,BD就必须在那个位置坐好,而且就需要为1和2每个段都编译一次库文件。也就是说使用静态库的时候,会将静态库的信息直接编译到可执行文件中

再看,

图2.png

图2中也是一个项目的两个代码段,1和2也分处不同的模块,也都需要库文件BD

这时候BD如果是静态库的话,就会根据实际的需求来进行插入,需要的时候就添加,不需要的时候不会参与。也就是说加载动态库时,操作系统会先检查动态库是否因为其它程序已经将这个动态库信息加载到了内存中。不需要多次的加载库文件。

特点总结 :

  • 静态库 :
    • 信息直接编译到可执行文件中
    • 优点 : 静态库被删除,对可执行文件不会造成影响。
    • 缺点 : 浪费内存空间,如果静态库需要修改,可执行文件需要重新编译。
  • 动态库 :
    • 优点 :
      • 动态库只被夹在到内存中一次,不会多次加载。共享内存,节约资源。
      • 编译程序并不会链接到目标代码中,而是在运行时才被载入,不需要每次修改库文件就要重新编译。
    • 缺点 : 因为运行时载入,所以运行时必然有性能损失,而且程序会依赖外部环境,一旦动态库修改出错,程序可能会发生问题。
    • 常见的 : UIKitlibdispatchlibobjc.dyld

二、思路和准备工作

1. 思路和概念

为了确定思路和探索顺序,就先来看app的启动流程图 :

app启动流程图.png

整个虚线框里面的就是我们要探索的dyld

那么首先说好概念问题,

什么叫dyld?

dyld

  • 英文全称 : the dynamic link editor
  • 中文名叫苹果动态链接器
  • 它是iOS中非常重要的部分。
  • app被打包成mach-o可执行文件后,dyld将对其进行链接(link)加载可执行文件(load)等后续操作。

2. 探索准备工作

准备条件 : dyld源码750.6,请自行下载。objc4-781。请看教程,里面有配置好的文件。libdispatchlibSystem都在这里,请自行搜索这两个字,下载喜欢的版本。

  1. 新建一个我们熟悉的Project--->iOS--->App后面我会把它叫Project,一说Project就是说这个东西。

  2. main.m中给main函数上断点,正常情况下,main函数是程序的入口吧,所以找它,然后执行。

我们看Debug Navigator信息,如下图2.1所示 :

图2.2.1.png
  • main函数前面还有一个start的存在,所以在main函数的前面,一定还有操作。
  • 那么看start函数,画红框的那里,发现start函数属于libdyld
  • 所以在进入main函数前,dyld就已经介入了。
  1. 还需要一个临界点。
    因为已经发现main函数不是最早执行的函数了,那么想要探索dyld,就必须找到一个相对更早的dyld的入口。于是在ViewController中实现它的一个更早的机制——load。可以证明一下,ViewControllerload是早于main函数调用的。

断点全部不要,在main函数里面随意NSLog内容,然后在ViewControllerload中随意打印内容。明显发现loadmain还早。

图2.2.2.png
  1. 实现load,并挂上断点,继续Run
图2.2.3.png
  1. lldb中输入bt,查看堆栈信息的调用。
图2.2.4.png

很明显,在Debug Nav和堆栈信息中,找到了同一个更早调用的_dyld_start

  1. 打开上面准备的dyld源码750.6,搜索__dyld_start

三、 dyld加载流程

上面已经找到了_dyld_start这个入口,那么就从_dyld_start开始流程探索。

主线 : dyld加载流程

1. dyldbootstrap::start

图2.3.1.png
  • 一定是arm64架构,并且看_dyld_start都做了什么,所以看bl跳转。找到dyldbootstrap::start。搜空格 + start(,因为这是一个函数,按照函数格式规范来搜。

  • return。进入dyld::_main

图2.3.2.png

2. dyld::_main的流程

就看一些有注释的,然后看主要流程。不用全都看得懂,主要是捋清楚这条线。按照经验来看,无论做什么都要先搞清楚环境,所以从环境入手,搜索env,找到如下图所示

  • 【第一步 : 环境变量配置】 : 根据环境变量进行对应的值的配置。获取当前的运行架构

图2.3.3.png
  • 【第二步 : 设置共享缓存】 : iOS中必须开启共享缓存,为动态库的使用做环境准备,并且共享缓存必须映射到共享区域,否则可能出现动态库无法使用的情况。
图2.3.4.png
  • 【第三步 : 尝试将dyld本身放入UUID列表】
图2.3.5.png
  • 【第四步 : 主可执行程序的实例化】 : instantiateFromLoadedImage会为主可执行程序生成镜像
图2.3.6.png
  • 【第五步 : 插入动态库】 : loadInsertedDylib加载所有插入的动态库
图2.3.7.png
  • 【第六步 : 链接主可执行程序】
图2.3.8.png
  • 【第七步 : 链接动态库】
图2.3.9.png
  • 【第八步 : 弱符号绑定】
图2.3.10.png
  • 【第九步 : 执行初始化方法】
图2.3.11.png
  • 【第十步 : 通知所有监听,该进程马上进入main()
图2.3.12.png
  • 【第十一步 : 寻找主可执行程序入口】

Load Commond读取LC_MAIN入口,如果没有就读取LC_UNIXTHREAD。最后进入的就是我们见到的main()函数

图2.3.13.png

四、关于主程序的初始化和执行初始化

上面是main()函数启动前的准备过程,这里挑重点的说,先说主程序的初始化。因为后面所有的步骤流程都是围绕着这个主程序来进行的。

图3.1.png

先来看一个串起了整条线的变量

sMainExecutable : 主程序。在所有的步骤线上,它都是中心点。

那么就找它的初始化方法instantiateFromLoadedImage进行探索。

1. instantiateFromLoadedImage主程序的初始化

//内核在dyld获得控制之前在,需要在主可执行文件中进行映射。
//我们需要为主可执行文件创建一个ImageLoader*
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    // try mach-o loader
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
        addImage(image);
        return (ImageLoaderMachO*)image;
    }
    
    throw "main executable not a known format";
}
  • 注释写了,主可执行文件并不是主程序,主程序是ImageLoader *的对象image,也即是镜像。
  • 这个主程序最后会被强转成ImageLoaderMachO类型的指针对象,也就是我们的mach-o可执行文件。

然后我们进入创建ImageLoader的创建方法instantiateMainExecutable

// create image for main executable
//为主可执行文件创建镜像
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
    //dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
    //  sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
    bool compressed;
    //段的数量
    unsigned int segCount;
    //库的数量
    unsigned int libCount;
    const linkedit_data_command* codeSigCmd;
    const encryption_info_command* encryptCmd;
    
    //sniffLoadCommands用于确定此mach-o文件是否有正常的或者压缩的LINKEDIT,以及它所拥有的段数
    sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
    // instantiate concrete class based on content of load commands
    //根据load commands的内容,实例化具体的类
    if ( compressed ) 
        return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
    else
#if SUPPORT_CLASSIC_MACHO
        return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
        throw "missing LC_DYLD_INFO load command";
#endif
}
  • 也就是说创建主镜像的重点就是sniffLoadCommands,它是获得mach-o可执行文件的代码,并且也可以获取load commods加载命令的信息。

说白了,就是镜像文件进到这里,主要就是被加载到load commonds里面,那么之后我们写的所有的代码都会被编译进来,也就是说我们写的东西都会被变成mach-o的形式。

比如,最上面我创建的那个Project打开它的可执行文件

图3.2.png

这里面的动态库都会被变成mach-o文件,都变成了这种数据段的形式存在于load commonds里面。

2. initializeMainExecutable执行初始化

void initializeMainExecutable()
{
    // record that we've reached this step
    //记录一下我们已经到达了主可执行程序的初始化
    gLinkContext.startedInitializingMainExecutable = true;

    // run initialzers for any inserted dylibs
    //对所有插入的动态库做初始化
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    
    //获取镜像根文件的数量
    const size_t rootCount = sImageRoots.size();
    
    //如果镜像根文件的数量比1多
    if ( rootCount > 1 ) {
        //循环执行初始化,这里的初始化和下面的主可执行文件及其所有关联文件的初始化函数是同一个
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }
    
    // run initializers for main executable and everything it brings up
    //运行主可执行文件和所有关联文件的初始化
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
    
    // register cxa_atexit() handler to run static terminators in all loaded images when this process exits
    //注册cxa_atexit()处理程序,在进程退出时在所有加载的镜像(image)中运行静态终止符
    if ( gLibSystemHelpers != NULL ) 
        (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

    // dump info if requested
    //如果需要转存信息
    if ( sEnv.DYLD_PRINT_STATISTICS )
        ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
    if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
        ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

还是看一下注释

  • 执行初始化的真正函数是runInitializers

runInitializers我就不贴出来了,因为我们就需要那一句processInitializers函数。

  • (1) processInitializers
//这里不用翻译这么多英文了,大概意思就是向上的链接全都放到向下的链接的处理后面
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
                                     InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
    uint32_t maxImageCount = context.imageCount()+2;
    ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
    ImageLoader::UninitedUpwards& ups = upsBuffer[0];
    ups.count = 0;
    // Calling recursive init on all images in images list, building a new list of
    // uninitialized upward dependencies.
    //在镜像列表中的所有镜像都利用递归进行init,建立一个没有进行初始化向上依赖关系的列表
    for (uintptr_t i=0; i < images.count; ++i) {
        images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
    }
    // If any upward dependencies remain, init them.
    //如果还存在向上的依赖关系,把它们全部初始化
    if ( ups.count > 0 )
        processInitializers(context, thisThread, timingInfo, ups);
}

就是对镜像列表中的镜像全部初始化,并且把依赖关系全部变成向下的。

  • (2) recursiveInitialization

代码太多,截图看,因为我们最主要的是让dyld知道了我需要用到你了。

看画红框的,一个通知notifySingle和一个初始化doInitialization

图3.3.png
  • (3) notifySingle

全局搜索notifySingle(

然后找到获取镜像文件的真实地址的那句代码

图3.4.png

一个函数指针,那么继续跟这个函数指针。看看这个指针是在哪里赋值的,

图3.5.png

注册Notify。再看谁注册了通知。接着搜索registerObjCNotifiers。找到了

图3.6.png

然后去objc4-781中搜索_dyld_objc_notify_register

图3.7.png

本身*sNotifyObjCInit就是一个函数回调,这就是数据在这dyldobjc中的传递。

本来dyld里面的sNotifyObjCInitinit,相当于是一个nil的,只有看objc_init什么时候调用_dyld_objc_notify_register,并给他这个load_imagessNotifyObjCInit才能执行起来。

所以下面接着看load_imagesobjc-781里面又干了什么,是不是真的调起了+(void)load

  • (3.1) load_images

下面都是跟着红框往里面进。

图3.8.png
图3.9.png
图3.10.png

这里就行了,因为看到了@selector(load)

然后我们对比一下xcode的堆栈信息。

图3.11.png

和上面的流程一模一样。

总结 :

+(void)load的源码链条 :
_dyld_start--->dyldbootstrap::start--->dyld::_main--->
dyld::initializeMainExecutable--->ImageLoader::runInitializers--->
ImageLoader::processInitializers--->ImageLoader::recursiveInitialization--->
dyld::notifySingle(函数指针,利用回调)--->
sNotifyObjCInit--->load_images(libobjc.A.dylib)

  • (4) doInitialization

现在知道它是执行初始化的真正步骤,那么看dyld的源码

图3.12.png

两种初始化方法,一个-init、一个静态构造器。分别进去看一下。

(4.1)doImageInit()

图3.13.png

两点 :

  • doImageInit()是通过-init做的初始化,并且是通过初始化函数的移动完成镜像的初始化。
  • libSystem必须是第一个进行初始化的库。
  • for循环加载方法的调用

(4.2)doModInitFunctions()

图3.14.png

你会发现和doImageInit差不多的内容,但是doModInitFunctions加载大多都是c++的文件。

做个验证 :

图3.15.png

这是最开始的Project,在main.m添加c++代码

__attribute__((constructor)) void jdFunc() {//挂上断点
    printf("%s",__func__);
}

上图3.15就是它的结果,画红框的就是doModInitFunctions

另外,可以下载libSystem的官方源码,搜索_initializer,会发现
(1). libSystem也是通过_dyld_initializer进行的初始化。然后就会进行libdispatch_init(void);,这一步libdispatch_init的初始化就通过了libdispatch.dyld的库。
(2). 其实还可以继续的,这里我就多说一下,因为的确图太多了,我就不全部贴了,感兴趣的可以再下载一份libdispatch的库,然后搜索libdispatch_init,会发现它的实际实现是通过_os_object_init,再进入_os_object_init,你就会发现_objc_init()

结论 :

库的初始化顺序 : dyld--->libSystem init--->libdispatch init--->objc init

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)

附:

一张dyld的加载流程图。

dyld加载流程.png

你可能感兴趣的:(第十三节—dyld加载流程)