关于程序(APP)启动以及dyld的内部原理解析

·  阅读 229

timg.jpeg

前言:

在前面的学习中,我们已经掌握了类结构里面的内容,并且前面我们是直接从objc_init开始的,从这一节开始将是新的篇章,本次主要围绕的是关于app程序启动的时候,系统到底为我们做了什么?系统是如何把我们的代码“跑”起来的?这是以往的学习中都没有涉及过得,也是在开发中不需要用到的,但是确实是我们作为程序员本身,职业操守而言应该去了解的,不然人家不管是外行还是内行问起,你一无所知,然而却开发了好多年程序,着实有点可笑了,基于这部分也是比较晦涩难懂的,我看了两遍还是没有完全理解,甚至对于主流程线还是不太通畅,还是需要多看多做多探究亲力亲为才能有所收获~~于是本人本着寓教于乐,也是帮助自己了解的心态,去更加仔细的梳理这其中的内容,本篇博客比较之前的会力求更加详细。

简单知识整理

关于库

我们在开发的时候,大多数的代码都依赖于底层的库,那么势必在装载时,事先把库文件装载到内存中。库分为动态库和静态库,两者的区别是链接不同。在我们创建一个app的时候,如果没有编译,那么在products文件下,.app文件是红色,直到我们编译完成,这个文件才存在,点击文件位置,显示包内容,这里会根据你是mac程序还是app程序,在对应目录下会生成一个exec的可执行文件。其实我们在之前通过反编译,也是成功的把这个可执行文件变成了汇编形式的代码。

截屏2021-07-11 上午10.06.45.png

截屏2021-07-11 上午10.14.11.png

截屏2021-07-11 上午10.14.19.png

静态库是一个一个装载进去,而动态库不是直接加载,是共享的,方便优化内存空间,减少了包的体积现在苹果的系统库基本都是动态库

截屏2021-07-11 上午10.16.20.png

这个可执行文件是可以直接打印的,然而很遗憾,如果是模拟器或者真机,直接放到终端是会报错的,其实在源代码中也是有判断的,可能是要做路径处理,这里还没找到解决的办法(主要是懒也没得时间)

截屏2021-07-11 上午10.21.14.png

截屏2021-07-11 上午10.25.26.png

所以我们暂时就在mac环境下试一下,就蹭蹭看看,确实可以输出KC短细软等一些内容。

截屏2021-07-11 上午10.29.31.png

关于dyld

简介

那么这些动态库是怎么加载到内存中的呢?这就关系到苹果一个很重要的链接器——dyld!!!动静态库都是依赖于这个链接器!

从宏观的角度来看,dyld是在程序启动时,加载libsystem,然后在注册notify的回调函数,加载所有的镜像文件,分别执行map_images和load_images方法,最终调用main函数。 科普:此处的image是镜像文件,也就是库文件,本身所有的库文件都存在于我们的mac系统下对应的文件下,但是在开发过程中,系统会将需要的库装载到内存中,而不是所有的库加载过去,这样也是合理的,那我们怎么看我们装载了多少库文件以及库文件所在的路径呢?很简单,在进入到main函数以后,打印image list即可。

截屏2021-07-11 上午10.34.49.png

截屏2021-07-11 上午10.42.26.png

我原先以为是本程序所以在的路径下,其实这个corefundation是在xcode对应的架构下面的lib下的库路径下面,猜测应该是拷贝路径。 截屏2021-07-11 上午10.44.20.png

寻找

在main方法中打断点,我们发现并不能找到具体的流程信息,于是换个思路,我们知道load方法是先执行的,所以在load方法中尝试,果然,找到了,一目了然,发起的源头就是_dyld_start,并且中间经过的步骤也是非常的清晰。

截屏2021-07-11 上午10.52.52.png

截屏2021-07-11 上午10.51.39.png

dyld底层系统库源码

本次下载的是最新版本的852,源码不可直接运行,因为依赖于更多底层的库。 上面的过程我们已经直到dyld的发起者是_dyld_start,全局搜索,看到.s文件,很明显就是汇编了,可以看到注释里有提到calldyldbootstrap::start,是c++的写法类似于calldyldbootstrap这个类中调start方法,巴拉巴拉不管这不是重点,然后再搜索calldyldbootstrap,找到start方法即可,直接往下找,可以看到返回到了_main函数,这里都是跟上面的流程图接轨了,不多说。

截屏2021-07-11 上午11.04.20.png

截屏2021-07-11 上午11.04.49.png

1625973011804.jpg

关于_main函数

首先说明,这个_main函数不是我们项目中的源头main函数,是dyld内部的main函数。前期大概一千行的代码讲的都是前期的准备,包括架构判断,环境,库文件路径等等。我这里只从宏观的角度来看,细节太多无暇顾及,具体每个模块的只需要看官方注释即可~~~

提一下:这里是系统级别的方法才能注册到共享缓存。。 截屏2021-07-11 上午11.20.13.png

思路转换

