底层探索 -- 类de加载过程分析(一)runtime初始化

660 阅读6分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

在之前几篇的几篇博客中,已经完成了对类的数据结构、方法查找的分析,本篇将开始对类的加载过程进行一些分析、记录。这个分析顺序的思路,举个大概的栗子就好像买了个毛坯房首先先了解了它的结构、布局如何,然后在将软硬装、家具等放进这个房子里。(本篇总字约2038字)

本篇重点:

  1. _objc_init 方法对于runtime环境的初始化、ReadImage、LoadImage的方法注册。
  2. Readimage 的加载过程、各种记录表的创建、对Cls的实现、Cls中的数据加载。
  3. Category 的附加过程、附加时机、附加算法。

一、runtime初始化

从静态代码 --> dyld链接image --> 加载进内存,代码是如何被装载到内存变成可以被调用、被执行的方法的?被创建的类中的ro、rw、rwe又是从哪里来的呢?带着疑问先从 _objc_init 着手开始分析:

38WQgwPlIMKTFoL2-TUVMbNQt1J_8jsEdbxdKd69VC0.png

首先从图左侧的调用堆栈可以看出,_objc_init 正好是从 _dyld_start汇编到C++源码的分界点,也是runtime的初始化节点。接下来先对_objc_init中的各个方法的作用做个总的说明,然后再分别展开分析:

  1. environ_init初始化环境变量。
  2. tls_init: **初始化 线程局部存储。**线程局部存储是操作系统为线程单独提供的私有空间,具有很有限的空间。
  3. static_init运行C++静态构造函数。
  4. runtime_init初始化unattachedCategoriesallocatedClasses 表。
  5. exception_init初始化异常处理环境。
  6. cache_t::init初始化缓存。
  7. _imp_implementationWithBlock_init启动回调机制。
  8. _dyld_objc_notify_registerdyld的注册回调。

1.1、environ_init

这个方法的主要作用是读取影响运行时的环境变量,如OBJC_DISABLE_NONPOINTER_ISA (只用RwaIsa)OBJC_PRINT_LOAD_METHODS (打印所有+Load) 等等,先进入方法内看一下代码:

ALoywjy7y6Op1Cp24xhTsMPocoN3y003nJfypUQE1pE.png

  1. 如图所示,整个方法最核心的代码就是红框标注的循环:读取并设置环境变量的值
  2. 最末尾For循环的则是根据OBJC_HELPPrintOptions等判断条件将变量设置的结果进行打印。
  3. 分析循环中代码可以看出来,无论上下哪个循环都是在对Settings[] 进行操作,那接着来看一下Settings又是什么结构?
    • Settings 是一个结构体数组,数组内包含objc-env.h 中定义的所有环境变量。
    • 除了在objc-env.h 中查看这些变量,还可以在Terminal中输入 export OBJC_HELP=1 来输出、命令查看。

ImPOGfljwsDRADRC2deG1js3Za6t3BwBZ5WGBXf6rEE.png

1、使用 环境变量

分析完environ_init 的代码逻辑,现在来记录一下如何使用这些变量。日常开发可以通过Xcode使用这些环境环境变量来帮助调试,可通过以下路径来编辑项目的 scheme,从而设置调试所需要的环境变量:

菜单Product --> Edit scheme --> 左侧Run栏目 --> 顶部Arguments --> Environment Variables

y1cmrBMAXUwxwnkmutyAwLrbNPVSZ6638otaOLqc2bU.png

  • 比如设置了OBJC_PRINT_LOAD_METHODS 环境变量,就可以将代码中实现过+[xxx load]的类统统输出到控制台中。

    ngIXz9ew9oznBvnC9dj9YW9UyJYGh2OMB2ZyLaxvzXU.png

1.2、static_init

这个方法的主要作用如其注释所描述:运行C++静态构造函数。但这里的C++构造函数指的并不是项目中所有的构造函数,而只是包含定义在libobjc.A.dyld 中的。至于其他的C++构造函数要等到dyld 中的ImageLoaderMachO::doModInitFunctions 方法调用是被执行。

注意一点:
为什么要在这么早的时机自己调用C++方法呢,而不等着dyld?
因为程序运行到_objc_init 时,已经在初始化整个runtime 运行环境了。其中有许多的C++构造函数比如defineLockOrder()_GLOBAL__sub_I_objc_class.mm_GLOBAL__sub_I_objc_errors.mm等等,都是runtime环境运行所必需使用的,所以这些构造函数都要提被执行。

