iOS底层原理-应用程序加载

833 阅读8分钟

前言

在如今的信息时代,移动应用的普及解决了很多的问题,为生活提供了更高的便利,我们每天使用的App当你打开时,应用程序是如何加载并运行的?从点击打开到第一个可视化界面,这期间都发生了什么?这篇文章主要了解一下应用程序的加载过程,是如何加载到内存的。

应用程序加载原理

编译流程

003.png

  • 源文件经过预编译,也就是通过词法语法分析
  • 预编译完成后提供给编译器进行编译
  • 编译后形成一些汇编文件
  • 汇编文件通过链接装载进来,然后得到可执行文件

静态库和动态库

就是可执行的二进制文件,被操作系统加载到内存。

  1. 库的形式
    • 静态库
    • 动态库
  2. 静态库和动态库的区别
    • 静态库一般是.a/.lib等格式,动态库一般是.so/.dll/.framework/.dylib等格式
    • 静态库通过静态链接装载,动态库通过动态链接装载
  3. 静态库和动态库的链接结构图
    • 静态库是一个一个装载,可能存在重复的静态库文件
    • 动态库可以通过共享库文件进行加载,节省空间 001.png

库是如何加载到内存的

加载流程图

002.png 动态链接器dyld的加载流程大致如下:

  • 在App启动时会加载libSystem
  • Runtime注册相关回调函数
  • 加载新的images(镜像文件),把库文件映射到内存中
  • 执行map_images()load_images()
  • 调用main函数
LLDB调试

根据上面的流程,我们在实际工程里通过LLDB调试来分析一下应用程序加载的流程,从App启动到main函数期间都做了什么?

  1. 首先创建一个空的工程,然后在main函数入口打个断点,开始运行

004.png 我们发现在main函数执行之前有个start函数,接下来看一下这个start

005.png 可以看到libdyld.dylib start,这个start是来自libdyld.dylib动态库,但如何加载进来并调用的,仅从上面的结果无法知晓,接下来再下个符号断点。

  1. 在项目里添加start符号断点,然后再次运行

006.png 结果发现没有断住,直接还是走到了main函数,从上面的流程可以看到load方法在main函数之前,接下来在ViewController+load方法里再打个断点。

  1. ViewControllerload方法打个断点,然后再次运行

007.png 发现在main函数之前先到load方法

  1. 输入bt打印堆栈信息

008.png

可以看到通过dyld调用了_dyld_start方法,接下来我们可以在苹果官网下载到最新的dyld的开源代码(本篇的版本是dyld-852),从源码中看看dyld的加载流程。

源码分析(正向推测)

从上面的分析我们得到了_dyld_start函数,接下来我们拿到dyld源码进行全局搜索。

006.png

dyldStartup.s文件中看到不同架构对应的汇编代码,根据注释可以看到dyldbootstrap::start这个C++函数,根据这个函数继续搜索,在dyldInitialization.cpp中找到了start的函数实现。

dyldbootstrap::start

//
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
    // 省略部分代码
    ......
    
    // 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);
}

可以看到return返回了dyld::_main函数,这里的main函数指的是dyldmain函数,接下来再看一下main函数的实现。

dyld::_main

dyld::_main函数里包含800多行代码,代码过多就不贴了,主要涉及以下一些流程:

  • 条件准备(环境、平台、版本、路径、主机信息...)
  • instantiateFromLoadedImage实例化主程序
  • loadInsertedDylib加载插入的动态库
  • mapSharedCache共享缓存加载
  • link主程序
  • link插入的动态库
  • weakBind弱引用绑定主程序
  • initializeMainExecutable初始化
  • notifyMonitoringDyldMain通知dyld可以进main函数了

根据dyld::_main的返回值来看,返回的result是由sMainExecutable函数执行相关操作得到,sMainExecutable就是加载相关的镜像文件。

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
    ......
    
    // <rdar://problem/12186933> do weak binding only after all inserted images linked
    sMainExecutable->weakBind(gLinkContext); // 弱引用绑定
    gLinkContext.linkingMainExecutable = false;

    sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
    ......
#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(); // 通知dyld可以进main函数了
    ......
    {
        // find entry point for main executable
        result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
        if ( result != 0 ) {
            ......
        } else {
            // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
            result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
            *startGlue = 0;
            }
        }
    }

    if (sSkipMain) {
        notifyMonitoringDyldMain();
	......
        result = (uintptr_t)&fake_main;
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    }
    return result;
}

initializeMainExecutable

我们在dyld::_main函数中可以看到一个重要的函数调用,就是initializeMainExecutable(),对所有的初始化操作,再继续看看这个函数的实现。

void initializeMainExecutable()
{
    .......
    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]);
    ......
}

initializeMainExecutable调用了runInitializers,再全局搜索看一下runInitializers的定义。

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这个的调用。

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这个函数定义

recursiveInitialization

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

    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        uint8_t oldState = fState;
        // break cycles
        fState = dyld_image_state_dependents_initialized-1;
        try {
            ......
        
            // 依赖文件的初始化
            context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
			
            // initialize this image
            bool hasInitializers = this->doInitialization(context);

            // let anyone know we finished initializing this image
            fState = dyld_image_state_initialized;
            oldState = fState;
            // 本身自己文件的初始化
            context.notifySingle(dyld_image_state_initialized, this, NULL);
            
            ......
        }
    }
    recursiveSpinUnLock();
}

