iOS App启动过程

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

(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345