这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
1. 应用程序加载
1.1库
每个程序的运行都会依赖一些基础的库,比如说UIKit
,CoreFoundation
等,库是一些可执行的二进制文件
,能被操作系统加载到内存中
。库有两种形式,就是静态库(.a , .lib
)和动态库(.so , .dll)
,两个库主要表现在链接的区别
。
静态库:静态库
在编译时加载
,在链接时
会完整的复制
到可执行文件
中,此时的静态库就不会在改变
了,因为它是编译时被直接拷贝一份,复制到目标程序里的
优点
:编译后
的执行文件不需要外部库的支持
,直接就能使用
。缺点
:有多个app使用就会被复制多份
,不能共享
且占用更多冗余内存
。所有的函数都在库中,因此当修改函数
时需要重新编译
。
动态库:程序
编译时并不会链接到目标程序中
,目标程序只会存储指向动态库的引用
,在程序运行时由系统动态加载到内存
。
优点
:库是动态的,运行时才载入
,因此修改库中函数时,不需要重新编译
。同一个库可以被多个程序使用
,内存只加载一次
,节省内存空间
。因为不需要拷贝至目标程序中,所以不会影响目标程序的体积,所以app体积相对静态库来说是减少的
。缺点
:动态库是运行时由系统加载到内存,如果环境缺少库
或者库的版本不正确
,那么程序就会无法运行
。并且因为在编译过程中没有整合到目标代码中,只有执行到该函数时候才去调用库中的函数,所以首次加载比较耗时
。
1.2 编译过程
其中编译过程如下图所示,主要分为以下几步:
源文件
:载入.h、.m、.cpp等文件
预编译
:替换宏,删除注释,展开头文件,产生.i文件
编译
:编译器将.i文件转换为汇编语言,产生.s文件
汇编
:将汇编文件转换为机器码文件,产生.o文件
链接
:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
1.3 DYLD(链接器)
介绍完了库,那么库是怎么加载到内存里的呢?这就要说到一个非常牛逼且重要的东西:DYLD(链接器)
。
DYLD(the dynamic link editor)是苹果的动态链接器
,是苹果操作系统的重要组成部分,在app被编译打包成可执行文件格式的Mach-O文件后,交由dyld负责连接,加载程序
。
1.4 APP 启动流程
在探索之前,先看一下APP的启动流程图。
2. dyld探索
这里正式开始dyld的探索,打开程序,然后在main
函数里面打下断点然后运行。运行之后可以看到,在main函数之
前有调用一个start
,那么在前面是否还有什么东西呢 ?
点进去start看到是libdyld
,也就是这次的主角,DYLD链接器。那么接下来该怎么探索呢?
试着添加一个start的断点
,运行后发现并没有卡住,还是停在了之前的main函数的地方,证明真正在下面调用的函数名不叫start,但是发现了load
方法被调用了。说明load方法是在main函数之前的
。
所以我们试着在main函数打下一个断点然后运行。可以看到最开始的地方是_dyld_start
。也就是之前在main函数那里看到的start。
可以在侧边栏查看
或者lldb输入bt查看
接下来就去看dyld源码。打开源码,搜索dyld_start,发现架构不同,写的汇编也不同,但是大体上都是调用了dyldbootstrap::start
这个方法,这里以arm64为例。
看到调用了dyldbootstrap::start方法,接下来就去搜索dyldbootstrap
这个命名空间,然后继续在其中找到start方法,发现其是返回了dyld::_main函数
的结果。
点击去dyld::_main,发现有将近千行的代码。
这里,就直接从返回值进行反推
。
查找哪里对result进行赋值,看到两个赋值都和sMainExecutable
有关。
接下来搜索sMainExecutable。
中途看到sMainExecutable做的一些bind
的工作,证明确实是我们要找的。
最终找到sMainExecutable赋值的地方。
点击查看instantiateFromLoadedImage
。
点进instantiateMainExecutable查看,其中sniffLoadCommands
中是获取Mach-O
类型文件的Load Command的相关信息,并对其进行各种校验。
接下来回到dyld::_main继续看到 initializeMainExecutable()
。
首先看到会拿到镜像文件的个数,然后挨个对镜像文件进行runInitializers
,然后对sMainExecutable进行runInitializers。
点击runInitializers。既然是初始化,那么其核心就是processInitializers
。
processInitializers点进去看到,这里对images进行了挨个的调用recursiveInitialization函数进行递归实例化
。
接下来搜索recursiveInitialization。发现这里对依赖文件的递归初始化,然后调用了2个重要的函数:
notifySingle
和doInitialization
。
先来看notifySingle
,发现点不进去,接下来就搜索notifySingle看看是在哪里被赋值的。
发现赋值的地方后点进去。这里的重点在(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())
。
接下来搜索sNotifyObjCInit
,发现类型是_dyld_objc_notify_init
。
继续找,发现这里有赋值操作,也就是说, 这里说将registerObjCNotifiers
的第二个参数赋值给sNotifyObjCInit
。
接下来寻找registerObjCNotifiers,发现在_dyld_objc_notify_register
调用。
然后在源码中搜索_dyld_objc_notify_register,发现在_objc_init
中对调用了_dyld_objc_notify_register。
接着在这里打个断点然后运行。_dyld_start到recursiveInitialization已经探索过了,接下来要探索_objc_init到recursiveInitialization的过程
。
_objc_init的上一个函数_os_object_init
在libdispatch
,所以下载libdispatch源码
然后搜索_os_object_init。发现确实调用了_objc_init。
接着搜索libdispatch_init
,发现确实调用了_os_object_init。
接着下载libSystem源码,然后寻找libSystem_initializer
,发现确实调用了libdispatch_init。
接下来,又回到了dyld,查找doModInitFunctions
。这里注释了libSystem initializer必须先跑起来 ,也就是说无论是libdispatch还是objc,都是依赖libSystem
。所以这里烘托了doModInitFunctions是在做libSystem的加载
。
这里的func就是libSystem 的initializer,func的调用也就是调用了libSystem 的initializer。
在看到哪里调用了doModInitFunctions,发现是在doInitialization
里面。
在看哪里调用了doInitialization,就回到了之前的recursiveInitialization
。
之前说过,notifySingle中的sNotifyObjCInit最终会在_dyld_objc_notify_register赋值,而_dyld_objc_notify_register则在_objc_init里面调用,_objc_init则是doInitialization调用的,那么doInitialization和notifySingle有什么关系呢?
_objc_init中调用了_dyld_objc_notify_register(&map_images, load_images, unmap_image),其中map_images是沟通前面的内容的一些非常关键的数据,但是只是对方法进行了赋值,有没有调用方法确是不知道的。只有当调用sNotifyObjCMapped的时候,才执行map_images方法的。
其实notifySingle就是相当于注册了一个通知,当image加载完毕了之后,就发一个通知给notifySingle。
那么是如何通知的呢,请听下回分解。