目录
前导知识
-
共享缓存机制
因为同一份动态库可以被多个程序使用,所以动态库也被称为共享库
所谓的共享缓存,其实就是共享库的缓存(即,动态库的缓存)
特别地,因为 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 的运行时环境

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

dyldbootstrap::start(...)函数主要用于 dyld 的自启动,它做了很多 dyld 初始化相关的工作,包括:rebaseDyld(...):dyld 的重定位mach_init(...):mach 消息初始化__guard_setup(...):栈溢出保护
初始化工作完成后,此函数会调用
dyld::_main(...)进行动态库加载等一系列流程
之后将dyld::_main(...)函数的返回值(Appmain(...)函数的地址)传递给__dyld_start,用于调用 App 的main(...)函数

dyld 本质上也是一个 MachO,而普通 MachO 的重定位工作又是由 dyld 来完成的。那么 dyld 的重定位工作又由谁来完成呢?
这是一个(鸡生蛋 蛋生鸡)的问题,为了解决这个问题,dyld 需要满足以下 2 个条件:- dyld 本身不依赖其他任何 MachO 文件
- dyld 本身所需要的全局和静态变量的重定位工作由它本身完成
第 1 个条件苹果在开发 dyld 的时候就已经做了规避
第 2 个条件要求 dyld 在启动时,必须有一段代码可以在获得自身的重定位表和符号表的同时,又不能用到全局变量和静态变量,甚至不能调用函数。这样的自启动代码被称为引导程序(Bootstrap)当系统内核将进程的控制权交给 dyld 时,dyld 的引导程序开始执行,它会找到 dyld 本身的重定位入口,进而完成其自身的重定位
在此之后 dyld 中的代码才可以开始使用自己的全局变量、静态变量和各种函数

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

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

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

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

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


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

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

运行 App,即可在 XCode 的控制台看到对应环境变量的输出结果:

② 加载系统共享缓存
- 函数调用流程图

checkSharedRegionDisable(...)函数用于检查共享缓存是否被禁用。该函数的 iOS 实现部分仅有一句注释:iOS 必须开启共享缓存机制

mapSharedCache(...)函数用于:
① 在加载共享缓存之前,构造用于解析共享缓存的参数
② 在加载共享缓存之后,更新全局状态
而mapSharedCache(...)函数里面实际上是调用了loadDyldCache(...)函数用于加载共享缓存

loadDyldCache(...)函数用于根据不同情况调用不同的解析共享缓存的函数,共享缓存的加载可以分为以下 3 种情况:
① 如果共享缓存仅加载到当前进程,则调用mapCachePrivate(...)函数解析和加载共享缓存
② 如果共享缓存已加载,则不做任何处理
③ 如果当前进程首次加载共享缓存,则调用mapCacheSystemWide(...)函数解析和加载共享缓存
mapCachePrivate(...)、mapCacheSystemWide(...)里面就是具体的共享缓存解析逻辑,感兴趣的读者可以自行分析

③ 实例化主程序
- 函数调用流程图

instantiateFromLoadedImage(...)函数用于为主程序初始化ImageLoader,用于后续的链接等过程
因为 主程序作为 dyld 加载过程中第一个被addImage(...)函数添加到全局镜像列表(sAllImages)中的镜像
所以 我们总是能够通过_dyld_get_image_header(0)、_dyld_get_image_name(0)等,索引到全局镜像列表中的第一个镜像(image)为主程序的相关信息

ImageLoaderMachO::instantiateMainExecutable(...)函数用于根据 MachO 文件不同的LinkEdit段类型为主程序创建不同的镜像
ImageLoader是抽象类,其子类负责把 MachO 文件实例化为镜像(image)
当sniffLoadCommands(...)函数解析完成以后,会根据compressed的值来决定调用哪个具体的子类进行镜像的实例化

ImageLoader 及其子类的继承关系如下:class ImageLoaderMachO : class ImageLoader class ImageLoaderMachOClassic : class ImageLoaderMachO class ImageLoaderMachOCompressed : class ImageLoaderMachOsniffLoadCommands(...)函数用于校验 MachO 文件的格式是否合法 && 获取一些与 MachO 文件相关的数据,包括:
①compressed:MachO 文件LinkEdit段的类型(true - 压缩类型,false - 经典类型)
②segCount:MachO 文件所包含的Segment(段)的数量
③libCount:MachO 文件所包含的Library(库)的数量
④codeSigCmd:获取代码签名加载命令结构体struct linkedit_data_command*
⑤encryptCmd:获取加密信息加载命令结构体struct encryption_info_command*

