美文网首页selector
dyld源码解读

dyld源码解读

作者: 正_文 | 来源:发表于2020-09-22 17:30 被阅读0次

    dyld 全称是 the dynamic link editor。他是苹果的动态链接器,是苹果操作系统一个重要的组成部分,系统加载、链接mach-O文件,就是由dyld负责的。
    前面聊到的应用启动加载过程类、分类加载,都不可避免的触及dyld,所以了解 dyld源码可以让我们更好的理解iOS应用的工作原理。

    本文篇幅较长,都是源码和堆栈分析,比较枯燥,因此先上结论:

    1. 加载从 _dyld_start开始
    2. 进入dyld::main函数
      2.1 配置一些环境变量
      2.2 加载共享缓存库(判断是否禁用,iOS无法被禁用)
      2.3 初例化主程序
      2.4 加载插入的动态库
      2.5 链接主程序、链接绑定插入的动态库、弱绑定
      2.6 初始化方法
      2.6.1 经过一系列的方法,会(回调_objc_initload_images函数)
      2.6.2 doModInitFunctions函数,内部会调用c++构造函数
      2.7 返回入口(找到主程序的main方法)

    一、_dyld_start

    首先我们要找到dyld的入口,即dyld调用可执行文件的地方。
    新建一个项目,添加load方法,并打上断点。

    load.png

    运行,

    _dyld_start.png

    上图就是我们app开始的地方,下面我们打开dyld的源码,找到对应的方法。

    二、dyldbootstrap::start

    从上面的堆栈我们可以看到 _dyld_start,调起的第一个方法就是dyldbootstrap::start这个是c++语法,dyldbootstrap是命名空间,start是方法名)。
    当然我们也可以全局搜索_dyld_start,找到真正的方法实现。

    start.png

    通过dyldbootstrap,找到start方法实现(简化):

    //
    //  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
        // ASLR=slide 滑块 内存地址布局随机化,防止缓存溢出
        slide = slideOfMainExecutable(dyldsMachHeader);
        bool shouldRebase = slide != 0;
        if ( shouldRebase ) {
            rebaseDyld(dyldsMachHeader, slide);
        }
    
        // allow dyld to use mach messaging
        mach_init();
        
        // set up random value for stack canary
        //栈溢出保护
        __guard_setup(apple);
    
        // 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);
    }
    

    appsMachHeader:这个就是mach-Oheader
    slide:这个其实就是ASLRmac OS10.7iOS4.3均开始导入ASLR

    具体的流程:根据计算出来的slide(ASLR)来重定向mach-O;初始化允许dyld通过mach传递消息;栈溢出保护;初始化完成,调用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
    //
    uintptr_t
    _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
            int argc, const char* argv[], const char* envp[], const char* apple[], 
            uintptr_t* startGlue){
          //方法太长,900+
    }
    
    3.1准备工作,配置环境变量
        uintptr_t result = 0;
        sMainExecutableMachHeader = mainExecutableMH;  //主程序的MachO的头
        sMainExecutableSlide = mainExecutableSlide;    //主程序的ASLR值
    
        //设置上下文信息
        setContext(mainExecutableMH, argc, argv, envp, apple);
    
        {
            //检测环境变量 如果受限  禁止加载第三方库
            checkEnvironmentVariables(envp);
            defaultUninitializedFallbackPaths(envp);
        }
    
        if ( sEnv.DYLD_PRINT_OPTS )
            printOptions(argv);
        if ( sEnv.DYLD_PRINT_ENV ) //打印machO地址
            printEnvironmentVariables(envp);
        //获取主程序的信息:架构、环境变量,准备工作完成
        getHostInfo(mainExecutableMH, mainExecutableSlide);
    

    checkEnvironmentVariables(envp):熟悉越狱插件的同学应该都很清楚,某些环境变量会直接影响该库是否会被加载,有些防护操作就是基于这个原理来做的。
    DYLD_PRINT_OPTSDYLD_PRINT_ENV:环境变量,通过scheme配置,分别可以打印mach-O即可执行文件地址、启动时环境变量及其值。

    3.2 加载共享缓存

    1:检测共享缓存的禁用状态checkSharedRegionDisableios不可以被禁用
    由于 iOS 系统中 UIKitFoundation 等系统动态库每个应用都会通过 dyld 加载到内存中,因此,为了节约空间,苹果将这些系统库放在了一个地方:动态库共享缓存区 dyld shared cache, Mac OS 一样有 。
    例如,NSLog 的函数实现地址,并不在我们自己的工程的 Mach-O 中。那么我们的工程想要调用 NSLog 方法,如何能找到其真实的实现地址呢???

    在工程编译时,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间其实就是符号表,存放在 _DATA 数据段中(因为 _DATA 段在运行时是可读可写的)
    编译时:工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址,(例如工程中有一个 NSLog,那么编译时就会在 Mach-O 中创建一个 NSLog 的符号,工程中的 NSLog 就指向这个符号)
    运行时:当 dyld 将应用进程加载到内存中时,根据 load commands 加载需要的库文件,然后去做相应的绑定操作。(如 NSLogdyld 就会去找到 FoundationNSLog 的真实地址,映射到 _DATA 段符号表的 NSLog 上)。

    这个过程被称为 PIC 技术(Position Independent Code:位置代码独立)。这里提一下fishhook,他的函数名称是 rebind_symbols,他的工作原理:将编译后系统库函数所指向的符号,在运行时重绑定到用户指定的函数地址,然后将原系统函数的真实地址赋值到用户指定的指针上

    2:加载共享缓存 mapSharedCache() -> loadDyldCache()

    bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
    {
        results->loadAddress        = 0;
        results->slide              = 0;
        results->errorMessage       = nullptr;
    
    #if TARGET_IPHONE_SIMULATOR
        // simulator only supports mmap()ing cache privately into process
        return mapCachePrivate(options, results);//模拟器 模拟器仅支持加载到当前进程
    #else
        if ( options.forcePrivate ) {
            // mmap cache into this process only  仅加载到当前进程
            return mapCachePrivate(options, results);
        }
        else {
            // fast path: when cache is already mapped into shared region
            bool hasError = false;
            // 共享缓存库已经被加载过
            if ( reuseExistingCache(options, results) ) {
                hasError = (results->errorMessage != nullptr);
            } else {
                // slow path: this is first process to load cache
                // 共享缓存第一次加载
                hasError = mapCacheSystemWide(options, results);
            }
            return hasError;
        }
    #endif
    }
    
    3.3 实例化主程序
    // instantiate ImageLoader for main executable
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    

    3.3.1:检测可执行程序格式,isCompatibleMachO会通过mach-Oheader里的magiccputypecpusubtype 去检测是否兼容。

    // The kernel maps in main executable before dyld gets control.  We need to 
    // make an ImageLoader* for the already mapped in main executable.
    static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
    {
        // try mach-o loader 检查machO格式是否合法
        if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
            //实例化主程序
            ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
            addImage(image);//添加到allimage
            return (ImageLoaderMachO*)image;
        }
        
        throw "main executable not a known format";
    }
    

    然后真正实例化主程序:instantiateFromLoadedImage -> instantiateMainExecutable -> sniffLoadCommands

    sniffLoadCommands,有几个参数(请结合mach-O文件看):
    compressed,根据 LC_DYLD_INFO_ONYL 来决定
    segCount,segment段命令数量 , 最大不能超过 255
    libCount,依赖库数量 , LC_LOAD_DYLIB (Foundation、UIKit等) , 最大不能超过 4095
    codeSigCmd , 应用签名
    encryptCmd , 应用加密信息

    3.3.2:实例化之后,addImage(image)会将这个image添加到static std::vector<ImageLoader*> sAllImages;,这是一个全局的镜像列表,设置好上下文,至此主程序的实例化已经完成。

    主程序会第一个被加载到镜像文件列表,所以image list的第一个永远是我们的主程序的可执行文件。

    3.3.3:继续往 dyld :: main 下面找,会发现一个方法

    // Bind and notify for the main executable now that interposing has been registered
    gLinkContext.notifyBatch(dyld_image_state_bound, false);
    

    跟进去,notifyBatch -> notifyBatchPartial -> (*sNotifyObjCMapped)(objcImageCount, paths, mhs);,这个回调函数最终会调起_objc_init里面的回调函数map_images

    3.4、加载插入动态库
    // 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 环境变量,可以决定是否加载插入的动态库。
    越狱的插件就是基于这个原理来实现的,只需要下载插件,就可以影响到应用,有部分防护手段就用到了这个环境变量。

    // record count of inserted libraries so that a flat search will look at 
    // inserted libraries, then main, then others.
    sInsertedDylibCount = sAllImages.size()-1; //记录 插入的动态库的数量
    
    3.5、链接主程序

    这里会多次调用link函数,循环加载动态库,对主程序的依赖库进行rebase、符号绑定、弱绑定操作。
    3.5.1 链接主程序

    // link main executable  链接主程序
    gLinkContext.linkingMainExecutable = true;
    
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
    

    3.5.2 链接主程序完成后,判断sInsertedDylibCount插入的动态库数量是否大于0,然后循环调用link链接插入的动态库。

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

    3.5.3 bind 符号绑定和弱绑定

    // Bind and notify for the main executable now that interposing has been registered
    uint64_t bindMainExecutableStartTime = mach_absolute_time();
    sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
    uint64_t bindMainExecutableEndTime = mach_absolute_time();
    ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
    gLinkContext.notifyBatch(dyld_image_state_bound, false);
    
    // 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);
        }
    }
    
    // <rdar://problem/12186933> do weak binding only after all inserted images linked
    // 弱绑定 完成符号绑定
    sMainExecutable->weakBind(gLinkContext);
    
    

    weakBind弱绑定, 懒加载绑定,也就是说弱绑定一定发生在其他库链接绑定完成之后。

    进入link函数后,(注意 linkingMainExecutable 变量)

    void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
    {
        //dyld::log("ImageLoader::link(%s) refCount=%d, neverUnload=%d\n", imagePath, fDlopenReferenceCount, fNeverUnload);
        
        // clear error strings
        (*context.setErrorStrings)(0, NULL, NULL, NULL);
    
        uint64_t t0 = mach_absolute_time();
        this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
        //在链接的时候,不仅仅是对主程序进行链接,还有很多依赖库也需要进行链接,所以首先循环加载依赖库
        context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
    
        // we only do the loading step for preflights
        if ( preflightOnly )
            return;
    
        uint64_t t1 = mach_absolute_time();
        context.clearAllDepths();
        this->recursiveUpdateDepth(context.imageCount());//递归依赖层级
    
        __block uint64_t t2, t3, t4, t5;
        {
            dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
            t2 = mach_absolute_time();
            this->recursiveRebase(context); //必须对主程序和依赖库做重定位rebase(由于ASLR的存在)
            context.notifyBatch(dyld_image_state_rebased, false);
    
            t3 = mach_absolute_time();
            if ( !context.linkingMainExecutable ) //符号绑定
                this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
    
            t4 = mach_absolute_time();
            if ( !context.linkingMainExecutable )
                this->weakBind(context); //弱绑定
            t5 = mach_absolute_time();
        }
    
        if ( !context.linkingMainExecutable )
            context.notifyBatch(dyld_image_state_bound, false);
        uint64_t t6 = mach_absolute_time(); 
    
        std::vector<DOFInfo> dofs;
        this->recursiveGetDOFSections(context, dofs); //注册DOF
        context.registerDOFs(dofs); //注册DOF
        uint64_t t7 = mach_absolute_time(); 
    
        // interpose any dynamically loaded images
        if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
            dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
            this->recursiveApplyInterposing(context);
        }
    
        // clear error strings
        (*context.setErrorStrings)(0, NULL, NULL, NULL);
    
        fgTotalLoadLibrariesTime += t1 - t0;
        fgTotalRebaseTime += t3 - t2;
        fgTotalBindTime += t4 - t3;
        fgTotalWeakBindTime += t5 - t4;
        fgTotalDOF += t7 - t6;
        
        // done with initial dylib loads
        fgNextPIEDylibAddress = 0;
    }
    

    到这里,主程序已经实例化完毕,但是还没有加载FrameWork已经加载完毕。
    思考:不同的FrameWork,谁会被先加载呢???其他他和category文件的加载顺利逻辑一样,都是根据二进制编译顺序相关,在Xcode中可以自由调整,mach-O也可以看到。

    3.6、初始化

    继续往 dyld :: main 下面找,我们会看到:

    // run all initializers
    initializeMainExecutable();
    

    跟进去:initializeMainExecutable -> runInitializers -> processInitializers -> recursiveInitialization 递归调用
    到这里就没办法跟了,cmd + shift + o搜索recursiveInitialization

    // let objc know we are about to initialize this image   load方法加载结束
    uint64_t t1 = mach_absolute_time();
    fState = dyld_image_state_dependents_initialized;
    oldState = fState;
    //
    context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
    
    // initialize this image
    bool hasInitializers = this->doInitialization(context);
    
    // let anyone know we finished initializing this image
    fState = dyld_image_state_initialized;
    oldState = fState;
    //
    context.notifySingle(dyld_image_state_initialized, this, NULL);
    
    if ( hasInitializers ) {
        uint64_t t2 = mach_absolute_time();
        timingInfo.addTime(this->getShortName(), t2-t1);
    }
    

    notifySingle函数跟进去,最后来到(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,这个回调函数,最终会调起_objc_init里面的回调函数load_images,这块内容,在 本文第五节 具体分析。

    3.6.2、doInitialization

    在上一段代码,也就是recursiveInitialization函数实现中,有一个函数doInitialization,搜索然后跟进去,这里会调用 c++构造方法

    bool ImageLoaderMachO::doInitialization(const LinkContext& context)
    {
        CRSetCrashLogMessage2(this->getPath());
    
        // mach-o has -init and static initializers
        doImageInit(context);
        doModInitFunctions(context);//加载c++构造函数
        
        CRSetCrashLogMessage2(NULL);
        
        return (fHasDashInit || fHasInitializers);
    }
    

    c++构造方法,在mach-Odata段中对应__mod_init_func

    3.7、找到主程序入口
    // 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();
    
    // find entry point for main executable 找到主程序的入口,调起main函数
    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");
    }
    

    result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN(); 找到真的main入口,至此dyld加载结束。

    四、代码验证

    4.1 新建一个Single View App项目,在ViewController.m 添加代码。

    @implementation ViewController
    
    + (void)load{
        NSLog(@"来了:%s", __func__);
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
    }
    
    @end
    
    //c++构造方法
    __attribute__((constructor)) void func1() {
        
        printf("\n func1来了 \n");
    }
    

    4.2 给当前项目添加一个FrameWork,并添加Inject类,然后添加代码。

    #import "Inject.h"
    
    @implementation Inject
    
    +(void)load
    {
        NSLog(@"来了:%s", __func__);
    }
    
    @end
    

    4.3 运行,查看控制台。

    dyld[15456:2366528] 来了:+[Inject load]
    dyld[15456:2366528] 来了:+[ViewController load]
    
    
     func1来了 
    

    打印结果刚好印证我们上述的的结论,当然如果你想看下多个FrameWork结果,你可以自行添加测试验证。

    五、 _objc_init

    上文,我们两次提到过notifySingle函数,下面我们来具体分析一下上文第二个notifySingle函数。

    5.1 notifySingle

    首先我么来回顾一下,我们第一节提到的一张图,_dyld_start 到 load()的调用堆栈图。

    image.png

    根据调用堆栈,下一步就是调用 load_images,但是cmd + shift + o搜索找到notifySingle函数实现,可是这里并没有找到 load_images 的影子??
    但是,注意这里有一句代码,如下:

    //方法回调
    (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
    

    这是个回调函数的调用,sNotifyObjCInit上面判断了并不会为空,那就代表一定是有值的。那我们在当前文件搜索一下 sNotifyObjCInit,看看什么时候被赋的值。

    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;
    
        // call 'mapped' function with all images mapped so far
        try {
            notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
        }
        catch (const char* msg) {
            // ignore request to abort during registration
        }
    
        // <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
        for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
            ImageLoader* image = *it;
            if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
                dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
                (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
            }
        }
    }
    

    然后我们发现(反推):registerObjCNotifiers <- _dyld_objc_notify_register,在往前就找不到了。
    这个时候,我们可以给我们的项目下一个符号断点_dyld_objc_notify_register,看看函数调用栈。

    image.png

    至此,我们就看到了runtime整个被加载的流程。这里涉及iOS多个库,我们不一一看。可以参考objc 750源码找到_objc_init

    5.2 _objc_init
    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);
    }
    

    在这里我们就看到_dyld_objc_notify_register被调用,传递了三个参数,我在ios应用启动加载过程类、分类加载 做过详细介绍。

    map_imagesdyldimage 加载进内存时,会触发该函数。
    load_images:dyld初始化 image会触发该方法。(我们所熟知的 load 方法也是在此处调用)。
    unmap_image:dyld 将 image 移除时,会触发该函数。

    我们可以通过LLDB验证一下:

    lldb.png
    5.3 dyld链接主程序 到 调起_objc_init的具体流程
    1. dyld 加载到开始链接主程序的时候,递归调用 recursiveInitialization 函数
    2. 这个函数第一次执行,进行 libsystem 的初始化,会走到 doInitialization -> doModInitFunctions -> libSystemInitialized
    3. LibSystem 初始化,会调用起 libdispatch_init,然后会调用 _os_object_init,这个函数里面调用了 _objc_init
    4. _objc_init 中注册并保存了 map_imagesload_imagesunmap_image 函数地址
    5. 注册完毕,继续回到 recursiveInitialization 递归下一次调用。例如 libobjc,当 dyld 来到 recursiveInitialization 调用时,会触发 libsystem 调用到 _objc_init 里注册好的回调函数进行调用,就来到了 libobjc,然后调用 load_images

    推荐参考:dyld详解

    相关文章

      网友评论

        本文标题:dyld源码解读

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