前言
在程序加载过程中,不是每句代码都是从0基础
开始的,都会依赖很多基础库,如UIKit
、CoreFoundation
、objc
等等。。。这些库都是可以被操作系统
加载到内存
里面的可执行二进制文件
。库总体分为静态库
和动态库
。还有在运行工程的时候,如果打上断点,也能在Xcode
的左边看到一些加载的过程,从start
一直到断点的方法处。但是在程序的启动开始,直到main
函数加载完成,这个过程中,到底经历了什么?接下来,就一一揭晓。
资源准备
dyld源码
:多个版本的dyldobjc源码
:多个版本的objc- 下载libdispatch库源码
- 下载libSystem库源码
- 冰?
进入正文
刚刚我们说到了静态库
和动态库
,那么程序是怎样把这些静态库
和动态库
给加载到工程里面去的了?需要一个动态链接器
—dyld
。到底是怎么样进行链接的了?下文中将详细分析。
- 静态库和动态库的区别
静态库
可能被重复添加,这样就会造成内存的浪费,而动态库
的话,就能避免这个问题,这也是为什么在苹果系统里面,使用的动态库占大多数的原因了。
- 编译过程
案例引入
提出问题:为什么会进入到_objc_init
函数?
首先,在objc
源码中,不做任何操作,直接运行:
会直接进入到_objc_init
函数里面。这个是objc
的初始化,为什么回来到这里了?
引入案例准备工作
重新建立一个工程,ViewController
里面实现+load
方法。然后在下图几处设置断点:
再运行工程,断点先来到+load
方法处,然后通过lldb
调试,执行bt
指令,查看堆栈信息:
从堆栈信息,可以看出程序是从_dyld_start
开始,共执行了13
步,最后到+[ViewController load]
,但是在Xcode的左边,只看到_dyld_start
–> load_images
–> +[ViewController load]
这个三个对外的方法展示,但是很多的内部方法都没有展示出来,通过打印堆栈信息,才能一一查看。
同样的,也可以通过汇编
,也可以一步步的查看整个加载流程。
现在对加载流程,有了一个初步的了解,接下来,就通过对dyld库
分析,来进行详细了解。文中分析的是用dyld-852库
。(因为这个库依赖底层系统库太多,所以运行不起来。嘿嘿)
dyld
的宏观流程
从刚刚的分析中,知道程序的启动,是从_dyld_start
开始的,那么在dyld-852库
里面的入手点,也就从这里开始。老规矩,在dyld
代码里面全文索引_dyld_start
,因为从dyld2
版开始,苹果系统为了减少预绑定工作量,拆分了多种架构,如:X86
、X86_64
、arm
、arm64
等。下面是以arm
架构为例,搜索结果如下图:
直接看汇编可能比较生涩,但是其中有注释一行C++
代码,dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
,看这个C++
的方法,再对照汇编代码和注释,就能大致清楚一二,通过计算得到相应的几个参数,然后再传值跳转。为了直观性,我们就直接通过C++
方法进行探索。由于C++定义方法的特性,直接索引dyldbootstrap::start
是不会得到结果的,先找到dyldbootstrap
,再查找dyldbootstrap
作用域里面的start
函数。
从汇编_dyld_start
到C++的dyldbootstrap::start
接着就在dyldbootstrap
作用域里面找到start
函数。(现在我们是探究dyld
的宏观流程
,那么就对函数里面的具体实现,不做过多的解释了,下文中的函数也是一样)
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
。。。。代码省略。。。。
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
复制代码
从start
函数的返回值可以看到,是dyld::_main()
(是dyld
作用域的_main()
函数)。接着就是进入dyld::_main()
里面。
从dyldbootstrap::start
进入dyld::_main()
同样,我们通过下图可以得到,光main()
函数的代码就是接近1000行
,这个main()
函数不是程序加载完成的main()
函数,而是dyld
内部的一个main
函数。
对于这么庞大的一段代码,该如何去分析了?我们知道,dyld
的作用是动态链接镜像文件(image)
,那么我们在源码中,只要找到这方面的代码,就能得到答案。查看_main
函数的返回值,返回了result
。那就接着查看在_main
函数里面result
的赋值情况,如下图:
从_main
函数里面的查询结果来看,一个是fake_main
函数,另外一个是sMainExecutable
函数。然而fake_main
函数,显然不是我们要找的。
int
fake_main()
{
return 0;
}
复制代码
那么只剩下sMainExecutable
函数了,接着就得看在_main
函数里面,对sMainExecutable
函数的是使用情况了。下图是在_main
函数里面调用sMainExecutable
函数的情况,如弱引用绑定(7229)、数据绑定(7215)、镜像文件绑定(7136)、实例化(7009)等等,这也从侧面反馈着,我们查找sMainExecutable
函数是查找对了,和我们查找的目标也相匹配(目标:加载所有的镜像文件以及相应的其他的),这就是反推法:
确定了sMainExecutable
函数是接下来的步骤后,就直接查看sMainExecutable
函数的实例化有关的方法,那就是
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
复制代码
这个初始化方法,就是镜像文件加载器。那么接下来就直接进入instantiateFromLoadedImage
函数里面。
实例化主程序instantiateFromLoadedImage()
从instantiateFromLoadedImage
函数里面的实现,知道需要传入macho_header
、slide
、path
,再加载image
(镜像文件)
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
// try mach-o loader
// if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return (ImageLoaderMachO*)image;
// }
// throw "main executable not a known format";
}
复制代码
对于这些传入的参数到底是什么了?我们直接可以在finder
中,拿到前文中的那个案例的包,然后显示包内容
,拿到其machO
文件:
拿到machO
文件,再用MachOView
这个工具打开,就能查看machO
文件里面的内容,里面包含了macho_header
、Load Commands
、代码段(TEXT)
、数据段(DATA)
、符号表(Symbol Table)
、字符串表(String Table)
等等。如下图:
加载插入的动态库loadInsertedDylib
根据注释load any inserted libraries
得知:
共享缓存加载mapSharedCache
根据注释load shared cache
得知:
link
主程序
链接主程序的可执行文件
、插入的动态库
:
弱引用绑定主程序weakBind
在所有的镜像文件都被链接后,才进行弱引用绑定:
通知dyld
可以进入main()
函数
通知所有监测进程,此进程将要进入main()
初始化initializeMainExecutable
run
所有的实例化内容:
进入初始化initializeMainExecutable
- 初始化镜像文件
- 初始化主程序可执行文件
都是调用了runInitializers
函数,那么就直接去看runInitializers
函数实现。
runInitializers
函数的执行内容
进行初始化的准备工作
processInitializers
初始化的准备
通过for
循环加载镜像文件
recursiveInitialization
递归初始化
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first 首先初始化低级库
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {//依赖文件的初始化
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
//①、单个通知的注入
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image ------ ②、 调用init方法
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
//③、通知初始化完成
context.notifySingle(dyld_image_state_initialized, this, NULL);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
复制代码
在这里面主要做了三件事:
1、加载依赖文件(①):
- 单个通知的注入
context.notifySingle
,为后面的流程做准备,准备完成之后,才能开启后面的流程 ——– 将要开始初始化镜像文件;
2、加载本身(②、③):
- 调用
init
方法doInitialization
——– 开始初始化镜像文件; - 通知初始化完成
context.notifySingle
——– 完成镜像文件初始化。
notifySingle
搜索notifySingle
赋值的地方,查看其处理的情况,我们最主要的是要查找镜像文件的加载,所以就下面这一截代码符合:
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
。。。。。 代码省略 。。。。。
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
//-----重点部分
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
。。。。。 代码省略 。。。。。
}
复制代码
要加载镜像文件(image
),就是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())
,其类型是sNotifyObjCInit
,接着就进入sNotifyObjCInit
。
sNotifyObjCInit
的详情
通过索引,可以知道其类型,static _dyld_objc_notify_init sNotifyObjCInit
。再看下sNotifyObjCInit
的赋值,直接就是init
:
而执行赋值的函数是registerObjCNotifiers
。其中有三个赋值对象:
-
sNotifyObjCMapped
=mapped
; -
sNotifyObjCInit
=init
; -
sNotifyObjCUnmapped
=unmapped
;
registerObjCNotifiers
的调用
当看到_dyld_objc_notify_register
函数时,在对应文章开端的objc源码
中的_objc_init
函数里面的实现,也有_dyld_objc_notify_register
函数的调用,如下图,是objc源码
中_obcj_init函数
:
notifySingle
–>_dyld_objc_notify_register
,而通过objc源码
可知,_dyld_objc_notify_register
在objc
的init
也会被调用,在汇编里面,_dyld_objc_notify_register
是在libobjc.A.dylib
这个镜像库里面的。
在objc源码
中的_dyld_objc_notify_register
函数传入的值是:
map_images
的函数地址—沟通前面内容的非常关键的以数据,如:class
、protocol
、property
、methodlist
等等数据;load_images
函数实现;unmap_image
函数实现。
结合上面dyld
里面源码registerObjCNotifiers
函数的赋值情况:
-
sNotifyObjCMapped
=mapped
=&map_images
; -
sNotifyObjCInit
=init
=load_images
; -
sNotifyObjCUnmapped
=unmapped
=unmap_image
;
可以说,objc_init
函数向dyld
中注册了三个函数,在dyld
加载镜像文件时,如果满足的条件,这三个函数会被调用执行。
根据上述分析,知道map_images
函数和load_images
函数是沟通objc
和dyld
之间的桥梁,那么map_images
函数和load_images
函数调用的情形是怎样的了?—– 请看文章末尾的注解
。
梳理小结1
文章到这里,已经讲了一条比较长的流程了,也许有些童鞋会感到有些迷惑了,那么我们来稍微梳理下,根据上面的分析,再回过头来,看案例里面的堆栈信息,就能感到不那么陌生了:
- 梳理流程:
_dyld_start
–>dyldbootstrap::start
–>dyld::_main
–>dyld::initializeMainExecutable
–>runInitializers
–>processInitializers
–>recursiveInitialization
。
这只是在dyld
库里面所进行的流程,要到达_objc_init
函数,还有其他库的参与,所以,我们还需要接着往下探索。
从_objc_init
函数反推
现在我们要想弄清楚接下来的流程。目前,我们已经知道在objc
源码里面,运行空工程,都能执行_objc_init
函数,那么就在这个函数上打上断点,查看他的堆栈信息:
通过这些信息,我们可以知道,在我们推导到的recursiveInitialization
函数之后,还需要执行多个函数,才能到_objc_init
函数,现在已知的就这两个条件了,那么需要进行反推,从_objc_init
函数开始,往前推导。
_os_object_init
函数的调用
根据堆栈信息,_objc_init
函数是由_os_object_init
函数调起的。堆栈信息里面,_os_object_init
函数在libdispatch库
里面,那么打开这个库的代码,查找该函数。
通过源码知道,_os_object_init
函数是调用了_objc_init
函数。接着,是libdispatch_init
函数,调用了_os_object_init
函数。所以还是要全局索引libdispatch_init
函数,
void
libdispatch_init(void)
{
。。。。。。省略配置代码。。。。。
#endif
_dispatch_hw_config_init();
_dispatch_time_init();
_dispatch_vtable_init();
_os_object_init();//--------调用了
_voucher_init();
_dispatch_introspection_init();
}
复制代码
从源码上,libdispatch_init
函数是调用了_os_object_init
函数。
libSystem_initializer
函数的调用
再根据堆栈信息,是libSystem_initializer
函数调用了libdispatch_init
函数,而由堆栈信息可以知道libSystem_initializer
函数是在libSystem库
里面,那么打开这个库的源码,进行查找。看源码:
在libSystem_initializer
函数里面,的确调用了libdispatch_init
函数。
doModInitFunctions
函数的调用
再回到堆栈信息里面,调用libSystem_initializer
函数的是doModInitFunctions
函数,而doModInitFunctions
函数数属于dyld
库里面的。看doModInitFunctions
函数的实现源码:
这里面有一句注释,libSystem initializer must run first
(必须最先加载libSystem
库)。根据前面的分析,我们知道,dyld
是加载所有的镜像文件。而libdispatch
库和objc
,都是依赖于libSystem
库的。所以,libSystem
库为第一要加载的库。由此就可以推断出,doModInitFunctions
函数必然是对libSystem
库进行加载。
也可以说,doModInitFunctions
函数加载了所有C++
文件。为什么这么说了,一个下案例:
从左侧的堆栈信息结果上看,在加载kcFunc()
函数时,先执行doModInitFunctions
函数。
doInitialization
函数的调用
还是回到堆栈信息里面,由doInitialization
函数调用了doModInitFunctions
函数(也可以在dyld
库里面,全文索引doModInitFunctions
函数)。看源码:
接这再往前找,也可以在dyld
库里面,全文索引doInitialization
函数,看源码:
是在recursiveInitialization
函数里面调用的doInitialization
函数。
至此,就和前面的推导衔接了起来。
梳理小结2
从dyld
库,再到libdispatch
库和libSystem
库,形成一个通顺的流程:
_dyld_start
–> dyldbootstrap::start
–> dyld::_main
–> dyld::initializeMainExecutable
–> ImageLoader::runInitializers
–> ImageLoader::processInitializers
–> ImageLoader::recursiveInitialization
–> doInitialization
–> doModInitFunctions
–>libSystem_initializer
(libSystem.B.dylib
) –> _os_object_init
(libdispatch.dylib
) –> _objc_init
(libobjc.A.dylib
)
main()
函数的调用
当执行完_objc_init
函数之后,就来到了main()
函数。那么他是怎样实现这一步的了?
根据dyld
源码,在_dyld_start
的汇编中,通过注释,可以知道是如何跳转进入main()
里面的:
当然,我们也能在我们的案例工程里面,通过打印得知。在汇编中,知道是存在rax
寄存器里面,那么在工程里面:
- 而
main
也是作为特定的符号,写在dyld
里面,不能随意修改。
注解:map_images
和load_images
调用情况
根据前面的分析,我们可以知道map_images
函数和load_images
函数是沟通objc
和dyld
之间的桥梁。需要我们把这里面的细节理清楚。
传入map_images
和load_images
,是在objc
的_objc_init
函数里面调用的_dyld_objc_notify_register
函数:
而_objc_init
函数是由dyld
里面的doModInitFunctions
函数对接初始化的。与此同时,在dyld
中,是由registerObjCNotifiers
进行赋值:
根据赋值情况,先看map_images
,就可以找有sNotifyObjCMapped
的使用地方了。在dyld
中全文索引:
在notifyBatchPartial
函数里面,当sNotifyObjCMapped
不为NULL
时,就直接调用。也就是map_images
的调用。
回到registerObjCNotifiers
里面,就能看到,notifyBatchPartial
函数就在这个函数里面调用了:
而load_images
也同样在registerObjCNotifiers
里面,且依照代码排列顺序,是map_images
先执行,load_images
后执行。完成注册情况。
我们也可以在objc源码
中做个测试,分别在map_images
函数和load_images
函数上打上断点,看执行的顺序:
先执行map_images
,由左侧的堆栈信息可知:_objc_init
–> _dyld_objc_notify_register
–> notifyBatchPartial
–> map_images
.
再执行load_images
,由左侧的堆栈信息可知:_objc_init
–> _dyld_objc_notify_register
–> registerObjCNotifiers
–> load_images
.
- 小结:
dyld
进行加载镜像文件,初始化主程序的时候,会加载libObjc.dylib
库,此时,objc
会向dyld
注册三个方法:map_images
、load_images
、unmap_image
,并且map_images
会先执行,而load_images
后执行!