前言
在程序加载过程中,不是每句代码都是从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后执行!