美文网首页iOS 面试
iOS:动态库的加载顺序

iOS:动态库的加载顺序

作者: 笑出zhu声 | 来源:发表于2020-07-09 00:28 被阅读0次

在写 《iOS:load方法能不能被hook?》《iOS启动优化:App启动耗时在线监控与AppDelegate管控》 两篇文章时都提到了动态库的加载,由于主题的原因,没有详细介绍,有同学对这个比较感兴趣,今天我们就来研究下在iOS中动态库的加载顺序是什么样子的。

1.实验篇

我们先通过demo看下几种Case:

  • 没有依赖关系:

    情形一
    我们制作dylibAdylibBdylibB这三个动态库(不了解动态库的制作的请问度娘...),且它们没有依赖关系,同时我们在每个库中添加一个Class,暂且以Class的load方法的调用顺序当做是动态库的加载顺序(先挖一个坑),比如dylibA
    @implementation ClassA
    + (void)load {
        NSLog(@"dylibA loaded");
    }
    @end
    

    将这三个库加到demo工程中,并且保证Build Phases-Link Binary With Libraries中的顺序:dylibA > dylibB > dylibC,运行结果:

    Demo[53199:17384949] dylibA loaded
    Demo[53199:17384949] dylibB loaded
    Demo[53199:17384949] dylibC loaded
    

    我们调整下Link Binary With Libraries中的顺序:dylibC > dylibB > dylibA,运行结果:

    Demo[53265:17397552] dylibC loaded
    Demo[53265:17397552] dylibB loaded
    Demo[53265:17397552] dylibA loaded
    

    通过实验我们知道:在没有依赖关系的情况下,动态库的加载顺序由Link Binary With Libraries中的顺序决定,当然我们可以通过Link Binary With Libraries来控制动态库的加载顺序。

  • 单一依赖关系

    情形二
    dylibA依赖dylibBdylibB依赖dylibC,我们简单改造下这三个库,如在dylibB中import下dylibC的头文件,dylibA中同理:
    #import "ClassB.h"
    #import <dylibC/dylibC.h>
    
    @implementation ClassB
    + (void)load {
        NSLog(@"dylibB loaded");
    }
    @end
    

    Link Binary With Libraries中的顺序:dylibA > dylibB > dylibC,运行结果:

    Demo[53570:17450857] dylibC loaded
    Demo[53570:17450857] dylibB loaded
    Demo[53570:17450857] dylibA loaded
    

    这次我们发现三个库的加载顺序是反的,我们修改下顺序:dylibC > dylibA > dylibB,运行结果不变。由实验结果可知:动态库的加载顺序还受依赖关系影响,被依赖的子节点优先加载。

  • 组合依赖关系


    组合关系1

    其中,dylibAdylibBdylibB 没有依赖关系,dylibA 依赖了dylibDdylibEdylibF

    Demo[97898:19286936] dylibD loaded
    Demo[97898:19286936] dylibF loaded
    Demo[97898:19286936] dylibE loaded
    Demo[97898:19286936] dylibA loaded
    Demo[97898:19286936] dylibB loaded
    Demo[97898:19286936] dylibC loaded
    

    通过修改dylibA-Link Binary With LibrariesdylibDdylibE的顺序调整为:

    组合关系2
    Demo[97982:19305235] dylibF loaded
    Demo[97982:19305235] dylibE loaded
    Demo[97982:19305235] dylibD loaded
    Demo[97982:19305235] dylibA loaded
    Demo[97982:19305235] dylibB loaded
    Demo[97982:19305235] dylibC loaded
    

    通过上面的尝试,我们可以看出动态库的加载顺序为:先根据配置的链接顺序加载,如有依赖的先递归式加载依赖。

2.源码篇

上面我们通过几个实验对动态库的加载有个大概的影响,下面我们通过源码进一步了解动态库的加载(dyld源码,本文使用版本:dyld-635.2,篇幅问题,只展示部分代码)。
为了便于的过程,我们在上面demo中的位于子节点的dylibF库的load方法添加一个符号断点,获取下这个load方法的调用栈:

  #0    +[ClassF load]
  #1    load_images ()
  #2    dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) ()
  #3    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #4    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #5    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #6    ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #7    ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) ()
  #8    ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) ()
  #9    dyld::initializeMainExecutable() 
  #10   dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) ()
  #11   dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) ()
  #12   _dyld_start ()