现在来看一下static_init 的代码实现:

rx--d9jwSDmDiUVfovFKBn-EBTMHgJne2-JhuN389aE.png

  1. 如图标注,根据getLibobjcInitializersgetLibobjcInitializerOffsets 的内部实现可以确定读取的Section

  2. 对于getLibobjcInitializers 的读取区域可以在_objc_init 上方自定义一个C++方法,通过动态调试来比较取出来的count值的变化来验证。或者进一步将断点设置在自定仪的C++方法内,查看断点是否在循环的最后被执行。

    oJJ1DwW8VxCJ7meKL_qmfB3cWyjFsBsXiA5KwkHd9Ig.png

  3. 不过getLibobjcInitializerOffsets 在分析过程中读出的count总是0,offsets也总是NULL 并且在dylib中也确实没找到对应的Section。

    zvl_zkkCdkTtrpa-tM_9E3MU5JHVzx-8hR3vxsFzGUo.png

1.3、runtime_init

这个方法的主要作用就是对于两张表的初始化。先来看一下方法的代码实现,再分析这2张表分别要存储什么信息:

4Az5MxauEbCqD1fyE4mACGQ_dSWWLeKGFV17l4Q4upA.png

  • unattachedCategories:Class、继承自ExplicitInitDenseMap 类,其中有扩展方法addForClassattachToClasseraseClass等。这张表用于存放的是还没附加到Cls的Categorys。(之后分析到Category的附加时,再展开分析)
  • allocatedClassesExplicitInitDenseSet 的类对象,注释中对这个标的描述是:所有已经被objc_allocateClassPair 方法Alloc的类(包括元类)。这个表再次见到不陌生,在前面的博客[[3.2 lookUpImpOrForward分析 id=b6562723-b86d-4fee-953c-95901ff7ae19]] 时,慢速查找流程的准备、检查过程中在checkIsKnownClass 中就是向这个表中查找Cls,用于验证Cls的合法与安全。

1.4、exception_init

这个方法的作用如其注释描述:初始化libobjc的异常处理系统。看一下代码实现:

J0mda2BY2fgIRvMf4mkPk0A9PiwLTswVydalzSlAL9g.png

  1. 首选,想要更好的理解exception_init 的作用方法,就要先解释一下std::set_terminate 库函数:

    • std::set_terminate :可以设置自己的terminate函数。自定义的terminate函数不能有参数,且返回值类型为void。另外,terminate函数不能返回也不能抛出异常,它必须终止程序,如果terminate函数被调用,这就意味着问题已经无法解决了。
  2. 自定义的_objc_terminate中,如果是objc的异常,会通过(*uncaught_handler)((id)e) 向上层抛出,uncaught_handler 同样是个句柄,在上层的代码中设置了对应的接收方法,就可以拦截到异常的信息。

    FRdDKhurrQ1PUMPyFEAxC2QB0w-bCdYjF2-cNCJHJwg.png

1.5、*_dyld_objc_notify_register

_dyld_objc_notify_register方法仅供Objc-Runtime调用,主要作用就是向dyld中注册回调,以便在注册镜像、加载镜像、卸载镜像时,dyld能够回调mapped。所以,_dyld_objc_notify_register将image加载到内存的重要枢纽也是对于类的加载过分析的关键_dyld_objc_notify_register方法的代码实现在dyld源码中,所以这里一笔带过,可能以后会另起篇幅进行分析。

首先来看看方法的三个入参:(&map_images, load_images, unmap_image) ,能够明显的看到第一个参数map_images 是使用& 进行了地址传递,这意味着在dyld中和runtime中map_images 的操作是同步的,那么就先从map_images -映射镜像展开分析。

DdanUCc4PW1CP5mOLABHEjrsR704tdkNuFah7mQM5fw.png

1.6、小结

以上就是对于_objc_init中对于runtime环境各个初始化方法的分析,最后从对_dyld_objc_notify_register方法的分析中引出了 map_images, 从map_images开始系统又在处理什么呢?下一篇继续。


本篇中分析、记录的内容如对你有帮助,欢迎点赞👍、收藏✨、评论✍️。如果错误,欢迎在评论中指正🙆🏻‍♂️