基础概念
什么是用户态,什么是内核态
内核态和用户态是操作系统的两种运行状态;
- 内核态:处于内核态的
CPU可以访问任意数据,包括外围设备,比如网卡、硬盘等,处于内核态的CPU可以从一个程序切换到另外一个程序,并且占用CPU不会发生抢占情况。 - 用户态:处于用户态的
CPU只能受限的访问内存,并且不允许访问外围设备,用户态下的CPU不允许独占,也就是说CPU能够被其他程序获取。
为什么会有用户态和内核态呢?
这个主要是访问能力的限制考量,计算机中有一些比较危险的操作,比如设置时钟、内存清理,这些都需要在内核态下完成,如果随意进行危险操作,极容易导致系统崩坏。
什么是Mach-O文件
Mach-O是iOS/macOS二进制文件的格式,一般包含以下几种类型:
Executable(产物为ipa包)Dynamic Library(产物为动态库)Bundle(产物为bundle文件)Static Library(产物为静态库)Relocatable Object File(重定向文件)
代码编译过程
编译阶段
预编译阶段-如把宏嵌入到对应的位置,将包含的文件插入到指定位置;编译阶段-进行词法分析、语法分析、语义分析、生成AST抽象语法树、生成相应的汇编代码文件;汇编阶段-将编译完的汇编代码文件翻译成机器指令,生成.o文件编译时链接:由于一个项目不可能在一个文件中从头写到尾,所以我们就需要链接器将项目中生成的多个Mach-O文件合并成一个,这也就是编译时链接器所做的工作。
graph LR
yuanwenjian[源文件.h.m.cpp等]-->yubianyi[预编译]
yubianyi-->bianyi[编译]
bianyi-->huibian[汇编]-->macho[形成machO文件]
链接阶段
动态库链接由于Mach-O文件是编译后的产物,而动态库在运行时才会被链接,并没参与Mach-O文件的编译和链接,所以Mach-O文件中并没有包含动态库里的符号定义,所以就需要动态库链接这些库到Mach-O文件中。 链接的共用库分为静态库和动态库:静态库:静态库是编译时链接的库,链接时完整地拷贝至可执行文件中,使可执行文件体积变大。如果多个APP都使用了同一个静态库,那么每个APP都会拷贝一份。动态库:动态库是运行时链接的库,使用dyld就可以实现动态加载。链接时不拷贝至可执行文件中,可执行文件只会存储指向动态库的引用。程序运行时由系统动态加载到内存中,系统只会加载一次, 多个APP共用一份。- 静态库的存在形式有两种:
.a静态库、.framework静态库 - 动态库的存在形式有两种:
.dylib动态库、系统的.framework动态库注:系统的.framework是系统SDK库,有Foundation.framework、UIKit.framework、MapKit.framework等。苹果把所有系统的.framework二进制文件统一打包到动态库共享缓存区(dyld shared cache)中。
共享缓存
在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而,很多系统库几乎是每个程序都会用到的,如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。
什么是dyld
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分。苹果系统如想要加载应用程序,在系统内核做好程序准备工作之后,需要加载被编译打包成可执行文件的 Mach-O 文件时,交由dyld负责链接,加载程序。在这一阶段,dyld主要做以下几件事:
1. dyld会找到可执行文件的依赖动态库。接着dyld会将所依赖的动态库加载到内存中。这是一个递归的过程,依赖的动态库可能还会依赖别的动态库,所以dyld会递归每个动态库,直至所有的依赖库都被加载完毕。
2. rebese和binding(rebase修复的是指向当前镜像内部的资源指针;而bind指向的是镜像外部的资源指针)
3. 调起main函数,也就是进入我们程序的入口。
dyld怎么启动?
对于iOS/macOS系统来说,操作系统内核是XNU(X is not Unix),当我们应用程序要启动的时候:
XNU通过execve()函数来加载Mach-O文件execve()调用__mac_execve()函数来fork一个新进程- 然后为
Mach-O分配内存 - 解析
Mach-O文件 - 读取
Mach-O头信息 - 遍历
load command信息, - 将
Mach-O映射到内存; - 调用
load_dylinker()函数,加载dyld - 调用
parse_machfile()函数对dyld解析 - 启动
dyld从单个文件生成Mach-o文件到应用程序加载启动,这一阶段整个流程如下:
graph TB
macho[生成的多个Mach-o文件]-->|编译时链接|jingtaibao[Executable文件 framework文件等]
xnu[xnu系统内核]-->loadmacho[加载Mach-o文件]-->fork[fork进程]
fork-->fenpeineicun[分配内存]-->loaddyld[加载dyld 并解析]-->qidong[启动dyld]
qidong-->lianjie[dyld链接]
jingtaibao.->loadmacho
kuwenjian[动态库静态库.a .lib等]-->lianjie.->diaoyongmain[调用main函数]
上述dyld启动流程基本是系统内核加载 App 的流程也就是内核态,而关于dyld加载的流程也就是用户态所做的事情,下面就说一下dyld加载的流程
dyld加载流程
由于dyld已经开源,能获取到的最新版本为dyld4(941)版本,这里我们也是用dyld4版本。
熟悉iOS的都知道load方法是在main函数执行前执行的;当然这句话也是很笼统,毕竟pre-main阶段做的事有很多,但如果我们从load函数下个断点,就可以知道在调用load之前大体上做了哪些。
这里我们可以看到在调用load之前的调用栈,大体上从start->prepare->runAllInitializersForMain函数,那么我们就可以通过全局搜索的方式找到 start(函数入口:
start
通过注释我们也可以知道这里是
dyld的入口,汇编代码__dyld_start会通过对齐堆栈跳转到该函数。
接着我们便在该函数中找到
prepare函数调用:
prepare
prepare函数的开头主要做些环境,路径,平台信息的准备工作;这里的gProcessInfo是一个结构体,该结构体中存储dyld所有镜像信息
struct dyld_all_image_infos* gProcessInfo = &dyld_all_image_infos;
这里也可以看出
dyld_all_image_infos包含信息非常多, 如mach_header, dyld_uuid_info, dyldVersion等等。
创建mainLoader
在prepare方法中,继续走,就能看到pre-build,创建mainLoader的过程:
just-in-time
just-in-time是dyld4的一个新特性, dyld4在保留了dyld3的 mach-o 解析器基础上,同时也引入了 just-in-time的加载器来优化, 这里稍微细说一下。
首先dyld3 出于对启动速度的优化的目的, 增加了预构建(闭包)。App第一次启动或者App发生变化时会将部分启动数据创建为闭包存到本地,那么App下次启动将不再重新解析数据,而是直接读取闭包内容。当然前提是应用程序和系统应很少发生变化,但如果这两者经常变化等, 就会导闭包丢失或失效。所以dyld4 采用了 pre-build + just-in-time 的双解析模式,预构建 pre-build 对应的就是 dyld3 中的闭包,just-in-time 可以理解为实时解析。当然just-in-time 也是可以利用 pre-build 的缓存的,所以性能可控。有了just-in-time, 目前应用首次启动、系统版本更新、普通启动,dyld4 则可以根据缓存是否有效选择合适的模式进行解析。
递归加载
然后就是递归加载主可执行文件。
初始化方法
之后就是运行初始化方法
整个
runAllInitializersForMain函数大体上做了一下工作:
- 运行
libSystem的初始化器; - 告诉
objc去运行所有的+load方法;(可执行文件、动态库等等上面的load) link动态库和主程序:runInitializersBottomUpPlusUpwardLinks
notifyObjCInit函数
在libSystem调用初始化器后,调用this->notifyObjCInit(this->libSystemLoader);方法,首先加载libSystem库,notifyObjCInit函数调用_notifyObjCInit函数。
下方截图我们就看到了
_notifyObjCInit的定义以及如何进行赋值。
通过全局搜索的方式我们可以找到调用
setObjCNotifiers进行赋值的地方。
至此我们发现
_dyld_objc_notify_register函数在DyldAPIs文件中,也就是说该函数为对外暴露的接口。
那么我们该如何找到调用该函数的地方呢?
_dyld_objc_notify_register
这里我们可以通过下符号断点的方式来进行调用跟踪:
这里我们看到调用
_dyld_objc_notify_register为objc库中的_objc_init函数,同时,从调用堆栈我们可以看到整个lib库的调用顺序:
- 先是
dyld库中的prepare调用runAllInitializersForMain函数 - 之后调用
libSystem库中的初始化方法libSystem_initializer libSystem库调用dispatch库中的libdispatch_init方法dispatch_init方法调用objc库中的_objc_init方法_objc_init方法调用_dyld_objc_notify_register方法
下面我们就看下源码验证下,首先是Libsystem库,这里在libSystem_initializer函数中调用libdispatch_init函数来初始化dispatch库
下方就是初始化diapatch库的方法,可以看到先调用了_os_object_init函数,然后调用_objc_init方法。
回到
objc中,我们就看到_objc_init函数调用_dyld_objc_notify_register,至此形成一个调用闭环。
map_images:dyld将image镜像文件加载进内存时,会触发该函数load_images:dyld初始化image会触发该函数unmap_image:dyld将image移除时会触发该函数 具体的map_images和load_images我们在下一章中探讨下。
回到runAllInitializersForMain函数中,前文中说到该函数还需要link动态库,也就是runInitializersBottomUpPlusUpwardLinks
在这里我们看到在
runInitializersBottomUp函数中回调了notifyObjCInit函数,也就是在objc中传入的load_images,在该函数中进行了回调,用来初始化Loader。
dyld流程总结:
流程图总结:
graph TB
subgraph dyld加载过程
subgraph objc库
objc-->objc_init[初始化objc objc_init]
objc_init -->register[调用 _dyld_objc_notify_register函数]
end
subgraph dispatch库
dispatch -->dispatch_init[dispatch初始化 dispatch_init]-->objc_init
end
subgraph libSystem库
libSystem -->libSystem_initializer[调用libsystem初始化器]-->dispatch_init
end
subgraph dyld库
dyld_start[dyld_start] --> startFunc[start函数]
startFunc --> prapare[prapare函数]
subgraph prapare过程
prapare .-> peizhi[配置环境]
peizhi .-> yugoujian[pre-build + just-in-time + mainLoader]
yugoujian .-> loadlib[装载内容 动态库可执行文件等]
loadlib .-> cache[插入缓存]
cache .-> initrun[调用runAllInitializersForMain]
initrun .-> mainFunc[调用main]
end
mainFunc-->endFunc[吊起main函数]
subgraph runAllInitializersForMain函数
initrun-->libSystemfirst[libSystem首先初始化]
libSystemfirst .- notifyObjCInit[调用notifyObjCInit加载所有动态库]
notifyObjCInit .- links[调用runInitializersBottomUpPlusUpwardLinks 链接link]
end
subgraph setObjCNotifiers函数
threePara[设置三个参数等].-
one[_notifyObjCMapped].-
two[_notifyObjCInit].-
three[_notifyObjCUnmapped]
end
threePara-->notifyObjCInit
register-->threePara
links -->initrun
libSystemfirst-->libSystem_initializer
end
end
部分内容参考: