iOS 底层探究:dyld加载分析下

1,387 阅读10分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

四、反推objc与dyld的关联

在上面的符号断点过程中可以看到在_dyld_objc_notify_registerdoModInitFunctions之间还有非dyld的库。
_objc_init中打个断点有如下调用栈:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
  * frame #0: 0x00000001002d2d44 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
    frame #1: 0x000000010044b0bc libdispatch.dylib`_os_object_init + 13
    frame #2: 0x000000010045bafc libdispatch.dylib`libdispatch_init + 282
    frame #3: 0x00007fff69543791 libSystem.B.dylib`libSystem_initializer + 220
    frame #4: 0x000000010002f1d3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
    frame #5: 0x000000010002f5de dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
    frame #6: 0x0000000100029ffb dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
    frame #7: 0x0000000100029f66 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
    frame #8: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #9: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #10: 0x0000000100016662 dyld`dyld::initializeMainExecutable() + 129
    frame #11: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
    frame #12: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #13: 0x0000000100015025 dyld`_dyld_start + 37

image.png 对于doModInitFunctions后面的流程是未知的。从doModInitFunctions->_objc_init流程是未知的。那么最好的方式就是从_objc_init反推调用到它的整个流程。

4.1 _os_object_init

_objc_init是被_os_object_init调用的,这个函数在libdispatch.dylib中。下载libdispatch最新源码1271.120.2直接搜索_os_object_init

void
_os_object_init(void)
{
        //_objc_init调用
    _objc_init();
    Block_callbacks_RR callbacks = {
        sizeof(Block_callbacks_RR),
        (void (*)(const void *))&objc_retain,
        (void (*)(const void *))&objc_release,
        (void (*)(const void *))&_os_objc_destructInstance
    };
    _Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
    const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
    if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
    v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
    if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
    v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
    if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}

发现确实是在_os_object_init中直接调用了_objc_init()
接着在libdispatch_init中发现了_os_object_init的调用:

image.png

  • 其中进行了TLS键值处理以及线程处理。

4.2 libSystem_initializer

libSystem_initializer是在libSystem库中,下载libSystem最新源码1292.120.1。 同样直接搜索libSystem_initializer

image.png

  • 其中直接调用了libdispatch_init,同样还调用了__malloc_init_dyld_initializer以及_libtrace_initlibSystem_initializerImageLoaderMachO::doModInitFunctions调用的,这样整个流程就回到了dyld中。整个流程就串起来了。 在doModInitFunctions中发现了如下代码:

image.png

  • libSystem库必须第一个被初始化。这也能被理解,因为要初始化dispatch以及objc。其它image都依赖它。
  • func是对c++构造函数的调用。 那么libSystem_initializer是在哪里调用的呢?在doModInitFunctions中并没有看到libSystem_initializer的调用。但是断点读取确实读取到了:

image.png 前面已经分析过了doModInitFunctions中是对c++构造函数的调用。libSystem_initializer正好是c++构造函数:

image.png 这样整个流程就通了。只不过libSystem_initializer这个c++构造函数被先调用。

image.png libSystemc++构造函数在dyldlibobjcFoundationc++构造函数之后,主程序之前执行。

五、 dyld注册objc回调简单分析

通过上面的分析在_objc_init中调用了_dyld_objc_notify_register进行回调注册,有如下赋值:

//第一个参数 map_images
sNotifyObjCMapped   = mapped;
//第二个参数 load_images
sNotifyObjCInit     = init;
//第三个参数 unmap_image
sNotifyObjCUnmapped = unmapped;

接下来将详细分析这3个回调的逻辑。

5.1 sNotifyObjCMapped(map_images)

sNotifyObjCMappeddyld中的调用只在notifyBatchPartial中:

image.pngnotifyBatchPartial的调用是在registerObjCNotifiersregisterImageStateBatchChangeHandler、以及notifyBatch中。那么根据之前的分析这里的调用就是registerObjCNotifiers注册回调后就在里面调用了。 在objc源码map_images中打个断点:

image.png 可以验证在注册回调后立马调用了map_images

map_images中直接加锁调用了map_images_nolock,其中进行了类的加载相关的操作。这块逻辑将单独写篇文章进行分析。

5.2 sNotifyObjCInit(load_images)

sNotifyObjCInitdyld中的调用分为以下情况:

  • 1.notifySingleFromCache中。
  • 2.notifySingle中。
  • 3.registerObjCNotifiersnotifySingleFromCachenotifySingle逻辑基本相同,无非就是有没有缓存的区别。 registerObjCNotifiers是在注册回调函数的时候直接进行的回调。直接在load_images中打个断点可以跟踪到如下信息:

image.png 可以看到系统的基础库在注册回调后就马上进行了load_images的调用。

而对于其他库是通过notifySingle走的回调逻辑:

image.png

5.2.1 load_images(objc-runtime-new.mm

sNotifyObjCInit其实就是load_images,它的实现如下:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //加载所有分类
        loadAllCategories();
    }

    // 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);
        //准备所有load方法
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    //调用 + load方法
    call_load_methods();
}
  • 加载所有分类。
  • 准备所有load方法。
  • 最终调用了call_load_methodsprepare_load_methods
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        //添加主类的load方法
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *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
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        //实现类
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        //添加分类的load方法。
        add_category_to_loadable_list(cat);
    }
}
  • 添加主类的load方法。
  • 添加分类的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
    //调度类的load方法,递归到nil
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
  • 递归调度类的load方法,直到父类为nil

add_class_to_loadable_list & add_category_to_loadable_list

void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();
    //load方法
    method = cls->getLoadMethod();
    if (!method) return;  // Don't bother if cls has no +load method
    
    if (PrintLoading) {
        _objc_inform("LOAD: class '%s' scheduled for +load", 
                     cls->nameForLogging());
    }
    //空间不足开辟空间
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    //将load方法添加到loadable_classes中。相当于一个下标中存储的是cls-method
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();
    //获取load方法
    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }
    //分类添加到loadable_categories中
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
  • 通过字符出那比较获取load方法。
  • 空间不足的情况下开辟空间吗,每次开辟的空间大小为(2倍+16)* 16 字节。
struct loadable_class {
  Class cls;  // may be nil
  IMP method;
};
  • 将对应的数据添加进loadable_classesloadable_categories中。 ⚠️加载过程中类和分类是有区分的。为什么区分将在后续的文章中详细分析。 getLoadMethod
IMP 
objc_class::getLoadMethod()
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    //递归所有的baseMethods,查找load方法。
    mlist = ISA()->data()->ro()->baseMethods();
    if (mlist) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            //匹配load
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }

    return nil;
}

IMP 
_category_getLoadMethod(Category cat)
{
    runtimeLock.assertLocked();

    const method_list_t *mlist;

    mlist = cat->classMethods;
    if (mlist) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }

    return nil;
}
  • load方法获取是通过字符出那比较获取的。

5.2.2 call_load_methods (objc-loadmethod.mm)

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();
    
    //循环调用 call_class_loads,类的load方法在这一刻被调用
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            //调用每个类的load
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        //调用分类load,这里也就说明分类的 load 在所有类的load方法调用后才调用。(针对image而言)
        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;
}
  • 调用call_class_loads加载类的+ load
  • 接着调用call_category_loads加载分类的+ load。这里也就说明分类的 load在所有类的load方法调用后才调用。(针对image而言)。 在这里也就调用到了+ load方法,这也就是+ loadmain之前被调用的原因。 call_class_loads
static void call_class_loads(void)
{
    int i;
    
    // Detach current loadable list.
    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;
        //从classes中获取method
        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
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}
  • 内部也是从loadable_classes中循环取到load方法进行调用。 call_category_loads
static bool call_category_loads(void)
{
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        //从cats中取出load
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }
……
}
  • 分类load的调用也是从loadable_categories循环取load方法进行调用。分类中内部处理逻辑更多一些。 所以在调用完+ load以及c++构造函数才返回LC_MAIN进行main函数的调用。可以通过汇编断点验证:

image.png 这样就和开头的时候对应上了。那么如果修改main函数的名称,编译的时候就报错了。主程序的入口main是写死的,可以通过Hook去操作main隐藏自己的逻辑。 根据以上分析可以看到dyld是按image list顺序从第1image调用runInitializers(可以看做是以image分组)。再调用下一个imagerunInitializers最后再调用主程序(下标为0)的runInitializers。在runInitializers内部先调用所有类的+load,再调用所有分类的+ load,最后调用c++的构造函数。
objc中调用loaddyld中调用doModInitFunctions
⚠️如果在+ load中做了防护,那么可以通过在+ load执行前断住外部符号做处理。这样就可以绕过防护了。 防护最重要的就是不让别人找到防护的逻辑,只要能找到那么破解就很容易了。

5.3 sNotifyObjCUnmapped(unmap_image)

sNotifyObjCUnmappeddyld中只有removeImage进行了调用:

image.png removeImagecheckandAddImagegarbageCollectImages_dyld_link_module调用。

  • garbageCollectImages:在link等其它异常以及回收的时候调用。
  • checkandAddImage:检测加载的image不在镜像列表中的时候直接删除。
  • _dyld_link_module:暂时不确定是哪里调用的。

5.3.1 unmap_image

unmap_image中调用了unmap_image_nolock,核心代码如下:

void 
unmap_image_nolock(const struct mach_header *mh)
{
 ……
    header_info *hi; 
 ……
    //释放类,分类相关资源。
    _unload_image(hi);

    // Remove header_info from header list
    //移除remove Header
    removeHeader(hi);
    free(hi);
}
  • 移除释放类,分类相关资源。
  • 移除Header信息。

六 、dyld3闭包模式分析

