MachO && dyld(五)

前导知识

  • 共享缓存机制

    因为同一份动态库可以被多个程序使用,所以动态库也被称为共享库
    所谓的共享缓存,其实就是共享库的缓存(即,动态库的缓存)
    特别地,因为 macOS 和 iOS 都使用 dyld 来加载和链接动态库,所以共享缓存有时也被称为 dyld 缓存

    iOS 有很多系统库几乎是每个 App 都会用到的(比如:Foundation.framework、UIKit.framework)
    与其等 App 需要时,再将这些系统库一个一个加载进内存;不如在一开始时,就先把这些系统库打包好一次加载进内存
    iOS 3.1开始,为了提高系统的性能,所有的系统库文件都被打包合并成了一个大的缓存文件,存放在 /System/Library/Caches/com.apple.dyld/ 目录下(并按不同的 CPU 架构类型分别保存)
    并且为了减少冗余,iOS 用于存放系统库的默认目录:/System/Library/Frameworks/ 下的系统库文件都被删除掉了

    iOS 系统共享缓存的路径为:/System/Library/Caches/com.apple.dyld/
    macOS 系统共享缓存的路径为:/var/db/dyld/

  • Objective-C 的 RunTime

    C 是一门静态语言,它在编译其间进行数据类型的检查,函数调用的确定。C 程序在编译完成之后,数据的类型与函数的调用,无任何二义性

    Objective-C 是一门动态语言,它将很多静态语言在 编译和链接 时所做的事放到了 运行 时来处理。比如:Objective-C 在编译时并不能真正决定调用哪个函数,只有在运行时才会根据函数的名称找到对应的函数实现进行调用。事实上,在编译阶段,Objective-C 可以调用任何函数,即使这个函数只有声明没有实现,只要进行了函数声明,编译器就不会报错。而 C 语言如果在编译阶段调用只有声明没有实现的函数,那么编译器就会马上报错

    与此同时,Objective-C 也是一门简单的语言,它有很大一部分内容基于 C,只是在语言层面扩展了些关键字和语法,使得 C 语言具备面向对象设计的能力。苹果通过以下两个层面的支持,使得基于 C 的 Objective-C 拥有了 动态的 面向对象的 特性:

    ① 编译器层面:
    Objective-C 的类和方法,在编译时,会被编译器转换成 C 的结构体和函数

    // Person 类在编译时会被编译器转换成以下结构体
    struct objc_class {
    	Class isa; 			// 实例的 isa 指针指向类对象,类对象的 isa 指针指向元类
    #if !__OBJC2__	
    	Class super_class;  // 指向父类
    	const char *name;  	// 类名
    	long version; 		// 类的版本信息,初始化默认为 0,可以通过 Runtime 函数 class_getVersion 和 class_setVersion 进行读取和修改
    	long info; 			// 一些标识信息,如 CLS_CLASS(0x1L) 表示该类为普通 class,其中包含对象方法和成员变量;CLS_META(0x2L) 表示该类为 metaclass,其中包含类方法静态成员变量
    	long instance_size;	// 该类的实例变量的大小(包括从父类继承下来的实例变量)
    	struct objc_ivar_list *ivars; 			// 成员变量列表
    	struct objc_method_list **methodLists; 	// 方法列表
    	struct objc_cache *cache;				// 方法缓存,存储最近使用的方法指针,用于提升效率
    	struct objc_protocol_list *protocols; 	// 协议列表
    #endif
    } OBJC2_UNAVAILABLE;
    -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
    // person 对象调用 eat 方法
    [person eat]
    // 在编译时会被编译器转换成以下 C 函数
    objc_msgSend(person, @selector(eat));
    

    ② 运行机制层面:
    要使 Objective-C 成为一门 动态的 面向对象的 语言,只有编译器层面的支持是不够的,我们还需要一个运行时系统来执行编译后的代码,这个运行时系统就是 RunTime。RunTime 是一套底层的 C 语言 API(这也是为什么编译器需要将 Objective-C 的类和方法编译成 C 结构体和函数的原因),为 iOS 系统的核心组件之一(在 iOS 系统中以 libobjc.dylib 动态共享库的形式存在)。RunTime 库使得 C 语言拥有了动态的特性和面向对象的能力。RunTime 库很小却很强大,其中最主要的就是消息机制,因此,Objective-C 的方法调用有时也被称为消息发送。关于 RunTime 的更多内容,后面会单独开一个章节来讲

    特别地,在使用 Objective-C 开发的 MachO 文件中,LoadCommands 区域会有一条 LC_LOAD_DYLIB (libobjc.A.dylib) 加载命令,用于加载 Objective-C 的运行时环境
    LC_LOAD_DYLIB(libobjc.A.dylib)

  • dyld 简介

    上一篇文章中我们讲解了 XNU 加载 MachO 和 dyld 的流程
    创建进程 -> 创建虚拟内存空间 ->
    解析和映射 MachO 可执行文件 ->
    解析和映射 dyld 动态链接器文件 ->
    进入动态链接器 dyld 的执行流程

    本篇文章我们来讲解:dyld 加载和链接(与 MachO 可执行文件相关的动态库)的流程

    dyld(动态链接器)英文全称为:dynamic loader(动态加载器)、dynamic link editor(动态链接编辑器),是 macOS 和 iOS 的重要组成部分,默认路径为:/usr/lib/dyld主要用于:加载和链接与 MachO 可执行文件相关的动态库
    注意:这里的 usr 可不是 user 的意思,而是 unix system resource 的缩写,macOS / iOS 的 dyld 都在此路径下

    注意:
    网上很多文章中都提到说 dyld 加载了主程序和动态库,这个理解明显是错误的
    我们在 XNU 加载 MachO 和 dyld 的源码讲解中已经知道,是内核 XNU 加载了主程序
    而接下来对 dyld 源码的讲解也会证明,dyld 只会负责动态库的加载和链接,并不会加载主程序(虽然在 dyld 的源码中,主程序也会以镜像的形式被 dyld 管理起来,但是这并不意味着 dyld 加载了主程序)

dyld 加载和链接动态库的流程

  • dyld && RunTime 源码版本

    本文所使用的 dyld 源码版本为:dyld-750.6开发者可以到苹果官网下载相应版本
    本文所使用的 RunTime 源码版本为:objc4-781开发者可以到苹果官网下载相应版本

  • dyld 执行流程的"起始点":dyld 自启动

    __dyld_start 是内核 XNU 加载完 dyld 之后,dyld 的入口函数(dyldStartup.s 中存储着各个 CPU 架构的 __dyld_start
    HCG00
    dyldbootstrap::start(...) 函数主要用于 dyld 的自启动,它做了很多 dyld 初始化相关的工作,包括:

    1. rebaseDyld(...):dyld 的重定位
    2. mach_init(...):mach 消息初始化
    3. __guard_setup(...):栈溢出保护

    初始化工作完成后,此函数会调用 dyld::_main(...) 进行动态库加载等一系列流程
    之后将 dyld::_main(...) 函数的返回值(App main(...) 函数的地址)传递给 __dyld_start,用于调用 App 的 main(...) 函数
    HCG01-0
    dyld 本质上也是一个 MachO,而普通 MachO 的重定位工作又是由 dyld 来完成的。那么 dyld 的重定位工作又由谁来完成呢?
    这是一个(鸡生蛋 蛋生鸡)的问题,为了解决这个问题,dyld 需要满足以下 2 个条件:

    1. dyld 本身不依赖其他任何 MachO 文件
    2. dyld 本身所需要的全局和静态变量的重定位工作由它本身完成

    第 1 个条件苹果在开发 dyld 的时候就已经做了规避
    第 2 个条件要求 dyld 在启动时,必须有一段代码可以在获得自身的重定位表和符号表的同时,又不能用到全局变量和静态变量,甚至不能调用函数。这样的自启动代码被称为引导程序(Bootstrap)

    当系统内核将进程的控制权交给 dyld 时,dyld 的引导程序开始执行,它会找到 dyld 本身的重定位入口,进而完成其自身的重定位
    在此之后 dyld 中的代码才可以开始使用自己的全局变量、静态变量和各种函数
    HCG01-1

  • dyld 执行流程的"总调度":dyld main 函数
    dyld::_main(...) 是整个 App 启动的关键函数,此函数的调用会完成动态库加载和链接的一系列过程,并返回 App main(...) 函数的入口,也就是 主程序 main(...) 的地址,并保持在 x0 寄存器 中。整个流程可细分为 9 步 :
    ① 设置运行环境
    ② 加载系统共享缓存
    ③ 实例化主程序
    ④ 加载插入的动态库
    ⑤ 链接主程序
    ⑥ 链接插入的动态库
    ⑦ 执行弱符号绑定
    ⑧ 执行初始化方法
    ⑨ 查找 App 入口点并返回
    HCG02

① 设置运行环境

  • 函数调用流程图
    ① 设置运行环境

  • setContext(...) 函数用于设置全局链接上下文(gLinkContext)的信息,包括一些:回调函数、参数、标志信息
    注意:全局链接上下文(gLinkContext)是定义在 dyld-750.6/src/ImageLoader.hstruct LinkContext 类型的结构体,里面包含大量的:函数指针、变量、标志位,用于控制 dyld 在 加载和链接 镜像时的行为
    HCG1.1

  • configureProcessRestrictions(...) 函数用于配置进程是否受限,其代码逻辑比较简单,主要也是设置全局链接上下文(gLinkContext
    HCG1.2

  • checkEnvironmentVariables(...) 函数用于检查环境变量,其内部调用 processDyldEnvironmentVariable(...) 函数用于处理并设置环境变量
    HCG1.3-1
    HCG1.3-2

  • getHostInfo(...) 函数用于获取 cpu 的类型与 cpu 的子类型
    HCG1.4

  • Demo:如何启用环境变量

    在 dyld 的源码中,有很多 DYLD_* 开头的环境变量,其实只要在 App 的工程中配置一下,即可让这些环境变量生效。我们通过 XCode 打开任意的 App,然后依次选择:Product - Scheme - Edit Scheme... - Run - Arguments,并在 Environment Variables 栏目中添加对应的环境变量,如下图所示:
    Demo:如何启用环境变量 00
    运行 App,即可在 XCode 的控制台看到对应环境变量的输出结果:
    Demo:如何启用环境变量 01

② 加载系统共享缓存

  • 函数调用流程图
    ② 加载系统共享缓存
  • checkSharedRegionDisable(...) 函数用于检查共享缓存是否被禁用。该函数的 iOS 实现部分仅有一句注释:iOS 必须开启共享缓存机制
    HCG2.1
  • mapSharedCache(...) 函数用于:
    ① 在加载共享缓存之前,构造用于解析共享缓存的参数
    ② 在加载共享缓存之后,更新全局状态
    mapSharedCache(...) 函数里面实际上是调用了 loadDyldCache(...) 函数用于加载共享缓存
    HCG2.2
  • loadDyldCache(...) 函数用于根据不同情况调用不同的解析共享缓存的函数,共享缓存的加载可以分为以下 3 种情况:
    ① 如果共享缓存仅加载到当前进程,则调用 mapCachePrivate(...) 函数解析和加载共享缓存
    ② 如果共享缓存已加载,则不做任何处理
    ③ 如果当前进程首次加载共享缓存,则调用 mapCacheSystemWide(...) 函数解析和加载共享缓存
    mapCachePrivate(...)mapCacheSystemWide(...) 里面就是具体的共享缓存解析逻辑,感兴趣的读者可以自行分析
    HCG2.3

③ 实例化主程序

  • 函数调用流程图
    ③ 实例化主程序
  • instantiateFromLoadedImage(...) 函数用于为主程序初始化 ImageLoader,用于后续的链接等过程
    因为 主程序作为 dyld 加载过程中第一个被 addImage(...) 函数添加到全局镜像列表(sAllImages)中的镜像
    所以 我们总是能够通过 _dyld_get_image_header(0)_dyld_get_image_name(0) 等,索引到全局镜像列表中的第一个镜像(image)为主程序的相关信息
    HCG3.1
  • ImageLoaderMachO::instantiateMainExecutable(...) 函数用于根据 MachO 文件不同的 LinkEdit 段类型为主程序创建不同的镜像
    ImageLoader是抽象类,其子类负责把 MachO 文件实例化为镜像(image
    sniffLoadCommands(...) 函数解析完成以后,会根据 compressed 的值来决定调用哪个具体的子类进行镜像的实例化
    HCG3.2
    ImageLoader 及其子类的继承关系如下:
    class ImageLoaderMachO			 : 	class ImageLoader
    class ImageLoaderMachOClassic 	 : 	class ImageLoaderMachO
    class ImageLoaderMachOCompressed : 	class ImageLoaderMachO
    
  • sniffLoadCommands(...) 函数用于校验 MachO 文件的格式是否合法 && 获取一些与 MachO 文件相关的数据,包括:
    compressed:MachO 文件 LinkEdit 段的类型(true - 压缩类型,false - 经典类型)
    segCount:MachO 文件所包含的 Segment(段)的数量
    libCount:MachO 文件所包含的 Library(库)的数量
    codeSigCmd:获取代码签名加载命令结构体 struct linkedit_data_command*
    encryptCmd:获取加密信息加载命令结构体 struct encryption_info_command*
    HCG3.3
  • 当主程序 MachO 文件的 LinkEdit 段为压缩类型时,调用 ImageLoaderMachOCompressed::instantiateMainExecutable(...) 函数为主程序创建镜像:
    HCG3.4
  • 当主程序 MachO 文件的 LinkEdit 段为经典类型时,调用 ImageLoaderMachOClassic::instantiateMainExecutable(...) 函数为主程序创建镜像:
    HCG3.5
  • addImag(...) 函数用于将镜像(image)加入到全局镜像列表(sAllImages),并将镜像(image)映射到申请的内存中
    HCG3.6

④ 加载插入的动态库

  • 函数调用流程图
    ④ 加载插入的动态库

  • loadInsertedDylib(...) 函数用于构造 LoadContext context 参数,并调用 load(...) 函数加载插入的动态库HCG4.1

  • load(...) 函数是查找动态库镜像的一系列流程的入口
    HCG4.2

  • ImageLoaderMachO::instantiateFromFile(...) 函数用于从 MachO 文件中映射被插入的动态库(即,用于从 MachO 文件中初始化被插入的动态库的镜像)
    HZP1.1

  • ImageLoaderMachOCompressed::instantiateFromFile(...) 用于从 MachO 文件中初始化要被插入的动态库的镜像
    注意:因为现在的 MachO 文件的 LinkEdit 段大多是压缩类型的,所以我们以 ImageLoaderMachOCompressed::instantiateFromFile(...) 函数为例进行讲解
    HZP1.2

  • ImageLoaderMachO::instantiateFromCache(...) 函数用于从系统共享缓存中映射被插入的动态库(即,用于从系统共享缓存中初始化被插入的动态库的镜像)
    HZP1.4

  • ImageLoaderMachOCompressed::instantiateFromCache(...) 用于从系统共享缓存中初始化要被插入的动态库的镜像
    注意:因为现在的 MachO 文件的 LinkEdit 段大多是压缩类型的,所以我们以 ImageLoaderMachOCompressed::instantiateFromCache(...) 函数为例进行讲解
    HZP1.5

  • checkandAddImage(...) 函数用于验证镜像(image)并将其加入到全局镜像列表(sAllImages)中
    HCG4.3

⑤ 链接主程序

  • 函数调用流程图
    ⑤ 链接主程序

  • dyld::link(...) 函数用于对镜像进行一些必要的检查和处理,然后调用 ImageLoader::link(...) 函数来完成镜像的链接
    HCG5.1

  • ImageLoader::link(...) 函数用于链接一个镜像(所谓的镜像包括:App主程序 + 动态库)
    本函数用于对实例化后的镜像的数据进行动态的修正,让镜像的二进制变为正常可用的状态(典型的就是主程序中符号表的修正操作)
    HCG5.2

  • ImageLoader::recursiveLoadLibraries(...) 函数采用递归的方式加载主程序依赖的动态库
    可以简单地理解为:加载 LoadCommands 区域的 LC_LOAD_DYLIBLC_LOAD_WEAK_DYLIBLC_REEXPORT_DYLIBLC_LOAD_UPWARD_DYLIB 加载命令所描述的动态库
    HCG5.3

  • ImageLoader::recursiveRebaseWithAccounting(...) 函数内部调用了 ImageLoader::recursiveRebase(...) 函数用于镜像的依赖库与镜像本身的重定位
    HCG5.4

  • ImageLoader::recursiveRebase(...) 函数内部首先进行了镜像依赖库的重定位,然后再进行镜像本身的重定位:
    ① 镜像依赖库的重定位调用的是 ImageLoader::recursiveRebase(...) 函数本身,为了防止依赖库间循环引用导致无限递归,因此设置了 fState 标志位
    ② 镜像本身的重定位则是调用的 ImageLoader::doRebase(...) 函数,ImageLoader::doRebase(...) 函数是一个虚函数,被 ImageLoaderMachO 重载
    HCG5.5

  • ImageLoaderMachO::doRebase(...) 函数将 __TEXT 段设置为可写后(i386 cpu),调用了 ImageLoaderMachO::rebase(...) 函数进行镜像的重定位。ImageLoaderMachO::rebase(...) 函数是一个虚函数,会被 ImageLoaderMachOCompressedImageLoaderMachOClassic 重载 HCG5.6

  • ImageLoaderMachOCompressed::rebase(...) 函数主要用于:
    ① 读取加载命令 LC_DYLD_INFOLC_DYLD_INFO_ONLY 中动态链接信息 struct dyld_info_commandrebase_offrebase_size 来确定 LinkEdit 段中用于重定位的信息(Dynamic Loader Info - Rebase Info)的偏移与大小
    ② 逐条解析 LinkEditDynamic Loader Info - Rebase Info 中的信息,获取立即数(immediate)与操作码(opcode),并根据不同的操作码(opcode)类型进行不同的地址修正

    具体的地址修正还会调用到:segActualLoadAddress(...)segActualEndAddress(...)read_uleb128(...)rebaseAt(...) 等函数,这些函数都是关于地址与指针的操作,有兴趣的读者可以自行研究

    因为现在绝大多数的 MachO 文件,其 LinkEdit 段为压缩类型,所以我们以 ImageLoaderMachOCompressed::rebase(...) 函数为例进行讲解
    HCG5.7

  • ImageLoader::recursiveBindWithAccounting(...) 函数内部调用了 ImageLoader::recursiveBind(...) 函数用于镜像的依赖库与镜像本身的绑定
    HCG5.8

  • ImageLoader::recursiveBind(...) 函数内部首先进行了镜像依赖库的绑定,然后再进行镜像本身的绑定:
    ① 镜像依赖库的绑定调用的是 ImageLoader::recursiveBind(...) 函数本身,为了防止依赖库间循环引用导致无限递归,因此设置了 fState 标志位
    ② 镜像本身的绑定则是调用的 ImageLoader::doBind(...) 函数,ImageLoader::doBind(...) 函数是一个虚函数,会被 ImageLoaderMachOCompressedImageLoaderMachOClassic 重载
    HCG5.9

  • ImageLoaderMachOCompressed::doBind(...) 函数主要用于:
    ① 定义绑定处理程序
    ② 调用 ImageLoaderMachOCompressed::eachBind(...) 函数
    因为现在绝大多数的 MachO 文件,其 LinkEdit 段为压缩类型,所以我们以 ImageLoaderMachOCompressed::doBind(...) 函数为例进行讲解
    HCG5.10

  • ImageLoaderMachOCompressed::eachBind(...) 函数主要用于:
    ① 读取加载命令 LC_DYLD_INFOLC_DYLD_INFO_ONLY 中动态链接信息 struct dyld_info_commandbind_offbind_size 来确定 LinkEdit 段中用于绑定的信息(Dynamic Loader Info - Binding Info)的偏移与大小
    ② 逐条解析 LinkEditDynamic Loader Info - Binding Info 中的信息,获取立即数(immediate)与操作码(opcode
    ③ 根据不同的立即数(immediate)与操作码(opcode)进行不同的变量赋值,输出打印,并调用绑定处理程序(bind_handler handler)进行符号地址绑定。绑定处理程序(bind_handler handler)的类型为:

    typedef uintptr_t (^bind_handler)(const LinkContext& context, ImageLoaderMachOCompressed* image, uintptr_t addr, uint8_t type,
    													 const char* symbolName, uint8_t symboFlags, intptr_t addend, long libraryOrdinal,
    													 ExtraBindData *extraBindData,
    													 const char* msg, LastLookup* last, bool runResolver);
    

    根据上一步的代码可知,绑定处理程序(bind_handler handler)中,主要调用了 ImageLoaderMachOCompressed::bindAt(...) 函数进行符号地址绑定
    HCG5.11

  • ImageLoaderMachOCompressed::bindAt(...) 函数用于:
    ① 进行符号表的解析
    ② 进行最终的绑定操作
    HCG5.12

  • ImageLoaderMachOCompressed::resolve(...) 函数用于解析符号表,返回符号地址
    HCG5.13

  • ImageLoaderMachO::bindLocation(...) 函数用于根据不同的绑定类型完成最终的符号地址绑定操作
    HCG5.14

⑥ 链接插入的动态库

  • 说明:

    无论是主程序还是动态库,对于 dyld 来说都是一个镜像(image),在 dyld 中都是 ImageLoader* 类型
    因此,动态库的链接过程与主程序的链接过程,基本上是一样的
    只是动态库在链接完成之后,还多了一步 : 注册动态库的插入

    ImageLoaderMachO::registerInterposing(...) 函数用于注册动态库的插入
    HCG6.3

⑦ 执行弱符号绑定

  • ImageLoader::weakBind(...) 函数用于进行主程序 MachO 弱符号的绑定
    HCG7.1

⑧ 执行初始化方法

  • 函数调用流程图
    ⑧ 执行初始化方法

  • initializeMainExecutable() 函数用于初始化动态库与主程序,dyld 会优先初始化动态库,然后再初始化主程序。initializeMainExecutable() 函数内部调用了 ImageLoader::runInitializers(...) 函数用于执行镜像的初始化操作
    HCG8.1

  • ImageLoader::runInitializers(...) 函数内部调用了 ImageLoader::processInitializers(...) 函数
    HCG8.2

  • ImageLoader::processInitializers(...) 函数内部调用 ImageLoader::recursiveInitialization(...) 函数

    struct UninitedUpwards
    {
    	uintptr_t	 						 count;
    	std::pair<ImageLoader*, const char*> imagesAndPaths[1];
    };
    

    HCG8.3

  • ImageLoader::recursiveInitialization(...) 函数内部调用了 dyld::notifySingle(...) 函数
    HCG8.4

  • notifySingle(...) 内部调用了 sNotifyObjCInit 这个回调。而回调 sNotifyObjCInitdyld::registerObjCNotifiers(...) 函数中被赋值
    HCG8.5

  • dyld::registerObjCNotifiers(...) 函数的第二个参数 _dyld_objc_notify_init init 会被赋值给回调 sNotifyObjCInit。而 dyld::registerObjCNotifiers(...) 函数又被位于 dyld-750.6/src/dyldAPIs.cpp 中的 _dyld_objc_notify_register(...) 函数所调用
    HCG8.6

  • _dyld_objc_notify_register(...) 函数的第二个参数 _dyld_objc_notify_init init 会被赋值给 dyld::registerObjCNotifiers(...) 函数的第二个参数 _dyld_objc_notify_init init
    即,_dyld_objc_notify_register(...) 函数的第二个参数 _dyld_objc_notify_init init 会被赋值给回调 sNotifyObjCInit
    HCG8.7
    综上所述,只要找到 _dyld_objc_notify_register(...) 函数的调用者,就能知道是谁为回调 sNotifyObjCInit 赋值
    但是在 dyld 工程中,_dyld_objc_notify_register(...) 函数没有被任何函数调用。那么究竟是谁调用了 _dyld_objc_notify_register(...) 函数呢?

    既然 dyld 工程内部找不到 _dyld_objc_notify_register(...) 函数的调用者,那么剩下的只有一种可能:
    _dyld_objc_notify_register(...) 函数是供 iOS/macOS 系统的其他模块调用的
    结合 ImageLoader::recursiveInitialization(...) 函数中的注释,我们可以推测 _dyld_objc_notify_register(...) 函数是被 Objective-CRunTime 机制所调用。即,回调 sNotifyObjCInit 是被 Objective-CRunTime 机制所赋值

    我们通过在 Demo 工程中对 libdyld.dylib 模块下符号断点:_dyld_objc_notify_register 来验证这一推测
    运行程序,成功命中符号断点,从调用栈看到是 libobjc.A.dylib_objc_init 函数调用了 _dyld_objc_notify_register(...) 函数。如下图所示:
    符号断点:_dyld_objc_notify_register

    _dyld_objc_notify_register(...) 函数是 dyld 提供给 Objective-CRunTime 库使用的
    Objective-CRunTime 库使用 _dyld_objc_notify_register(...) 函数向 dyld 注册镜像初始化完成的通知

  • _objc_init(...) 函数用于:
    ① 初始化引导程序
    ② 在 dyld 中注册镜像初始化完成的通知

    _objc_init(...) 函数内部在注册镜像初始化通知时,使用了 load_images(...) 函数为 _dyld_objc_notify_register(...) 函数的第二个参数 _dyld_objc_notify_init init 赋值。我们可以简单地理解为:回调 sNotifyObjCInit == load_images(...) 函数
    HCG8.8

  • load_images(...) 函数用于:在 dyld 正在映射的镜像中循环调用各个类的 +load 方法
    HCG8.9

  • ImageLoaderMachO::doInitialization(...) 函数用于对镜像进行初始化:
    ① 首先调用了 doImageInit(...) 来执行镜像的初始化函数,也就是加载命令 LC_ROUTINES_COMMAND 中记录的函数
    ② 然后调用了 doModInitFunctions(...) 来解析和执行 Section64(__DATA, __mod_init_func) 中保存的函数。Section64(__DATA, __mod_init_func) 中保存的是全局 C++ 对象的构造函数以及所有带__attribute__((constructor) 的 C 函数
    HCG8.10

⑨ 查找主程序入口点并返回

  • 函数调用流程图
    ⑨ 查找主程序入口点并返回
  • ImageLoaderMachO::getEntryFromLC_MAIN() 函数用于从加载命令 LC_MAIN 中获取主程序的入口点HCG9.1
  • ImageLoaderMachO::getEntryFromLC_UNIXTHREAD() 函数用于从加载命令 LC_UNIXTHREAD 中获取主程序的入口点
    HCG9.2

总结

  • 可执行文件的使命主要有 2 个(MachO 文件也不例外):

    ① 在编译、链接时为开发者提供可扩展的封装结构
    ② 在执行时为操作系统内核提供内存映射信息

  • App 的启动流程可以分为 2 大部分

    ① 内核 XNU:
    创建进程 -> 创建虚拟内存空间 ->
    解析和映射 MachO 可执行文件 ->
    解析和映射 dyld 动态链接器文件 ->
    进入动态链接器 dyld 的执行流程

    ② 动态链接器 dyld:
    自启动 ->
    设置运行环境 ->
    加载系统共享缓存 ->
    实例化主程序 ->
    加载插入的动态库 ->
    链接主程序 ->
    链接插入的动态库 ->
    执行弱符号绑定 ->
    执行初始化方法 ->
    查找 App main(...) 函数的入口点并执行

补充:验证 iOS 函数的调用顺序

  • ① 新建一个 iOS Demo 工程:InitializeDemo,并向工程中添加两个动态库的 Target:HCGServiceHZPService
  • ② 在 InitializeDemoHCGServiceHZPService 中分别添加一个 Objective-C 的 Log 类与一个 C++ 的 Person 类
    Demo:验证 iOS 函数的调用顺序 - 2
  • ③ 在每一个 Log 类中添加一个单纯打印字符串的 +log 方法,并重写 +load 方法
    Demo:验证 iOS 函数的调用顺序 - 3.1
    Demo:验证 iOS 函数的调用顺序 - 3.2
  • ④ 在 Person 类中添加一个无参的构造函数与一个 __attribute__((constructor))
    Demo:验证 iOS 函数的调用顺序 - 4.1
    Demo:验证 iOS 函数的调用顺序 - 4.2
  • ⑤ 在 InitializeDemo 中引用动态库 HCGServiceHZPService,引用顺序如下
    Demo:验证 iOS 函数的调用顺序 - 5
  • ⑥ 在 InitializeDemo - ViewController.mm 进行 Objective-CC++ 的混编并运行程序,控制台输出如下
    Demo:验证 iOS 函数的调用顺序 - 6
  • ⑦ 在 InitializeDemo 中改变动态库 HCGServiceHZPService 的引用顺序
    Demo:验证 iOS 函数的调用顺序 - 7
  • ⑧ 在不改变代码的情况下,重新运行 InitializeDemo,控制台输出如下
    Demo:验证 iOS 函数的调用顺序 - 8
  • ⑨ 对 InitializeDemo - DemoLog 类的 +load 方法下断点并重新运行 InitializeDemo,可以看到函数的调用栈如下
    Demo:验证 iOS 函数的调用顺序 - 9
  • 结论
  1. dyld 会根据 App 主程序中对动态库的编译顺序来初始化动态库的镜像(先编译先初始化,后编译后初始化)
  2. dyld 会优先初始化动态库的镜像,然后再初始化 App 主程序的镜像(App 主程序的镜像最后初始化)
  3. 在同一个镜像内,Objective-C 的 +load 方法 会比 C 的 __attribute__((constructor) 函数 先调用
  4. 所有镜像(包括 App 主程序的镜像)中的 +load 方法__attribute__((constructor) 函数 都会比 主程序的 main 函数 先调用
  5. 步骤 ⑨ 中的函数调用栈与前面第八小节:⑧ 执行初始化方法 中的源码分析结果是一致的

补充:懒绑定函数的调用流程

  • ① 前面我们在讲解 iOS 系统的懒绑定机制时,知道了:MachO 在进行 Lazy Symbol 的绑定时,会调用位于 Section64(__TEXT, __stub_helper) 中的懒绑定函数 dyld_stub_binder。实际上对于 MachO 文件来说,dyld_stub_binder 也是一个外部符号,其实现位于 dyld 的源码中
    HZP2.1
  • dyld::fastBindLazySymbol(...) 函数用于获取需要进行懒绑定的镜像,并调用 ImageLoader::doBindFastLazySymbol(...) 函数执行懒绑定
    ImageLoader::doBindFastLazySymbol(...) 是一个虚函数,会根据不同的镜像类型调用不同的实现
    HZP2.2
  • ImageLoaderMachOCompressed::doBindFastLazySymbol(...) 函数为压缩类型的镜像进行懒绑定并返回真实的符号地址
    HZP2.3
  • ImageLoaderMachOClassic::doBindFastLazySymbol(...) 函数的实现只有一句代码:抛出异常
    这是因为经典类型的镜像的 LinkEdit 段没有 LC_DYLD_INFOLC_DYLD_INFO_ONLY,不能进行压缩类型的懒绑定
    HZP2.4

思考

  • Question:根据对 MachO 文件格式的介绍 && 对 dyld 源码的分析,如果要向 MachO 文件中注入一个动态库,那么我们该怎么做?

  • Answer:在 dyld 的执行流程中,有两个节点可以注入动态库

    节点一:④ 加载插入的动态库,用于加载环境变量 DYLD_INSERT_LIBRARIES 中指定的动态库列表
    节点二:⑤ 链接主程序,用于加载 MachO 文件中加载命令 LC_LOAD_DYLIB 中指定的动态库

    在非越狱的情况下,我们无法修改系统的环境变量 DYLD_INSERT_LIBRARIES。因此,如果需要在(节点一)注入动态库,则需要对 iOS 系统进行越狱
    在代码签名机制下,我们无法修改 MachO 文件的加载命令 LC_LOAD_DYLIB。因此,如果需要在(节点二)注入动态库,则需要对 MachO 文件进行重签名

    根据 dyld 的执行流程:(系统环境变量 DYLD_INSERT_LIBRARIES 中指定的动态库)会比(MachO 文件加载命令 LC_LOAD_DYLIB 中指定的动态库)优先加载。那么:(系统环境变量 DYLD_INSERT_LIBRARIES 中动态库镜像的初始化方法)会比(MachO 文件加载命令 LC_LOAD_DYLIB 中动态库镜像的初始化方法)优先执行

    还有一点,④ 加载插入的动态库:加载环境变量 DYLD_INSERT_LIBRARIES 中指定的动态库列表,是 iOS 系统在 dyld 中给自己预留的加载动态库的接口(例如加载 XCode 的 ViewDebugMainThreadChecker),而并非是面向 iOS 开发者的设计

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值