当代码过多,且前期的代码大多是准备代码不关乎到主流程时候,我们可以从后往前看,也就是循着结果去找逻辑——反推法

所有的result返回都跟sMainExecutable这个方法有关。 截屏2021-07-11 上午11.25.31.png sMainExecutable方法确实也做了一些系统库绑定的操作等等。那么就可以尝试继续定位。 1625974017681.jpg

1625973965232.jpg

找到sMainExecutable定义的地方 1625974430180.jpg

找到赋值的出处,这里第一个mh是一种格式,就叫mh,具体的你可以把可执行文件直接拖到烂苹果里面,里面展示的就是这一张张的表,这里就是读取这些表内容,不存在递归。 截屏2021-07-11 上午11.34.16.png

截屏2021-07-11 上午11.39.49.png

main函数还没执行完呢,在sMainExecutable以后,插入动态库,link主程序。

截屏2021-07-11 上午11.43.47.png

准备工作全部结束以后,run执行。

截屏2021-07-11 上午11.45.26.png

initializeMainExecutable

既然已经拿到了所有想要的数据,接下来如何run?如何处理呢? 拿到了所有的镜像文件,遍历并且初始化,可以看到每次遍历都会走runInitializers,那么看看这个方法做了什么?

E40AC020-19B2-470B-A462-1EFD423CCC51.png

runInitializers里每次又调用了processInitializers,这里从传参就可以看出这个方法就是核心方法。 89CBE91A-F1E6-4711-B53E-5E745DACCB15.png

processInitializers里每次又会调recursiveInitialization,传参基本都相同。 C7370533-B01A-482B-85B4-01A71693B371.png

recursiveInitialization

直接搜recursiveInitialization这个跳出来的会比较多,可能存在同名的构造方法,所以还是加上类型,就比较方便定位了。 68316977-E091-4236-9EB3-0286F84E450A.png

这里通过存在两次调用notifySingle的方法,从名字也可以看出来,这是单个注册通知的方法,从注释可以看出,第一个是注册依赖文件的通知,首先得加载必要的依赖文件。

问题补充1

为什么这里要这么写?

首先如果是系统库进来,那他就没有依赖库,走下面的doinit方法初始化,如果是存在依赖库进来,那么在依赖库进来的时候,就会一起初始化完毕了,然后再调用notify时没有问题的。

问题补充2

c++方法 load方法,main方法的调用顺序?如果在主程序的main方法里面写一个c++方法,为什么c+方法是先load后走c++?打印断点bt信息,明明发起者是doini方法。 load->c++->main ===== c+方法必然要写在main主程序中,而不是系统的文件中。 在进入recursiveInitialization递归初始化以后,拿到数据回调然后才会调用load方法,load方法是在notify调用的,所以必然是系统的c++先调用,然后再load,然后再到main。

331433B2-EFC3-46D9-94A1-156061B38B7A.png

接下来就看notify这个方法里的实现,在这个里面存在指针函数的调用,虽然下面有一个通知注册的方法,但是那个在注释里写了是unload images,可先不看。

截屏2021-07-11 下午12.25.23.png

全局搜索这个指针函数。 截屏2021-07-11 下午12.16.21.png

_dyld_objc_notify_init 作为第二个参数被registerObjCNotifiers方法调用。 截屏2021-07-11 下午12.16.37.png

registerObjCNotifiers方法调用点。

截屏2021-07-11 下午12.17.05.png

竟然是_dyld_objc_notify_register这个时候再比较一下我们第一个节课拿到的关于objc代码也就是libobjc.dylib,单个镜像文件的加载就会走到这个通知!!!

程序的加载!

离光明只差一步~

虽然说两边的回调函数已经接通,但是中间流程还是不清晰,如何更加清楚地找到中间的关联?这个时候可以从已知的libobjc.dylib中的objc_init方法去寻找思路。

方法

这里的方法很重要,既然已经知道会走到libobjc.dylib中的objc_init方法,那么直接在此处打下断点,看看前面的流程是怎么走的

截屏2021-07-11 下午12.47.42.png

具体的方法就是点进去,看他这个方法是哪个开源库里面的,一步一步验证。 libobjc.dylib objc_init <- libdispatch.dylib _os_object_init: <- libdispatch.dyliblibdispatch_init: <- libSystem.B.dylib libSystem_initializer: <- dyld ImageLoaderMachO::doModInitFunctions:

走到这,可以看到又回到了dyld,之前那边为什么不能继续探索了,因为真的是已经断了,无法再继续探索下去了,全局搜索_dyld_objc_notify_register这个搜不到了。

全局搜索doModInitFunctions

截屏2021-07-11 下午1.09.45.png

首先加载libSystem,任何库都依赖于此。 截屏2021-07-11 下午1.07.46.png 判断是否加载完libSystem,再执行其他。 截屏2021-07-11 下午1.08.19.png

全局搜索doModInitFunctions调用的地方,doInitialization这个方法 58E80EAA-79C6-4427-8554-A63484F34C8D.png