- 当主程序 MachO 文件的
LinkEdit段为压缩类型时,调用ImageLoaderMachOCompressed::instantiateMainExecutable(...)函数为主程序创建镜像:

- 当主程序 MachO 文件的
LinkEdit段为经典类型时,调用ImageLoaderMachOClassic::instantiateMainExecutable(...)函数为主程序创建镜像:

addImag(...)函数用于将镜像(image)加入到全局镜像列表(sAllImages),并将镜像(image)映射到申请的内存中

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

-
loadInsertedDylib(...)函数用于构造LoadContext context参数,并调用load(...)函数加载插入的动态库
-
load(...)函数是查找动态库镜像的一系列流程的入口

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

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

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

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

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

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

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

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

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

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

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

-
ImageLoaderMachO::doRebase(...)函数将__TEXT段设置为可写后(i386 cpu),调用了ImageLoaderMachO::rebase(...)函数进行镜像的重定位。ImageLoaderMachO::rebase(...)函数是一个虚函数,会被ImageLoaderMachOCompressed和ImageLoaderMachOClassic重载
-
ImageLoaderMachOCompressed::rebase(...)函数主要用于:
① 读取加载命令LC_DYLD_INFO,LC_DYLD_INFO_ONLY中动态链接信息struct dyld_info_command的rebase_off与rebase_size来确定LinkEdit段中用于重定位的信息(Dynamic Loader Info - Rebase Info)的偏移与大小
② 逐条解析LinkEdit段Dynamic Loader Info - Rebase Info中的信息,获取立即数(immediate)与操作码(opcode),并根据不同的操作码(opcode)类型进行不同的地址修正具体的地址修正还会调用到:
segActualLoadAddress(...),segActualEndAddress(...),read_uleb128(...),rebaseAt(...)等函数,这些函数都是关于地址与指针的操作,有兴趣的读者可以自行研究因为现在绝大多数的 MachO 文件,其
LinkEdit段为压缩类型,所以我们以ImageLoaderMachOCompressed::rebase(...)函数为例进行讲解

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

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

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

-
ImageLoaderMachOCompressed::eachBind(...)函数主要用于:
① 读取加载命令LC_DYLD_INFO,LC_DYLD_INFO_ONLY中动态链接信息struct dyld_info_command的bind_off与bind_size来确定LinkEdit段中用于绑定的信息(Dynamic Loader Info - Binding Info)的偏移与大小
② 逐条解析LinkEdit段Dynamic 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(...)函数进行符号地址绑定

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

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

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

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

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

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

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

-
ImageLoader::runInitializers(...)函数内部调用了ImageLoader::processInitializers(...)函数

-
ImageLoader::processInitializers(...)函数内部调用ImageLoader::recursiveInitialization(...)函数struct UninitedUpwards { uintptr_t count; std::pair<ImageLoader*, const char*> imagesAndPaths[1]; };
-
ImageLoader::recursiveInitialization(...)函数内部调用了dyld::notifySingle(...)函数

-
notifySingle(...)内部调用了sNotifyObjCInit这个回调。而回调sNotifyObjCInit在dyld::registerObjCNotifiers(...)函数中被赋值

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

-
_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

综上所述,只要找到_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-C的RunTime机制所调用。即,回调sNotifyObjCInit是被Objective-C的RunTime机制所赋值我们通过在 Demo 工程中对
libdyld.dylib模块下符号断点:_dyld_objc_notify_register来验证这一推测
运行程序,成功命中符号断点,从调用栈看到是libobjc.A.dylib的_objc_init函数调用了_dyld_objc_notify_register(...)函数。如下图所示:

_dyld_objc_notify_register(...)函数是 dyld 提供给Objective-C的RunTime库使用的
Objective-C的RunTime库使用_dyld_objc_notify_register(...)函数向 dyld 注册镜像初始化完成的通知 -
_objc_init(...)函数用于:
① 初始化引导程序
② 在 dyld 中注册镜像初始化完成的通知_objc_init(...)函数内部在注册镜像初始化通知时,使用了load_images(...)函数为_dyld_objc_notify_register(...)函数的第二个参数_dyld_objc_notify_init init赋值。我们可以简单地理解为:回调sNotifyObjCInit==load_images(...)函数

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

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

⑨ 查找主程序入口点并返回
- 函数调用流程图

ImageLoaderMachO::getEntryFromLC_MAIN()函数用于从加载命令LC_MAIN中获取主程序的入口点
ImageLoaderMachO::getEntryFromLC_UNIXTHREAD()函数用于从加载命令LC_UNIXTHREAD中获取主程序的入口点

总结
-
可执行文件的使命主要有 2 个(MachO 文件也不例外):
① 在编译、链接时为开发者提供可扩展的封装结构
② 在执行时为操作系统内核提供内存映射信息 -
App 的启动流程可以分为 2 大部分
① 内核 XNU:
创建进程 -> 创建虚拟内存空间 ->
解析和映射 MachO 可执行文件 ->
解析和映射 dyld 动态链接器文件 ->
进入动态链接器 dyld 的执行流程② 动态链接器 dyld:
自启动 ->
设置运行环境 ->
加载系统共享缓存 ->
实例化主程序 ->
加载插入的动态库 ->
链接主程序 ->
链接插入的动态库 ->
执行弱符号绑定 ->
执行初始化方法 ->
查找 Appmain(...)函数的入口点并执行
补充:验证 iOS 函数的调用顺序
- ① 新建一个 iOS Demo 工程:
InitializeDemo,并向工程中添加两个动态库的 Target:HCGService、HZPService - ② 在
InitializeDemo、HCGService、HZPService中分别添加一个Objective-C 的 Log 类与一个C++ 的 Person 类

- ③ 在每一个
Log类中添加一个单纯打印字符串的+log方法,并重写+load方法


- ④ 在
Person类中添加一个无参的构造函数与一个__attribute__((constructor))


- ⑤ 在
InitializeDemo中引用动态库HCGService、HZPService,引用顺序如下

- ⑥ 在
InitializeDemo-ViewController.mm进行Objective-C与C++的混编并运行程序,控制台输出如下

- ⑦ 在
InitializeDemo中改变动态库HCGService、HZPService的引用顺序

- ⑧ 在不改变代码的情况下,重新运行
InitializeDemo,控制台输出如下

- ⑨ 对
InitializeDemo - DemoLog类的+load方法下断点并重新运行InitializeDemo,可以看到函数的调用栈如下

- 结论
- dyld 会根据 App 主程序中对动态库的编译顺序来初始化动态库的镜像(先编译先初始化,后编译后初始化)
- dyld 会优先初始化动态库的镜像,然后再初始化 App 主程序的镜像(App 主程序的镜像最后初始化)
- 在同一个镜像内,
Objective-C 的 +load 方法会比C 的 __attribute__((constructor) 函数先调用 - 所有镜像(包括 App 主程序的镜像)中的
+load 方法和__attribute__((constructor) 函数都会比主程序的 main 函数先调用 - 步骤 ⑨ 中的函数调用栈与前面第八小节:⑧ 执行初始化方法 中的源码分析结果是一致的
补充:懒绑定函数的调用流程
- ① 前面我们在讲解 iOS 系统的懒绑定机制时,知道了:MachO 在进行
Lazy Symbol的绑定时,会调用位于Section64(__TEXT, __stub_helper)中的懒绑定函数dyld_stub_binder。实际上对于 MachO 文件来说,dyld_stub_binder也是一个外部符号,其实现位于 dyld 的源码中

- ②
dyld::fastBindLazySymbol(...)函数用于获取需要进行懒绑定的镜像,并调用ImageLoader::doBindFastLazySymbol(...)函数执行懒绑定
ImageLoader::doBindFastLazySymbol(...)是一个虚函数,会根据不同的镜像类型调用不同的实现

- ③
ImageLoaderMachOCompressed::doBindFastLazySymbol(...)函数为压缩类型的镜像进行懒绑定并返回真实的符号地址

ImageLoaderMachOClassic::doBindFastLazySymbol(...)函数的实现只有一句代码:抛出异常
这是因为经典类型的镜像的LinkEdit段没有LC_DYLD_INFO、LC_DYLD_INFO_ONLY,不能进行压缩类型的懒绑定

思考
-
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 的ViewDebug、MainThreadChecker),而并非是面向 iOS 开发者的设计
本文深入解析了dyld加载和链接动态库的过程,包括环境设置、系统缓存加载、主程序实例化、动态库加载与链接等关键步骤。同时介绍了Objective-C运行时机制与dyld的交互。
&spm=1001.2101.3001.5002&articleId=108579159&d=1&t=3&u=9b28bc28a0a547e982eff2d0eb60a355)
2624

被折叠的 条评论
为什么被折叠?



