dyld在app启动过程中的作用

在前面的文章中介绍过,app启动过程中,首先是操作系统内核进行一些处理,比如新建进程,分配内存等。在iOS/Mac OS系统中,操作系统内核是XNU。在XNU完成相关的工作后,会将控制权交给dyld。dyld,即动态链接器,用于加载动态库。dyld是运行在用户态的,从XNU到dyld,完成了一次内核态到用户态的切换。那么,后续dyld做了哪些事情呢?幸运的是,dyld是开源的,我们通过分析dyld的源码,来看一下dyld在app启动过程中做了哪些工作。

dyld入口

在之前的文章中介绍过,dyld入口函数是__dyld_start,我们看一下__dyld_start里面做了那些操作。dyld中的部分源码是汇编语言,__dyld_start源码就是汇编。__dyld_start部分代码如下:

__dyld_start:
    // 这里调用了dyldbootstrap::start()函数,此函数会完成动态库加载过程,并返回主程序main函数入口
    bl  __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
    mov x16,x0                  // save entry point address in x16
    ldr     x1, [sp]
    cmp x1, #0

    // LC_MAIN case, set up stack for call to main()
Lnew:   mov lr, x1          // simulate return address into _start in libdyld.dylib
    ldr     x0, [x28, #8]       // main param1 = argc
    add     x1, x28, #16        // main param2 = argv
    add x2, x1, x0, lsl #3  
    add x2, x2, #8      // main param3 = &env[0]
    mov x3, x2

__dyld_start内部调用了dyldbootstrap::start()函数,看一下dyldbootstrap::start()内部的实现:

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
                intptr_t slide, const struct macho_header* dyldsMachHeader,
                uintptr_t* startGlue)
{
    // if kernel had to slide dyld, we need to fix up load sensitive locations
    // we have to do this before using any global variables
    if ( slide != 0 ) {
        rebaseDyld(dyldsMachHeader, slide);
    }
    // 调用dyld中的_main()函数,_main()函数返回主程序的main函数入口,也就是我们App的main函数地址
    return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

查找App main函数地址的操作主要是在_main函数中,_main函数中做了较多的操作,看一下_main()函数是如何实现的。

_main()函数

_main()函数中代码比较多,做的事情也比较多。主要完成了上下文的建立,主程序初始化成ImageLoader对象,加载共享的系统动态库,加载依赖的动态库,链接动态库,初始化主程序,返回主程序main()函数地址。接下来分别看一下每个功能的具体实现。

instantiateFromLoadedImage

instantiateFromLoadedImage()函数主要是将主程序Mach-O文件转变成了一个ImageLoader对象,用于后续的链接过程。ImageLoader是一个抽象类,和其相关的类有ImageLoaderMachO,ImageLoaderMachO是ImageLoader的子类,ImageLoaderMachO又有两个子类,分别是ImageLoaderMachOCompressed和ImageLoaderMachOClassic。这几个类之间的关系如下:

在app启动过程中,主程序和其相关的动态库,最后都被转化成了一个ImageLoader对象。看一下instantiateFromLoadedImage中做的操作。

static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
    // 检测mach-o header的cputype与cpusubtype是否与当前系统兼容
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
        addImage(image);
        return (ImageLoaderMachO*)image;
    }
}

isCompatibleMachO主要是检测mach-o文件的cputype和cpusubtype是否与当前系统兼容,之后调用了instantiateMainExecutable()函数,看一下instantiateMainExecutable()函数的实现:

// 初始化ImageLoader
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
    bool compressed;
    unsigned int segCount;
    unsigned int libCount;
    // sniffLoadCommands主要获取加载命令中compressed的值(压缩还是传统)以及segment的数量、libCount(需要加载的动态库的数量)
    sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
    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
}

instantiateMainExecutable()函数根据Mach-O文件是否被压缩过,分别调用了ImageLoaderMachOCompressed::instantiateMainExecutable()和ImageLoaderMachOClassic::instantiateMainExecutable()。现在的Mach-O文件都是被压缩过的,因此我们只看一下ImageLoaderMachOCompressed::instantiateMainExecutable的实现。

