十三、应用程序的加载

一、dyld初识

1.1. 什么是dyld?

dyld 是英文 the dynamic link editor 的简写,翻译过来就是动态链接器,是苹果操作系统的一个重要的组成部分。在 iOS/Mac OSX 系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由 动态链接器dyld 来完成的,也就是符号绑定动态链接器dyld 在系统中以一个用户态的可执行文件形式存在,一般应用程序会在 Mach-O 文件部分指定一个 LC_LOAD_DYLINKER 的加载命令,此加载命令指定了 dyld 的路径,通常它的默认值是 /usr/lib/dyld 。系统内核在加载 Mach-O 文件时,都需要用 dyld(位于 /usr/lib/dyld )程序进行链接。

dyld 是开源的,我们可以通过 官网 下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节 。本文是在dyld-750.6版本下的源码进行调试的。

1.2. dyld共享缓存
image

上图展示了应用程序的编译流程,在链接阶段会将应用程序的动态库(.so、.framework、.dylib)静态库(.a、.lib) 和我们的应用程序进行链接。

静态库可以看成是一堆对象文件 (object files) 的归档。当链接这样一个库到应用中时,静态链接器static linker 将会从库中收集这些对象文件并把它们和应用的对象代码一起打包到一个单独的二进制文件中。这意味着应用的可执行文件大小将会随着库的数目增加而增长。另外,当应用启动时,应用的代码(包含库的代码)将会一次性地导入到程序的地址空间中去。

动态库是可以被多个 app 的进程共用的,所以在内存中只会存在一份;如果是静态库,由于每个 appMach-O 文件中都会存在一份,则会存在多份。相对静态库,使用动态库可以减少 app 占用的内存大小。动态库不能直接运行,而是需要通过系统的 动态链接加载器dyld 进行加载到内存后执行。

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

二、应用程序加载

在应用程序的入口 main()函数之前断点,查看堆栈信息

图片.png

可以看到,先于main函数调用的是 start,同时,这一流程是由libdyld.dylib库执行的。dyld 是开源库,可以下载源码探索。点击下载dyld 源码

为了看到更详细的调用过程,我们在项目中的 ViewController+ (void) load 方法打断点。详细堆栈信息如下

图片.png

2.1 _dyld_start

可见,调用流程是从 _dyld_start 开始的,我们在下载好的源码中搜索 _dyld_start 。在 dyldStartup.s 文件中找到了入口,这里是用汇编实现的,尽管在不同架构下有所区别,但都是会调用 dyldbootstrap 命名空间下的start 方法,这和上面的堆栈顺序也是相同的。

图片.png

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

2.2 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 
    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);
}

dyldbootstrap::start中,主要过程为:

①使用全局变量之前,对dyld进行rebase操作,以修复为 real pointer 来运行;

②设置参数和环境变量;

③读取 app二进制文件 Mach-Oheader 得到偏移量 appSlide,然后调用dyld 命名空间下的_main 方法。

2.3 dyld::_main

这里是dyld的入口。内核加载了dyld然后跳转到 _dyld_start 来设置一些寄存器的值之后 进入这个方法。返回 _dyld_start所跳转到的目标程序的main函数地址。精简的代码如下:

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

    // 设置运行环境,可执行文件准备工作
    ......

    // load shared cache   加载共享缓存
    mapSharedCache();
    ......

reloadAllImages:

    ......
    // instantiate ImageLoader for main executable 加载可执行文件并生成一个ImageLoader实例对象
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

    ......

    // load any inserted libraries   加载插入的动态库
    if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
        for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
            loadInsertedDylib(*lib);
    }

    // link main executable  链接主程序
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

    ......
    // link any inserted libraries   链接所有插入的动态库
    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);
            }
        }
    }

    ......
    //弱符号绑定
    sMainExecutable->weakBind(gLinkContext);

    sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);

    ......
    // run all initializers   执行初始化方法
    initializeMainExecutable(); 

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

    return result;
}

复制代码

主要过程:

①第一步: 设置运行环境,为可执行文件的加载做准备工作;

②第二步: 映射共享缓存到当前进程的逻辑内存空间;

③第三步: 实例化主程序;

④第四步: 加载插入的动态库;

⑤第五步: 链接主程序;

⑥第六步: 链接插入的动态库;

⑦第七步: 执行弱符号绑定(weakBind);

⑧第八步: 执行初始化方法;

⑨第九步: 查找程序入口并返回main( ).

  • 注1: sMainExecutable = instantiateFromLoadedImage(....) 与 loadInsertedDylib(...)

这一步 dyld 将我们可执行文件以及插入的 lib 加载进内存,生成对应的imagesMainExecutable 对应着我们的可执行文件,里面包含了我们项目中所有新建的类。 InsertDylib 一些插入的库,他们配置在全局的环境变量 sEnv 中,我们可以在项目中设置环境变量 DYLD_PRINT_ENV 为1来打印该 sEnv 的值。

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

isCompatibleMachO 是检查Mach-O的subtype是否是当前cpu可以支持; 内核会映射到主可执行文件中,我们需要为映射到主可执行文件的文件,创建ImageLoader。

instantiateMainExecutable 就是实例化可执行文件, 这个期间会解析LoadCommand, 这个之后会发送 dyld_image_state_mapped 通知; 在此方法中,读取image,然后addImage() 到镜像列表。

  • 注2: link(sMainExecutable,...) 和 link(image,....)

对上面生成的 Image 进行链接。这个过程就是将加载进来的二进制变为可用状态的过程。其主要做的事有对image进行 load(加载),rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
    ......
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);  
    ......
    this->recursiveRebaseWithAccounting(context);
    ......
    this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
}

  • 注2.1: recursiveLoadLibraries(context, preflightOnly, loaderRPaths) 递归加载所有依赖库进内存。

  • 注2.2:recursiveRebase(context) 递归对自己以及依赖库进行rebase操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现 ASLR(Address space layout randomization,地址空间布局随机化),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。

  • 注2.3:recursiveBindWithAccounting(context, forceLazysBound, neverUnload); 对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。

2.4 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]);
}

这一步主要是调用所有imageInitalizer方法进行初始化。先为所有插入并链接完成的动态库执行初始化操作

sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);

再为主程序可执行文件执行初始化操作

sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

具体流程为: ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization

详细代码如下:

2.5 ImageLoader::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

2.6 ImageLoader::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 ,创建一个未初始化的向上依赖新表。如果依赖中未初始化完毕,则继续执行processInitializers,直到全部初始化完毕。

2.7 ImageLoader::recursiveInitialization

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
                                          InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    recursive_lock lock_info(this_thread);
    recursiveSpinLock(lock_info);

    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles
        fState = dyld_image_state_dependents_initialized-1;
        try {
            // initialize lower level libraries first
            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.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
                        uninitUps.count++;
                    }
                    else if ( dependentImage->fDepth >= fDepth ) {
                        dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
                    }
                }
            }

            // record termination order
            if ( this->needsTermination() )
                context.terminationRecorder(this);

            // 重点 1: let objc know we are about to initialize this image
            uint64_t t1 = mach_absolute_time();
            fState = dyld_image_state_dependents_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

            // 重点 2: initialize this image   
            bool hasInitializers = this->doInitialization(context);

            // 重点 3: let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            context.notifySingle(dyld_image_state_initialized, this, NULL);

            if ( hasInitializers ) {
                uint64_t t2 = mach_absolute_time();
                timingInfo.addTime(this->getShortName(), t2-t1);
            }
        }
        catch (const char* msg) {
            // this image is not initialized
            fState = oldState;
            recursiveSpinUnLock();
            throw;
        }
    }

    recursiveSpinUnLock();
}

recursiveInitialization 函数中,我们重点关注

  • context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
  • doInitialization(context)
  • context.notifySingle(dyld_image_state_initialized, this, NULL);
  • context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

通知objc我们要初始化这个镜像,这里 通过 notifySingle 函数对sNotifyObjCInit 进行函数调用。

2.7.1 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo)

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
    ......

    if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
        uint64_t t0 = mach_absolute_time();

        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());

    }
    ......  
}

获取镜像文件的真实地址 【*sNotifyObjCInit)(image->getRealPath(), image->machHeader() 】,而 sNotifyObjCInit 是 通过 registerObjCNotifiers 中传递的参数(_dyld_objc_notify_init)进行赋值的。

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

继而找到,registerObjCNotifiers 的 拉起函数 _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);
}

_dyld_objc_notify_register 函数是供 objc runtime 使用的,当objc镜像被映射,取消映射,和初始化时 被调用的注册处理器。我们可以在 libobjc.A.dylib 库里,_objc_init函数中找到其调用。

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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(); // C++
    runtime_init(); // runtime 初始化
    exception_init(); // 异常初始化
    cache_init(); // 缓存初始化
    _imp_implementationWithBlock_init(); //

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

runtime初始化后,在_objc_init中注册了几个通知,从dyld这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的load方法等。

就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。

当所有的依赖库的lnitializer都调用完后,dyld::main 函数会返回程序的main()函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。

那么 _objc_init 又是如何被调用的呢?

图片.png

看调用堆栈,在 ImageLoader::recursiveInitialization 函数中,我们之前关注的重点2: doInitialization

  • this->doInitialization(context);
// 重点 2: initialize this image   
bool hasInitializers = this->doInitialization(context);

2.7.2 ImageLoaderMachO::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);
}

在 doModInitFunctions之后 会 先执行 libSystem_initializer,保证系统库优先初始化完毕,在这里初始化 libdispatch_init,进而在_os_object_init 中 调用 _objc_init

由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理

runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法。

至此,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,在这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)

总结:

图片.png

APP是由内核引导启动的,kernel内核做好所有准备工作后会得到线程入口及main入口,但是线程不会马上进入main入口,因为还要加载动态链接器(dyld),dyld会将入口点保存下来,等dyld加载完所有动态链接库等工作之后,再开始执行main函数。

系统kernel做好启动程序的初始准备后,交给dyld负责。

dyld接手后,系统先读取 App 的可执行文件(Mach-O文件),从里面获取dyld的路径,然后加载dylddyld去初始化运行环境,开启缓存策略,配合 ImageLoader 将二进制文件按格式加载到内存,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main()函数地址,main()函数被调用。

这个过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像 GCD、XPC 等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是 main 函数执行之前,系统做了茫茫多的加载和初始化工作,最终引入那个熟悉的main函数。

你可能感兴趣的:(十三、应用程序的加载)