面试宝点

dyld:启动流程解析

2021-06-17  本文已影响0人  康小曹

前言

dyld2 和 dyld3 的主要变化体现在源码上就是 dyld-400 和 dyld-600的版本,比如较低版本的模拟器采用的仍然是 dyld-433 的版本,而 iOS12 之后的真机基本上都采用 dyld-655 以后的版本。

dyld3 在很早就引入,但是一开始只用于 Apple 相关的 App 或者系统库(库还是 App 有待考究)。而在 iOS13 之后,dyld3 正式替代 dyld2,用于加载所有的 App。

dyld-433 版本的源码是比较纯粹 dyld2 的逻辑,而 dyld-655 就能看到很多 dyld3 的优化代码了。dyld3 在流程上有所改进,且源码上也有了很多变化,但是 dyld2 仍然是基础,源码的参考价值仍然比较高,因此本文采用 dyld-433.5 的版本研究 dyld2 的基础流程。偶尔也会对比 dyld-655.1.1 和 dyld-733.6的版本;

一、dyld自举

一般而言,bootstrap 操作由 crt 或者 dyld 来完成,而 dyld 需要自己完成这个操作,所以称为 dyld 的自举;

这个过程包含以下步骤:

1. dyld 的重定位

重定位操作在 dyld-433 的版本中逻辑相对简单:

// 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 都需要进行 rebase,而 rebaseDyld 函数就是完成这个操作;

dyld 未引用其他的共享库,所以 dyld 中的非懒加载符号都是指向自己内部的,需要在这个过程中进行 rebase,另外就是重定位表中的符号需要进行 rebase,所以这个过程步骤为:

  1. 找出 LC_DYSYMTAB__LINKEDIT 这两个 Load Command,获取符号表的偏移;
  2. 对非懒加载表和符号表进行 rebase;
  3. 取出 relocation 对应位置的指针,加上 slide,进行 rebase;

具体代码就不贴了,贴张例图吧:

rebaseDyld

如上图,还未加载进入内存时,mh_header + offset 就等于 Dynamic Synbol Table 的位置,这个位置再加上 Slide 就是内存中动态符号表的位置了。

符号主要分为两种:内部符号和外部符号。外部符号是指本 Mach-O 之外的符号,保存在 Indirect Symbols 中,会经常看到链接之前的单个 Mach-O 会有很多外部符号。链接之后的外部符号一般来自系统的动态库,比如 NSLog。例子:

image.png

内部符号一般存储在 Symbol 表中,比如:

image.png

因为 dyld 未引用其他动态库,其内部的 Indirect Symbols 都指向自己:

image.png

至于为什么不把这些 Indirect Symbol 直接放到 Symbol Table 中呢?可以观察到这些 Indirect Symbols 内部指向的是非懒加载符号表,所以猜测这些符号需要在链接过程中就初始化,负责 dyld 无法完成后续工作。而那些 Symbols 中的符号可以在用到时再初始化(懒加载)。

rebase 操作算是自举中最重要的步骤。因为如果不进行 rebase,dyld 被加载到虚拟缓存中的所有地址都是相对于 mach-o 文件在磁盘中的位置来计算的,即默认计算方式为 slide 等于 0。如果不 rebase,虚拟内存中的函数地址,全局变量的地址等等都是不正确的,直接进行访问肯定是不正确的,所以自举完成之后 dyld 才能使用自己内部的各种代码和数据。

dyld-655 版本中除了对非懒加载表重定位表的 rebase 操作,还新增了 opcode 的处理,这个暂时还不知道是啥,暂不深究~~

2. 初始化 mach 和参数

这里相对简单,就是直接调用函数:

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

具体什么含义,以后再看~~

3. 栈保护

// set up random value for stack canary
__guard_setup(apple);

4. 调用 dyld 初始化函数

#if DYLD_INITIALIZER_SUPPORT
    // run all C++ initializers inside dyld
    runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

这里就是调用 dyld 内部的 C++ 的初始化函数,也就是调用被 __attribute__((constructor)) 修饰的函数。大家最熟悉的 objc_init 也是通过这种方法来调用的。这里先不深究,后面的流程中还会重点讨论;

5. 获取主工程 slide 并调用 _main 函数;

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

至此,dyld 的自举流程完毕,可以正式开始 dyld 的工作了。这里先获取主工程在内存中的起始位置,然后调用 dyld::main 函数正式开始 dyld 的工作;

其实,这里可以看下 slideOfMainExecutable 这个函数的源码:

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;
            }
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
    return 0;
}

