应用程序的加载——dyld动态链接器的工作流程

2021/02/17 09:59
阅读数 89

每一个应用程序都会依赖很多底层库、第三方库、自己的组件库、模块库等,这些库本质上是可执行的二进制文件,而这些二进制文件可以被操作系统写入到内存的,我们日常所说的加载库就是指的将库写入到内存中。


库分为静态库(.a、.lib)和动态库(framework),它们都可以被加载到内存中,那么它们在加载过程中有什么区别呢?


首先来简单了解一下从源代码到可执行文件之间经历的过程:


静态库(.a、.lib)可以看成是一堆对象文件的归档,在链接阶段,静态链接器(static linker)会从库中收集这些对象文件,并把它们与汇编生成的目标文件一起打包拷贝到一个单独的二进制可执行文件中,这意味着应用程序的可执行文件大小会随着静态库的数目增加而增长,另外,如果需要在多个进程中使用这个静态库,那么就会有多份冗余的拷贝,如下图:


动态库在链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存,并且还可以减少App打包之后的包大小,如下图:

实际上,苹果是禁止我们开发人员在开发过程中使用自定义的动态库的,不然的话我们iOS的热修复也不会这么复杂,直接使用动态库就可以做到随时修改应用程序的内容了。

既然苹果不允许我们使用自定义的动态库,那么我们的项目中有动态库吗?答案是有的,UIKit、Foundation、CoreFoundation、libdispatch、libobjc等系统库都是动态库


现在我们知道了,动态库是在程序启动的时候被加载到内存中的,那么它是怎么被加载到内存中的呢?答案就是通过系统的动态链接器dyld


什么是dyld


dyld是英文 the dynamic link editor 的简写,翻译过来就是动态链接器,是苹果操作系统的一个重要组成部分。在iOS/Mac操作系统当中,只有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以Mach-O镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接使用,在启动的时候还必须要通过这些引用进行内容的填补,这个填补工作就是通过动态链接器dyld来完成的,这也就是所谓的符号绑定


动态链接器dyld在系统中会以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个LC_LOAD_DYLINKER的加载命令,此加载命令指定了dyld的路径,默认是/usr/lib/dyld。系统内核在加载Mach-O文件时,都需要使用dyld(位于/usr/lib/dyld)程序进行链接。


dyld在加载的时候,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被dyld映射到内存中,之后,当任何的Mach-O映像加载时,dyld首先会检查该Mach-O映像所需的动态库是否在共享缓存中,如果存在,则直接将它在共享缓存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的时候,这种做法能够大大改善应用程序的启动性能。


dyld是开源的,我们可以到如下地址下载其源码:

https://opensource.apple.com/tarballs/dyld/


dyld的加载流程


<1>找程序入口


我们随便新建一个工程,然后随便在一个类中复写方法,并且打好断点:

然后运行,断点信息如下:

经过分析断点处的堆栈信息,我们可以知道,动态链接器dyld的入口函数极有可能是_dyld_start函数。


另外说一点,我们也可以通过lldb指令来查看堆栈信息,如下:


你可也会有这么一个疑问,为什么要在+load方法里面打断点。如果你已经是一个具有三五年经验的程序员,相比有一点你肯定已经非常清楚了:+load方法会在应用程序的main函数之前调用。而应用程序的main函数是应用程序的入口函数,也就是说,+load方法会在应用程序执行之前被调用,而应用程序执行之前的最后一步就是动态链接,因此我在+load方法里面打的断点实际上就是定位到了动态链接的阶段,然后通过查看调用堆栈就能找到动态链接的入口函数了。


现在我们已经找到dyld的入口函数了,接下来就打开下载的dyld源码,然后全局搜索【_dyld_start】,根据搜索结果我们发现,dyld源码中按照不同系统架构(比如 i386、x86_64、arm64、arm)对_dyld_start分别做了不同的实现处理 。


接下来我们就来看一下真机arm64架构下的定义:


红框内,我们看到了bl字段,说明这是一个跳转,然后根据注释我们也明白了,这里是调用dyldbootstrap::start函数。其实在我们LLDB 调试堆栈的时候已经看到  _dyld_start 函数之后走的是 dyldbootstrap::start 函数。

call 就是调用函数的指令 , ( 同 bl ) . 这个函数也就是我们 app 开始的地方


<2> dyldbootstrap :: start


首先,我们在源码中搜索dyldbootstrap::start,结果如下:

结果并没有找到想要的内容。 


dyldbootstrap::start 就是指 dyldbootstrap 这个命名空间作用域里的 start 函数


