美文网首页IOS
iOS App启动过程

iOS App启动过程

作者: 赵哥窟 | 来源:发表于2020-06-19 17:43 被阅读0次

    总结来说,大体分为如下步骤:

    (1) 系统为程序启动做好准备

    • 当kernel(内核)做好程序的启动准备工作之后,系统的执行由内核态转换为用户态,由 dyld 首先开始工作

    (2) 系统将控制权交给 Dyld,Dyld 会负责后续的工作

    (3) Dyld 加载程序所需的动态库

    (3) Dyld 对程序进行 rebase 以及 bind 操作

    (4) Objc SetUp

    (5) 运行初始化函数

    (6) 执行程序的 main 函数

    image.png

    dyld

    dyld(the dynamic link editor), 动态链接器,是专门用来加载动态库以及主程序的库.当kernel做好程序的启动准备工作之后,系统的执行由内核态转换为用户态,由 dyld 首先开始工作,iOS 中用到的所有系统framework都是动态库,比如最常用的UIKit.framework,Foundation.framework等都是通过dyld加载进来的。

    dyld 主要的工作有
    • 初始化 App 运行环境
    • 链接依赖的动态库以及主程序
    • rebase / binding
    • 返回 main.m 的函数地址

    接下来分析下dyld 的源码


    截屏2020-06-22 10.43.17.png

    可以看到入口函数事在 dyid_start方法里的dyldbootstrap::start方法,接下来去源码里看看. 在 dyld 源码里找到dyldStartup.s找到了__dyld_start,这里只截取了arm架构的部分.

    image.png

    通过注释可以看到有调用dyldbootstrap::start,那顺着调用再往下看. 在dyldInitialization.cpp中找到了start

    image.png
    • 首先通过slideOfMainExecutable拿到随机地址的偏移量
    • 调用rebaseDyld重定位
    • mach_init() mach消息初始化
    • __guard_setup() 栈溢出保护 接下来调用了dyld::_main,将返回值传递给__dyld_start的调用main.m函数.

    dyld::_main

    dyld::_main是dyld中的关键方法,代码也非常多,它的实现可以分为以下几步: (关键部分有注释)

    • 设置运行环境
    • 加载共享缓存
    • 加载主程序
    • 加载动态库
    • 链接主程序
    • 链接动态库
    • 初始化主程序
    • 返回入口地址

    设置运行环境

    
        // Grab the cdHash of the main executable from the environment
        uint8_t mainExecutableCDHashBuffer[20];
        const uint8_t* mainExecutableCDHash = nullptr;
        // 获取主程序hash
        if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
            mainExecutableCDHash = mainExecutableCDHashBuffer;
    
        // Trace dyld's load
        // 通知kernal内核dyld文件已加载
        notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
    #if !TARGET_IPHONE_SIMULATOR
        // Trace the main executable's load
        // 通知kernal内核mach-o文件已加载
        notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
    #endif
    
        CRSetCrashLogMessage("dyld: launch started");
        //设置上下文
        setContext(mainExecutableMH, argc, argv, envp, apple);
    
        // Pickup the pointer to the exec path.
        // 获取主程序路径
        sExecPath = _simple_getenv(apple, "executable_path");
    
        // <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
        if (!sExecPath) sExecPath = apple[0];
    
        // mach-o 绝对路径
        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;
            }
        }
    

    加载共享缓存

    // load shared cache
        // 判断共享缓存库是否被禁用
        checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    #if TARGET_IPHONE_SIMULATOR
        // <HACK> until <rdar://30773711> is fixed
        gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
        // </HACK>
    #endif
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
            // 映射共享缓存到共享区
            mapSharedCache();
        }
    

    checkSharedRegionDisable是检查共享缓存是否禁用,里面可以看到一行注释,iOS 必须开启共享缓存才能运行.

    static void checkSharedRegionDisable(const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide)
    {
    #if __MAC_OS_X_VERSION_MIN_REQUIRED
        // if main executable has segments that overlap the shared region,
        // then disable using the shared region
        if ( mainExecutableMH->intersectsRange(SHARED_REGION_BASE, SHARED_REGION_SIZE) ) {
            gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
            if ( gLinkContext.verboseMapping )
                dyld::warn("disabling shared region because main executable overlaps\n");
        }
    #if __i386__
        if ( !gLinkContext.allowEnvVarsPath ) {
            // <rdar://problem/15280847> use private or no shared region for suid processes
            gLinkContext.sharedRegionMode = ImageLoader::kUsePrivateSharedRegion;
        }
    #endif
    #endif
        // iOS cannot run without shared region
    }
    

    接下来调的mapSharedCache()就是加载共享缓存的逻辑,就不深入了.

    加载主程序

    // add dyld itself to UUID list
    addDyldImageToUUIDList();
    
    CRSetCrashLogMessage(sLoadingCrashMessage);
    // instantiate ImageLoader for main executable
    // 主程序实例化
    // 这里调用比较深,后续再看
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    gLinkContext.mainExecutable = sMainExecutable;
    gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
    
    

    这一步将主程序 Mach-O 加载进内存,并实例化了一个ImageLoader.先看下instantiateFromLoadedImage的调用栈:


    image.png

    其中ImageLoader是一个抽象类,它的两个子类ImageLoaderMachOCompressed、ImageLoaderMachOClassic负责把 Mach-O 实例化为 Image.但要用哪个子类来进行实例化是通过sniffLoadCommands来判断Mach-O 文件的 LINKEDIT 是classic或者compressed.

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

    加载动态库

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

    遍历DYLD_INSERT_LIBRARIES环境变量,然后调用loadInsertedDylib加载.

    链接主程序

    // link 主程序
    // link调用比较深,后续来看
    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;
    }
    

    调用 link链接主程序,内核调用的是ImageLoader::link 函数,主要是做了加载动态库、rebase、binding 等操作,代码比较多,我就不贴了,在附件的源码上有我写的详细注释.

    链接动态库

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

    这一步将前面调用 addImage()函数保存在sAllImages 中的动态库列表循环调用 link进行链接,然后调registerInterposing注册符号替换. 注意这里的 i+1, 因为sAllImages中第一项是主程序,所以取 i+1项.

    初始化主程序

        CRSetCrashLogMessage("dyld: launch, running initializers");
            // 初始化主程序
        #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()
            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);
            }
            notifyMonitoringDyldMain();
    

    这一步由initializeMainExecutable()完成。dyld会优先初始化动态库,然后初始化主程序。该函数首先执行runInitializers(),内部再依次调用processInitializers()、recursiveInitialization(),在recursiveInitialization()函数里找到了 notifySingle();

    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    复制代码
    

    再往下找到sNotifyObjCInit,再去找它的赋值找到registerObjCNotifiers,从函数注释来看是用objc runtime来调的,这块之后再看.在查阅一些资料之后得知,这里的sNotifyObjCInit就是调用 objc 中的 load_images,它调用所有的 load 方法,在调用完 load 方法以后调用了

    bool hasInitializers = this->doInitialization(context);
    复制代码
    

    doInitialization又调用了doModInitFunctions, 也就是constuctor方法,关于这个方法可以参看链接.

    返回入口地址

    
            // find entry point for main executable
            // 从 mach-o 中读取程序入口, 主程序则读取LC_UNIXTHREAD, 就是 main.m
            result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
            if ( result != 0 ) {
                // main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
                if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
                    *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
                else
                    halt("libdyld.dylib support not present for LC_MAIN");
            }
            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;
    

    这里调用主程序的getEntryFromLC_MAIN,就是从``Load Command中读取LC_MAIN入口,如果没有,就读取LC_UNIXTHREAD,然后跳到入口处执行,就回到了我们熟悉的main.m`.

    App启动逻辑

    最后 dyld 会调用 main() 函数。main() 会调用 UIApplicationMain(),程序启动。

    main.m文件,此处就是应用的入口了。程序启动时,先执行main函数,main函数是ios程序的入口点,内部会调用UIApplicationMain函数,UIApplicationMain里会创建一个UIApplication对象 ,然后创建UIApplication的delegate对象 —–(您的)AppDelegate ,开启一个消息循环(main runloop),每当监听到对应的系统事件时,就会通知AppDelegate。

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    
    image.png
    说明

    带注释 dyld源码地址: Github

    相关文章

      网友评论

        本文标题:iOS App启动过程

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