该函数的逻辑为:

  1. 遍历 load command 获取第一个 filesize 不为 0 的 segment 对应的 command;
  2. 获取 segCmd->vmaddr;
  3. 计算 slide = mh - segCmd->vmaddr;

所以,即使存在 __PAGEZERO 这个 Segment,最后也不会依据其 vmaddr 来计算 slide,这是因为 __PAGEZERO 对应的 filesize 为 0。而更深层次的原因是因为 mh_header 和 load command 都是存储在 __TEXT 中,也就是 __text 这个 section 之前;

还有一点需要注意,到此时有且仅有可执行文件(主工程)和 dyld 的 mach-O 文件被加载进入虚拟内存,可以使用 image list 进行查看:

image list

这里需要区分后面步骤的实例化主程序,实例化主程序是解析虚拟内存中的 mach-O 文件并且实例化一个 image 对象,以支持后面对主程序的 link 等操作。而 addImage 也是发生在实例化主程序的过程中,即: addImage 和 实例化主程序都不代表主程序的加载,主程序文件的加载在 dyld 自举之前已经完成;

二、_main 函数

此函数才是 dyld 对可执行文件的主要操作函数,其大致步骤为:

1. 模拟器的处理逻辑

#if __MAC_OS_X_VERSION_MIN_REQUIRED
    // if this is host dyld, check to see if iOS simulator is being run
    const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
    if ( rootPath != NULL ) {
        // Add dyld to the kernel image info before we jump to the sim
        notifyKernelAboutDyld();

        // look to see if simulator has its own dyld
        char simDyldPath[PATH_MAX]; 
        strlcpy(simDyldPath, rootPath, PATH_MAX);
        strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
        int fd = my_open(simDyldPath, O_RDONLY, 0);
        if ( fd != -1 ) {
            const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
            if ( errMessage != NULL )
                halt(errMessage);
            return result;
        }
    }
#endif

如上代码,检测到如果是模拟器,则直接使用了 my_open 加载固定路径下的 dyld_sim 程序,并且直接将执行结果返回,_main 函数;这也是为什么在模拟器上跑程序,紧跟 dyld 的是 dyld_sim:

dyld_sim

2. 设置环境变量

这里就不赘述了,开启启动日志的设置就是在这个阶段判断并起作用;常用的两个:

DYLD_PRINT_ENV:打印环境信息;
DYLD_PRINT_STATISTICS_DETAILS:打印启动信息,如时间等;

3. 实例化主程序

来看下实例化的代码:

// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

继续看 instantiateFromLoadedImage 这个函数:

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

这里需要注意几点:

  1. 注释上写的很清除了,实例化操作就是从已经映射到虚拟内存中的主程序来实例化一个 ImageLoaderMachO 对象;
  2. addImage 不代表加载,只是代表向全局数组中添加主 image,这个数组和 image list 展示的 image 内容没有关系;

后面的实例化函数就不具体看了,总结下来实例化的过程就是按照 Mach-O 文件的规则来解析整个 mach-O 文件并以对象的形式保存下来,以供后续的使用。

具体解析规则就不赘述了,不了解的可以看 mach-O文件结构分析

后面的插入动态库的实例化、依赖库的实例化都会有这个过程。和主程序实例化不同的是,这些过程中包含 load 进入内存的过程,而主工程不需要,主工程在进程(App)启动时就被加载进入内存了;

dyld3 的主要优化点之一就是将 mach-O 文件的解析结果保存,下次启动时直接读取而不用重复解析 mach-O,以此来节省启动时间,这些优化点在 dyld-655 上也有体现,感兴趣的可以看看;

4. 加载共享缓存信息

这里不是直接把共享缓存加载进入虚拟内存(用脚也想得到),而是获取当前共享缓存中共享库相关的信息,以备后续使用:

// load shared cache
checkSharedRegionDisable();

#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
    mapSharedCache();
} else {
        ......
}
#endif

checkSharedRegionDisable 方法源码的注释中写到 iPhoneOS cannot run without shared region,此时该方法啥也没干,所以 iOS 中必须开启共享缓存:

共享缓存

操作系统会将常用的系统动态库存入内存中特定的位置,这样多个程序都使用到这个动态库时,就不需要重复加载了,也不需要每个进程都 Copy 一份,优化了启动时间,同时节省了内存。

