探究APP启动到main函数之间的操作流程。
应用程序加载 例子
- 创建了一个简单的工程,在入口main函数中加打印
1223333,同时加上一个 为kcFunc()的c++函数,在viewcontroller 加上了一个load()方法
2.运行结果发现
- 1.调用了load,
- 2.调用了c++函数
- 3.最后才进入main 打印了1223333
3.这样的一个结果我不太明白为什么先调用了load,和c++ 函数才进入main呢,main是一个app的入口呀。说明在app启动之前,系统做了什么样的操作呢。
1.1、库
每个程序的运行都会依赖一些基础的库,比如说UIKit,CoreFoundation等。
库是一些可执行的二进制文件,能被操作系统加载到内存中。
库有两种形式,就是静态库(.a , .lib)和动态库(.so , .dll),两个库主要表现在链接的区别。
静态库:静态库在编译时加载,在链接时会完整的复制到可执行文件中,此时的静态库就不会在改变了,因为它是编译时被直接拷贝一份,复制到目标程序里的。- 优点:编译后的执行文件不需要外部库的支持,直接就能使用。
- 缺点:有多个app使用就会被复制多份,不能共享且占用更多冗余内存。所有的函数都在库中,因此当修改函数时需要重新编译。
动态库:程序编译时并不会链接到目标程序中,目标程序只会存储指向动态库的引用,在程序运行时由系统动态加载到内存。-
优势:
- 减少打包之后app的大小:因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,与静态库相比,减少了app的体积大小
共享内存,节约资源:同一份库可以被多个程序使用- 通过更新动态库,达到更新程序的目的:由于运行时才载入的特性,可以随时对库进行替换,而不需要重新编译代码
-
缺点:动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境,如果环境缺少了动态库,或者库的版本不正确,就会导致程序无法运行
-
编译过程
源文件:载入.h、.m、.cpp等文件
预编译:替换宏,删除注释,展开头文件,产生.i文件
编译:编译器将.i文件转换为汇编语言,产生.s文件
汇编:将汇编文件转换为机器码文件,产生.o文件
链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
其中编译过程如下图所示,主要分为以下几步:
-
生成的可执行文件
-
把这个
MachO文件,拖拽到MachOView里面就可以查看MachO的结构。 -
Header 头部,包含可以执行的CPU架构,比如x86,arm64
-
Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
-
Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。
dyly
我理解为就是将可执行文件和手机系统的库产生链接,即可执行文件通过动态链接一些系统库,才能真正的在手机上运行。
dyld 全名 The dynamic link editor . 它是苹果的动态链接器,是苹果操作系统一个重要组成部分 ,在应用被编译打包成可执行文件格式的 Mach-O 文件之后 ,交由 dyld 负责链接 , 加载程序 。
- runtime 注册回调函数:
_dyld_objc_notify_register(&map_images, load_images, unmap_image); - image : 库 映射完成后的镜像文件,
- 映射:从磁盘copy一份到内存
- 做一个拓展小tip 找到系统的库文件。
- 1.断点(随便打个断点)
- 2.LLDB 输入指令 image list
- 3.搜索 CoreFoundation
- 4.复制路径前往文件就可以找到系统的库,
- 5.我这个是在模拟器上的。
2 dyld流程
- 1.在控制器前加+load方法,打上断点。LLDB bt 打印函数栈,再看左侧的栈打印,其实就是一一对应的。
- 流程大致上就是
- 1._dyld_start
- 2.dyldbootstrap::start
- 3.dyld::_main
- 4.dyld::useSimulatorDyld 这个我猜测应该是我调用的时模拟器的,所以使用的是模拟器的dyld
- 5.start_sim
- 6.dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*)
- 7.dyld::initializeMainExecutable()
- 8.ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&)
- 9.ImageLoader::processInitializers(
- 10.ImageLoader::recursiveInitialization
- 11.dyld::notifySingle
- 12.load_images
- APP加载过程
- 1.app启动
- 2.加载libSystem
- 3.Runtime向dyld注册回调函数
- 4.加载新的image
- 5.执行map_images、load_images
- 6.调用main函数
- 现在我们在main入口打上断点。过掉上个断点走到这里,这里是程序的入口,说明控制器的加载是在程序进来之前完成的。
目标 研究程序加载 从dyldstart -> main 中间所做的事情
- 1.即弄明白这段是做了什么。
- 2.下载dyld 852 源码地址
- 3.load 函数在 main 函数调用之前
- 4.断点load -> LLDB bt
-
frame #12: 0x00000001130d4025 dyld_dyld_start + 37` 知道这个是 dyld的入口
1、找到目标
- 1.查找汇编 _dyld_start
- 2.找到目标,不同架构,选择不同,但是主体流程相同。
- 3.调用了这个函数
dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue) - 4.接下来就去搜索
dyldbootstrap这个命名空间,然后继续在其中找到start方法,发现其是返回了dyld::_main函数的结果。c++的语法问题。类似于 dyldbootstrap这个类的 start 方法。 - 5.返回_main函数
- 6.跳转到main
1000+行代码选择性查看学习
2.进入了主要流程中的main函数
-
第一步- 条件准备 : 环境、平台、版本、路径、主机信息。。。-
getHostInfo(mainExecutableMH, mainExecutableSlide);架构信息的准备
-
- 镜像文件平台信息
- 镜像文件平台信息
-
- 文件路径
- 文件路径
-
checkEnvironmentVariables(envp);//检查设置的环境变量 -
defaultUninitializedFallbackPaths(envp);//如果DYLD_FALLBACK为nil,将其设置为默认值 -
如果设置了DYLD_PRINT_OPTS环境变量则打印,如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量
-
-
- 加载共享缓存,系统级别处理。
- 2.1调用
checkSharedRegionDisable函数检查并加载共享缓存库(iOS无法禁用共享缓存库) mapSharedCache(mainExecutableSlide);检查共享缓存是否映射到共享区域
-
3.
addDyldImageToUUIDList();将dyly本身添加到UUID列表 -
4.主程序初始化 sMainExecutable
-
4.1我们从结果result 的反推得知由
sMainExecutable创建 -
4.2.反向推到
sMainExecutable创建通过查看源码查看 , 结合函数调用栈 , 我们跟进去调用流程 找到sMainExecutable。- 加载可执行文件,并生成一个ImageLoader实例对象 镜像文件加载器 addImage(image)
- 加载可执行文件,并生成一个ImageLoader实例对象 镜像文件加载器 addImage(image)
-
-
5.加载动态库并链接动态库
-
5.1
loadInsertedDylib(*lib);加载插入的动态库,遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载插入的动态库。 -
5.2.链接库
- 遍历 :
sInsertedDylibCount ImageLoader* image = sAllImages[i+1];link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);link插入的动态库this->recursiveApplyInterposing(context);插入任何动态加载的镜像文件
- 遍历 :
-
-
6.link主程序
-
7.weakBind 弱引用绑定主程序
-
8.主程序初始化运行,运行所有初始化程序
-
9.寻址主程序入口
-
10.监听dyly的main
notifyMonitoringDyldMain();
3.主程序运行流程 initializeMainExecutable();
现在我们连探究主程序初始化到底做了什么
sMainExecutable表示主程序变量,查看其赋值,是通过instantiateFromLoadedImage方法初始化
1.instantiateFromLoadedImage->instantiateMainExecutable
-
进入
instantiateFromLoadedImage函数查看 -
进入
instantiateMainExecutable函数查看 -
其中
sniffLoadCommands中是获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验。确定此mach-o文件是否具有压缩的LINKEDIT 以及 段数 -
进入
sniffLoadCommands中函数查看
2.主程序运行
主程序初始化完成后,进入主程序的运行,我们返回代码重新到这里
initializeMainExecutable();
- 进入
initializeMainExecutable()查看- 插入的动态库遍历调用runInitializers
- 主程序调用runInitializers
- 全局搜索进入
runInitializers()查看 - 1.初始化准备:processInitializers 准备镜像文件准备
- 1.进入
processInitializers函数的源码实现,其中对镜像列表调用recursiveInitialization函数进行递归实例化 - 2.recursiveInitialization 底层的依赖文件加载
- 3.两个操作,1context.notifySingle->2.doInitialization,然后notifySingle
- 1.进入
- 先查找这个
context.notifySingle - 找到这个
(*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*); - 全局搜索找到这里
- 全局搜索
sNotifyObjCInit,发现没有找到实现,有赋值操作 - 查看
registerObjCNotifiers的调用时机_dyld_objc_notify_register函数调用
-
在objc源码库的objc_init中调用这个方法。
-
整理一下顺序
context.notifySingle -> (*sNotifyObjCInit)sNotifyObjcInit 是在 registerOBjCNotifiers函数调用的init赋值的,即:sNotifyObjCInit = init_dyly_objc_notify_register 调用了 registerObjCNotifiers, init = init_dyly_objc_notify_register 传递了三个参数进来- &map_images
- load_images
- unmap_image
- 得到 load_images = init = sNotifyObjcInit
load_images
load_images->call_load_methods()- prepare_load_methods
-
classref_t const *classlist = _getObjc2NonlazyClassList(mhdr, &count);取出所有加载进去的类列表 -
遍历:schedule_class_load
- 递归父类:schedule_class_load(cls->superclass)
- add_class_to_loadable_list 把这个类的load方法加到list中
-
classref_t *classlist = getObjc2NonlazyClassList(mhdr,&count);
-
-
进入查看
call_load_methods循环调用了call_calss_loads()还有call_category_loads();分类的load -
进入
call_calss_loads()查看 了解到这里调用的load方法证实我们前文提及的类的load方法,
-
objc_autoreleasePoolPush() 压栈自动释放池
-
class_class_loads()
-
call_category_loads()
-
objc_autoreleasePoolPop(pool);出栈
-
所以,
load_images调用了所有的load函数,以上的源码分析过程正好对应堆栈的打印信息
map_images
-> map_images_nolock
-> _read_images
- 类 gdb_objc_readied_classes
- 方法 nameSelectors
- 协议 protocol_map
- 主要作用
- 初始化类 : realizeClass 设置rw/ro
- 处理分类: 把分类中的方法、协议、属性添加到类中
- 加载相应的哈希表
现在只有知道开始调用
objc_init才能给这个sNotifyObjcInit赋值才能走通context.notifySingle
- 全局搜索是完全没有的
- 所以先放一下看看那个
doInitialization
doInitialization
-
现在重新回去来到
this->doInitialization(context)镜像文件初始化 看1657行, -
进入
doInitialization查看 发现调用了两个方法。1.doImageInit(),2.doModInitFunctions()咱也不知道他们干了啥都看下 -
进入
doImageInit(context)查看 找到这个Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);//Mach-O地址偏移,得到一个函数方法,libSystemInitializedlibSystem 初始化必须提前 不懂放弃 -
进入
doModInitFunctions(context);C++函数处理 - libSystem.B.dylib 'libSystem_initializer:-> - libdispatch.dylib 'libdispatch_init:-> - _os_object_init(libdispatch.dylib)-> - _objc_init(libobjc) -
这里的堆栈信息打印了提示我们看这个。
进入
doModInitFunctions源码实现,这个方法中加载了所有Cxx文件
- 在
libsystem中查找libSystem_initializer,查看其中的实现\
- 根据前面的堆栈信息,我们发现走的是
libSystem_initializer中会调用libdispatch_init函数,而这个函数的源码是在libdispatch开源库中的,在libdispatch中搜索libdispatch_init\
- 进入
_os_object_init源码实现,其源码实现调用了_objc_init函数
- 所以整体思路可以查看函数调用栈来解释这个流程,以及找到相对应的源码进行学习。