谈谈dyld

今天我们坐下来聊聊dyld(the dynamic linker editor),dyld在整个流程中扮演了什么角色,做了哪些事情呢?dyld发生在main函数之前,将mach-o文件加载到内存,加载插入的动态库并链接,初始化,并对objc进行回调等。

那么这些流程具体是怎么样的,到底怎么实现的?

初始化处

在讨论该话题之前,我们还是要找到源头,有了源头我们才可顺藤摸瓜,摸清事情的来龙去脉。对main函数处进行断点,我们看到如下:

谈谈dyld_第1张图片
start

通过 dyld-97.1源码,我们找到了我们的关键先生:
谈谈dyld_第2张图片

也许是为了各平台的兼容,所以这边是用汇编代码来编写。

通过dyldbootstrap::start(app_mh, argc, argv, slide),我们找到

uintptr_t start(const struct mach_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
{
    // _mh_dylinker_header is magic symbol defined by static linker (ld), see 
    const struct macho_header* dyldsMachHeader =  (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);
    
    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }
    
    uintptr_t appsSlide = 0;
    
    // set pthread keys to dyld range
    __pthread_tsd_first = 1;
    _pthread_keys_init();
    
    // enable C++ exceptions to work inside dyld
    dyld_exceptions_init(dyldsMachHeader, slide);
    
    // allow dyld to use mach messaging
    mach_init();
    // 这边不展开了,有兴趣的可以查看具体源码
    ......
    // now that we are done bootstrapping dyld, call dyld's main
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
}

这部分主要是bootstrap dyld启动的一些初始化操作,具体的可以自行翻阅源码查看。该段源码中我们看到了本文的话题dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple)

分析各阶段

下面是dyld::_main的几个主要阶段,本文会对部分阶段进行阐述。

  1. 设置运行环境,环境变量
  2. 实例化Image:可执行文件
  3. 加载共享缓存
  4. 加载插入的动态库
  5. 链接可执行文件
  6. 链接动态库
  7. 初始化
uintptr_t
_main(const struct mach_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[])
{   
    setContext(mainExecutableMH, argc, argv, envp, apple);
    
    // 设置运行环境和环境变量
    sExecPath = apple[0];
    bool ignoreEnvironmentVariables = false;
#if __i386__
    if ( isRosetta() ) {
        // under Rosetta (x86 side)
        // When a 32-bit ppc program is run under emulation on an Intel processor,
        // we want any i386 dylibs (e.g. any used by Rosetta) to not load in the shared region
        // because the shared region is being used by ppc dylibs
        gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
        ignoreEnvironmentVariables = true;
    }
#endif
    if ( sExecPath[0] != '/' ) {
        // have relative path, use cwd to make absolute
        char cwdbuff[MAXPATHLEN];
        if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
            // maybe use static buffer to avoid calling malloc so early...
            char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
            strcpy(s, cwdbuff);
            strcat(s, "/");
            strcat(s, sExecPath);
            sExecPath = s;
        }
    }
    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;
    sMainExecutableIsSetuid = issetugid();
    if ( sMainExecutableIsSetuid )
        pruneEnvironmentVariables(envp, &apple);
    else
        checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
    if ( sEnv.DYLD_PRINT_OPTS ) 
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    getHostInfo();
    // install gdb notifier
    stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
    // make initial allocations large enough that it is unlikely to need to be re-alloced
    sAllImages.reserve(200);
    sImageRoots.reserve(16);
    sAddImageCallbacks.reserve(4);
    sRemoveImageCallbacks.reserve(4);
    sImageFilesNeedingTermination.reserve(16);
    sImageFilesNeedingDOFUnregistration.reserve(8);
    
    try {
        // 实例化image-可执行文件
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        sMainExecutable->setNeverUnload();
        gLinkContext.mainExecutable = sMainExecutable;
        // 加载共享缓存
        checkSharedRegionDisable();
    #if DYLD_SHARED_CACHE_SUPPORT
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
            mapSharedCache();
    #endif
        // 加载插入的动态库
        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;

        // 链接主程序
        gLinkContext.linkingMainExecutable = true;
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
        gLinkContext.linkingMainExecutable = false;
        if ( sMainExecutable->forceFlat() ) {
            gLinkContext.bindFlat = true;
            gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
        }
        result = (uintptr_t)sMainExecutable->getMain();

        // 链接动态库
        if ( sInsertedDylibCount > 0 ) {
            for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                ImageLoader* image = sAllImages[i+1];
                link(image, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
            }
        }
        
    #if SUPPORT_OLD_CRT_INITIALIZATION
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
    #endif
        // 初始化
        initializeMainExecutable(); // run all initializers
    }
    catch(const char* message) {
        halt(message);
    }
    catch(...) {
        dyld::log("dyld: launch failed\n");
    }

    return result;
}

实例化Image

sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

static ImageLoader* instantiateFromLoadedImage(const struct mach_header* mh, uintptr_t slide, const char* path)
{
    // try mach-o loader
    if ( isCompatibleMachO((const uint8_t*)mh) ) {
        ImageLoader* image = new ImageLoaderMachO(mh, slide, path, gLinkContext);
        addImage(image);
        return image;
    }
    
    throw "main executable not a known format";
}

对于dyld来说,可执行文件其实是个image,配合ImageLoader加载到内存中,再从可执行文件 image 递归加载所有符号。动态库也是如此,一个image对应一个ImageLoader。当然该image(镜像)不是我们常规以为的那个image(图片)

加载共享缓存

checkSharedRegionDisable();
    #if DYLD_SHARED_CACHE_SUPPORT
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
            mapSharedCache();
    #endif

当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些 framework 和 动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间

为了缩短这个处理过程所花费时间,在 OS X 和 iOS 上的动态链接器使用了共享缓存。当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查 共享缓存 看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。

link

link包含主程序link和动态库link。link整个过程就是将加载进来的二进制转化为可用的过程。
其中主要的两个组成部分rebase->binding
通过ImageLoader::link函数,我们看到如下:

void ImageLoader::recursiveRebase(const LinkContext& context)
{ 
    if ( fState < dyld_image_state_rebased ) {
        // break cycles
        fState = dyld_image_state_rebased;
        
        try {
            // rebase lower level libraries first
            for(unsigned int i=0; i < fLibrariesCount; ++i){
                DependentLibrary& libInfo = fLibraries[i];
                if ( libInfo.image != NULL )
                    libInfo.image->recursiveRebase(context);
            }
                
            // rebase this image
            doRebase(context);
            
            // notify
            context.notifySingle(dyld_image_state_rebased, this->machHeader(), fPath, fLastModified);
        }
        catch (const char* msg) {
            // this image is not rebased
            fState = dyld_image_state_dependents_mapped;
            throw;
        }
    }
}




void ImageLoader::recursiveBind(const LinkContext& context, bool forceLazysBound)
{
    // Normally just non-lazy pointers are bound immediately.
    // The exceptions are:
    //   1) DYLD_BIND_AT_LAUNCH will cause lazy pointers to be bound immediately
    //   2) some API's (e.g. RTLD_NOW) can cause lazy pointers to be bound immediately
    if ( fState < dyld_image_state_bound ) {
        // break cycles
        fState = dyld_image_state_bound;
    
        try {
            // bind lower level libraries first
            for(unsigned int i=0; i < fLibrariesCount; ++i){
                DependentLibrary& libInfo = fLibraries[i];
                if ( libInfo.image != NULL )
                    libInfo.image->recursiveBind(context, forceLazysBound);
            }
            // bind this image
            this->doBind(context, forceLazysBound); 
            this->doUpdateMappingPermissions(context);
            // mark if lazys are also bound
            if ( forceLazysBound || this->usablePrebinding(context) )
                fAllLazyPointersBound = true;
                
            context.notifySingle(dyld_image_state_bound, this->machHeader(), fPath, fLastModified);
        }
        catch (const char* msg) {
            // restore state
            fState = dyld_image_state_rebased;
            throw;
        }
    }
}

为何rebase
原因:doRebase(const LinkContext& context) = 0 do any fix ups in this image that depend only on the load address of the image 说的直白点,其实是修正指向当前镜像内部的资源指针。可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要修正镜像中的资源指针,来指向正确的地址。

