12.iOS底层探究之程序启动流程dyld

2021-10-10  本文已影响0人  牛牛大王奥利给

关于研究程序启动流程dyld,我是带着几个问题去了解和学习的。
1、dyld是什么?
2、具体流程是什么?
我查阅了网络好多参考资料已经WWDC19关于程序启动的视频来探究一上来两个我想知道的问题。

dyld简介

dyld(Dynamic Loader)

The dynamic loader for Darwin/OS X is called dyld, and it is responsible for loading all frameworks, dynamic libraries, and bundles (plug-ins) needed by a process.
译:Darwin/OSX的动态加载程序称为dyld,它负责加载进程所需的所有框架、动态库和捆绑包(插件)。

共享缓存技术

在程序启动运行的时候会依赖很多系统的动态库,系统动态库会通过dyld加载到内存中。但是很多动态库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的加载一次,势必会造成cpu开销的资源浪费,启动速度慢,为了优化启动速度和程序的性能,苹果就采用共享缓存技术,将所有默认的系统库编译成一个大的缓存文件(dyld_shared_cache),放到/System/Library/Caches/com.apple.dyld/目录下,按照不同的架构分别保存着。

程序启动流程dyld

我们想去了解dyld具体的流程的话,需要准备一份dyld的源码,这样我们去源码里面查看具体的细节。通过上面我们了解到dyld是为程序加载各种库的,那也就是说app启动完毕之后当前程序需要的各种库已经被加载到内存里了,所以我们在程序运行起来的时候打一个断点来了解下发生了什么。

我首先在appdelegate的生命周期didfinishlaunch返回yes之前插入了一个断点,查看堆栈信息如下:

861633753952_.pic_hd.jpg
发现此时程序已经运行了起来main已经执行完了,于是就找一个在main函数调用之前的方法来打上断点,在+load里打了一个断点,堆栈信息如下:
871633764275_.pic_hd.jpg
程序从dyld_start开始。
于是我们去dyld源码里面查一下有没有dyld_start,检索一下发现有好几个dyld_start,分别点进去看了一下具体的实现,发现是不同架构下的dyld_start,有x86,i386,arm,arm64这几个,我们着重看下arm64下的dyld_start
image.png
在代码里可以看到 dyldbootstrap::start,我们的堆栈调用图片上调用完dyld_start之后调用的也是dyldbootstrap,然后调用的是dyld::main。

所以,程序先走了dyld_start,然后dyld_start调用两个方法,一个是dyldbootstrap::start,还有一个是方法dyld::main。接下来着重看下这两个方法里又分别做了什么。

dyldbootstrap::start

全局去搜索dyldbootstrap,看到的搜索结果如下:


image.png

可以了解到关于dyldbootstrap的定义的地方只有图中标记的这一个地方,其余的基本都是call这个函数的调用。文件名是dyldInitialization.cpp,看起来像dyld的初始化文件,所以吧这个文件里定义的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

    _subsystem_init(apple);

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

方法start主要调用了方法:
· rebaseDyld 重定位
· __guard_setup 栈溢出保护
·dyld::_main 函数调用
这里着重说下重定位,主要因为我自己好奇这是什么,所以看了下rebaseDyld的实现如下

//
// On disk, all pointers in dyld's DATA segment are chained together.
// They need to be fixed up to be real pointers to run.
//
static void rebaseDyld(const dyld3::MachOLoaded* dyldMH)
从注释了解到:
"在磁盘上,所有的指针在dyld的数据段是链接在一起的,他们需要被修改成真正可以运行的指针。"
也就是说,不执行这个rebaseDyld方法之前指针都是假的 是不可以运行的。然后又上网进一步结合资料了解到关于苹果重定位技术。

rebaseDyld重定位

app本身是一个二进制的ipa文件,里面全都是二进制的元数据指针,不论谁下载下来一个应用程序,那么ipa里面的数据结构都是相同的,所以为了防止别人去猜到某一个功能模块儿的固定内存位置,苹果运用地址空间随机化布局技术ASLR(Address Space Layout Radomization)来给指针的起始地址一个随机的偏移量,并且dyld的任务之一就是重定位二进制ipa文件中的元数据指针指向,来纠正起始量。所以方法rebaseDyld,是来纠正指针起始量的。