和系统动态库不同的是,我们自己生成并嵌入到工程中的动态库依然会被 Copy 到内存且和主工程是两个 Image,内存位置独立。所以可以通过这种方法来规避一些符号表重复的问题。比如主工程中使用到了 FMDB,而 SDK 中也使用到了 FMDB,如果 SDK 是静态库,SDK 在链接阶段会以二进制的形式复制到主工程中,此时就会报符号重复的错误。将 SDK 改成动态库,静态链接阶段,动态库还没有被链接。动态链接阶段,动态库以独立的 Image 形式被加载进入内存,也就不会符号重复了。

dyld-433 和 dyld-655 中实例化主程序和加载共享缓存的顺序不一致,具体原因未知,以后再深究~~~

5. 加载插入的动态库

代码如下:

// load any inserted libraries
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;

如上,根据 sEnv.DYLD_INSERT_LIBRARIES 参数来加载插入的动态库,这里实际使用就不演示。

Apple 会通过插入动态库的功能来做一些额外的支持,比如调试功能。在 scheme 中设置 DYLD_PRINT_ENV 即可以打印环境信息:

DYLD_PRINT_ENV

总结:

6. 链接主程序

该步骤的真实作用代码在 ImageLoader::link 中,精简如下:

// 递归加载依赖的动态库
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

// 递归刷新依赖库层级
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());

// 递归rebase
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased, false);

// 递归bind
this->recursiveBind(context, forceLazysBound, neverUnload);

// weakBind
if ( !context.linkingMainExecutable )
    this->weakBind(context);
context.notifyBatch(dyld_image_state_bound, false);
......

这是 _main 函数中的重头戏,也就是动态链接的过程。因为静态库在静态链接时期就以二进制的形式被 Copy 到了主工程,所以动态链接的过程基本都是针对动态库的,系统的动态库占大头。

这个过程分为:

  1. 递归加载依赖库;

这一步会加载主工程所有的依赖库。使用 context.inSharedCache(requiredLibInfo.name) 优先从共享缓存中查找,如果没有则最终会走到 ImageLoaderMachO::instantiateFromFile 方法,即从磁盘上加载动态库;

到这一步,App 执行所需要的所有代码都已经被加载进入了虚拟缓存中了,后续不会再有增量代码。即动态链接器 dyld 可以获取到所有符号相关的信息;

严格意义上来讲,后面还有插入的动态库的链接操作,仍然会加载新的代码进入虚拟内存。只不过插入的动态库中的功能是 Apple 自己用于做一些支持操作的,和 App 的功能相对独立,代码无关;

  1. 递归刷新层级;

这一步就是刷新依赖库的层级,按照注释,其目的是让被依赖的库在列表的前面,应该是为了后面的 rebase、rebind 操作做铺垫,否则依赖层级过于混乱,后面的步骤就需要很多条件判断或者重复操作。

  1. 递归 rebase;

rebase 第一步是找到 Dynamic Loader Info 的地址:

rebase

基地址 + rebase_off 得到 Dynamic Loader Info 表的实际位置,其中基地址就是 __LINKEDIT 之前的 segment 在 vm 上相对于 file 中多出的 size,rebase_off 是指 Dynamic Loader Info 表在文件中相对于起始位置的偏移;

Load Command 中有一个 dyld_info_command,该 command 记录了 Dynamic Loader Info 表的偏移以及单个 rebase info 的大小:

dyld_info_command

除了 weak bind,所有需要进行 rebase 的位置信息都存储在 Dynamic Loader Info 表中:

rebase info

该步骤就是根据该表中的信息对指定位置进行 rebase,而 weak bind 则在后面单独出来;

opcode 的代码主要是对 opcode 相关数据的解码,具体用法暂不深究

  1. 递归 bind;

bind 主要是依据上一步中提到的 Dynamic Loader Info 表中的 binding info 进行符号绑定,ImageLoaderMachOCompressed::eachBind 的主要代码如下:

eachBind

于上一步不同的是,rebase 是去替换 __TEXT 段中对懒加载/非懒加载符号的调用时使用的指针,而 bind 则是找到函数的实际地址后,去替换懒加载/非懒加载表中指针具体的值;

  1. weakBind;

其实在 Link 主工程时不会进行 weak bind,因为设置了 linkingMainExecutable 为 true:

linkingMainExecutable

在 weak bind 之前进行了判断:

weak bind

只有在插入的动态库完成链接之后才进行 weak bind:


weak bind

什么是 weak bind?后文会讲~~

  1. 通知

略~

至此,主工程动态链接完毕,其所依赖的动态库的链接也全部完毕,dyld 已经完成了大部分工作;

7. 链接插入的动态库

这里不难理解,插入了动态库当然需要链接插入的动态库。其过程和主工程的链接大同小异,不再赘述;

8. 调用初始化函数

代码精简如下:

void initializeMainExecutable()
{
    // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }
    
    // run initializers for main executable and everything it brings up 
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
    
...print代码省略...
}

如上代码:

  1. 先从 index = 1 开始执行每个依赖库的初始化函数;
  2. 执行主 image 的初始化函数;

doModInitFunctions 源码精简如下:

// 省略 S_MOD_INIT_FUNC_POINTERS 的寻找流程
......

Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);

for (size_t j=0; j < count; ++j) {
    Initializer func = inits[j];

    if ( ! dyld::gProcessInfo->libSystemInitialized ) {
        // <rdar://problem/17973316> libSystem initializer must run first
        const char* installPath = getInstallPath();
        if ( (installPath == NULL) || (strcmp(installPath, LIBSYSTEM_DYLIB_PATH) != 0) )
            dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
    }

    func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}

初始化函数的寻找逻辑如下:

mod_init_func

其步骤为:

  1. 遍历 load command,找到 segment 类型的 command;
  2. 遍历 segment command 中的 section command,找到 S_MOD_INIT_FUNC_POINTERS 对应的 section command;
  3. 根据 offset 找到 __mod_init_func 表,遍历表中的函数,经过一些判断之后执行;

上述代码需要注意的是:

  1. 省略了很多判断代码,但是保留了 libSystem initializer;
  2. libSystem 可以看做是包含了很多系统库的一个包装库,其初始化函数需要优先被调用,其中就包括 objc 的初始化;
  3. 寻找 section command 的代码看太多了,都是一样的,就省略了;

最后,来看看 _objc_init 完整的调用栈做下收尾吧:

_objc_init调用栈

按照代码注释,libsystem初始化函数需要优先被调用,但是符号断点打在 dyld_simImageLoaderMachO::doModInitFunctions 中时,实际测试发现并不是第一个 doModInitFunctions 就走到了 libSystem.B.dyliblibSystem_initializer 的断点,即:第一个执行初始化方法的动态库不是 libSystem.B.dylib,猜测可能是其依赖库;但是对汇编不是很溜,没找到打印出 image 具体信息的方法o(╯□╰)o,暂不深究吧~~~

9. 执行 main 函数

至此,所有的准备工作已经完成,可以查找 App 的入口函数正式执行程序了:

// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();

// find entry point for main executable
result = (uintptr_t)sMainExecutable->getThreadPC();

if ( result != 0 ) {
    // main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
    if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
        *startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
    else
        halt("libdyld.dylib support not present for LC_MAIN");
} else {
    // main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
    result = (uintptr_t)sMainExecutable->getMain();
    *startGlue = 0;
}