什么是binding
解释:将这个二进制调用的外部符号进行绑定的过程。 比如我们objc代码中需要使用到NSObject, 即符号OBJC_CLASS$_NSObject,但是这个符号又不在我们的二进制中,在系统库 Foundation.framework中,因此就需要binding这个操作将对应关系绑定到一起

void ImageLoaderMachO::doBind(const LinkContext& context, bool forceLazysBound)
{
    // set dyld entry points in image
    this->setupLazyPointerHandler(context);

    // if prebound and loaded at prebound address, and all libraries are same as when this was prebound, then no need to bind
    // note: flat-namespace binaries need to have imports rebound (even if correctly prebound)
    if ( this->usablePrebinding(context) ) {
        // if image has coalesced symbols, then these need to be rebound, unless this is the only image with weak symbols
        if ( this->needsCoalescing() && (fgCountOfImagesWithWeakExports > 1) ) {
            this->doBindExternalRelocations(context, true);
            this->doBindIndirectSymbolPointers(context, true, true, true);
        }
        else {
            ++fgImagesRequiringNoFixups;
        }
        
        // skip binding because prebound and prebinding not disabled
        return;
    }
    
    // values bound by name are stored two different ways in mach-o:
    
    // 1) external relocations are used for data initialized to external symbols
    this->doBindExternalRelocations(context, false);
    
    // 2) "indirect symbols" are used for code references to external symbols
    // if this image is in the shared cache, there is noway to reset the lazy pointers, so bind them now
    this->doBindIndirectSymbolPointers(context, true, forceLazysBound || fInSharedCache, false);
}

初始化

// initialize lower level libraries first
            for(unsigned int i=0; i < fLibrariesCount; ++i){
                DependentLibrary& libInfo = fLibraries[i];
                // don't try to initialize stuff "above" me
                if ( (libInfo.image != NULL) && (libInfo.image->fDepth >= fDepth) )
                    libInfo.image->recursiveInitialization(context, this_thread);
            }
            
            // record termination order
            if ( this->needsTermination() )
                context.terminationRecorder(this);
            
            // let objc know we are about to initalize this image
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_dependents_initialized, this->machHeader(), fPath, fLastModified);

            // initialize this image
            this->doInitialization(context);
            // let anyone know we finished initalizing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_initialized, this->machHeader(), fPath, fLastModified);

初始化方法中,我们注意到几个关键点:

  • 状态的改变
  • 通知回调
  • 初始化image
  • 结束初始化,并通知回调

我们似乎发现了不得了的东西,dyld是如何与objc联系起来的。为了验证这个想法,我们通过断点来验证。

dyld与objc

通过objc的源码,我们知道objc的入口函数_objc_init

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();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

通过源码,我们看到了objc的各项初始化和dyld的回调函数注册.
断点验证:


谈谈dyld_第3张图片

通过断点,我们发现经:过dyld的处理之后,objc_init是在 libsystem 中的一个initialize方法 libsystem_initializer中初始化了 libdispatch, 然后libdispatch调用了_os_object_int, 最终调用了 _objc_init.

在objc初始化关于dyld事件回调中,我们看到load_images

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

从源码中我们也清晰的看到了+load方法发生在main之前,此外,我们在call_load_methods函数中,我们看到了老伙伴objc_autoreleasePoolPush , objc_autoreleasePoolPop,整个过程都被自动释放池包围着,这也是我们为什么说在启动的时候不用手动增加autoreleasePool的原因。

简单总结

到此为止,我们关于dyld的话题似乎到了一个该了解的时刻了。里面涉及的内容比较多,所以只是抓了几个重点进行了阐述,一些讲的不清楚的地方敬请谅解。

main函数之前执行的所有事件,由 dyld 主导,完成运行环境的初始化后,配合 ImageLoader 将二进制文件按格式加载到内存,
动态链接依赖库,所有初始化工作结束后,dyld 调用真正的 main 函数。

一切事件结束之后,dyld会清理现场,只剩下了main函数

带给人一个错觉:main函数是事件开始的地方

引申

有了对上述的分析之后,结合环境变量DYLD_PRINT_STATISTICS,我们就可以着手优化启动时长(启动时长=main之前(本文)+main之后)。当然该话题不是本文的范围,具体有兴趣的可以查看头条的文章今日头条iOS客户端启动速度优化

你可能感兴趣的:(谈谈dyld)