_dyld_startdyldbootstrap::start使用汇编实现,我们重点看下dyld::_main开始的实现。

  • dyld::_main,动态链接器的入口函数,代码较多,只展示部分代码:

    //
    // Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
    // sets up some registers and call this function.
    //
    // Returns address of main() in target program which __dyld_start jumps to
    //
    uintptr_t
    _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
            int argc, const char* argv[], const char* envp[], const char* apple[], 
            uintptr_t* startGlue)  {
        //1.配置环境
        ...很多代码
        //2.加载共享缓存
        checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
        //3.实例化主程序
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
        gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
        //4.插入动态库(使用动态库注入代码就是通过这个实现的)
        if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
              for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                  loadInsertedDylib(*lib);
          }
        //5.链接主程序
        gLinkContext.linkingMainExecutable = true;
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        //6.链接插入的动态库
        ...很多代码
        //7.初始化主程序
        initializeMainExecutable(); 
        //...快结束了 
    }
    

    和我们今天主题相关的为link()initializeMainExecutable()两个过程,分别负责链接(符号rebase,binding)和初始化(调用load__attribute__((constructor)) 修饰的c函数等)的工作。

  • link()

    void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const 
      ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
    {
      // add to list of known images.  This did not happen at creation time for bundles
      if ( image->isBundle() && !image->isLinked() )
          addImage(image);
    
      // we detect root images as those not linked in yet 
      if ( !image->isLinked() )
          addRootImage(image);
      
      // process images
      try {
          const char* path = image->getPath();
    #if SUPPORT_ACCELERATE_TABLES
          if ( image == sAllCacheImagesProxy )
              path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
    #endif
          image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
      }
      catch (const char* msg) {
          garbageCollectImages();
          throw;
      }
    }
    

    link()中又调用ImageLoader::link方法,ImageLoader是一个基类负责加载image文件(主程序,动态库)每个image对应一个ImageLoader子类的实例,我们继续看下它的实现:

  • ImageLoader::link

     void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
     {
      //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
      
      // clear error strings
      (*context.setErrorStrings)(0, NULL, NULL, NULL);
    
      uint64_t t0 = mach_absolute_time();
      this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
      context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
    
      // we only do the loading step for preflights
      if ( preflightOnly )
          return;
    
      uint64_t t1 = mach_absolute_time();
      context.clearAllDepths();
      this->recursiveUpdateDepth(context.imageCount());
    
      //省略符号rebase 和binding过程...
    }
    

    ImageLoader::link中使用ImageLoader::recursiveLoadLibraries加载动态库,截取部分代码:

  • ImageLoader::recursiveLoadLibraries

     void ImageLoader::recursiveLoadLibraries(const LinkContext& context, bool preflightOnly, const RPathChain& loaderRPaths, const char* loadPath)
    {
        //...省略很多代码
        // 获取当前image依赖的动态库
        DependentLibraryInfo libraryInfos[fLibraryCount]; 
        this->doGetDependentLibraries(libraryInfos);
        //...省略很多代码
    
        for(unsigned int i=0; i < fLibraryCount; ++i){ 
           //加载动态库
           DependentLibraryInfo& requiredLibInfo = libraryInfos[i];
           dependentLib = context.loadLibrary(requiredLibInfo.name, true, this->getPath(), &thisRPaths, enforceIOSMac, cacheIndex);
           //...省略很多代码
        }
        
        //保存加载完成的动态库
        setLibImage(i, dependentLib, depLibReExported, requiredLibInfo.upward);
    
       // 告诉此image依赖的动态库们递归的加载各自依赖的动态库
       for(unsigned int i=0; i < libraryCount(); ++i) {
          ImageLoader* dependentImage = libImage(i);
          if ( dependentImage != NULL ) { 
              dependentImage->recursiveLoadLibraries(context, preflightOnly, thisRPaths, libraryInfos[i].name);
          }
       }
       //...省略很多代码
    }
    

    此方法也比较长,它主要做的事情是:1)获取当前image依赖的的动态库;2)循环加载当前image依赖的动态库;3)告诉当前image依赖的动态库们递归的加载各自依赖的动态库;
    具体的加载方法在dyld.cppImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex),我们不多做介绍。从上面的代码可以知道,dyld通过doGetDependentLibraries获取了image依赖的动态库信息,然后循环加载,这个函数的实现会影响动态库的加载顺序,我们看下doGetDependentLibraries的实现(doGetDependentLibraries是个虚函数,只有在ImageLoaderMachO.cpp中找到了它的实现):

  • ImageLoaderMachO::doGetDependentLibraries

    void ImageLoaderMachO::doGetDependentLibraries(DependentLibraryInfo libs[])
    {
      if ( needsAddedLibSystemDepency(libraryCount(), (macho_header*)fMachOData) ) {
          DependentLibraryInfo* lib = &libs[0];
          lib->name = LIBSYSTEM_DYLIB_PATH;
          lib->info.checksum = 0;
          lib->info.minVersion = 0;
          lib->info.maxVersion = 0;
          lib->required = false;
          lib->reExported = false;
          lib->upward = false;
      }
      else {
          uint32_t index = 0;
          const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
          const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
          const struct load_command* cmd = cmds;
          for (uint32_t i = 0; i < cmd_count; ++i) {
              switch (cmd->cmd) {
                  case LC_LOAD_DYLIB:
                  case LC_LOAD_WEAK_DYLIB:
                  case LC_REEXPORT_DYLIB:
                  case LC_LOAD_UPWARD_DYLIB:
                  {
                      const struct dylib_command* dylib = (struct dylib_command*)cmd;
                      DependentLibraryInfo* lib = &libs[index++];
                      lib->name = (char*)cmd + dylib->dylib.name.offset;
                      //lib->name = strdup((char*)cmd + dylib->dylib.name.offset);
                      lib->info.checksum = dylib->dylib.timestamp;
                      lib->info.minVersion = dylib->dylib.compatibility_version;
                      lib->info.maxVersion = dylib->dylib.current_version;
                      lib->required = (cmd->cmd != LC_LOAD_WEAK_DYLIB);
                      lib->reExported = (cmd->cmd == LC_REEXPORT_DYLIB);
                      lib->upward = (cmd->cmd == LC_LOAD_UPWARD_DYLIB);
                  }
                  break;
              }
              cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
          }
      }
    }
    

    dyld通过读取当前Mach-O文件的Load Commonds段来获取到它依赖的动态库,涉及到的字段有LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIBLC_LOAD_UPWARD_DYLIB,遍历时通过&libs[]记录。而影响Load Commonds中动态库顺序的是Xcode中Link Binary With Libraries的顺序。下图是上文Demo工程的Mach-O中Load Commonds的示例截图:

    Load Commonds

