iOS底层探索 --- dyld加载流程

dyld加载流程图

建议大家在阅读文章的时候,结合流程图阅读。这样方便理解这个流程,可以将图片下载到本地,一边阅读一边比对。


dyld加载流程图

1、dyld

1.1 简介
dyld(The dynamic link editor) --- Apple的动态链接器。
是苹果操作系统一个重要的组成部分,在应用被编译打包成Mach-O文件之后,交由dyld负责链接,加载程序。在MacOS系统中,其在/usr/lib/dyld目录下。
1.2 共享缓存
在日常开发的过程中,我们会用到很多的系统库,比如:UIKitFoundation等等;这些系统库都是dyld帮我们加载到内存中的。但是不同的APP会用到相同的系统库,如果每一个APP运行的时候,dyld都去加载一遍,那岂不是对内存极大的浪费。于是,为了节省空间,Apple将这些系统库统一的放在了一个地方:动态库共享区(dyld shared cache)


2、dyld加载流程

  • 2.1 Demo准备
    在探索dyld加载流程之前,我们先做好准备工作。
    首先请大家思考一个问题:我们APP的启动,是先执行main函数吗?在main函数之前,还有其他的操作吗?
    这里我们创建一个Demo,一起来探索一下。我们在ViewController中重写load方法,然后在main.m中添加一个C++方法,然后观察一下它们的执行顺序:

    我们会发现,作为APP入口的main函数并不是第一个被执行的函数。这又是为什么呢?
    接下来我们就一步一步的探索。
  • 2.2 APP启动流程探索
    通过上面,我们知道,load方法是最先被执行的,那么我们就在load处打一个断点,来看看一下load之前还有没有什么操作。
    load 断点

    通过bt指令,在控制台打印堆栈信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 2.1
  * frame #0: 0x0000000104ea9d8c test`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
    frame #1: 0x00000001a922c25c libobjc.A.dylib` + 944
    frame #2: 0x0000000104ef621c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
    frame #3: 0x0000000104f075e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
    frame #4: 0x0000000104f05878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
    frame #5: 0x0000000104f05940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
    frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216
    frame #7: 0x0000000104efb928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
    frame #8: 0x0000000104ef5208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
    frame #9: 0x0000000104ef5038 dyld`_dyld_start + 56

可以看到在[ViewController load]还有很多dyld的方法被执行。最早的一个是_dyld_start
这个时候就需要我们去下载dyld源码去分析一下了。源码地址

本次使用的是dyld-832.7.3

  • 2.2.1 _dyld_start
    我们在源码中搜索_dyld_start,会发现是由汇编实现的,我们找到arm64架构对应的代码如下:
    _dyld_start --- arm64架构

    看到这里大家可能会懵,这些汇编代码都是干什么用的?
    其实我们没必要每一句代码都弄清楚,我们只需要找到关键信息,理解其意图就可以了。首先我们会看到下面这一段代码以及注释:
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl  __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm

通过注释可以看到,这段代码进入的是dyldbootstrap::start这个函数;这里请注意,有没有感觉这个函数名有点熟悉。没错,我们在上面打印堆栈信息的时候,_dyld_start紧跟着的就是dyldbootstrap::start

  • 2.2.2 dyldbootstrap::start
    dyldbootstrap::start 是指 dyldbootstrap这个命名空间作用域内的start函数。
    cmd + Shift + o 搜索dyldbootstrap :
    dyldbootstrap

    在作用域内搜索start函数:
    start函数

    可以看到start函数的返回值是通过dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue)获得的;这是dyldmain函数。
  • 2.2.3 dyld::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)
{
......
......
......
}

由于main函数的代码过长,这里就不展示出来了,下面我们把关键的部分讲清楚就可以了。

  • 2.2.3-1 配置相关环境变量
    1️⃣ :获取相关环境信息
    配置环境变量