上述代码首先通过 getThreadPC 函数寻找入口函数的指针,如果有返回值,则进行跳转,而 getThreadPC 的代码如下:

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) {
        if ( cmd->cmd == LC_MAIN ) {
            entry_point_command* mainCmd = (entry_point_command*)cmd;
            void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
            // <rdar://problem/8543820&9228031> verify entry point is in image
            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;
}

如上代码,可以很清楚的看到通过 LC_MAIN 来查找入口函数指针并返回,在 Mach-O 上可以很直观的看到:

LC_MAIN

获取到指针之后经过一些判断就开始执行了,而这个关键函数就是 gLibSystemHelpers->startGlueToCallExit;,这是个什么呢?这个函数被定义在 LibSystemHelpers 结构体中:

startGlueToCallExit

其实 LibSystemHelpers 这个结构体在很多地方起到作用,比如刚刚初始化函数的执行时,在 doModInitFunctions 函数中就根据这个结构体来判断了 libsystem 的初始化函数是否有被执行:

libSystemHelper

那么这个结构体在什么时候被赋值的呢?全局查找到如下函数:

static void registerThreadHelpers(const dyld::LibSystemHelpers* helpers)
{
    dyld::gLibSystemHelpers = helpers;
    
#if !SUPPORT_ZERO_COST_EXCEPTIONS
    if ( helpers->version >= 5 )  {
        // create key use by dyld exception handling
        pthread_key_t key;
        int result = helpers->pthread_key_create(&key, NULL);
        if ( result == 0 )
            __Unwind_SjLj_SetThreadKey(key);
    }
#endif
}

很明显,可以打个符号断点来确定:

符号断点

运行之后:

断点

结论:该函数在 libsystem 初始化函数中被赋值;

至于startGlueToCallExit具体代码肯定是在 libsystem 里面了,自然是看不到了~~~

LibSystemHelpers 这个结构体在 dyld 中很多地方被使用,估计这就是为什么需要优先调用 libsystem 的初始化方法的原因吧~~

总结下初始化方法的优先级和顺序吧:

  1. libsystem 依赖的库最先执行初始化方法;
  2. 依赖库的初始化方法执行完毕之后,libsystem 执行初始化方法,优先级很高;
  3. libsystem 的初始化方法中完成了很多初始化操作,如 LibSystemHelpers,还有例如 _objc_init 等的调用;
  4. libsystem 的初始化方法调用完毕之后执行主工程依赖库的初始化方法;
  5. 依赖库的初始化方法执行完毕之后,执行主工程的初始化方法;

至此,dyld 的流程全部分析完毕。

三、几点补充

1. weak bind

关于弱符号的解释:

若两个或两个以上全局符号(函数或变量名)名字一样,而其中之一声明为 weak symbol(弱符号),则这些全局符号不会引发重定义错误。链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。

默认情况下 Symbol 是 strong 的,weak symbol在链接的时候行为比较特殊:

  1. strong symbol 必须有实现,否则会报错;
  2. 不可以存在两个名称一样的 strong symbol;
  3. strong symbol 可以覆盖 weak symbol 的实现;

应用场景:用 weak symbol 提供默认实现,外部可以提供strong symbol 把实现注入进来,可以用来做依赖注入。

此外还有个概念叫 weak linking,这个在做版本兼容的时候很有用:比如一个动态库的某些特性只有iOS 10以上支持,那么这个符号在iOS 9上访问的时候就是 NULL 的,这种情况就可以用就可以用weak linking。

可以针对单个符号,符号引用加上weak_import 即可:

extern void demo(void) __attribute__((weak_import));
if (demo) {
    printf("Demo is not implemented");
}else{
    printf("Demo is implemented");
}

总结:

weak symbol 就是相对于 strong symbol 的一个优先级更低的符号,一般用来做依赖注入,而 weak bind 就是对这种符号进行绑定,猜测其大概流程是先判断强符号是否存在,没有则使用弱符号的地址,有则使用强符号的地址;

这也是为什么 weak bind 要在主工程和所有的依赖库全部被加载并绑定完毕之后才做 weak bind,因为只有在这个时候才能确定 weak symbol 是否存在覆盖的情况;

2. load函数的调用逻辑

image.png

感觉核心在 notifySingle 这个函数,首先回到初始化函数的调用逻辑上,在recursiveInitialization 函数中对 notifySingle 调用如下:

recursiveInitialization

上图可看出:

  1. 在初始化操作之前调用了一次 notify,根据注释可以看出,应该是即将初始化对应 image 的一个通知;
  2. 初始化操作之后之后,发送了初始化完成的通知;

这里的重点在第一次 notify 的 dyld_image_state_dependents_initialized,来看看 notifySingle 函数中的关键代码:

if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
    uint64_t t0 = mach_absolute_time();

    (*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);
    }
}

不看 time 相关的代码,关键代码逻辑就是:

  1. 判断 sNotifyObjCInit 是否存在;
  2. 存在则执行 sNotifyObjCInit,传递 image 的 path 和 mh_header 的地址;

那么 sNotifyObjCInit 是个啥?全局搜一下找到:

registerObjCNotifiers

而 registerObjCNotifiers 又是啥呢?将继续全局搜索:

_dyld_objc_notify_register

很明显,这个是 Api,也就是 dyld 供其他库调用的函数,所以接下来直接去 objc 的源码搜索这个 Api,只有一个结果:

_objc_init

其实 fishhook 也是用到了该文件下的 Api,只不过是 _dyld_register_func_for_add_image函数,该方法是添加 image 相关的回调,大概逻辑有点类似,具体就不赘述了;

总结下逻辑:

  1. libobjc.dylib 在初始化函数 _objc_init 调用 dyld 的 Api 设置了依赖库被加载时的回调;
  2. 依赖库即将被加载时,触发回调;
  3. 回调执行预先设置的函数,也就是 objc 中的 load_images 函数;
  4. load_images 函数执行 objc 的类加载的逻辑,触发 +load 方法的调用;

其实这里挺重要的,必须在所有依赖库的初始化函数执行之前进行 objc 的 load 程序;因为该 image 初始化函数可能使用了自身定义的类,而 load 函数就是将该 image 的 objc 类加载进入 runtime,如果不优先执行 load 操作,那么执行初始化方法时可能因为找不到对应的类而出错;

四、dyld3 相关

这块研究不多,贴 dyld-655 或者 dyld-750 的代码意义也不大,暂略吧~

上一篇下一篇

猜你喜欢

热点阅读