好了,我们通过实验和源码的方式了解到了动态库的加载顺序:1)先从当前image文件(可执行文件/动态库)的Load Commonds中获取动态库的信息(包括顺序,名字,路径等);2)然后循环加载它依赖的动态库;3)加载完后,再根据当前image依赖的动态库列表递归加载各自依赖的动态库。

3.填坑篇

文章开始我们就挖了一个坑:实验的方式是建立在“load方法的调用顺序当做是动态库的加载顺序”这一假设上的,那么这一假设成立吗?
从上文中+[ClassF load]的调用栈可知:load 方法的调用是在initializeMainExecutable()中,在link()之后,initializeMainExecutable()后又调用ImageLoader::runInitializers()ImageLoader::processInitializers(),我们看下代码:

  • ImageLoader::processInitializers中调用了ImageLoader::recursiveInitialization,从名字看进行了递归方式的初始化。
    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.
      for (uintptr_t i=0; i < images.count; ++i) {
          images.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
      }
      // If any upward dependencies remain, init them.
      if ( ups.count > 0 )
          processInitializers(context, thisThread, timingInfo, ups);
     }
    
  • ImageLoader::recursiveInitialization,截取部分代码:
    void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                            InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
    {
      
      //优先初始化fDepth深的库
      for(unsigned int i=0; i < libraryCount(); ++i) {
          ImageLoader* dependentImage = libImage(i);
          if ( dependentImage != NULL ) {
              // don't try to initialize stuff "above" me yet
              if ( libIsUpward(i) ) {
                  uninitUps.images[uninitUps.count] = dependentImage;
                  uninitUps.count++;
              }
              else if ( dependentImage->fDepth >= fDepth ) {
                  dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
              }
          }
      }
    
    
      // 告诉objc我们准备初始化image了
      uint64_t t1 = mach_absolute_time();
      fState = dyld_image_state_dependents_initialized;
      oldState = fState;
      context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    
      // 初始化image
      bool hasInitializers = this->doInitialization(context);
    
      // 告诉objc初始化完成
      fState = dyld_image_state_initialized;
      oldState = fState;
      context.notifySingle(dyld_image_state_initialized, this, NULL);
    
     //...
    }
    
    从上面的代码可以看出,初始化的动态库是从libImage()中获取,而libImage()的数据是在加载动态库的ImageLoader::recursiveLoadLibraries中的setLibImage()进行保存的。通过比较fDepth,优先初始化fDepth大的库,我们再看下fDepth的赋值:
  • ImageLoader::recursiveUpdateDepth
    unsigned int ImageLoader::recursiveUpdateDepth(unsigned int maxDepth)
    {
      // the purpose of this phase is to make the images sortable such that 
      // in a sort list of images, every image that an image depends on
      // occurs in the list before it.
      if ( fDepth == 0 ) {
          // break cycles
          fDepth = maxDepth;
          
          // get depth of dependents
          unsigned int minDependentDepth = maxDepth;
          for(unsigned int i=0; i < libraryCount(); ++i) {
              ImageLoader* dependentImage = libImage(i);
              if ( (dependentImage != NULL) && !libIsUpward(i) ) {
                  unsigned int d = dependentImage->recursiveUpdateDepth(maxDepth);
                  if ( d < minDependentDepth )
                      minDependentDepth = d;
              }
          }
      
          // make me less deep then all my dependents
          fDepth = minDependentDepth - 1;
      }
      
      return fDepth;
    }
    
    在整个依赖树的关系中,层级越深,fDepth的值越大,ImageLoader::recursiveUpdateDepth的调用在ImageLoader::recursiveLoadLibraries之后,通过上面的分析过程我们知道:在加载动态库时将动态库的信息通过setLibImage()保存,加载完成后dyld根据依赖关系递归地给各个库都计算了依赖深度:fDepth,在初始化的时候递归地根据fDepth进行初始化,每初始化一个image都会使用 context.notifySingle()通知Objc通过load_images()调用load方法。从分析结果看:不同库中的load方法的调用顺序可以当做是动态库的加载顺序。(dyld过程比较复杂,本人水平有限,如有错误敬请指出~)