// 根据macho_header,返回一个ImageLoaderMachOCompressed对象
ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, 
                                                                        unsigned int segCount, unsigned int libCount, const LinkContext& context)
{
    ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
    image->setSlide(slide);
    image->disableCoverageCheck();
    image->instantiateFinish(context);
    image->setMapped(context);

    return image;
}

通过这样一系列的操作,最终,一个Mach-O文件被转变为了一个ImageLoaderMachOCompressed对象。

mapSharedCache

mapSharedCache()负责将系统中的共享动态库加载进内存空间,比如UIKit就是动态共享库,这也是不同的App之间能够实现动态库共享的机制。不同App间访问的共享库最终都映射到了同一块物理内存,从而实现了共享动态库。

在Mac OS系统中,动态库共享缓存以文件的形式存放在/var/db/dyld目录下,更新共享缓存的程序是update_dyld_shared_cache,该程序位于 /usr/bin 目录下。update_dyld_shared_cache通常只在系统的安装器安装软件与系统更新时调用。接下来看一下mapSharedCache()的内部实现逻辑。

mapSharedCache()中的代码比较多,我们只看部分代码:

// 将本地共享的动态库加载到内存空间,这也是不同app实现动态库共享的机制
// 常见的如UIKit、Foundation都是共享库
static void mapSharedCache()
{
    // _shared_region_***函数,最终调用的都是内核方法
    if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
        // 共享库已经被映射到内存中
        sSharedCache = (dyld_cache_header*)cacheBaseAddress;
        if ( strcmp(sSharedCache->magic, magic) != 0 ) {
            // 已经映射到内存中的共享库不能被识别
            sSharedCache = NULL;
            if ( gLinkContext.verboseMapping ) {
                return;
            }
        }
    }
    else {
        // 共享库没有加载到内存中,进行加载
        // 获取共享库文件的句柄,然后进行读取解析
        int fd = openSharedCacheFile();
        if ( fd != -1 ) {
            if ( goodCache ) {
                // 做一个随机的地址偏移
                cacheSlide = pickCacheSlide(mappingCount, mappings);
                //使用_shared_region_map_and_slide_np方法将共享文件映射到内存,_shared_region_map_and_slide_np
                // 内部实际上是做了一个系统调用
                if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, cacheSlide, slideInfo, slideInfoSize) == 0) {
                    // successfully mapped cache into shared region
                    sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
                    sSharedCacheSlide = cacheSlide;
                }
            }       
        }
    }
}

mapSharedCache()中调用了内核中的一些方法,最终实际上是做了系统调用。mapSharedCache()的主要逻辑就是:先判断共享动态库是否已经映射到内存中了,如果已经存在,则直接返回;否则打开缓存文件,并将共享动态库映射到内存中。

loadInsertedDylib

共享动态库映射到内存后,dyld会把app 环境变量DYLD_INSERT_LIBRARIES中的动态库调用loadInsertedDylib()函数进行加载。可以在xcode中设置环境变量,打印出app启动过程中的DYLD_INSERT_LIBRARIES环境变量,这里看一下我们开发的app的DYLD_INSERT_LIBRARIES环境变量:

DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libBacktraceRecording.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMainThreadChecker.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib

看一下loadInsertedDylib中的实现逻辑:

static void loadInsertedDylib(const char* path)
{
    // loadInsertedDylib方法中主要调用了load方法
    ImageLoader* image = NULL;
    try {
        LoadContext context;
        context.useSearchPaths      = false;
        context.useFallbackPaths    = false;
        context.useLdLibraryPath    = false;
        image = load(path, context, cacheIndex);
    }
}

loadInsertedDylib()函数中主要是调用了load()函数,看一下load()函数的实现:

// load函数是一系列查找动态库的入口
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
    // 根据路径进行一系列的路径搜索、cache查找等
    ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
    if ( image != NULL ) {
        CRSetCrashLogMessage2(NULL);
        return image;
    }
    // 查找失败,再次查找
    image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
    if ( (image == NULL) && cacheablePath(path) && !context.dontLoad ) {
        if ( (myerr == ENOENT) || (myerr == 0) )
        {
            // 从缓存里面找
            if ( findInSharedCacheImage(resolvedPath, false, NULL, &mhInCache, &pathInCache, &slideInCache) ) {
                struct stat stat_buf;
                try {
                    image = ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
                    image = checkandAddImage(image, context);
                }
            }
        }
    }
}

load()函数是查找动态库的入口,在load()函数中,会调用loadPhase0,loadPhase1,loadPhase2,loadPhase3,loadPhase4,loadPhase5,loadPhase6,对动态库进行查找。最终在loadPhase6中,对mach-o文件进行解析,并最终转成一个ImageLoader对象。看一下loadPhase6中的实现逻辑:

// 进行文件读取和mach-o文件解析,最后调用ImageLoaderMachO::instantiateFromFile生成ImageLoader对象
static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
{
    uint64_t fileOffset = 0;
    uint64_t fileLength = stat_buf.st_size;
    // 最小的mach-o文件大小是4K
    if ( fileLength < 4096 ) {
        if ( pread(fd, firstPages, fileLength, 0) != (ssize_t)fileLength )
            throwf("pread of short file failed: %d", errno);
        shortPage = true;
    } 
    else {
        if ( pread(fd, firstPages, 4096, 0) != 4096 )
            throwf("pread of first 4K failed: %d", errno);
    }
    // 是否兼容,主要是判断cpuType和cpusubType
    if ( isCompatibleMachO(firstPages, path) ) {
        // 只有MH_BUNDLE、MH_DYLIB、MH_EXECUTE 可以被动态的加载
        const mach_header* mh = (mach_header*)firstPages;
        switch ( mh->filetype ) {
            case MH_EXECUTE:
            case MH_DYLIB:
            case MH_BUNDLE:
                break;
            default:
                throw "mach-o, but wrong filetype";
        }
        // 使用instantiateFromFile生成一个ImageLoaderMachO对象
        ImageLoader* image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPages, headerAndLoadCommandsSize, fileOffset, fileLength, stat_buf, gLinkContext);
        return checkandAddImage(image, context);
    }
}

loadPhase6中使用了ImageLoaderMachO::instantiateFromFile()函数来生成ImageLoader对象,ImageLoaderMachO::instantiateFromFile()的实现和上面提到的instantiateMainExecutable实现逻辑类似,也是先判断mach-o文件是否被压缩过,然后根据是否被压缩,生成不同的ImageLoader对象,这里不做过多的介绍。

在将主程序以及其环境变量中的相关动态库都转成ImageLoader对象之后,dyld会将这些ImageLoader链接起来,链接使用的是ImageLoader自身的link()函数。看一下具体的代码实现:

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
    // 递归加载所有依赖库
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);

    // 递归修正自己和依赖库的基地址,因为ASLR的原因,需要根据随机slide修正基地址
    this->recursiveRebase(context);

    // recursiveBind对于noLazy的符号进行绑定,lazy的符号会在运行时动态绑定
    this->recursiveBind(context, forceLazysBound, neverUnload);
}

link()函数中主要做了以下的工作:
1. recursiveLoadLibraries递归加载所有的依赖库
2. recursiveRebase递归修正自己和依赖库的基址
3. recursiveBind递归进行符号绑定

在递归加载所有的依赖库的过程中,加载的方法是调用loadLibrary()函数,实际最终调用的还是load()方法。经过link()之后,主程序以及相关依赖库的地址得到了修正,达到了进程可用的目的。

initializeMainExecutable

link()函数执行完毕后,会调用initializeMainExecutable()函数,可以将该函数理解为一个初始化函数。实际上,一个app启动的过程中,除了dyld做一些工作外,还有一个重要的角色,就是runtime,而且runtime和dyld是紧密联系的。runtime里面注册了一些dyld的回调通知,这些通知是在runtime初始化的时候注册的。其中有一个通知是,当有新的镜像加载时,会执行runtime中的load-images()函数。接下来看一些runtime中的源码,分析一下load-images()函数做了哪些操作。