dyld::_main

dyld关于_main的源代码好长,下面的源代码我会截取一部分我认为比较关键的源代码出来,并且带有注释:

// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
// Returns address of main() in target program which __dyld_start jumps to
//注释翻译: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){

  // Grab the cdHash of the main executable from the environment
 //从环境中获取主可执行文件cdHash (这个从下面的代码实现中可以看到cdHash这个东西是一个叫“executable_cdhash"这个的环境变量,_simple_getenv这个是获取相关的环境变量,最后赋值给了mainExecutableCDHash)
    uint8_t mainExecutableCDHashBuffer[20];
    const uint8_t* mainExecutableCDHash = nullptr;
    if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
        unsigned bufferLenUsed;
        if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
            mainExecutableCDHash = mainExecutableCDHashBuffer;
    }
      //然后这个是根据上面拿到的系统的环境变量去拿主机信息,getHostInfo的实现是不同架构下的cpu信息等等设置的不同。
    getHostInfo(mainExecutableMH, mainExecutableSlide);
......

       CRSetCrashLogMessage("dyld: launch started"); //开始加载
    setContext(mainExecutableMH, argc, argv, envp, apple);//设置相关环境变量
......
      configureProcessRestrictions(mainExecutableMH, envp);//这个里面在iPhone下主要调用了isFairPlayEncrypted方法,根据现有的环境变量envp进行配置进程并且进行加密。
......
// load shared cache 加载共享缓存 (这些都是根据一开始拿到的主机信息,设置的环境变量去加载的)
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide); //先检测共享缓存是不是可使用
    mapSharedCache(mainExecutableSlide); //加载共享缓存的方法
......(此处省略检查各种闭包,创建闭包等,判断是不是过期了等等)
 reloadAllImages:  //刷新镜像文件
         // instantiate ImageLoader for main executable 为主程序初始化镜像加载
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        gLinkContext.mainExecutable = sMainExecutable;
    gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
......
// get path of dyld itself 获取动态库的路径
        void*  addressInDyld = (void*)&__dso_handle;
#endif
        char dyldPathBuffer[MAXPATHLEN+1];
        int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
        if ( len > 0 ) {
            dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
            if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
                gProcessInfo->dyldPath = strdup(dyldPathBuffer);
        }

        // load any inserted libraries 加载插入的动态库
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // record count of inserted libraries so that a flat search will look at  记录动态库的数量
        // inserted libraries, then main, then others.
        sInsertedDylibCount = sAllImages.size()-1;

        // link main executable 链接主程序
        gLinkContext.linkingMainExecutable = true;
......

// 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();
            }
            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);
                }
            }
        }
......
// <rdar://problem/12186933> do weak binding only after all inserted images linked
//在所有的镜像链接完毕以后才能做弱引用绑定
        sMainExecutable->weakBind(gLinkContext);
        gLinkContext.linkingMainExecutable = false;
        sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
        CRSetCrashLogMessage("dyld: launch, running initializers");
    // run all initializers 执行初始化方法
        initializeMainExecutable(); 
......
    // notify any montoring proccesses that this process is about to enter main() 
//通知任何监视进程此进程即将进入main(),就是回调main函数
    notifyMonitoringDyldMain();

    return result;
}

经过上述关于main的略读,可以了解到这个流程大致分为九个部分:
1、获取环境变量主机信息等,mainExecutableCDHashBuffer;
2、(根获取到的主机的信息以及设置的环境变量去)加载共享缓存,mapSharedCache;
3、初始化主程序,instantiateFromLoadedImage;
4、插入动态库,loadInsertedDylib;
5、链接主程序,linkingMainExecutable ;
6、链接插入的动态库,registerInterposing;
7、弱引用绑定主程序,weakBind;
8、执行初始化方法,initializeMainExecutable;
9、返回主程序的入口,notifyMonitoringDyldMain;

上一篇 下一篇

猜你喜欢

热点阅读