因此,我们就想,dyldbootstrap作用域里面的start方法应该是C++方法,因此我们就搜下面的字段【 start(】(空格 + start + 小括号),果不其然,搜到了:


还是借用 LLDB 调试堆栈结果,我们在上面源码的最后一行,找到了 dyldbootstrap::start 函数之后走的是 dyld::_main 函数:

return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);


<3> dyld::_main

_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,         int argc, const char* argv[], const char* envp[], const char* apple[],         uintptr_t* startGlue){
......
reloadAllImages:#endif

#if TARGET_OS_OSX gLinkContext.strictMachORequired = false; // <rdar://problem/22805519> be less strict about old macOS mach-o binaries ((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) { if ( (platform == dyld3::Platform::macOS) && (sdk >= DYLD_PACKED_VERSION(10,15,0)) ) { gLinkContext.strictMachORequired = true; } }); if ( gLinkContext.iOSonMac ) gLinkContext.strictMachORequired = true; #else // simulators, iOS, tvOS, watchOS, are always strict gLinkContext.strictMachORequired = true; #endif

CRSetCrashLogMessage(sLoadingCrashMessage); // instantiate ImageLoader for main executable sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); gLinkContext.mainExecutable = sMainExecutable; gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
......
}

然后我们找到了reloadAllImages:

在其下面会调用函数instantiateFromLoadedImage


<4> instantiateFromLoadedImage
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";}


通过函数注释我们了解到这个函数的主要目的:dyld 获得控制权之前,内核会映射到可执行文件,这一步正是创建了可执行文件的映射 ImageLoader, 返回给我们的主程序 sMainExecutable , 加在了我们的镜像 image 里面。



这个函数内容比较少,只有一个函数跳转,接着我们跳转到 instantiateMainExecutable 函数。


<5> instantiateMainExecutable


// create image for main executableImageLoader* 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(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);  // instantiate concrete class based on content of 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}


这里几个参数稍微说明下 :

  • compressed -> 根据 LC_DYLD_INFO_ONLY 来决定 。

  • segCount 段命令数量 , 最大不能超过 255 个。

  • libCount 依赖库数量 , LC_LOAD_DYLIB (Foundation / UIKit ..) , 最大不能超过 4095 个。

  • codeSigCmd , 应用签名 。

  • encryptCmd , 应用加密信息 , ( 我们俗称的应用加壳 , 我们非越狱环境重签名都是需要砸过壳的应用才能调试) 。


instantiateMainExecutable 里 , 真正实例化主程序是用 sniffLoadCommands 这个函数去做的。


<6> sniffLoadCommands


还是 ImageLoaderMachO 这个作用域里的 sniffLoadCommands 函数。我们稍微看一下:

// determine if this mach-o file has classic or compressed LINKEDIT and number of segments it hasvoid ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,                      unsigned int* segCount, unsigned int* libCount, const LinkContext& context,                      const linkedit_data_command** codeSigCmd,                      const encryption_info_command** encryptCmd){......}


每一个应用程序,它使用到的每一个库、主程序中的每一行代码,都会以Moch-O镜像文件的形式存储在LoadCommands里面的。


这里sniffLoadCommands函数的作用就是,将所有的Mach-O镜像文件加载到LoadCommands中,如下:


经过以上步骤 , 主程序的实例化就已经完成了 。


<7> 重回 dyld::_main


然后我们再回到 dyld::_main 函数分析一下主程序:

_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,        int argc, const char* argv[], const char* envp[], const char* apple[],        uintptr_t* startGlue){    ...省略一些无关紧要的代码...
// 1,配置环境变量 configureProcessRestrictions(mainExecutableMH, envp);
...省略一些无关紧要的代码...
// 2,加载共享缓存 // load shared cache checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {#if TARGET_OS_SIMULATOR if ( sSharedCacheOverrideDir) mapSharedCache(mainExecutableSlide); // 共享缓存#else mapSharedCache(mainExecutableSlide); // 共享缓存#endif }
...省略一些无关紧要的代码...
try { // add dyld itself to UUID list addDyldImageToUUIDList();
#if SUPPORT_ACCELERATE_TABLES#if __arm64e__ // Disable accelerator tables when we have threaded rebase/bind, which is arm64e executables only for now. if ((sMainExecutableMachHeader->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E) sDisableAcceleratorTables = true;#endif bool mainExcutableAlreadyRebased = false; if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && !dylibsCanOverrideCache() && !sDisableAcceleratorTables && (sSharedCacheLoadInfo.loadAddress->header.accelerateInfoAddr != 0) ) { struct stat statBuf; if ( dyld3::stat(IPHONE_DYLD_SHARED_CACHE_DIR "no-dyld2-accelerator-tables", &statBuf) != 0 ) sAllCacheImagesProxy = ImageLoaderMegaDylib::makeImageLoaderMegaDylib(&sSharedCacheLoadInfo.loadAddress->header, sSharedCacheLoadInfo.slide, mainExecutableMH, gLinkContext); }
reloadAllImages:#endif

#if TARGET_OS_OSX gLinkContext.strictMachORequired = false; // <rdar://problem/22805519> be less strict about old macOS mach-o binaries ((dyld3::MachOFile*)mainExecutableMH)->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t minOS, uint32_t sdk) { if ( (platform == dyld3::Platform::macOS) && (sdk >= DYLD_PACKED_VERSION(10,15,0)) ) { gLinkContext.strictMachORequired = true; } }); if ( gLinkContext.iOSonMac ) gLinkContext.strictMachORequired = true; #else // simulators, iOS, tvOS, watchOS, are always strict gLinkContext.strictMachORequired = true; #endif

CRSetCrashLogMessage(sLoadingCrashMessage); // 3,加载所有需要的Mach-O镜像文件 // instantiate ImageLoader for main executable sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); gLinkContext.mainExecutable = sMainExecutable; gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
...省略一些无关紧要的代码...
// 4,加载所有插入的动态库 // load any inserted libraries if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) { for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) loadInsertedDylib(*lib); } // record count of inserted libraries so that a flat search will look at // inserted libraries, then main, then others. sInsertedDylibCount = sAllImages.size()-1;
// 5,链接主程序 // link main executable gLinkContext.linkingMainExecutable = true;#if SUPPORT_ACCELERATE_TABLES if ( mainExcutableAlreadyRebased ) { // previous link() on main executable has already adjusted its internal pointers for ASLR // work around that by rebasing by inverse amount sMainExecutable->rebase(gLinkContext, -mainExecutableSlide); }#endif link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1); sMainExecutable->setNeverUnloadRecursive(); if ( sMainExecutable->forceFlat() ) { gLinkContext.bindFlat = true; gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding; }
// 6,链接插入的库(即动态库) // link any inserted libraries // do this after linking main executable so that any dylibs pulled in by inserted // dylibs (e.g. libSystem) will not be in front of dylibs the program uses if ( sInsertedDylibCount > 0 ) { for(unsigned int i=0; i < sInsertedDylibCount; ++i) { ImageLoader* image = sAllImages[i+1]; link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1); image->setNeverUnloadRecursive(); } if ( gLinkContext.allowInterposing ) { // only INSERTED libraries can interpose // register interposing info after all inserted libraries are bound so chaining works for(unsigned int i=0; i < sInsertedDylibCount; ++i) { ImageLoader* image = sAllImages[i+1]; image->registerInterposing(gLinkContext); } } }
if ( gLinkContext.allowInterposing ) { // <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) { ImageLoader* image = sAllImages[i]; if ( image->inSharedCache() ) continue; image->registerInterposing(gLinkContext); } } ...省略一些无关紧要的代码... // 7,主程序初始化 #if SUPPORT_OLD_CRT_INITIALIZATION // Old way is to run initializers via a callback from crt1.o if ( ! gRunInitializersOldWay ) initializeMainExecutable(); #else // run all initializers initializeMainExecutable(); #endif
// notify any montoring proccesses that this process is about to enter main() notifyMonitoringDyldMain(); if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) { dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2); } ARIADNEDBG_CODE(220, 1);
...省略一些无关紧要的代码...
// 8,调用应用程序的main函数 if (sSkipMain) { notifyMonitoringDyldMain(); if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) { dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2); } ARIADNEDBG_CODE(220, 1); result = (uintptr_t)&fake_main; *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit; }
return result;}


1.配置环境变量

configureProcessRestrictions(mainExecutableMH, envp);


2.加载共享缓存

    // load shared cache    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {#if TARGET_OS_SIMULATOR        if ( sSharedCacheOverrideDir)            mapSharedCache(mainExecutableSlide); // 共享缓存#else        mapSharedCache(mainExecutableSlide); // 共享缓存#endif    }


3.加载所有需要的Mach-O镜像文件


4.加载所有插入的动态库

// load any inserted librariesif    ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {    for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)        loadInsertedDylib(*lib);}


5.链接主程序

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);


6.链接插入的库(动态库)

// link any inserted libraries// do this after linking main executable so that any dylibs pulled in by inserted// dylibs (e.g. libSystem) will not be in front of dylibs the program usesif ( sInsertedDylibCount > 0 ) {    for(unsigned int i=0; i < sInsertedDylibCount; ++i) {        ImageLoader* image = sAllImages[i+1];        link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);        image->setNeverUnloadRecursive();    }    if ( gLinkContext.allowInterposing ) {        // only INSERTED libraries can interpose        // register interposing info after all inserted libraries are bound so chaining works        for(unsigned int i=0; i < sInsertedDylibCount; ++i) {            ImageLoader* image = sAllImages[i+1];            image->registerInterposing(gLinkContext);        }    }}


7.主程序初始化

 #if SUPPORT_OLD_CRT_INITIALIZATION        // Old way is to run initializers via a callback from crt1.o        if ( ! gRunInitializersOldWay )            initializeMainExecutable();    #else        // run all initializers        initializeMainExecutable();    #endif


8.调用应用程序的main函数

// notify any montoring proccesses that this process is about to enter main()notifyMonitoringDyldMain();


<8> 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(); 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 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函数。


<9> runInitializers


void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo){  uint64_t t1 = mach_absolute_time();  mach_port_t thisThread = mach_thread_self();  ImageLoader::UninitedUpwards up;  up.count = 1;  up.imagesAndPaths[0] = { this, this->getPath() };  processInitializers(context, thisThread, timingInfo, up);  context.notifyBatch(dyld_image_state_initialized, false);  mach_port_deallocate(mach_task_self(), thisThread);  uint64_t t2 = mach_absolute_time();  fgTotalInitTime += (t2 - t1);}