recursiveInitialization函数的主要流程:

  • 拿到镜像文件路径,初始化操作
  • 依赖文件初始化
  • 本身自己文件的初始化 接着再全局搜索看一下notifySingle的定义。

notifySingle

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();
        dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
        // 镜像路径加载和machHeader加载
        (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        ......
    }
    ......
}

再看哪里调用了sNotifyObjCInit

sNotifyObjCInit

得到有这样的定义

static _dyld_objc_notify_init		sNotifyObjCInit;

根据_dyld_objc_notify_init搜索发现是registerObjCNotifiers的第2个参数。

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

sNotifyObjCInit = init,再根据这个函数查找哪里调用了registerObjCNotifiers函数。

registerObjCNotifiers

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这个函数,而_dyld_objc_notify_register 来自于 libobjc.dylib_objc_init 初始化调用,再回顾一下_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();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    // 这里调用了dyld的notify_register方法
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

LLDB调试

通过上面的推导和源码分析,我们在实际工程中运行一下,验证流程是否和分析的一样。在_objc_init方法里打个断点,运行然后在lldb输入bt,打印当前的堆栈信息。

007.png

可以看到红框标记的流程就是我们上面正向推测分析的流程,但红框上面的调用暂时还不知道,接着需要再分析后面的流程,通过反向推导再继续去探索。根据上面的堆栈信息,可以看到_objc_init调用之前是_os_object_init,再点击看一下具体信息。

008.png

详细信息里可以看到_os_object_init函数来自libdispatch.dylib文件,我们再从苹果的开源库下载libdispatch的源代码。

源码分析(反向推导)

堆栈流程信息

010.png

根据上面的堆栈输出我们定位到了_os_object_init函数,接下来在libdispatch源码中搜索_os_object_init

_os_object_init

void
_os_object_init(void)
{
    _objc_init();
    Block_callbacks_RR callbacks = {
        sizeof(Block_callbacks_RR),
        (void (*)(const void *))&objc_retain,
        (void (*)(const void *))&objc_release,
        (void (*)(const void *))&_os_objc_destructInstance
    };
    _Block_use_RR2(&callbacks);
    ......
}

_os_object_init里调用了_objc_init,在libdispatch里搜索一下_objc_init

009.png

可以看到_objc_init来自于libobjc,结合上面的堆栈输出流程,大概可以列出这样的流程: libSystem_initializer ~> libdispatch_init ~> _os_object_init ~> _objc_init

libdispatch_init

libdispatch的源码中搜索libdispatch_init

void
libdispatch_init(void)
{
    ......
    
    _dispatch_hw_config_init();
    _dispatch_time_init();
    _dispatch_vtable_init();
    _os_object_init();
    _voucher_init();
    _dispatch_introspection_init();
}

可以看到调用了_os_object_init,验证了上面的流程的正确性,而对于libSystem_initializer还需要下载libSystem源代码。

libSystem_initializer

// libsyscall_initializer() initializes all of libSystem.dylib
// <rdar://problem/4892197>
__attribute__((constructor))
static void
libSystem_initializer(int argc,
		      const char* argv[],
		      const char* envp[],
		      const char* apple[],
		      const struct ProgramVars* vars)
{
    ......

    libdispatch_init();
    _libSystem_ktrace_init_func(LIBDISPATCH);

    ......
}

libSystem源代码搜索libSystem_initializer方法也调用了libdispatch_init,而在libSystem_initializer之前是dylddoModInitFunctions函数,接下来我们又回到dyld。所以现在再调整一下调用流程。

doModInitFunctions ~> libSystem_initializer ~> libdispatch_init ~> _os_object_init ~> _objc_init

doModInitFunctions

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
    ......
    
    Initializer* inits = (Initializer*)(sect->addr + fSlide);
    
    ......

    Initializer func = inits[j];
   
    ......
    
    // 获取libSystem的路径,执行Initializer
    func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
    ......
}

再看一下doModInitFunctions在哪里调用的

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

doInitialization函数里调用了doModInitFunctions,而doInitialization就是我们上面正向推测的recursiveInitialization函数里执行了,这样整个流程就形成了闭合。

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

    if ( fState < dyld_image_state_dependents_initialized-1 ) {
        ......
        try {
            ......

            // 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);

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

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

            ......
        }
        ......
    }
}

总结

从源码分析流程和结合工程调试我们得到了这样一个函数调用链:

_dyld_start ~> dyld::_main ~> initializeMainExecutable ~> runInitializers ~> processInitializers ~> recursiveInitialization ~> doInitialization ~> doModInitFunctions ~> libSystem_initializer ~> libdispatch_init ~> _os_object_init ~> _objc_init

recursiveInitialization调用的notifySingle,而定位到的函数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
    }
}

再搜索sNotifyObjCMapped看在哪里调用的,经过搜索得知在notifyBatchPartial函数里调用的,而notifyBatchPartial这个方法就是上面的函数赋值后注册了回调函数去执行了。

sNotifyObjCInit方法是在notifySingle调用的,而notifySingle就是上面的recursiveInitialization里执行了。因为recursiveInitialization是个递归流程,当第一次进来先做初始化流程,再load相关镜像文件。

参考视频

有关dyld2dyld3的介绍,可以参考苹果WWDC的视频介绍。