关于闭包模式在开启闭包模式的情况下就直接return了,所以核心逻辑就在launchWithClosure中了:

static bool launchWithClosure(const dyld3::closure::LaunchClosure* mainClosure,
                              const DyldSharedCache* dyldCache,
                              const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide,
                              int argc, const char* argv[], const char* envp[], const char* apple[], Diagnostics& diag,
                              uintptr_t* entry, uintptr_t* startGlue, bool* closureOutOfDate, bool* recoverable)
{
    ……
    libDyldEntry->runInitialzersBottomUp((mach_header*)mainExecutableMH);
    ……
}

launchWithClosure中发现了runInitialzersBottomUp的调用:

void AllImages::runInitialzersBottomUp(const closure::Image* topImage)
{
    // walk closure specified initializer list, already ordered bottom up
    topImage->forEachImageToInitBefore(^(closure::ImageNum imageToInit, bool& stop) {
        // get copy of LoadedImage about imageToInit, but don't keep reference into _loadedImages, because it may move if initialzers call dlopen()
        uint32_t    indexHint = 0;
        LoadedImage loadedImageCopy = findImageNum(imageToInit, indexHint);
        // skip if the image is already inited, or in process of being inited (dependency cycle)
        if ( (loadedImageCopy.state() == LoadedImage::State::fixedUp) && swapImageState(imageToInit, indexHint, LoadedImage::State::fixedUp, LoadedImage::State::beingInited) ) {
            // tell objc to run any +load methods in image
            if ( (_objcNotifyInit != nullptr) && loadedImageCopy.image()->mayHavePlusLoads() ) {
                dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)loadedImageCopy.loadedAddress(), 0, 0);
                const char* path = imagePath(loadedImageCopy.image());
                log_notifications("dyld: objc-init-notifier called with mh=%p, path=%s\n", loadedImageCopy.loadedAddress(), path);
                //+load
                (*_objcNotifyInit)(path, loadedImageCopy.loadedAddress());
            }

            // run all initializers in image
            //c++构造函数
            runAllInitializersInImage(loadedImageCopy.image(), loadedImageCopy.loadedAddress());

            // advance state to inited
            swapImageState(imageToInit, indexHint, LoadedImage::State::beingInited, LoadedImage::State::inited);
        }
    });
}
  • _objcNotifyInit最终调用到了+ load方法。
  • runAllInitializersInImage调用c++构造函数,其中包括注册回调。
void AllImages::runAllInitializersInImage(const closure::Image* image, const MachOLoaded* ml)
{
    image->forEachInitializer(ml, ^(const void* func) {
        Initializer initFunc = (Initializer)func;
#if __has_feature(ptrauth_calls)
        initFunc = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)initFunc, 0, 0);
#endif
        {
            ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)ml, (uint64_t)func, 0);
            //c++构造函数
            initFunc(NXArgc, NXArgv, environ, appleParams, _programVars);

        }
        log_initializers("dyld: called initialzer %p in %s\n", initFunc, image->path());
    });
}

在真机/模拟器调试中对_dyld_objc_notify_register下符号断点发现_dyld_objc_notify_register()的注册回调是dyld3::_dyld_objc_notify_register调用的:

image.png 但是最终的回调以及调用方确是dyld2的逻辑。看下源码:

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
    if ( gUseDyld3 )
        return dyld3::_dyld_objc_notify_register(mapped, init, unmapped);

    DYLD_LOCK_THIS_BLOCK;
    typedef bool (*funcType)(_dyld_objc_notify_mapped, _dyld_objc_notify_init, _dyld_objc_notify_unmapped);
    static funcType __ptrauth_dyld_function_ptr p = NULL;

    if(p == NULL)
        dyld_func_lookup_and_resign("__dyld_objc_notify_register", &p);
    p(mapped, init, unmapped);
}

那么就说明gUseDyld3NULL,走了dyld2的逻辑。但是如果走dyld3可以得到以下信息,注册的三个回调函数指针与dyld2名称不同:

_objcNotifyMapped   = map;
_objcNotifyInit     = init;
_objcNotifyUnmapped = unmap;
  • _objcNotifyInit已经清楚了是在runInitialzersBottomUp中调用的。
  • _objcNotifyUnmapped是在garbageCollectImages ->removeImages中调用的。
  • _objcNotifyMapped是在runImageCallbacks中调用的,它有两个调用方applyInitialImages以及loadImage
    • applyInitialImages是被_dyld_initializer调用的。_dyld_initializer在第四部分已经明确了是在libSystem_initializer中调用的。而由于_dyld_initializer是在libdispatch_init之前调用的,所以这个时候应该还没有注册回调。
    • loadImage是在dlopen中调用的。

由于真机和模拟器以及mac都没有办法进入闭包模式调试验证。并且闭包模式代码逻辑可读性比较差,所以这里只是根据源码得出的结论,不一定成立。

image.png