这里面会调用到processInitializers函数。


<10> 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.  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);}


这里每一个递归初始化的时候都会使用到recursiveInitialization函数


<11> recursiveInitialization



这里面会调用到notifySingle函数。


<12> notifySingle


static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo){  //dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());  std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);  if ( handlers != NULL ) {    dyld_image_info info;    info.imageLoadAddress  = image->machHeader();    info.imageFilePath    = image->getRealPath();    info.imageFileModDate  = image->lastModified();    for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {      const char* result = (*it)(state, 1, &info);      if ( (result != NULL) && (state == dyld_image_state_mapped) ) {        //fprintf(stderr, "  image rejected by handler=%p\n", *it);        // make copy of thrown string so that later catch clauses can free it        const char* str = strdup(result);        throw str;      }    }  }  if ( state == dyld_image_state_mapped ) {    // <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache    // <rdar://problem/50432671> Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches    if (!image->inSharedCache()      || (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {      dyld_uuid_info info;      if ( image->getUUID(info.imageUUID) ) {        info.imageLoadAddress = image->machHeader();        addNonSharedCacheImageUUID(info);      }    }  }  if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {    uint64_t t0 = mach_absolute_time();    dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());    uint64_t t1 = mach_absolute_time();    uint64_t t2 = mach_absolute_time();    uint64_t timeInObjC = t1-t0;    uint64_t emptyTime = (t2-t1)*100;    if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {      timingInfo->addTime(image->getShortName(), timeInObjC);    }  }    // mach message csdlc about dynamically unloaded images  if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {    notifyKernel(*image, false);    const struct mach_header* loadAddress[] = { image->machHeader() };    const char* loadPath[] = { image->getPath() };    notifyMonitoringDyld(true, 1, loadAddress, loadPath);  }}


现在我们想一下,在C++中如何做到通知呢?答案就是通过函数指针的回调


在上面的第35行,就是调用一个通知,方式就是通过函数指针的形式。接下来我们就看一下sNotifyObjCInit这个函数指针是在哪里定义的。


<13> registerObjCNotifiers


void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped){  // record functions to call  sNotifyObjCMapped  = mapped;  sNotifyObjCInit    = init;  sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far try { notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true); } catch (const char* msg) { // ignore request to abort during registration }
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem) for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) { ImageLoader* image = *it; if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) { dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0); (*sNotifyObjCInit)(image->getRealPath(), image->machHeader()); } }}


经过全局搜索,我们找到了sNotifyObjCInit函数指针定义的地方,它就是registerObjCNotifiers函数,接下来看一下该函数是在哪里调用的呢?经过搜索,我发现它是在_dyld_objc_notify_register中调用的。


<14> _dyld_objc_notify_register


