dyld:启动流程解析
前言
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,所以这个过程步骤为:
- 找出
LC_DYSYMTAB
和__LINKEDIT
这两个 Load Command,获取符号表的偏移; - 对非懒加载表和符号表进行 rebase;
- 取出 relocation 对应位置的指针,加上 slide,进行 rebase;
具体代码就不贴了,贴张例图吧:
rebaseDyld如上图,还未加载进入内存时,mh_header + offset 就等于 Dynamic Synbol Table 的位置,这个位置再加上 Slide 就是内存中动态符号表的位置了。
符号主要分为两种:内部符号和外部符号。外部符号是指本 Mach-O 之外的符号,保存在 Indirect Symbols 中,会经常看到链接之前的单个 Mach-O 会有很多外部符号。链接之后的外部符号一般来自系统的动态库,比如 NSLog
。例子:
内部符号一般存储在 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;
}
该函数的逻辑为:
- 遍历 load command 获取第一个 filesize 不为 0 的 segment 对应的 command;
- 获取 segCmd->vmaddr;
- 计算 slide = mh - segCmd->vmaddr;
所以,即使存在 __PAGEZERO 这个 Segment,最后也不会依据其 vmaddr 来计算 slide,这是因为 __PAGEZERO 对应的 filesize 为 0。而更深层次的原因是因为 mh_header 和 load command 都是存储在 __TEXT 中,也就是 __text 这个 section 之前;
还有一点需要注意,到此时有且仅有可执行文件(主工程)和 dyld 的 mach-O 文件被加载进入虚拟内存,可以使用 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:
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";
}
这里需要注意几点:
- 注释上写的很清除了,实例化操作就是从已经映射到虚拟内存中的主程序来实例化一个 ImageLoaderMachO 对象;
- 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
即可以打印环境信息:
总结:
- 插入动态库和依赖库没有什么关系,其实是 Apple 为自己预留的功能,Apple 通过这个功能实现了 MainThreadCheck 和栈帧查看的调试功能。只不过这个入口后来被用在了逆向技术上,逆向技术上,可以使用这个入口来调试其他 App 、查看 App 的布局等;
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 到了主工程,所以动态链接的过程基本都是针对动态库的,系统的动态库占大头。
这个过程分为:
- 递归加载依赖库;
这一步会加载主工程所有的依赖库。使用 context.inSharedCache(requiredLibInfo.name)
优先从共享缓存中查找,如果没有则最终会走到 ImageLoaderMachO::instantiateFromFile
方法,即从磁盘上加载动态库;
到这一步,App 执行所需要的所有代码都已经被加载进入了虚拟缓存中了,后续不会再有增量代码。即动态链接器 dyld 可以获取到所有符号相关的信息;
严格意义上来讲,后面还有插入的动态库的链接操作,仍然会加载新的代码进入虚拟内存。只不过插入的动态库中的功能是 Apple 自己用于做一些支持操作的,和 App 的功能相对独立,代码无关;
- 递归刷新层级;
这一步就是刷新依赖库的层级,按照注释,其目的是让被依赖的库在列表的前面,应该是为了后面的 rebase、rebind 操作做铺垫,否则依赖层级过于混乱,后面的步骤就需要很多条件判断或者重复操作。
- 递归 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 相关数据的解码,具体用法暂不深究
- 递归 bind;
bind 主要是依据上一步中提到的 Dynamic Loader Info 表中的 binding info 进行符号绑定,ImageLoaderMachOCompressed::eachBind
的主要代码如下:
于上一步不同的是,rebase 是去替换 __TEXT 段中对懒加载/非懒加载符号的调用时使用的指针,而 bind 则是找到函数的实际地址后,去替换懒加载/非懒加载表中指针具体的值;
- weakBind;
其实在 Link 主工程时不会进行 weak bind,因为设置了 linkingMainExecutable
为 true:
在 weak bind 之前进行了判断:
weak bind只有在插入的动态库完成链接之后才进行 weak bind:
weak bind
什么是 weak bind?后文会讲~~
- 通知
略~
至此,主工程动态链接完毕,其所依赖的动态库的链接也全部完毕,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代码省略...
}
如上代码:
- 先从 index = 1 开始执行每个依赖库的初始化函数;
- 执行主 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其步骤为:
- 遍历 load command,找到 segment 类型的 command;
- 遍历 segment command 中的 section command,找到
S_MOD_INIT_FUNC_POINTERS
对应的 section command; - 根据 offset 找到
__mod_init_func
表,遍历表中的函数,经过一些判断之后执行;
上述代码需要注意的是:
- 省略了很多判断代码,但是保留了 libSystem initializer;
- libSystem 可以看做是包含了很多系统库的一个包装库,其初始化函数需要优先被调用,其中就包括 objc 的初始化;
- 寻找 section command 的代码看太多了,都是一样的,就省略了;
最后,来看看 _objc_init
完整的调用栈做下收尾吧:
按照代码注释,libsystem初始化函数需要优先被调用,但是符号断点打在 dyld_sim
ImageLoaderMachO::doModInitFunctions 中时,实际测试发现并不是第一个 doModInitFunctions 就走到了 libSystem.B.dylib
libSystem_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 结构体中:
其实 LibSystemHelpers
这个结构体在很多地方起到作用,比如刚刚初始化函数的执行时,在 doModInitFunctions
函数中就根据这个结构体来判断了 libsystem 的初始化函数是否有被执行:
那么这个结构体在什么时候被赋值的呢?全局查找到如下函数:
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 的初始化方法的原因吧~~
总结下初始化方法的优先级和顺序吧:
- libsystem 依赖的库最先执行初始化方法;
- 依赖库的初始化方法执行完毕之后,libsystem 执行初始化方法,优先级很高;
- libsystem 的初始化方法中完成了很多初始化操作,如 LibSystemHelpers,还有例如 _objc_init 等的调用;
- libsystem 的初始化方法调用完毕之后执行主工程依赖库的初始化方法;
- 依赖库的初始化方法执行完毕之后,执行主工程的初始化方法;
至此,dyld 的流程全部分析完毕。
三、几点补充
1. weak bind
关于弱符号的解释:
若两个或两个以上全局符号(函数或变量名)名字一样,而其中之一声明为 weak symbol(弱符号),则这些全局符号不会引发重定义错误。链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。
默认情况下 Symbol 是 strong 的,weak symbol在链接的时候行为比较特殊:
- strong symbol 必须有实现,否则会报错;
- 不可以存在两个名称一样的 strong symbol;
- 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
调用如下:
上图可看出:
- 在初始化操作之前调用了一次 notify,根据注释可以看出,应该是即将初始化对应 image 的一个通知;
- 初始化操作之后之后,发送了初始化完成的通知;
这里的重点在第一次 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 相关的代码,关键代码逻辑就是:
- 判断 sNotifyObjCInit 是否存在;
- 存在则执行 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 相关的回调,大概逻辑有点类似,具体就不赘述了;
总结下逻辑:
- libobjc.dylib 在初始化函数 _objc_init 调用 dyld 的 Api 设置了依赖库被加载时的回调;
- 依赖库即将被加载时,触发回调;
- 回调执行预先设置的函数,也就是 objc 中的
load_images
函数; -
load_images
函数执行 objc 的类加载的逻辑,触发 +load 方法的调用;
其实这里挺重要的,必须在所有依赖库的初始化函数执行之前进行 objc 的 load 程序;因为该 image 初始化函数可能使用了自身定义的类,而 load 函数就是将该 image 的 objc 类加载进入 runtime,如果不优先执行 load 操作,那么执行初始化方法时可能因为找不到对应的类而出错;
四、dyld3 相关
这块研究不多,贴 dyld-655 或者 dyld-750 的代码意义也不大,暂略吧~