首先,大家应该都知道 _objc_init
函数是 OC 中类加载比较关键的一个函数,这个函数的调用栈如下:
那么,objc_init
这个函数是如何被调用的呢?又和 OC 中的类加载有什么关系?类又是如何被加载并以什么形式存在于运行时呢?OC 中的成员变量、方法、协议、分类,这些都是如何实现的?
1. objc_init 的调用流程
从调用栈可以看到,_objc_init
起始于 doModinitFunctions
这个方法。这个方法在 dyld 中,因为 dyld3 都已经在 iOS12 被全面使用了,dyld-433 仍然是 dyld2 的版本,dyld-655 已经是 dyld3 的版本了,所以这里以 dyld-655 的源码来探索 _objc_init
的调用流程。
首先,doModinitFunctions
这个函数属于 dyld 流程的“初始化方法调用”阶段。这一阶段是整个流程的倒数第二步,也就是执行 main 函数之前的阶段。
dyld 详细流程见dyld:启动流程解析。
doModinitFunctions
函数在 ImageLoader::recursiveInitialization
中被调用,关键代码如下:
先看看 doInitialization
方法的逻辑:
bool ImageLoaderMachO::doInitialization(const LinkContext& context) {
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
很明显,关键逻辑在于 doImageInit
和 doModInitFunctions
这两个函数。
doImageInit
内部经过逻辑主要是找出该 Image 对应的 mach-O 文件中 LC_ROUTINES
表内的函数进行调用:
LC_ROUTINES
的定义可以直接在 mach-o 库的 loader.h 中看到,如果 dyld 源码中无法跳转,可以在自己的项目中 import <mach-o/loader.h>
来看到具体的内容:
/*
* The routines command contains the address of the dynamic shared library
* initialization routine and an index into the module table for the module
* that defines the routine. Before any modules are used from the library the
* dynamic linker fully binds the module that defines the initialization routine
* and then calls it. This gets called before any module initialization
* routines (used for C++ static constructors) in the library.
*/
struct routines_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_ROUTINES */
uint32_t cmdsize; /* total size of this command */
uint32_t init_address; /* address of initialization routine */
uint32_t init_module; /* index into the module table that */
/* the init routine is defined in */
uint32_t reserved1;
uint32_t reserved2;
uint32_t reserved3;
uint32_t reserved4;
uint32_t reserved5;
uint32_t reserved6;
};
根据注释来看,LC_ROUTINES
大概就是动态库在调用初始化函数之前需要被调用的函数。找了几个动态库,也没有找到包含 LC_ROUTINES
这个 load command 的动态库,暂时不深究吧~~~
紧接着,就来到了调用栈上最初的 doModInitFunctions
函数了,这个函数做了这么几件事:
- 递归寻找 Load Command,找到
S_MOD_INIT_FUNC_POINTERS
这个 section 对应的 Load Command; - 根据 slide 计算
S_MOD_INIT_FUNC_POINTERS
的具体位置,并且取出这个表中的函数指针; - 进行一系列判断之后调用这些函数;
- 在函数调用前后进行判断,如果函数调用使得
dyld::gLibSystemHelpers
有值了,证明 libSystem 初始化完成,此时将dyld::gProcessInfo->libSystemInitialized
标志置为 true;
关键代码:
简而言之:
- dyld 在动态链接完成之后会执行所有动态库的初始化函数,最后执行主工程的初始化函数;
- 初始化函数需要使用
__attribute__
修饰,编译器识别之后会存储在 Mach-O 文件的__mod_init_func
中; - 因为 libSystem 是一系列系统库的集合,被很多动态库依赖,优先级更高,libSystem 的初始化函数会在比较靠前的顺序开始执行(不是第一)。而 objc 就被包含在这个库中。objc 库的初始化方法
objc_init
就是在 libSystem 的初始化函数中被调用; -
objc_init
方法中包含了 OC 类的加载逻辑;
至此,可以做个阶段性总结了:
- dyld 初始化函数调用阶段会去递归调用 image 的初始化函数;
- libSystem 库在比较靠前的位置被调用,进而触发了
_objc_init
函数的调用;
2. _objc_init 方法做了什么
来看下 objc_init
方法里面的代码吧:
void _objc_init(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
// 环境初始化相关
environ_init();
// 线程相关
tls_init();
// objc库初始化方法调用,即objc库中被__attribute__修饰的方法
static_init();
// 暂无任何逻辑
lock_init();
// NSSetUncaughtExceptionHandler()的基础
exception_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
如上,两个点可以稍微关注下:
-
static_init();
方法调用了 objc 库内部的初始化方法。一般而言 image 的初始化方法在 dyld 的第八步中被调用,而 objc 则主动调用了自己的初始化函数,有兴趣的可以见后文; -
exception_init();
方法内部实现是 iOS 中使用NSSetUncaughtExceptionHandler()
的基础。该方法可以设置 crash 后的处理逻辑,也是早起友盟、bugly 等三方 crash 监控 SDK 获取 crash 堆栈信息的基础:
接着看代码,从 objc_init()
代码来看,貌似 objc 并没有进行类的加载?此时就需要关注 _dyld_objc_notify_register
以及对应的三个回调了,这个方法是怎么个逻辑?
3. dyld 和 objc 的联系
_dyld_objc_notify_register
这个方法由 dyld 提供,定义如下:
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
注释的大意是:
- 该方法专门为 objc-runtime 提供;
- 三个回调会在 image 被 mapped、unmapped、initialized 时分别被触发;
- dyld 在调用 mapped 这个回调时,会传递一个 image 数组,这些 image 都是 objc 相关的 image;
- objc 不需要再调用
dlopen()
方法来加载或者维持这些 image。后续有新的 image 被载入时,仍然会调用 mapped 相关的回调; - dyld 会在调用 image 初始化函数阶段触发 init 回调,而这个回调就是 objc 调用 +load 方法的时机;
紧接着,一一验证上述的注释。首先在 dyld 中找到这个函数:
_dyld_objc_notify_register
只是一个对外包装接口,关键方法在 registerObjCNotifiers
:
根据注释,dyld 会通过 notifyBatchPartial
函数触发 mapped 回调。因为 mapped 的回调被绑定到了 sNotifyObjCMapped
这个指针,所以我们看代码时只需要关注 sNotifyObjCMapped
的调用逻辑即可,来看看这个函数的关键代码
打个断点来验证:
咦?有点不一样?别慌,这个是用的 iOS15 的模拟器,很明显,dyld4 已经都被用上了。用 iOS12 的 iPhone7 看看:
完美,结论被完美验证~~~
即:
map_images()
是 objc 中类初始化的主要函数。该函数在 _objc_init()
调用 dyld 进行回调绑定时就会通过 notifyBatchPartial
被触发,进而 objc 会对当前所有 objc 相关的 image 进行类的初始化操作。
4. +load 函数的调用逻辑
+load
函数调用栈:
感觉核心在 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
又是啥呢?继续全局搜索:
很明显,又是 _dyld_objc_notify_register
函数,这个函数注册了三个回调,load_image
就是在 image 的初始化函数即将被调用之前会被触发的回调。
其实 fishhook 也是用到了该文件下的 Api,只不过是
_dyld_register_func_for_add_image
函数,该方法是添加 image 相关的回调,大概逻辑有点类似,具体就不赘述了,详见:iOS逆向:fishhook原理分析;
总结下逻辑:
- objc 在初始化函数
_objc_init
调用 dyld 的 Api 设置了依赖库被加载时的回调; - 依赖库即将被调用初始化方法时,通过通知触发回调;
- 回调执行预先设置的函数,也就是 objc 中的
load_images
函数; -
load_images
函数执行 objc 的类加载的逻辑,触发 +load 方法的调用;
另外,需要关注一点:notifySingle()
函数的触发时在初始化函数调用之前,也就是说,必须在所有依赖库的初始化函数执行 之前 (也就是两个通知的前者)进行 objc 的 +load
逻辑。
实现了
+load
方法的类会被添加到费懒加载类,在 map 的回调中就会调用realizeClassWithoutSwift
进行加载和初始化。在+load
调用之前,为了防止遗漏,仍然会进行一次realizeClassWithoutSwift
的调用。另外,+load
方法最初的设计目的是什么?
5. initialize 方法调用流程
6. 补充:objc 自己调用初始化函数
比较好玩的一点是:objc 库中的初始化函数是 objc 自己调用的,而不是 dyld。
这里首先要从我们经常涉及到的 objc_init
来说起:
该方法通过 static_init
方法完成了 objc 库中初始化方法的调用:
看看 getLibobjcInitializers
是个啥?
本质上是常见的 GETSECT
方法,但是这里最关键的是 __objc_init_func
。objc 用这个标记来在 mach-O 文件中来标识 objc 独有的初始化函数。
但是,初始化方法不是一般都存放在 __mod_init
这个 section 中吗? dyld 内部也是这个逻辑:
这个 S_MOD_INIT_FUNC_POINTERS
在 mach-o 相关的源码中:
实际测试结果:
结论:dyld 通过 mach-O 文件中的 __mod_init
这个 section 来记录并调用初始化函数;
看到这里会想当疑惑,难道 objc 的初始化方法不是 dyld 加载的?继续查找 objc 源码,看看 objc 对这个 __objc_init_func
做了什么? 在 markgc 的 main 函数中做了这么一个操作:
markgc 是个啥?猜测是个脚本之类的东西?markgc 的 main 函数最终会触发这个 dosect 方法。也就是说 markgc 这个程序在 objc 的 mach-O 文件生成之后(可以理解成被编译成了动态库之后),手动修改了初始化方法对应的 sectionName 和 type。
而 dyld 调用初始化方法是通过 mach-O 文件中的 __mod_init
这个 section 来完成调用的。objc 做了这么个骚操作之后, dyld 就不会(没办法,因为找不到对应的 section)在初始化阶段(倒数第二步,即调用 main 函数之前)去调用这些初始化函数了。
按照 Apple 给出的理由是,dyld 调用 objc 的初始化函数的时机太晚,主要是晚于 libc 的调用:
libc calls _objc_init() before dyld would call our static constructors, so we have to do it ourselves
总结:
- libc 可能被包装在了 libSystem 中,而 libc 需要调用 objc,且这个调用发生在 dyld 调用 objc 初始化函数之前,所以 objc 需要自己来调用初始化函数;
- objc 通过 markgc 程序将
__mod_init
修改为__objc_init_function
,从而适配自己的static_init
逻辑,同时也避免了 dyld 对 objc 初始化函数的重复调用;
7. 一个疑问
如上图,断点打在 ImageLoaderMachO::doModInitFunctions
时,libSystem 显然不是第一个 image,此时就有个疑问:
- 为什么自己嵌入的动态库
UPBase
那么靠前? - 如果按照这个 image list 顺序进行初始化调用,那么 UPBase 被初始化时肯定 libSystem 还没有初始化。虽然
map_images()
在后续被调用时会遍历所有 image,但是如果是涉及到 UPBase 中有初始化函数调用,那么此时 objc 仍然没有初始化的,这样会不会有问题? - 如果没问题,那逻辑是怎样的呢? dyld 是进行了 image 顺序调整,类似于依赖层级调整?或者说 image list 指令打印的不是当前 dyld 中的 image list 中的顺序?
解释:主工程 image 虽然在第一个,但是给到 objc 的 image 数组进行了顺序调整,且给过去之后 objc 是逆序读取 image,所以 objc 相关的 libSystem 库比较靠前,优先加载。
可以通过在 map_image_nolock
方法中打断点,然后查看寄存区,获取入参,通过打印入参的方式查看 image list 的排序:
因为字符串是多个字符加上 \0
组成,所以字符串本身就是一个数组,使用 char *
表示。而 path 是字符串数组,所以指向一个字符串数组,需要使用 char **
表示:
如上图可以看到,因为打印 path[306]
出了异常,所以数组总个数是 306。另外,可以直接打印 x0 寄存器就可以知道 imageCount,只不过上图没有空间显示了。
上图可以看到,逆序之后 libSystem
并不是处于第一个位置,所以库的优先级和初始化顺序大概有几种:
- 优先级大于 libSystem 的系统库;
- libSystem;
- 系统库,如 libobjc 等;
- 工程中插入的动态库;
- 主工程;
8. 总结
一张图做个总结吧: