OC底层原理探索之应用程序加载

485 阅读9分钟

应用程序的加载

库:可执行的二进制文件,可以被系统加载到内存。库分为两种,一种是静态库,一种是动态库(.so .dll .framework...) 静态库:按顺序加载,可能会重复添加 动态库:当用到的时候才加载,不会重复,共享内存减少包的大小,所以苹果的都是动态库

编译过程image.png

可执行文件

随便build一个工程,成功之后点击 image.png Show in Finder -> 显示包内容,这个黑色的就是可执行文件 mach-O。把一个mach-O文件直接拖到终端就可以运行,这里使用ios的工程需要授权打开模拟器,使用mac的工程则直接运行。 image.png

dyld动态链接器

image:库映射到内存就是image

动态链接库.png

dyld加载过程

探索辅助我们翻看源码定位到_objc_init函数也能稍微窥探一二,观察到这里的注释, 1.引导启动初始化,用dyld来注册镜像通知 2.在库被加载之前调用libSystem

/***********************************************************************
* _objc_init  
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    // 修复延迟初始化直到找到一个可用的objc镜像
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

在iOS工程的main函数打上断点,我们发现在main函数调用之前,ViewControllerload方法已经调用了,说明load方法在main调用之前。 image.png 聚焦load方法,在这里继续打上断点可以看到在load之前调用了哪些方法 image.png 因为这里是个栈结构,先进后出,所以从下往上分析。此时我们发现了dyld库,我们从opensource上下载最新的dyld库来分析,这个库底层依赖的比较多,所以暂时运行不起来,但是不妨碍我们分析 1._dyld_start 2.dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) 3.dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) 4.dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*, unsigned long*) 5.dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) 6.dyld::initializeMainExecutable() 7.ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) 8.ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) 9.dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) 10.load_images 我们也打开dyld这个库,从方法1来到了方法2 image.png 继续搜索dyldbootstrap找到这个命名空间里面的start函数 对应控制台的方法2 image.png start函数最后一行返回了dyld::_main也就是上面的方法3,现在我们已经完成了dyld的引导bootstrap,调用dyld的main函数, image.png 翻了一下这里面大概有1000多行。不知从何看起,我们先从return入手吧,直接看return看到返回的是result这个函数里面有result的地方并不多,看赋值的地方和注释 「找到主 可执行文件的入口点」sMainExecutable image.png 继续搜索sMainExecutable看看它做了什么,大致浏览了下,发现思路没有错,result跟sMainExecutable有关,那么我们就找到它的相关的初始化地方 1.为主可执行文件初始化镜像文件加载器,实例化主程序 image.png 2.加载任何插入的库 image.png 3.link主程序 image.png 4.link插入的动态库 注释(在link主程序之后才执行此操作 以便dylibs被插入) image.png 5.弱引用绑定主程序 (在所有的镜像文件链接完毕之后) image.png 6.运行所有的初始化 run起来 对应控制台的方法6 image.png 6.1运行主可执行文件和所有的库 对应控制台的方法7 image.png 6.2镜像文件加载初始化过程 对应控制台的方法8 image.png 我们看ImageLoader::processInitializers注释:向上的dylib初始化器非常快,为了处理向上链接而不是向下链接的悬空dylibs,所有向上链接的dylibs都将其初始化延迟到通过向下链接的dylibs的递归完成之后。602行注释:在镜像列表中调用镜像文件的递归init,构建一个未初始化向上依赖的新列表 image.png 全局搜索recursiveInitialization(const定位到这个镜像文件的递归初始化方法,看注释让Objc知道我们将要初始化这个镜像首先初始化镜像的依赖库,这里找到了notifySingle函数 对应控制台的方法9 image.png 6.3 查找notifySingle函数,通知objc镜像文件初始化static _dyld_objc_notify_init sNotifyObjCInit image.png 找到这个赋值的地方_dyld_objc_notify_init发现了一个熟悉的身影 _dyld_objc_notify_register这就是我们_objc_init的底层源码出现过的嘛,此时就正式从dyld跳出到了底层C行程了一个闭环 image.png 7.通知任何监视进程 该进程即将进入mainimage.png

_dyld_objc_notify_register(&map_images, load_images, unmap_image)

&map_images第一个参数是类的加载,协议属性ro/ rw类的初始化, 懒加载等等 load_imagesload方法的集合 我们需要知道里面的三个参数的赋值和调用时机。

// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
	dyld::registerObjCNotifiers(mapped, init, unmapped);
}

进入dyld::registerObjCNotifiers方法,看到在这个方法里赋值

// _dyld_objc_notify_init
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;
 	// ...
}

sNotifyObjCMapped调用时机

全局搜索定位到了static void notifyBatchPartial这个方法,我们看到里面有一行调用

				(*sNotifyObjCMapped)(objcImageCount, paths, mhs);

继续搜索static void notifyBatchPartial这个方法,我们发现这里赋值

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
}

也就是说,在sNotifyObjCMapped赋值完之后,就调用了(*sNotifyObjCMapped)

sNotifyObjCInit的调用时机

全局搜索在这个函数找到了调用,发现有两个地方调用,一个是在registerObjCNotifiers函数中

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;
	...
	(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}

也就是说,在sNotifyObjCInit赋值完之后,也调用了(*sNotifyObjCInit);另外一个地方是在static void notifySingle

为什么load比C++函数先调用?

由上面我们知道(*sNotifyObjCInit)在方法notifySingle的调用中,由方法的调用顺序我们知道,在镜像文件的递归加载中调用了notifySingle函数 image.png 按照上面的顺序,我们自然而然的会认为c++的方法调用顺序在load方法之前,那么真的是这样的吗?我们来实验下 1.在main函数的地方写上一个c++函数

__attribute__((constructor)) void testFunc(){
    printf("来了 : %s \n",__func__);
}

2.在Person中实现load()方法

+ (void)load {
    printf("来了 : %s \n",__func__);
}

3.在源码中也实现一个c++函数 image.png 此时在main函数上断点。发现调用顺序是这样的 image.png 所以可以得出顺序 镜像文件内的C++ > load > 当前工程内的C++

libSystem引入

通过上面分分析,我们知道在dyld加载镜像文件完成之后注册通知告知objc _dyld_objc_notify_register此时和_objc_init这里遥相呼应,那么我们直接在源码里下断点,可以看到调用栈里面的方法调用顺序,在_dyld_start ->_os_object_init -> _objc_init中间有一步我们还没有分析,那就是_os_object_init这是一个libdispatch的库,我们还是直接从openSource上下载 image.png

libdispatch/libSystem

打开libdispatch库,搜索定位到_os_object_init方法调用的时候内部调用了_objc_init(这个就是objc的方法不是dispatchd的init)方法,最后在libdispatch_init的方法中调用的, image.png libSystem.B.dyliblibSystem_initializer-> _libdispatch_init->libdispatch.dyliblibdispatch_init -> libdispatch.dylib_os_object_init->libobjc.A.dylib_objc_init 总而言之,言而总之再次验证了一下上图用调用栈圈出来的那四行代码的执行涉及到了libdispatchlibSystem

dyld加载过程

重新打开dyld,搜索doModInitFunctions我们看到了一行注释libSystem initializer 必须第一个加载。可以猜测这个方法就是加载libSystem库,这个方法的调用确实在ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&)中搜索到,而在上面的6.2中镜像文件依赖的加载下面就是调用的这个方法 image.png 由此,整个分析的闭环形成。 dyld的链接过程.png

补充

image list

查看当前项目使用的images,使用image list命令就可以查到本地库的路径 image.png

dyld2和dyld3的区别

在iOS 13系统中,iOS将全面采用新的dyld 3以替代之前版本的dyld 2。dyld 3带来了可观的性能提升,减少了APP的启动时间,在开始之前,先展示一张来自wwdc2017/413 session中的截图,直观的展示了dyld 2和dyld 3的功能区别 image.png

dyld2

根据上图以及dyld源码可知,dyld 2主要工作流程为:

  • dyld的初始化,主要代码在dyldbootstrap::start,接着执行dyld::_main,dyld::_main代码较多,是dyld加载的核心部分;
  • 检查并准备环境,比如获取二进制路径,检查环境变量,解析主二进制的image header等信息;
  • 实例化主二进制的image loader,校验主二进制和dyld的版本是否匹配;
  • 检查shared cache是否已经map,没有的话则先执行map shared cache操作;
  • 检查DYLD_INSERT_LIBRARIES,有的话则加载插入的动态库(实例化image loader);
  • 执行link操作。这个过程比较复杂,会先递归加载依赖的所有动态库(会对依赖库进行排序,被依赖的总是在前面),同时在这阶段将执行符号绑定,以及rebase,binding操作;
  • 执行初始化方法。OC的+load以及C的constructor方法都会在这个阶段执行;
  • 读取Mach-O的LC_MAIN段获取程序的入口地址,调用main方法。

dyld3

dyld 3并不是WWDC19推出来的新技术,早在2017年就被引入至iOS 11,当时主要用来优化系统库。现在,在iOS 13中它也将用于启动第三方APP,将完全替代dyld 2。由于dyld 3的代码并未开源,目前仅能通过官方披露的资料来了解到底做了什么改进。dyld 3最大的特点就是部分是进程外的且有缓存的,在打开APP时,实际上已经有不少工作都完成了. ​

dyld 3包含三个组件:

  • 本进程外的Mach-O分析器/编译器

在dyld 2的加载流程中,Parse mach-o headers和Find Dependencies存在安全风险(可以通过修改mach-o header及添加非法@rpath进行攻击),而Perform symbol lookups会耗费较多的CPU时间,因为一个库文件不变时,符号将始终位于库中相同的偏移位置,这两部分在dyld 3中将采用提前写入把结果数据缓存成文件的方式构成一个lauch closure(可以理解为缓存文件)。

  • 本进程内执行lauch closure的引擎

验证”lauch closures“是否正确,映射dylib,执行main函数。此时,它不再需要分析mach-o header和执行符号查找,节省了不少时间。

  • lauch closure的缓存

系统程序的”lauch closure“直接内置在shared cache中,而对于第三方APP,将在APP安装或更新时生成,这样就能保证”lauch closure“总是在APP打开之前准备好。总体来说,dyld 3把很多耗时的操作都提前处理好了,极大提升了启动速度。