dyld_main.png

相关文章

  • iOS:动态库的加载顺序

    在写 《iOS:load方法能不能被hook?》 和 《iOS启动优化:App启动耗时在线监控与AppDele...

  • ptrance 反调试--2之 动态库加载顺序

    1、动态库加载顺序 1、AntDebug、inject 顺序执行 1、inject、AntDebug 顺序执行

  • iOS---10--- _read_images 浅析 类的加载

    [toc] 前言 在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库...

  • 10--- _read_images 浅析 类的加载

    [toc] 前言 在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库...

  • iOS代码层获取电量

    1.加载动态链接库 iOS是给予Linux内核,在Linux调用如下函数来加载动态链接库:dlopen,dlsym...

  • DYLD 摘录

    原文: iOS中dyld缓存的实现原理是怎样的? 在iOS系统中,几乎所有的程序都会用到动态库,而动态库在加载的时...

  • iOS App启动优化:动态库手动加载

    一、前言 在介绍动态库手动加载方式之前,我们简单了解下动态库,又名共享库在iOS中是个特殊的存在,除了系统库以外,...

  • Cydia的基石:MobileSubstrate

    在MAC与IOS平台上,动态库的后缀一般是dylid,而加载这些动态库的程序叫做dynamic linker(dy...

  • 通过dlopen使用动态库

    动态库制作dlopen 动态加载Frameworks使用dlopen和dlsym方法动态加载库和调用函数动态库使用...

  • 静态库(.a)和动态库(dylib)

    区别 1、加载 静态库:静态加载 动态库:动态加载 2、初始化 动态库可以在加载时进行初始化,并在客户端应用程序正...

网友评论

    本文标题:iOS:动态库的加载顺序

    本文链接:https://www.haomeiwen.com/subject/kjhfahtx.html