void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,                                _dyld_objc_notify_init      init,                                _dyld_objc_notify_unmapped  unmapped){  dyld::registerObjCNotifiers(mapped, init, unmapped);}


接着我们去 objc源码 中找一下 _dyld_objc_notify_register 这个函数,我们发现在 _objc_init 函数调用的时候,会调用 _dyld_objc_notify_register 函数:

void _objc_init(void){    static bool initialized = false;    if (initialized) return;    initialized = true;        // fixme defer initialization until an objc-using image is found?    environ_init();    tls_init();    static_init();    runtime_init();    exception_init();    cache_init();    _imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__ didCallDyldNotifyRegister = true;#endif}


来到这里 , 我们就看到了 _dyld_objc_notify_register 被调用 , 传递了三个参数

  • map_images : dyld 将 image 加载进内存时 , 会触发该函数。

  • load_images : dyld 初始化 image 会触发该方法。( 我们所熟知的 load 方法也是在此处调用 )

  • unmap_image : dyld 将 image 移除时 , 会触发该函数。


objclib只是一个库,它不能拿到应用程序的各种数据的,如果想要拿到,就得通过_dyld_objc_notify_register与dyld进行交互

也就是说 _objc_init 中注册并保存了 map_images , load_images , unmap_image 函数地址


基于上面的探讨,我们知道,_objc_init函数是势必要被调用的,不然map_images , load_images , unmap_image就都是空,那就没有意义了,那么_objc_init函数什么时候被调用呢?


走到这里我们还是没有找到堆栈的闭环。那么我们回到<11> recursiveInitialization 当中的这行代码进行查看 doInitialization 方法:

// initialize this imagebool hasInitializers = this->doInitialization(context);

<15> doInitialization


bool ImageLoaderMachO::doInitialization(const LinkContext& context){  CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers doImageInit(context); doModInitFunctions(context); CRSetCrashLogMessage2(NULL); return (fHasDashInit || fHasInitializers);}


这里面调用了doImageInit函数。


<16> doImageInit


void ImageLoaderMachO::doImageInit(const LinkContext& context){  if ( fHasDashInit ) {    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_ROUTINES_COMMAND:          Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);#if __has_feature(ptrauth_calls)          func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);#endif          // <rdar://problem/8543820&9228031> verify initializers are in image          if ( ! this->containsAddress(stripPointer((void*)func)) ) {            dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());          }          if ( ! dyld::gProcessInfo->libSystemInitialized ) {            // <rdar://problem/17973316> libSystem initializer must run first            dyld::throwf("-init function in image (%s) that does not link with libSystem.dylib\n", this->getPath());          }          if ( context.verboseInit )            dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());          {            dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);            func(context.argc, context.argv, context.envp, context.apple, &context.programVars);          }          break;      }      cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);    }  }}


简单分析一下这个函数,其实就是将函数方法的指针进行平移,最终找到其初始化的实现。


我们通过 // <rdar://problem/17973316> libSystem initializer must run first 这里可以看到,这个函数必须要首先被执行 , 进行 libsystem 的初始化。


doInitialization函数中会调用doModInitFunctions;

doModInitFunctions中会调用libSystemInitialized进行Libsystem库 的初始化 ,libsystem是系统库。

libSystemInitialized会调用起 libdispatch_init 来进行libdispatch库的初始化,libdispatch库是多线程相关的库。

libdispatch 的 init 会调用 _os_object_init , _os_object_init 这个函数里面调用了 _objc_init来进行objc库的初始化


<17> _objc_init


前面我们说过, _objc_init 中注册并保存了 map_images , load_images , unmap_image这三个函数地址,到这里终于 找到了 load images 调用的地方LLDB 调用堆栈的结果终于形成闭环了。

其实根据注释提醒 libSystem initializer must run first(系统库必须先初始化) ,系统库也是以镜像的形式被 dyld 加载,所以 load images 也可以形成闭环的。


总结


今天我们主要讲了dyld。

dyld是动态链接器,用于链接动态库。

那么为什么需要链接动态库呢?动态库与静态库有什么区别呢?这些问题上面都有答案。

dyld是如果调用到objc库的初始化的?主要是通过递归初始化,通过系统库、Dispatch库,最后走到objc库的初始化。

还有一个问题需要考虑,libobjc里面需要镜像文件image,而镜像文件image只有在dyld里面有,这里就涉及到的dyld和libobjc库的通讯,这个通讯是怎么做的?答案是通过函数指针的回调来实现通知的功能。


以上。

本文分享自微信公众号 - iOS小生活(iOSHappyLife)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部
返回顶部
顶部