如上面的:
getHostInfo(mainExecutableMH, mainExecutableSlide);就是获取当前运行环境的架构信息。
sMainExecutableMachHeader = mainExecutableMH;设置MachHeader(这个我就不多做解释,不了解的同学可以看这里3、iOS强化 --- Mach-O 文件).
sMainExecutableSlide = mainExecutableSlide; 设置slide;这个slide就是ASLR计算出来的一个随机值,保证程序每一次运行的偏移值都不一样,防止黑客通过固定地址进行恶意攻击。
2️⃣ :设置上下文信息,检测进程是否受限
i、 调用setContext函数,传入MachHeader以及一些参数设置上下文。
iiconfigureProcessRestrictions检测进程是否受限,在上下文中做出对应的处理。

// _main函数
setContext(mainExecutableMH, argc, argv, envp, apple);
......
configureProcessRestrictions(mainExecutableMH, envp);

3️⃣ :配置环境变量

// 检查设置的环境变量
checkEnvironmentVariables(envp);
// 如果DYLD_FALLBACK为nil,将其设置为默认值
defaultUninitializedFallbackPaths(envp);
  • 2.2.3-2 共享缓存
    共享缓存

1️⃣ :检查是否开启了共享缓存 checkSharedRegionDisable(iOS下不会被禁用)。
2️⃣ :加载共享缓存库 mapSharedCache ---> loadDyldCache ;加载共享缓存有几种情况:
i :仅加载到当前进程 mapCachePrivate,模拟器仅支持加载到当前进程。
ii :共享缓存是第一次被加载,就去做加载操作 mapCacheSystemWide
iii :共享缓存不是第一次被加载,那么就不做任何处理。

loadDyldCache

  • 2.2.3-3 实例化主程序
    实例化主程序
// 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
    if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
        ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
        addImage(image);
        return (ImageLoaderMachO*)image;
    }
    
//  throw "main executable not a known format";
}

isCompatibleMachO((const uint8_t*)mh, path) --- 通过macho_header里面的magiccputypecpusubtype去检测是否兼容。


当检测通过之后,执行instantiateMainExecutable(实例化image),接着将image添加到镜像列表中(addImage(image))。

instantiateMainExecutable中,使用sniffLoadCommands来实例化主程序;下面我们来简单了解一下这个函数:

void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
                                            unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
                                            const linkedit_data_command** codeSigCmd,
                                            const encryption_info_command** encryptCmd)
{
    *compressed = false;
    *segCount = 0;
    *libCount = 0;
    *codeSigCmd = NULL;
    *encryptCmd = NULL;
        ......
        ......
        ......
// fSegmentsArrayCount is only 8-bits
    if ( *segCount > 255 )
        dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

    // fSegmentsArrayCount is only 8-bits
    if ( *libCount > 4095 )
        dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

    if ( needsAddedLibSystemDepency(*libCount, mh) )
        *libCount = 1;

    // dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
    if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
        *compressed = true;
}

compressed --- 根据LC_DYLD_INFO_ONLY 来决定。
segCount --- MachO文件中segment的数量,最大不能超过 255 个。
libCount --- 依赖库数量,最大不能超过 4095个。
codeSigCmd --- 应用签名。
encryptCmd --- 应用加密信息

  • 2.2.3-4 加载插入动态库
    插入动态库

    ○ 首先利用DYLD_INSERT_LIBRARIES环境变量来判断,是否需要插入动态库。
    ○ 然后调用loadInsertedDylib加载插入动态库。
    ○ 最后记录插入动态库的数量,sInsertedDylibCount = sAllImages.size()-1;
    插入动态库这整个是一个名字,这样的机制给我逆向的时候的代码注入提供了可能。
  • 2.2.3-5 链接主程序
    链接主程序
  • 2.2.3-6 链接动态库
    链接动态库
  • 2.2.3-7 符号绑定
    sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true); 递归绑定符号表
    sMainExecutable->weakBind(gLinkContext); 弱符号绑定
  • 2.2.3-8 执行初始化方法
    我们在上面打印的函数调用栈里面,main之后就是:
frame #6: 0x0000000104ef66d8 dyld`dyld::initializeMainExecutable() + 216

我们进入initializeMainExecutable内部看一下:

initializeMainExecutable

我们发现initializeMainExecutable内部有一个循环遍历,每次都会执行runInitializers
cmd + shift + o 搜索runInitializers,跟进去:
runInitializers

发现runInitializers中又调用processInitializers为初始化做准备。那么我们跟进processInitializers:
processInitializers

注意看processInitializers里面的for循环recursiveInitialization递归初始化镜像。

同样的搜索recursiveInitialization,跟进去:

recursiveInitialization

我们会发现,在recursiveInitialization中关键的两步是:
i : 初始化镜像 --- doInitialization
ii :镜像初始化完成后,发送广播通知 --- notifySingle

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* 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::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 ) { // 是否被映射
        //  Save load addr + UUID for images from outside the shared cache
        //  Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
        if (!image->inSharedCache()
            || (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
            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)(image->getRealPath(), image->machHeader());

全局搜索sNotifyObjCInit,并没有发现其函数实现,但是我们找到了它的赋值操作:


这样我们继续跟进,查找registerObjCNotifiers在哪里被调用了:

⚠️⚠️⚠️ :这个时候,我们发现在函数_dyld_objc_notify_register中,registerObjCNotifiers被调用。_dyld_objc_notify_register的函数调用,我们dyld源码中并没有找到,这个时候,我们需要去libobjc源码中去寻找。(本次使用的是objc4-818.2

  • objc4-818.2中我们找到了这个一段代码
    _objc_init

    我们仔细的比对会发现,_dyld_objc_notify_register传入的第二个参数是load_images,那么也就是说给sNotifyObjCInit赋的值就是load_images;而load_images会调用所有的+load方法。因此:notifySingle是一个回调函数
  • load_images
    load_images

    接着跟进call_load_methods:
    call_load_methods

    我们会发现call_load_methods的核心是通过do-while循环调用call_class_loads()。那我们就继续跟进:
    call_class_loads

    这里就可以很清晰的看到,最终循环调用了所有的+load方法。

这个时候我们来梳理一下load的函数调用栈:_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle --> sNotifyObjCInit --> load_images

_dyld_objc_notify_register_objc_init_objc_init
我们继续往下探索

doInitialization

doInitialization

我们会发现,doInitialization主要分成两部分:
idoImageInit
iidoModInitFunctions

  • doImageInit
    doImageInit

    进入doImageInit源码发现,其核心是一个for循环,注意看代码的注释,libSystem的初始化必须先执行
  • doModInitFunctions
    doModInitFunctions

    doModInitFunctions这个方法内部会调用全局C++对象的构造函数__attribute__((constructor))C函数
    这个我们可以通过测试Demo的函数调用栈来验证一下:

探索到这里,我们仍然没有发现_objc_init被调用的相关信息。这个时候,我们就需要下一个符号断点,来查看_objc_init对应的堆栈信息了。



符号断点埋下之后,运行程序,bt打印堆栈信息:

大家观察此时的函数调用栈,是不是清晰了很多。

我们先来捋一下_objc_init的函数调用栈(此时用的是模拟器,整理函数调用栈的时候,省略了模拟器相关的一些函数调用):_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> ImageLoaderMachO::doInitialization --> ImageLoaderMachO::doModInitFunctions --> libSystem_initializer(libSystem.B.dylib) --> libdispatch_init(libdispatch.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)

这里我们一起来回忆一下,在初始化_objc_init的时候,调用_dyld_objc_notify_registerload_images被注册;在我们分析notifySingle的时候,发现了重要的函数sNotifyObjCInit,并且我们之后还发现 sNotifyObjCInit = init = load_images。这样就形成了一个闭环。

  • 2.2.3-9 寻找主程序入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();

探索了那么多,我们来串一下dyld::main的调用流程:

dyld::main

recursiveInitialization

  • 参考文档
    iOS-底层原理 15:dyld加载流程
    iOS探索 浅尝辄止dyld加载流程
    iOS 底层 - 从头梳理 dyld 加载流程

你可能感兴趣的:(iOS底层探索 --- dyld加载流程)