dyld 的原理

作者: coder_feng | 来源:发表于2020-05-10 14:24 被阅读0次
    一张图引发了我对dyld的一些认识 Snip20200427_13.png

    这个_dyld_start 从哪里来呢?它是怎么寻找到main load等函数入口的呢?

    dyld 概念

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

    Snip20200427_14.png 从这张图可以看到__dyld_start 函数是会调用dyldbootstrap::start(dyld3::MachOLoaded const, int, char const, dyld3::MachOLoaded const, unsigned long*) 这个方法的,我们从dyld的源码寻找这个函数:__dyld_start这个方法在汇编文件dyldStartup.s文件中获取,在这个文件里面可以找到__dyld_start 函数,因为现在大多数iOS手机都是64位,所以这里选取了64位的源码来分析
    #if __arm64__
        .data
        .align 3
    __dso_static:
        .quad   ___dso_handle
    
        .text
        .align 2
        .globl __dyld_start
    __dyld_start:
        mov     x28, sp
        and     sp, x28, #~15       // force 16-byte alignment of stack
        mov x0, #0
        mov x1, #0
        stp x1, x0, [sp, #-16]! // make aligned terminating frame
        mov fp, sp          // set up fp to point to terminating frame
        sub sp, sp, #16             // make room for local variables
    #if __LP64__
        ldr     x0, [x28]               // get app's mh into x0
        ldr     x1, [x28, #8]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
        add     x2, x28, #16            // get argv into x2
    #else
        ldr     w0, [x28]               // get app's mh into x0
        ldr     w1, [x28, #4]           // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
        add     w2, w28, #8             // get argv into x2
    #endif
        adrp    x4,___dso_handle@page
        add     x4,x4,___dso_handle@pageoff // get dyld's mh in to x4
        adrp    x3,__dso_static@page
        ldr     x3,[x3,__dso_static@pageoff] // get unslid start of dyld
        sub     x3,x4,x3        // x3 now has slide of dyld
        mov x5,sp                   // x5 has &startGlue
    
        // call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
        bl  __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
        mov x16,x0                  // save entry point address in x16
    #if __LP64__
        ldr     x1, [sp]
    #else
        ldr     w1, [sp]
    #endif
        cmp x1, #0
        b.ne    Lnew
    
        // LC_UNIXTHREAD way, clean up stack and jump to result
    #if __LP64__
        add sp, x28, #8             // restore unaligned stack pointer without app mh
    #else
        add sp, x28, #4             // restore unaligned stack pointer without app mh
    #endif
    
        // call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
        bl  __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
    

    从注释中,可以看到dyldbootstrap::start 函数是会被调用的,接下来从dyldInitialzation.cpp 这个文件的start 函数中的解释可以找到这个函数就是我们要找到的dyld的启动函数

    //
    //  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
    //  In dyld we have to do this manually.
    //
    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
        slide = slideOfMainExecutable(dyldsMachHeader);
        bool shouldRebase = slide != 0;
    #if __has_feature(ptrauth_calls)
        shouldRebase = true;
    #endif
        if ( shouldRebase ) {
            rebaseDyld(dyldsMachHeader, slide);
        }
    
        // allow dyld to use mach messaging
        mach_init();
    
        // 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(dyldsMachHeader, slide, argc, argv, envp, apple);
    #endif
    
        // now that we are done bootstrapping dyld, call dyld's main
        uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
        return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
    }
    
    • slideOfMainExecutable:这个方法是获取该次运行的ASLR
    static uintptr_t slideOfMainExecutable(const struct macho_header* mh)
    {
        const uint32_t cmd_count = mh->ncmds;
        const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
        const struct load_command* cmd = cmds;
        for (uint32_t i = 0; i < cmd_count; ++i) {
            if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
                const struct macho_segment_command* segCmd = (struct macho_segment_command*)cmd;
                if ( (segCmd->fileoff == 0) && (segCmd->filesize != 0)) {
                    return (uintptr_t)mh - segCmd->vmaddr;
                }
            }
    //没有符合条件的话,就继续遍历下一个command_PAGEZERO->_TEXT_DATA_LINKEDIT
            cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
        }
        return 0;
    }
    

    从mach-o 头文件中获取

    struct mach_header_64 {
        uint32_t    magic;        /* 魔数,用于快速确认该文件用于64位还是32位 */
        cpu_type_t    cputype;    /* CPU类型,比如 arm */
        cpu_subtype_t    cpusubtype;    /* CPU对应的具体类型,比如arm64、armv7 */
        uint32_t    filetype;    /* 文件类型,比如可执行文件、库文件、Dsym文件 */
        uint32_t    ncmds;        /* 加载命令条数 */
        uint32_t    sizeofcmds;    /* 所有加载命令的大小 */
        uint32_t    flags;        /* 标志位该字段用位表示二进制文件支持的功能,
                                   主要是和系统加载,链接相关 */
        uint32_t    reserved;    /* 保留字段 */
    };
    
    结合MachOView工具,可以知道这个方法就是用来求偏移地址的 Snip20200427_15.png

    大致的意思就是传递一个mach-header进来这个方法,然后获取loadCommand方法数目

    const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
    const struct load_command* cmd = cmds;
    这两条指令的意思就是跳过header内存地址,赋值cmd 的值为指针指向的load Commands的开始
    

    然后执行for循环,进入for循环,首先判断cmd->cmd 是否等于LC_SEGMENT_COMMAND(LC_SEGMENT_64),然后const struct macho_segment_command* segCmd = (struct macho_segment_command*)cmd; 转化为_PAGEZERO,_TEXT等macho_segment_command 等命令,之后判断其file offset == 0和 file size > 0,如果成立的话,return (uintptr_t)mh - segCmd->vmaddr(在_TEXT的时候就成立了);退出循环

    • rebaseDyld:这个方法的大概意思就是在获取到ASLR的地址之后,需要对mach-header二进制文件进行重定向,让相关的函数,变量指向正确的地址
     if ( shouldRebase ) {
            rebaseDyld(dyldsMachHeader, slide);
        }
    
    • mach_init(); 消息初始化
    • __guard_setup(apple);栈溢出保护
      加下来就是dyld的重头戏了dyld::main函数,代码太长,不方便粘贴,所以这里先用文字大概叙述一下,然后再分段代码分析

    dyld::main 函数大概做了如下几点

    - 1.设置运行环境
    - 2.加载共享缓存
    - 3.加载动态库
    - 4.链接主程序
    - 5.链接动态库
    - 6.初始化程序
    - 7.返回入口地址
    
    • 1.设置运行环境
        // Grab the cdHash of the main executable from the environment
        uint8_t mainExecutableCDHashBuffer[20];
        const uint8_t* mainExecutableCDHash = nullptr;
        if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
            mainExecutableCDHash = mainExecutableCDHashBuffer;
    
        // Trace dyld's load
        notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
    #if !TARGET_IPHONE_SIMULATOR
        // Trace the main executable's load
        notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
    
        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];
        
        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;
            }
        }
    
    • 2.加载共享缓存
        // 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 必须开启共享缓存才能运行.
    
    • 3.加载主程序
        try {
            // add dyld itself to UUID list
            addDyldImageToUUIDList();
    
    #if SUPPORT_ACCELERATE_TABLES
    #if __arm64e__
            // Disable accelerator tables when we have threaded rebase/bind, which is arm64e executables only for now.
            if (sMainExecutableMachHeader->cpusubtype == CPU_SUBTYPE_ARM64_E)
                sDisableAcceleratorTables = true;
    #endif
            bool mainExcutableAlreadyRebased = false;
            if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && !dylibsCanOverrideCache() && !sDisableAcceleratorTables && (sSharedCacheLoadInfo.loadAddress->header.accelerateInfoAddr != 0) ) {
                struct stat statBuf;
                if ( ::stat(IPHONE_DYLD_SHARED_CACHE_DIR "no-dyld2-accelerator-tables", &statBuf) != 0 )
                    sAllCacheImagesProxy = ImageLoaderMegaDylib::makeImageLoaderMegaDylib(&sSharedCacheLoadInfo.loadAddress->header, sSharedCacheLoadInfo.slide, mainExecutableMH, gLinkContext);
            }
    
    reloadAllImages:
    #endif
    
            CRSetCrashLogMessage(sLoadingCrashMessage);
            // instantiate ImageLoader for main executable
            sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
            gLinkContext.mainExecutable = sMainExecutable;
            gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
    }
    
    • 4.加载插入的动态库
     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;
    
    
    //可以尝试在xcode argument中加入这个参数,并设置为1,可以看到会多了很多动态库的输出信息
    
    • 6.链接动态库
        // Bind and notify for the inserted images now interposing has been registered
            if ( sInsertedDylibCount > 0 ) {
                for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                    ImageLoader* image = sAllImages[i+1];
                    image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
                }
            }
    一步将前面调用 addImage()函数保存在sAllImages 中的动态库列表循环调用 link进行链接,然后调registerInterposing注册符号替换. 注意这里的 i+1, 因为sAllImages中第一项是主程序,所以取 i+1项.
    
    • 7.初始化程序
            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);
    
    
    

    notifySingle

    static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
    {
        //dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
        std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
        if ( handlers != NULL ) {
            dyld_image_info info;
            info.imageLoadAddress   = image->machHeader();
            info.imageFilePath      = image->getRealPath();
            info.imageFileModDate   = image->lastModified();
            for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
                const char* result = (*it)(state, 1, &info);
                if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
                    //fprintf(stderr, "  image rejected by handler=%p\n", *it);
                    // make copy of thrown string so that later catch clauses can free it
                    const char* str = strdup(result);
                    throw str;
                }
            }
        }
        if ( state == dyld_image_state_mapped ) {
            // <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
            if ( !image->inSharedCache() ) {
                dyld_uuid_info info;
                if ( image->getUUID(info.imageUUID) ) {
                    info.imageLoadAddress = image->machHeader();
                    addNonSharedCacheImageUUID(info);
                }
            }
        }
        if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
            uint64_t t0 = mach_absolute_time();
            dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
            (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
            uint64_t t1 = mach_absolute_time();
            uint64_t t2 = mach_absolute_time();
            uint64_t timeInObjC = t1-t0;
            uint64_t emptyTime = (t2-t1)*100;
            if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
                timingInfo->addTime(image->getShortName(), timeInObjC);
            }
        }
        // mach message csdlc about dynamically unloaded images
        if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
            notifyKernel(*image, false);
            const struct mach_header* loadAddress[] = { image->machHeader() };
            const char* loadPath[] = { image->getPath() };
            notifyMonitoringDyld(true, 1, loadAddress, loadPath);
        }
    }
    
    

    再往下找到sNotifyObjCInit,再去找它的赋值找到registerObjCNotifiers,从函数注释来看是用objc runtime来调的,这块之后再看.在查阅一些资料之后得知,这里的sNotifyObjCInit就是调用 objc 中的 load_images,它调用所有的 load 方法,在调用完 load 方法以后调用了
    bool hasInitializers = this->doInitialization(context);
    doInitialization又调用了doModInitFunctions, 也就是constuctor方法
    这个sNotifyObjCInit 怎么知道是调用objc中的load_images的呢,我们从源码中可以看到这是一个指针函数,那么这个值是从哪里赋值的?

    void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
    {
        // record functions to call
        sNotifyObjCMapped   = mapped;
        sNotifyObjCInit     = init;
        sNotifyObjCUnmapped = unmapped;
        ...
    }
    

    可以看到是从registerObjCNotifiers这个方法进行赋值的,再接着找registerObjCNotifiers函数调用,最终找到这里:

    dyldAPIs.cpp

    void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                    _dyld_objc_notify_init      init,
                                    _dyld_objc_notify_unmapped  unmapped)
    {
        dyld::registerObjCNotifiers(mapped, init, unmapped);
    }
    
    
    再接着找这个的调用_dyld_objc_notify_register,发现在dyld文件里面没有找到?这个时候我们可以新建一个项目,然后设置一个Symbol断点(_dyld_objc_notify_register)来拦截一下 Snip20200428_16.png

    ,可以看到是objc_init 调用了这个方法的,我们再去到objc的源码中查看一波

    objc-os.mm

    void _objc_init(void)
    {
        static bool initialized = false;
        if (initialized) return;
        initialized = true;
        
        // fixme defer initialization until an objc-using image is found?
        environ_init();
        tls_init();
        static_init();
        lock_init();
        exception_init();
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    }
    void
    load_images(const char *path __unused, const struct mach_header *mh)
    {
        // Return without taking locks if there are no +load methods here.
        if (!hasLoadMethods((const headerType *)mh)) return;
    
        recursive_mutex_locker_t lock(loadMethodLock);
    
        // Discover load methods
        {
            mutex_locker_t lock2(runtimeLock);
            prepare_load_methods((const headerType *)mh);
        }
    
        // Call +load methods (without runtimeLock - re-entrant)
        call_load_methods();
    }
    void call_load_methods(void)
    {
        static bool loading = NO;
        bool more_categories;
    
        loadMethodLock.assertLocked();
    
        // Re-entrant calls do nothing; the outermost call will finish the job.
        if (loading) return;
        loading = YES;
    
        void *pool = objc_autoreleasePoolPush();
    
        do {
            // 1. Repeatedly call class +loads until there aren't any more
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
    
            // 2. Call category +loads ONCE
            more_categories = call_category_loads();
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    
        objc_autoreleasePoolPop(pool);
    
        loading = NO;
    }
    
    • 8.返回函数入口地址
    // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
                result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
                *startGlue = 0;
    

    参考

    参考链接1
    参考链接2

    相关文章

      网友评论

        本文标题:dyld 的原理

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