iOS应用程序加载的原理

一、序言

作为iOSer,我们天天在xcode上写代码,然后打包发布或者测试,可曾想过代码是如何到内存种的?app是如何从点击图标到整个程序运行的呢?现在我们来探究一下。

二、库

什么是库?库就是能够被系统加载到内存中的可执行的二进制文件,库有这种形式: 静态库(.a .o等)和动态库(.framework .so .dylib等)

1、动态库和静态库的区别

我们梳理一下库的加载过程,首先我们的代码(.h .m .cpp .swift等)经过预编译、编译成汇编,然后链接装载生成可执行文件,大致流程图如下:

未命名文件-2.png

1.1、静态库

链接时,静态库会被挨个挨个完整地装载到可执行文件中,被多次使用就有多份冗余拷贝,如图:

未命名文件-3.png

图中会很明显的有重复的库,这样就会浪费很多性能。

1.2、动态库

动态库在加载时才进行链接,比如加载到某个节点发现需要某一个库时并不是直接加载,而是共享这个动态库。相比于静态库,动态库会被共用,这样会节省更多的空间和时间。加载的大致流程如下所示:

动态库.jpg

三、链接器

库是如何加载到内存种的呢?这就需要一个非常重要的东西了dyld

1、App的启动入口

App是如何启动的呢?我们都知道所有程序的入口都是main函数,那么我们来验证一下。首先创建一个空的iOS项目,并在main函数上打个断点,执行程序,如图:

20220125013354.jpg 很容易发现在main之前有个start_sim,我们下个下个符号断点start,再次运行程序,如图:

20220125013747.jpg 很遗憾还是在这里,好像失去了方向。但是经验告诉我们有个方法会在main之前执行,对,就是load方法,我们随便添加个load并断点,如图:

20220125014039.jpg 运行起来,如图:

20220125014158.jpg 山穷水复疑无路,柳暗花明又一村。赶紧查看一下堆栈信息,如图:

20220125014423.jpg 好像这里才是我们程序的起点。

2、APP的启动流程

刚刚我们找到了APP的启动入口,那么从启动到main函数这个中间经历了什么呢?干了什么事情呢?我们需要到dyld的源码中寻找,打开dyld的源码,搜索_dyld_start(由于源码编译依赖太多就没有流程可以跟踪。还有因系统版本Xcode打开dyld源码闪退的问题,可以将dyld的源码拖入到新建的工程中,不要copy,可以避免闪退),如下所示:

635B744A-F5D8-4756-8EB6-BFBC3F795FA0.png

全在深奥的汇编中,这不要紧,因为这里都写的注释。我们很容易发现所有的架构类型中都有call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)这个注释,直接搜索dyldbootstrap:: 没有任何结果,这是c++语法,要先找到命名空间dyldbootstrap,如图:

20220223154758.jpg 然后再搜索start(,如图所示:

20220223154922.jpg 这里返回的是个dyld::_main(我们再次去找实现,同样先找命名空间,在找函数,如图:

20220223155935.jpg 13180条结果,太多了,过滤一下,搜索namespace dyld {,如图:

20220223160105.jpg 锐减到14条结果,挨个搜索_main(,找到了实现如下所示:

20220223160436.jpg 好长,收起来一看近1000行代码,如图:

20220223160808.jpg

为了找准核心代码,我们可以直接渠道最后查看返回值,然后根据返回值找关键的东西,如图:

20220223225529.jpg 返回值是result,那么找出result赋值相关的代码,如图:

20220223225842.jpg

20220223225707.jpg fake_main返回值是0,如图:

20220223225946.jpg

所以可以忽略不看,那么result的值很大可能与sMainExecutable相关。接下来找sMainExecutable相关的东西,如图:

20220223231719.jpg 这里的绑定可以说明sMainExecutable就是我们需要找的代码。所以我们要看sMainExecutable的初始化,如图:

20220223231916.jpg 然后进入到instantiateFromLoadedImage方法中,如图:

20220223232043.jpg 大概意思是为已经映射到可执行文件中的对象创建一个ImageLoader*,然后往下,如图:

20220223233818.jpg 加载动态库,然后如图:

20220223233937.jpg link主程序,再然后:

20220223234034.jpg link库。在所有镜像文件都链接完毕之后进行弱引用绑定符号表:

20220223234403.jpg 然后初始化执行主程序:

20220223234757.jpg 具体代码如图:

20220223235141.jpg 即拿到各个镜像文件,然后runInitializers,如图:

20220224085704.jpg 其中processInitializers代码如下:

20220224225116.jpg 第一个for大概意思就是对镜像文件列表中的镜像文件递归初始化,第二个for是继续初始化向上以来的文件。具体的初始在recursiveInitialization中,继续进去查看,如图:

20220224225906.jpg 里面是个try catch,那么我们从try中查找即可,如图:

20220224230622.jpg 再继续看notifySingle,如图:

20220224231148.jpg notifySingle定义的是一个函数,接下来来就需要找到他赋值的地方,如图:

20220224231938.jpg 在然后实现的地方,如图:

20220224232052.jpg 接下来看1112行的sNotifyObjCInit,搜索到定义和赋值如下:

20220224232857.jpg 20220224232834.jpg sNotifyObjCInit的赋值源于registerObjCNotifiers函数的第二个参数,所以继续搜索registerObjCNotifiers的调用,如图:

20220224233209.jpg 继续搜索,如图:

20220224234604.jpg 貌似没有找到调用,线索断了?放松一下。

3、从_objc_init开始的逆向推导

打开objc源码搜索registerObjCNotifiers,如图:

20220225000636.jpg 发现在_objc_init有调用registerObjCNotifiers,且objc源码是可编译的,断点伺候,如图: 20220228230044.jpg 点击_os_object_init,如图: 20220228230110.jpglibdispatch.dylib库中的_os_object_init有调用_objc_init,所以我们去libdispatch.dylib查找_os_object_init,如图:

20220228231127.jpg 果真在_os_object_init有调用_objc_init,继续往下找,如图: 20220228231001.jpg 在同一个库中的libdispatch_init中找到了_os_object_init的调用。那么libdispatch_init的调用是在哪里呢?当前库也搜索不到,所以我们回到objc源码中,在控制台输入bt查看堆栈信息,如图:

20220228231913.jpglibSystem.B.dylib中有调用,那么我们去libSystem.B.dylib查找libdispatch_init,如图:

20220228232141.jpglibSystem库的libSystem_initializer调用了libdispatch_init,继续搜索libSystem_initializer并没有找到的调用,再看堆栈信息,在dyld中的doModInitFunctions(ImageLoader::LinkContext const&)调用,所以回到dyld中搜索ImageLoaderMachO::doModInitFunctions,如图:

20220301000118.jpg 根据注释判断出该方法是加载libSystem的。其中funclibSystem_initializer,根据参数判断。然后查找doModInitFunctions的调用,如图:

20220301001723.jpg 再查找doInitialization的调用处,如图:

20220301001903.jpg 再次回到了recursiveInitialization中,大概理清楚了通知注册的方法,由于目前xcode直接打开dyld库会闪退,影响部分分析,请各位大神多多指导。

猜你喜欢

转载自juejin.im/post/7069801565971808286