void load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

load_images()中首先调用了prapare_load_methods()函数,接着调用了call_load_methods()函数。看一下parpare_load_methods()的实现:

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
    classref_t *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        realizeClass(cls);
        assert(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

_getObjc2NonlazyClassList获取到了所有类的列表,而remapClass是取得该类对应的指针,然后调用了schedule_class_load()函数,看一下schedule_class_load的实现:

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize
    if (cls->data()->flags & RW_LOADED) return;
    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

分析这段代码,可以知道,在将子类添加到加载列表之前,其父类一定会优先加载到列表中。这也是为何父类的+load方法在子类的+load方法之前调用的根本原因。

然后我们在看一下call_load_methods()函数的实现:

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;
    loadMethodLock.assertLocked();
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
        more_categories = call_category_loads();
    } while (loadable_classes_used > 0  ||  more_categories);
    objc_autoreleasePoolPop(pool);
    loading = NO;
}

call_load_methods中主要调用了call_class_loads()函数,看一下call_class_loads的实现:

static void call_class_loads(void)
{
    int i;
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 
        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }

    if (classes) free(classes);
}

其主要逻辑就是从待加载的类列表loadable_classes中寻找对应的类,然后找到@selector(load)的实现并执行。

getThreadPC

getThreadPC是ImageLoaderMachO中的方法,主要功能是获取app main函数的地址,看一下其实现逻辑:

void* ImageLoaderMachO::getThreadPC() const
{
    const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
    const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
    const struct load_command* cmd = cmds;
    for (uint32_t i = 0; i < cmd_count; ++i) {
        // 遍历loadCommand,加载loadCommand中的'LC_MAIN'所指向的偏移地址
        if ( cmd->cmd == LC_MAIN ) {
            entry_point_command* mainCmd = (entry_point_command*)cmd;
            // 偏移量 + header所占的字节数,就是main的入口
            void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
            if ( this->containsAddress(entry) )
                return entry;
            else
                throw "LC_MAIN entryoff is out of range";
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
    return NULL;
}

该函数的主要逻辑就是遍历loadCommand,找到’LC_MAIN’指令,得到该指令所指向的便宜地址,经过处理后,就得到了main函数的地址,将此地址返回给__dyld_start。__dyld_start中将main函数地址保存在寄存器后,跳转到对应的地址,开始执行main函数,至此,一个app的启动流程正式完成。

总结

在上面,已经将_main函数中的每个流程中的关键函数都介绍完了,最后,我们看一下_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)
{
    uintptr_t result = 0;
    sMainExecutableMachHeader = mainExecutableMH;
    // 处理环境变量,用于打印
    if ( sEnv.DYLD_PRINT_OPTS )
        printOptions(argv);
    if ( sEnv.DYLD_PRINT_ENV ) 
        printEnvironmentVariables(envp);
    try {
        // 将主程序转变为一个ImageLoader对象
        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
        if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
            // 将共享库加载到内存中
            mapSharedCache();
        }
        // 加载环境变量DYLD_INSERT_LIBRARIES中的动态库,使用loadInsertedDylib进行加载
        if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
            for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                loadInsertedDylib(*lib);
        }
        // 链接
        link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        // 初始化
        initializeMainExecutable(); 
        // 寻找main函数入口
        result = (uintptr_t)sMainExecutable->getThreadPC();
    }
    return result;
}

本篇文章介绍了从dyld处理主程序Mach-O开始,一直到寻找到主程序Mach-O main函数地址的整个流程。需要注意的是,这也仅仅是一个大概流程的介绍,实际上,除了文章中所写的这些,源码中还有非常多的细节处理,以及一些没有介绍到的知识点。无论是XNU,还是dyld,阅读其源码都是一个巨大的工程,需要在日后不断的学习、回顾。

完。

发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/81609604