1. 程序加载原理

系统内核将可执行文件从磁盘中加载到内存中,内存中的二进制文件,我们称之为image镜像文件

之后,系统会加载动态链接器dylddyld只会负责动态库的加载,主程序也会作为镜像形式被dyld管理起来

dyld从可执行文件的依赖开始, 递归加载所有依赖的动态库。无论是动态链还是App本身的可执行文件,它们都是image镜像,而每个App都是以image为单位进行加载的

2. 编译过程

image.png

  • 源文件:.h.m.cpp等文件
  • 预编译:替换宏,删除注释,展开头文件,词法分析、语法分析,生成.i文件
  • 编译:转换成汇编语言,生成.s文件
  • 汇编:把汇编语言文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件

3. 静态库 & 动态库

库(Library):就是⼀段编译好的⼆进制代码,可以被系统加载,加上头⽂件就可以供别⼈使⽤

常⽤库⽂件格式:.a.dylib.framework.xcframework.tdb

3.1 什么时候会⽤到库?

  • 某些代码需要给别⼈使⽤,但是我们不希望别⼈看到源码,就需要以库的形式进⾏封装,只暴露出头⽂件
  • 对于某些不会进⾏⼤改动的代码,我们想减少编译的时间,就可以把它打包成库。因为库是已经编译好的⼆进制,编译的时候只需要Link⼀下,不会浪费编译时间

3.2 什么是链接?

库(Library)在使⽤的时候需要链接(Link

链接的⽅式有两种:

  • 静态
  • 动态

3.3 什么是静态库?

静态库即静态链接库:可以简单的看成⼀组⽬标⽂件的集合。即很多⽬标⽂件经过压缩打包后形成的⽂件。Windows下的.libLinuxMac下的.aMac独有的.framework

缺点:浪费内存和磁盘空间,模块更新困难

3.4 什么是动态库?

与静态库相反,动态库在编译时并不会被拷⻉到⽬标程序中,⽬标程序中只会存储指向动态库的引⽤。等到程序运⾏时,动态库才会被真正加载进来。格式有:.framework.dylib.tbd

缺点:会导致⼀些性能损失。但是可以优化,⽐如延迟绑定(Lazy Binding)技术

4. dyld

4.1 dyld是什么?

dyld:动态链接器,加载所有的库和可执行文件

libdyld.dylib:给我们的程序提供在Runtime期间能使⽤动态链接功能

4.2 加载程序的过程

  • 调⽤fork函数,创建⼀个process(进程)调⽤execve或其衍⽣函数,在该进程上加载,执⾏我们的Mach-O⽂件
  • 将⽂件加载到内存
  • 开始分析Mach-O中的mach_header,以确认它是有效的Mach-O⽂件
  • 验证通过,根据mach_header解析load commands。根据解析结果,将程序各个部分加载到指定的地址空间,同时设置保护标记
  • LC_LOAD_DYLINKEN中加载dyld
  • dyld开始⼯作

4.3 dyld的⼯作是什么?

  • 执⾏⾃身初始化配置加载环境LC_DYLD_INFO_ONLY
  • 加载当前程序链接的所有动态库到指定的内存中LC_LOAD_DYLIB
  • 搜索所有的动态库,绑定需要在调⽤程序之前⽤的符号(⾮懒加载符号)LC_DYSYMTAB
  • 在间接符号表(indirect symbol table)中,将需要绑定的导⼊符号真实地址替换LC_DYSYMTAB
  • 向程序提供在Runtime时使⽤dyld的接⼝函数(存在libdyld.dylib中,由LC_LOAD_DYLIB提供)
  • 配置Runtime,执⾏所有动态库/image中使⽤的全局构造函数
  • dyld调⽤程序⼊⼝函数,开始执⾏程序LC_MAIN

4.4 程序启动的初始方法

ViewController中,加入load方法

  1. @implementation ViewController
  2. + (void)load{
  3. NSLog(@"ViewController load方法");
  4. }
  5. @end

main.m中,加入C++构造函数

  1. __attribute__((constructor)) void func(){
  2. printf("\n C++构造函数:%s \n",__func__);
  3. }

main函数中,增加NSLog打印

  1. int main(int argc, char * argv[]) {
  2. NSString * appDelegateClassName;
  3. @autoreleasepool {
  4. NSLog(@"main函数");
  5. // Setup code that might create autoreleased objects goes here.
  6. appDelegateClassName = NSStringFromClass([AppDelegate class]);
  7. }
  8. return UIApplicationMain(argc, argv, nil, appDelegateClassName);
  9. }
  10. -------------------------
  11. //输出结果:
  12. ViewController load方法
  13. C++构造函数:func
  14. main函数
  • load方法→C++构造函数→main函数

main函数为程序入口,但load方法和C++构造函数的执行时机比main函数更早,它们是被谁调用的?

load方法中设置断点,查看函数调用栈
image.png

应用启动时的初始方法,由dyld中的_dyld_start开始的

5. 源码分析

5.1 _dyld_start

打开dyld-852源码,搜索_dyld_start

_dyld_start由汇编代码实现,内部调用dyldbootstrap::start函数
image.png

5.2 dyldbootstrap::start

dyldbootstrap::startC++代码实现

搜索dyldbootstrap,找到命名空间及start函数
image.png

来到start函数
image.png

  • 重定位dyld,进程启动,它的虚拟内存地址就要进行重定位
  • 对于栈溢出的保护
  • 初始化dyld
  • 调用dyld_main函数

5.3 dyld::_main

5.3.1 【第一步】配置环境变量

image.png

  • 内核检测
  • 获取主程序可执行文件
  • 获取当前架构的信息

image.png

  • 设置MachO HeaderASLR
  • 设置上下文,全部存储在gLinkContext对象中

image.png

  • 配置进程是否受限,苹果进程受AFMI保护(Apple Mobile File Integrity苹果移动文件保护)
  • 判断是否强制使用dyld3
  • 判断环境变量,如果发生改变,再次调用setContext设置上下文。否则检测环境变量,设置默认值

image.png

  • 在项目中配置DYLD_PRINT_OPTSDYLD_PRINT_ENV环境变量,可以进行打印

5.3.2 【第二步】加载共享缓存

image.png

  • 加载共享缓存,UIKitFoundation等系统动态库,都存储在共享缓存中。在iOS中,必须有共享缓存
  • 检测共享缓存是否映射到公共区域,调用mapSharedCache函数,传递ASLR

进入mapSharedCache函数
image.png

  • 调用loadDyldCache函数

进入loadDyldCache函数
image.png

  • 满足条件,依赖库只加载到当前进程
  • 如果已经加载共享缓存,不做任何处理
  • 否则,首次加载,调用mapCacheSystemWide函数

加载App之前,首先加载的就是共享缓存。每个App都需要UIKitFoundation等系统动态库,但程序之前的进程不互通,所以系统动态库存放在共享缓存中

自己写的动态库和其他三方库,不会存储在共享缓存中

5.3.3 【第三步】实例化主程序

中间不论执行dyld2还是dyld3的流程,后面都会执行实例化主程序的代码

dyld加载的第一个image镜像就是主程序
image.png

  • 调用instantiateFromLoadedImage函数,传入主程序的MachO HeaderASLR,路径,创建一个ImageLoader实例对象

进入instantiateFromLoadedImage函数
image.png

  • 调用instantiateMainExecutable函数,为主可执行文件创建映像,返回一个ImageLoader类型的image对象

进入instantiateMainExecutable函数
image.png

  • 调用sniffLoadCommands函数,获取MachO类型文件的Load Command的相关信息,并对其进行各种校验

进入sniffLoadCommands函数
image.png

  • compressed:分析MachO文件获取的值
  • segCountSegment总数
  • libCount:依赖库总数
  • codeSigCmd:签名
  • encryptCmd:加密

来到sniffLoadCommands函数结尾处
image.png

  • 程序的Segment总数,不能超过2552
  • 程序的依赖库总数,不能超过4095

回到instantiateMainExecutable函数
image.png

  • 根据compressed判断,使用相应的子类实例化主程序,返回实例对象

回到instantiateFromLoadedImage函数
image.png

  • 拿到实例化后的image对象
  • image对象添加到image列表中
  • 返回image对象

所以image列表中,第一个image一定是主程序

5.3.4 【第四步】加载插入的动态库

回到_main函数
image.png

  • 检测代码,检查设备、系统版本等

image.png

  • 设置加载动态库的版本

image.png

  • 判断环境变量,是否有插入的动态库
  • 如果有,遍历插入的动态库,依次调用loadInsertedDylib函数

5.3.5 【第五步】链接主程序

image.png

  • 调用link函数,链接主程序

进入link函数
image.png

  • 记录起始时间
  • 递归加载主程序依赖的库,完成之后发通知
  • 重定向,修正ASLR
  • 绑定非懒加载符号
  • 绑定弱引用符号

image.png

  • 递归应用插入的动态库
  • 注册
  • 记录结束时间
  • 计算时间差,当项目配置环境变量,用于显示各步骤耗时

5.3.6 【第六步】链接插入的动态库

image.png

  • 循环绑定插入的动态库

5.3.7 【第七步】绑定弱引用符号

image.png

  • 绑定弱引用符号

5.3.8 【第八步】初始化main方法

image.png

5.3.9 【第九步】返回主程序入口

image.png

  • 读取MachOLC_MAIN,找到主程序的main函数地址
  • 返回main函数

6. 初始化main方法的流程分析

6.1 initializeMainExecutable函数

image.png

  • 初始化插入的动态库
  • 初始化主程序

6.2 runInitializers函数

image.png

  • 调用processInitializers函数

6.3 processInitializers函数

image.png

  • images调用recursiveInitialization函数,进行递归实例化

6.4 recursiveInitialization函数

image.png

  • 调用notifySingle函数

6.5 调用objc中的load_images函数

从函数调用栈来看,在notifySingle函数中,会调用load_images函数
image.png

但是在notifySingle函数中,并没有找到load_images的相关线索。并且load_imagesobjc的函数,在dyld中又是如何调用的?

  1. frame #1: 0x000000019cb943bc libobjc.A.dylib`load_images + 944

6.5.1 notifySingle函数

image.png

  • 如果sNotifyObjCInit不为空,使用回调指针,执行一个回调函数

6.5.2 registerObjCNotifiers函数

搜索sNotifyObjCInit,找到它在项目中被赋值的地方

找到registerObjCNotifiers函数,将参数init赋值给sNotifyObjCInit
image.png

6.5.3 _dyld_objc_notify_register函数

搜索registerObjCNotifiers,找到函数的调用者

找到_dyld_objc_notify_register函数
image.png

搜索_dyld_objc_notify_register,项目中无法找到该函数的调用者

使用符号断点寻址新的线索

设置_dyld_objc_notify_register符号断点,运行项目
image.png

  • 找到函数调用者,在objc源码中,被_objc_init调用

6.5.4 _objc_init函数

打开objc4-818.2源码,搜索_objc_init

找到_objc_init函数
image.png

6.5.5 load_images函数

image.png

  • 准备load方法
  • 调用load方法

6.5.6 prepare_load_method函数

准备load方法
image.png

  • 整理所有类的load方法
  • 整理所有分类的load方法

6.5.7 schedule_class_load函数

整理所有类的load方法
image.png

  • 递归Superclass,确保父类优先
  • 整理所有类的load方法

add_class_to_loadable_list函数
image.png

  • 获取clsload方法,不存在返回
  • 存在记录到数组中,loadable_classes_used为下标,存储clsload方法

getLoadMethod函数
image.png

  • 循环方法列表,方法名称为load,返回imp

6.5.8 add_category_to_loadable_list函数

整理所有分类的load方法
image.png

  • 获取分类的load方法,不存在返回
  • 存在记录到数组中,loadable_categories_used为下标,存储分类和load方法

_category_getLoadMethod函数
image.png

  • 循环分类下的方法列表,方法名称为load,返回imp

6.5.9 call_load_methods函数

image.png

  • 循环调用类和分类的load方法

sNotifyObjCInit的赋值流程:libobjc:_objc_initlibdyld:_dyld_objc_notify_registerdyld::registerObjCNotifierssNotifyObjCInit = init(load_images)

我们找到了sNotifyObjCInit的赋值流程,但是_objc_init又是如何在dyld中被调用的?

6.6 doInitialization函数

打开dyld源码,回到recursiveInitialization函数
image.png

进入doInitialization函数
image.png

  • 分别调用doImageInitdoModInitFunctions函数

6.8.1 调用_objc_init函数

进入doImageInit函数
image.png

  • 要求:必须先运行libSystem初始化器

此时,还未找到_objc_init函数的相关代码,使用符号断点寻址新的线索

设置_objc_init符号断点,运行项目
image.png


打开Libsystem源码,搜索libSystem_initializer

进入libSystem_initializer函数
image.png

  • 调用libdispatch源码中的libdispatch_init函数

打开libdispatch源码,搜索libdispatch_init

进入libdispatch_init函数
image.png

进入_os_object_init函数
image.png

  • 调用libobjc源码中的_objc_init函数

_objc_init调用流程:_dyld_startdyldbootstrap::startdyld::_maindyld::initializeMainExecutableImageLoader::runInitializersImageLoader::processInitializersImageLoader::recursiveInitializationImageLoaderMachO::doInitializationImageLoaderMachO::doModInitFunctionslibSystem:libSystem_initializerlibdispatch:libdispatch_initlibdispatch:_os_object_initlibobjc:_objc_init

6.8.2 调用C++构造函数

进入doModInitFunctions函数
image.png

  • 遍历执行所有C++构造函数

6.7 map_images & load_images

objc源码中,传入_dyld_objc_notify_register函数中的map_imagesload_images,它们在dyld中是如何被调用的?

参数经过_dyld_objc_notify_register传递给registerObjCNotifiers函数

进入registerObjCNotifiers函数
image.png

  • map_images赋值给sNotifyObjCMapped
  • load_images赋值给sNotifyObjCInit

6.7.1 sNotifyObjCMapped的调用

搜索sNotifyObjCMapped,只找到一处调用代码

找到notifyBatchPartial函数
image.png

搜索notifyBatchPartial,找到函数的调用者

找到registerObjCNotifiers函数,在回调函数执行后,立刻就会调用sNotifyObjCMapped
image.png

6.7.2 sNotifyObjCInit的调用

搜索sNotifyObjCInit,找到两处调用代码

1、找到registerObjCNotifiers函数,在回调函数执行后,立刻就会调用sNotifyObjCInit
image.png

2、找到notifySingle函数,当statedyld_image_state_dependents_initialized时,调用sNotifyObjCInit
image.png

6.7.3 处理系统库的调用流程

处理共享缓存中的系统库,会调用多次recursiveInitialization函数,此时sNotifyObjCInit被回调函数registerObjCNotifiers触发,时机在主程序的image之前,虽然调用load_images函数,但不触发load方法
image.png

处理系统库的调用流程:ImageLoader::recursiveInitializationImageLoaderMachO::doInitializationImageLoaderMachO::doModInitFunctionslibSystem:libSystem_initializerlibdispatch:libdispatch_initlibdispatch:_os_object_initlibobjc:_objc_initlibdyld:_dyld_objc_notify_registerdyld::registerObjCNotifierslibobjc:load_images

6.7.4 load方法的调用流程

recursiveInitialization函数处理主程序的image时,调用notifySingle函数,传入statedyld_image_state_dependents_initialized,调用sNotifyObjCInit
image.png

load方法的调用流程:_dyld_startdyldbootstrap::startdyld::_maindyld::initializeMainExecutableImageLoader::runInitializersImageLoader::processInitializersImageLoader::recursiveInitializationdyld::notifySingleload_images(回调函数:sNotifyObjCInit

7. 返回主程序入口的流程分析

_dyld_start汇编代码的结束位置
image.png

  • 跳转至x16寄存器
  • x16main函数的地址

dyld源码中,找到dyldStartup.s汇编代码
image.png

  • dyld中跳转到主程序入口

8. dyld2 & dyld3

8.1 dyld3闭包模式

iOS11后,引入dyld3的闭包模式,以回调的方式加载,加载更快,效率更高

iOS13后,动态库和三方库,也使用闭包模式加载

image.png

  • 判断sClosureMode,如果是闭包模式,执行else代码分支
  • 配置如何加载MachO

image.png

  • 闭包也是实例对象,优先从共享缓存中获取实例对象
  • 如果对象不为空,但对象已失效,重新将对象设置为nullptr

image.png

  • 再次判断对象是否为空,如果为空,在缓存中获取对象
  • 如果缓存中未找到对象,调用buildLaunchClosure函数创建

image.png

  • 判断对象不为空,调用launchWithClosure函数启动,传入闭包对象,返回是否成功的结果
  • 如果启动失败并且过期,再创建一次
  • 判断再次创建的对象不为空,再次启动
  • 如果启动成功,拿到主程序main的函数,直接返回结果

8.2 dyld2流程

如果不是dyld3的闭包模式,进入dyld2流程
image.png

  • 不使用dyld3的闭包模式,将变量设置为0,表示使用旧模式加载
  • 把两个回调地址放到stateToHandlers数组中
  • 分配初始化空间,尽量分配足够大的空间,以供后续使用
  • dyld加入到UUID的列表中

总结

程序加载原理:

  • 系统内核将可执行文件从磁盘中加载到内存中
  • 系统会加载动态链接器dyld
  • dyld从可执行文件的依赖开始, 递归加载所有依赖的动态库

编译过程:

  • 源文件:.h.m.cpp等文件
  • 预编译:替换宏,删除注释,展开头文件,词法分析、语法分析,生成.i文件
  • 编译:转换成汇编语言,生成.s文件
  • 汇编:把汇编语言文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件

静态库 & 动态库:

  • 库(Library):就是⼀段编译好的⼆进制代码,可以被系统加载,加上头⽂件就可以供别⼈使⽤
  • 库在使⽤的时候需要链接(Link
  • 链接的⽅式有两种:静态、动态
  • 静态库:可以简单的看成⼀组⽬标⽂件的集合
  • 动态库:在编译时并不会被拷⻉到⽬标程序中,运⾏时才会被真正加载进来

dyld

  • dyld:动态链接器,加载所有的库和可执行文件
  • libdyld.dylib:给我们的程序提供在Runtime期间能使⽤动态链接功能

源码分析:

  • _dyld_start:由汇编代码实现

◦ 内部调用dyldbootstrap::start函数

  • dyldbootstrap::start:由C++代码实现

◦ 初始化dyld
◦ 调用dyld_main函数

  • dyld::_main

◦ 【第一步】配置环境变量
◦ 【第二步】加载共享缓存
◦ 【第三步】实例化主程序
◦ 【第四步】加载插入的动态库
◦ 【第五步】链接主程序
◦ 【第六步】链接插入的动态库
◦ 【第七步】绑定弱引用符号
◦ 【第八步】初始化main方法
◦ 【第九步】返回主程序入口

初始化main方法的流程分析:

  • sNotifyObjCInit的赋值流程:libobjc:_objc_initlibdyld:_dyld_objc_notify_registerdyld::registerObjCNotifierssNotifyObjCInit = init(load_images)
  • _objc_init调用流程:_dyld_startdyldbootstrap::startdyld::_maindyld::initializeMainExecutableImageLoader::runInitializersImageLoader::processInitializersImageLoader::recursiveInitializationImageLoaderMachO::doInitializationImageLoaderMachO::doModInitFunctionslibSystem:libSystem_initializerlibdispatch:libdispatch_initlibdispatch:_os_object_initlibobjc:_objc_init
  • 处理系统库的调用流程:ImageLoader::recursiveInitializationImageLoaderMachO::doInitializationImageLoaderMachO::doModInitFunctionslibSystem:libSystem_initializerlibdispatch:libdispatch_initlibdispatch:_os_object_initlibobjc:_objc_initlibdyld:_dyld_objc_notify_registerdyld::registerObjCNotifierslibobjc:load_images
  • load方法的调用流程:_dyld_startdyldbootstrap::startdyld::_maindyld::initializeMainExecutableImageLoader::runInitializersImageLoader::processInitializersImageLoader::recursiveInitializationdyld::notifySingleload_images(回调函数:sNotifyObjCInit

返回主程序入口的流程分析:

  • dyld中,由汇编代码实现,跳转到主程序入口

dyld2 & dyld3

  • dyld3闭包模式
  • dyld2流程