再全局搜索doInitialization这个方法,啊~~多么熟悉的陌生人,这里不就是recursiveInitialization方法吗??一切似乎都连起来了。

7002FF58-A03E-4AB6-8A07-4C29D80CC882.png

串联

那么还剩下一个问题,在recursiveInitialization方法中,notify方法和doInitialization方法之间的关系是什么?他们是怎么串起来的呢?

整理

有必要在这里整理一下,之前所探讨的内容。在进入到recursiveInitialization方法中的doInitialization这个方法时,必然会走之前那一套流程跳到objc_init方法初始化。这个方法里调用了_dyld_objc_notify_register,里面有三个参数(&map_images, load_images, unmap_image);map_images里包含一些分类、协议、类方法等等。。

7DC33598-864F-4098-848A-5E085204AE18.png

个人理解是,其实在走到_dyld_objc_notify_register这个方法时,也拿到了map_images,但是何时调用,不得而知,调用完了以后如何又回到了notify中,notify必然是做了一个类似block的回调处理,来处理你初始化好的镜像文件。因为在image加载的过程中,每个image的加载是不一样的,内部的处理由他自己去实现罢了,主流程不用去管他。

dyld - > objc ??

什么意思?双胞胎?没错,他们就是一个意思,只是两个库里的名字不一样罢了。

截屏2021-07-11 下午1.49.58.png

在从libobjc库回来的时候,_dyld_objc_notify_register方法会调用并且赋值,全局搜索registerObjCNotifiers

截屏2021-07-11 下午2.35.46.png

registerObjCNotifiers方法被调起。

897DA168-8838-41FC-BB8D-597B193FBBD4.png

调用sNotifyObjCMapped函数

4F50C83B-0D00-437A-A87F-C027D40C30E3.png

从dyld->main

直接跳过去的.. 1.看dyld汇编代码

截屏2021-07-11 下午3.14.50.png

2.在主函数main之前用汇编通过寄存器打印。

截屏2021-07-11 下午3.16.15.png

WWDC2017对于dyld的简单概述

1.苹果致力于更加快速的启动app,减少启动时间,减少dyld的链接代码,减少库引用,减少初始化代码等等,另外还推荐使用swift,因为不存在初始化器,不存在不对齐的初始化数据结构等,可见swift可能是以后ios开发的主流了。

2.dyld历史,一代始于1996年基于unix,还不是很成熟,貌似用二进制写的,对于边界控制不够完善,需要更多的时间和消耗更多性能,打包器是在c++动态库之前做的,一代也加入了预绑定计数,差不多就是提前找好地址方便下次加载,但是安全性比较差。

3.二代是一代的重生版本,也就是重写了,更高效的支持c++库,扩展了macho的格式,减少了预绑定的操作,从而进一步提升了速度,更多的架构支持dyld2,安全性能的提高在于,每次加载库的地址都是随机分配的;性能的提高在于,去除预绑定,转而使用共享代码(预先生成一些数据结构供dyld使用)。

4.dyld3是2017年开始推广,并且在之后大规模使用逐步大部分代替dyld2,而开发dyld3动态链接器依然是为了增强性能,提高安全性。

5.对比。改善了设计。dyld2的启动分析:首先是通过mach0找到所有的文件,然后递归找到依赖库,直到获取所有的dyld所需要的结构,然后映射所有的文件放到地址空间,然后符号找到,通过系统方法找系统库,然后找到他的地址复制到程序中的函数指针,然后进行绑定,指针地址随机,最后运行初始化器。然后进入到main函数。dyld3":不需要分析mach0文件或者执行符号查找,更多的安全性能的分析放到了共享缓存中。

后面还讲了一些结构对齐等等。。巴拉巴拉太专业了,听不懂。自我感觉在我们的开发中,很少会去照顾到性能问题,而在底层的开发,不断地在提高运行的效率,也是可能因为这个原因,我们的开发可以肆意挥霍,根本感觉不出来效率的问题。讲的很牛批,然而有一半好像是对牛弹琴,期待wwdc办一个培训班,到时候超越LG,我一定第一个报名!

后记:

1.终于在看了第二遍以后,一点点的摸索清楚了其中的大体流程,整个流程的主线大概就是从dyld的start到最后的回调notify回调完成整个通路,中间有经过好几个库的来回调用,其中最主要的就是要理清楚递归那一片notify的原理,其实就是区分系统库和其他库,做依赖关系。第一遍听还是云里雾里的,主要是因为第一,本来就不会,再加上自己没有摸索过这些代码,所在来回看的时候会更加晕第二,对计算机特别是底层基础比较薄弱。好在回看的时候可以配合视频讲解+源码自己一点点查看。要感谢KC老师的耐心讲解!

2.在拷贝跳转库的时候,我终于知道掘金里面的文字是怎么变色的了。。。之前试了好几次markdown的语法都不行?!

3.KC DXR

分类:
iOS
标签: