美文网首页
iOS应用程序加载大致流程分析

iOS应用程序加载大致流程分析

作者: 深圳_你要的昵称 | 来源:发表于2020-10-02 02:19 被阅读0次

    前言

    今天我们重点来分析一下,iOS App运行时,在main()方法执行之前,程序到底做了哪些事?

    准备工作

    示例,新建一个iOS应用工程,查看方法加载的顺序

    __attribute__((constructor)) void lg_cFunction() {
    //    printf();
        NSLog(@"%s -- 来了", __func__);
    }
    
    int main(int argc, char * argv[]) {
        NSString * appDelegateClassName;
        @autoreleasepool {
            // Setup code that might create autoreleased objects goes here.
            appDelegateClassName = NSStringFromClass([AppDelegate class]);
            NSLog(@"%s -- 来了", __func__);
        }
        return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }
    ---------------------------------分割线---------------------------------
    @implementation ViewController
    
    + (void)load {
        NSLog(@"%s -- 来了", __func__);
    }
    @end
    

    运行后

    2020-09-28 11:06:39.756959+0800 Test1[49592:4931930] +[ViewController load] -- 来了
    2020-09-28 11:06:39.757473+0800 Test1[49592:4931930] lg_cFunction -- 来了
    2020-09-28 11:06:39.757671+0800 Test1[49592:4931930] main -- 来了
    2020-09-28 11:06:39.800886+0800 Test1[49592:4931930] result is 0
    

    发现当前3个方法的调用顺序是load --> lg_cFunction(c++方法) -->main入口,why?

    编译过程

    带着上述方法调用顺序的疑问,我们先来大致了解下,App编译的一个过程:


    大致流程是:
    源文件(.h .m .cpp)-->预编译(检查语法)-->编译(转化为汇编)-->汇编(生成机器码文件) -->链接(也包括一些库的链接)-->生成可执行文件(在生成的.app中右键打开包文件,里头的exec)
    其中链接这一步苹果系统使用的就是dyld库来完成的。那什么是dyld呢?

    dyld动态链接器

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

    找入口

    你想知道系统调用load之前走了什么流程?很简单,在load方法里打断点,然后lldb bt指令查看调用堆栈信息。

    指令名称 释义
    bt 查看调用堆栈信息,加all可打印多有thread的堆栈

    上图红框处可知,最开始是从_dyld_start_开始的,我们全局搜索_dyld_start_,寻找入口。
    _dyld_start_.png
    看注释,我们注意到会调用dyldbootstrap::start
    image.png
    再次全局搜索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 <rdar://46878536>
        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);
    }
    

    最终调用dyld::_main,这个就是我们要找的入口!

    dyld::_main流程大致分析

    这个函数实现代码量巨大,我们一步步看。

    1. 首先看函数的返回值result,如下图:

      再搜索result的赋值地方,

    上图是初始化

    上图是一个if特殊情况条件里的返回,不考虑。


    上图是个宏编译的if条件的,不考虑。


    上图也是一个if条件中的赋值,并return,不作考虑。

    同理,不考虑。


    首先3是宏条件编译,不考虑,然后1和2是主要赋值的地方,都用到一个共同的变量sMainExecutable,应该是关键。

    也是if条件里的,不考虑。

    最终发现,赋值result的都是通过变量sMainExecutable,那我们再搜索sMainExecutable的赋值情况:

    sMainExecutable.png
    第一个就找到了,通过方法instantiateFromLoadedImage,再看注释// instantiate ImageLoader for main executable -->为主可执行文件实例化ImageLoader,接着我们重点看看instantiateFromLoadedImage函数
    // The kernel maps in main executable before dyld gets control.  We need to 
    // make an ImageLoader* for the already mapped in main executable.
    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";
    }
    

    关键代码是ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);

    // create image for main executable
    ImageLoader* 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
    }
    

    首先sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);

    sniffLoadCommands.png
    根据注释,应该是定义mach-o文件的格式,定义什么格式呢?
    这时需要用到查看mach-o文件的软件MachOView,举个例子来看看:
    先找到工程的.app文件所在位置:

    再右键显示包内容:

    选择exec可执行文件

    拖入到MachOView中:

    所以,sniffLoadCommands定义的就是mach-o里的区间Load Commands里的格式。
    格式定义完成后,接着进行初始化instantiateMainExecutable

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

    不论是压缩模式ImageLoaderMachOCompressed,还是标准模式ImageLoaderMachOClassic,最终都是生成ImageLoader对象,完成一个初始化sMainExecutable的过程。

    至此,我们通过返回值result反向搜索赋值,找到sMainExecutable的初始化流程(第6577行),那么在sMainExecutable的初始化前,又走了哪些流程?我们一点点的往下看:














    上述所有图,大致描述了dyld::_main的大致流程:

    1. 环境变量配置
    2. 共享缓存处理
    3. 主程序表初始化
    4. 插入动态库
    5. 链接主程序表
    6. 链接动态库
    7. 弱符号绑定
    8. 初始化所有
    9. 主程序入口处理
    第8步初始化流程详细分析

    大家肯定想知道:我们平时写的对象到底是如何初始化的呢,说白了就是我们之前讨论的_objc_init是在哪里触发被调用的呢?带着这个问题,我们首先看看initializeMainExecutable源码:

    再看看runInitializers源码:

    继续processInitializers

    继续recursiveInitialization


    很明显,这里面,需要分成两部分探索,一部分是notifySingle函数,一部分是doInitialization函数

    首先探索notifySingle函数
    小技巧-->全局搜索notifySingle(函数

    红框里是核心代码
    再搜索sNotifyObjCInit,看看哪里赋值处理


    上图赋值的是在函数registerObjCNotifiers,再搜索其被调用的地方

    是在 _dyld_objc_notify_register进行了调用,但是_dyld_objc_notify_register的函数需要在libobjc源码中搜索

    终于,是在 _objc_init中,这不正是我们最开始要找的问题所在吗,哈哈!
    _objc_init源码中调用了_dyld_objc_notify_register,并传入了参数load_images,那么sNotifyObjCInit = load_images,而load_images中会调用所有的+load方法。

    整个load方法的调用链路就是:
    _dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle(是一个回调处理) --> sNotifyObjCInit --> load_images(libobjc.A.dylib)

    load方法的调用栈信息
    load调用栈
    前面的流程跟我们之前分析的一样:_dyld_start-->dyld::main-->initialzeMainExecutetable()初始化主程序表-->dyld::notifySingle回调结果-->load_images加载文件,紧接着就调用了load方法,具体调用的位置如下图:
    load调用
    cxx方法调用栈信息

    同理,在cxx方法处打断点,查看调用栈:

    cxx调用
    load不同的是,在recursiveInitialization之后,



    和load不同的是,在doInitialization里触发的cxx方法。那我们具体看看doInitialization的流程是如何处理的。

    先看doImageInit

    再看doModInitFunctions,和doImageInit差不多的流程

    接着搜索LC_SEGMENT_COMMAND

    以64位为例,看看LC_SEGMENT_64

    看来是_TEXT区间相关,我们查看mach-o符号文件:
    _TEXT.png
    所以doModInitFunctions就是在编译调用上图红框处的区间里所有的函数,其中就包含cxx函数的调用触发。

    cxx函数调用链
    _dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions-->func()

    那么回到之前的问题,objc_init是在哪里被调用呢?在initializeMainExecutable没找到答案,那么接下来,只能使用终极大招了-->符号断点

    符号断点查看objc_init

    前面的流程和cxx函数调用大致相同,在第3步时,会调用libSystem库,再去到libdispatch库,然后触发_os_object_init-->_objc_init,请看下列图:



    上图可知:objc_init调用链如下:
    1. _dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::doModInitFunctions
    2. libSystem_initlializer-->libdispatch_init-->_os_object_init-->_objc_init
    3. _objc_init-->注册观察者_dyld_objc_notify_register(&map_images, load_images, unmap_image)-->第2个参数load_images == sNotifyObjCInit,然后再在dyld加载的ImageLoader::recursiveInitialization这一步里notifySingle-->sNotifyObjCInit触发回调,让_objc_init和dyld加载过程形成一个闭环。
    main入口

    上面我们知道了loadcxx函数的调用链,还剩下main()了,它是在哪里被调用的呢?智能在cxx函数里打断点,然后打开汇编模式,跟着断点一步步看了。


    然后按住按键control,点击step over

    上图红框里发现,其实和之前分析的流程基本一致,还是回到了_dyld_start,看来我们只有回到最初的汇编代码里,去寻找main入口了:

    在第3步也是_dyld_start的最后,找到了main(),此时才调用,当然比loadcxx函数的调用时机都晚!

    总结

    借用Style_月月iOS-底层原理 15:dyld加载流程的dyld加载流程图:

    dyld加载流程

    相关文章

      网友评论

          本文标题:iOS应用程序加载大致流程分析

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