本文为L_Ares个人写作,以任何形式转载请表明原文出处。
想探索dyld
的加载流程,还是需要一些比较常识性的东西,我们就从库
这个东西开始说。
一、关于库
平常我们经常会挂在嘴边的CoreFoundation
、UIKit
、OpenGL
等等,这些都是我们开发的过程中可能需要依赖的库。
我们会把这些必用或者常用的内容封装成一个库,准备在某个位置,有需要的时候就会直接调取,这样更加的方便。
那么,
1. 库是什么?
库是一份可执行的代码的二进制。它们可以被操作系统载入到内存,并且被识别。
2. 库的常见分类
- 静态库
比如.a
、.lib
等
- 动态库(共享库)
比如.framework
、.so
(安卓的应该很熟吧)、.dll
3. 动态库和静态库的区别
想了解他们的区别,就必须知道一个源文件变成可执行文件的过程。这个其实大家都是知道的,而且在之前的章节说运行时的时候也说过了。
这里再啰嗦一次编译流程 :
源文件--->预编译--->编译--->汇编--->链接--->可执行文件
那么库文件一般都是在链接
的时候开始介入和源文件
发生一些事情。
来看,
图1中是一个项目的两个代码段,1和2分处不同的模块,但是同时都需要库文件B
和库文件D
。
如果是静态库的话,B
和D
就必须在那个位置坐好,而且就需要为1和2每个段都编译一次库文件。也就是说使用静态库的时候,会将静态库的信息直接编译到可执行文件中
再看,
图2中也是一个项目的两个代码段,1和2也分处不同的模块,也都需要库文件B
和D
。
这时候B
和D
如果是静态库的话,就会根据实际的需求来进行插入,需要的时候就添加,不需要的时候不会参与。也就是说加载动态库时,操作系统会先检查动态库是否因为其它程序已经将这个动态库信息加载到了内存中。不需要多次的加载库文件。
特点总结 :
- 静态库 :
- 信息直接编译到可执行文件中
- 优点 : 静态库被删除,对可执行文件不会造成影响。
- 缺点 : 浪费内存空间,如果静态库需要修改,可执行文件需要重新编译。
- 动态库 :
- 优点 :
- 动态库只被夹在到内存中一次,不会多次加载。共享内存,节约资源。
- 编译程序并不会链接到目标代码中,而是在运行时才被载入,不需要每次修改库文件就要重新编译。
- 缺点 : 因为运行时载入,所以运行时必然有性能损失,而且程序会依赖外部环境,一旦动态库修改出错,程序可能会发生问题。
- 常见的 :
UIKit
、libdispatch
、libobjc.dyld
二、思路和准备工作
1. 思路和概念
为了确定思路和探索顺序,就先来看app
的启动流程图 :
整个虚线框里面的就是我们要探索的dyld
。
那么首先说好概念问题,
什么叫dyld
?
dyld
- 英文全称 :
the dynamic link editor
- 中文名叫
苹果动态链接器
。- 它是
iOS
中非常重要的部分。- 在
app
被打包成mach-o
可执行文件后,dyld
将对其进行链接(link)
,加载可执行文件(load)
等后续操作。
2. 探索准备工作
准备条件 : dyld源码750.6,请自行下载。objc4-781。请看教程,里面有配置好的文件。
libdispatch
和libSystem
都在这里,请自行搜索这两个字,下载喜欢的版本。
新建一个我们熟悉的
Project--->iOS--->App
。后面我会把它叫Project
,一说Project
就是说这个东西。在
main.m
中给main
函数上断点,正常情况下,main
函数是程序的入口吧,所以找它,然后执行。
我们看Debug Navigator
信息,如下图2.1所示 :
- 在
main
函数前面还有一个start
的存在,所以在main
函数的前面,一定还有操作。 - 那么看
start
函数,画红框的那里,发现start
函数属于libdyld
。 - 所以在进入
main
函数前,dyld
就已经介入了。
- 还需要一个临界点。
因为已经发现main
函数不是最早执行的函数了,那么想要探索dyld
,就必须找到一个相对更早的dyld
的入口。于是在ViewController
中实现它的一个更早的机制——load
。可以证明一下,ViewController
的load
是早于main
函数调用的。
断点全部不要,在main
函数里面随意NSLog
内容,然后在ViewController
的load
中随意打印内容。明显发现load
比main
还早。
- 实现
load
,并挂上断点,继续Run
。
- 在
lldb
中输入bt
,查看堆栈信息的调用。
很明显,在Debug Nav
和堆栈信息中,找到了同一个更早调用的_dyld_start
。
- 打开上面准备的dyld源码750.6,搜索
__dyld_start
。
三、 dyld加载流程
上面已经找到了_dyld_start
这个入口,那么就从_dyld_start
开始流程探索。
主线 : dyld加载流程
1. dyldbootstrap::start
一定是
arm64
架构,并且看_dyld_start
都做了什么,所以看bl
跳转。找到dyldbootstrap::start
。搜空格 + start(
,因为这是一个函数,按照函数格式规范来搜。找
return
。进入dyld::_main
2. dyld::_main的流程
就看一些有注释的,然后看主要流程。不用全都看得懂,主要是捋清楚这条线。按照经验来看,无论做什么都要先搞清楚环境,所以从环境入手,搜索env
,找到如下图所示
- 【第一步 : 环境变量配置】 : 根据环境变量进行对应的值的配置。获取当前的运行架构
- 【第二步 : 设置共享缓存】 : iOS中必须开启共享缓存,为动态库的使用做环境准备,并且共享缓存必须映射到共享区域,否则可能出现动态库无法使用的情况。
- 【第三步 : 尝试将dyld本身放入
UUID
列表】
- 【第四步 : 主可执行程序的实例化】 :
instantiateFromLoadedImage
会为主可执行程序生成镜像
- 【第五步 : 插入动态库】 :
loadInsertedDylib
加载所有插入的动态库
- 【第六步 : 链接主可执行程序】
- 【第七步 : 链接动态库】
- 【第八步 : 弱符号绑定】
- 【第九步 : 执行初始化方法】
- 【第十步 : 通知所有监听,该进程马上进入
main()
】
- 【第十一步 : 寻找主可执行程序入口】
从Load Commond
读取LC_MAIN
入口,如果没有就读取LC_UNIXTHREAD
。最后进入的就是我们见到的main()
函数
四、关于主程序的初始化和执行初始化
上面是main()
函数启动前的准备过程,这里挑重点的说,先说主程序的初始
化。因为后面所有的步骤流程都是围绕着这个主程序来进行的。
先来看一个串起了整条线的变量
sMainExecutable
: 主程序。在所有的步骤线上,它都是中心点。
那么就找它的初始化方法instantiateFromLoadedImage
进行探索。
1. instantiateFromLoadedImage主程序的初始化
//内核在dyld获得控制之前在,需要在主可执行文件中进行映射。
//我们需要为主可执行文件创建一个ImageLoader*
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";
}
- 注释写了,主可执行文件并不是主程序,主程序是
ImageLoader *
的对象image
,也即是镜像。 - 这个主程序最后会被强转成
ImageLoaderMachO
类型的指针对象,也就是我们的mach-o
可执行文件。
然后我们进入创建ImageLoader
的创建方法instantiateMainExecutable
。
// create image for main executable
//为主可执行文件创建镜像
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
//段的数量
unsigned int segCount;
//库的数量
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
//sniffLoadCommands用于确定此mach-o文件是否有正常的或者压缩的LINKEDIT,以及它所拥有的段数
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
//根据load commands的内容,实例化具体的类
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}
- 也就是说创建主镜像的重点就是
sniffLoadCommands
,它是获得mach-o
可执行文件的代码,并且也可以获取load commods
加载命令的信息。
说白了,就是镜像文件进到这里,主要就是被加载到load commonds
里面,那么之后我们写的所有的代码都会被编译进来,也就是说我们写的东西都会被变成mach-o
的形式。
比如,最上面我创建的那个Project
打开它的可执行文件
这里面的动态库都会被变成mach-o
文件,都变成了这种数据段的形式存在于load commonds
里面。
2. initializeMainExecutable执行初始化
void initializeMainExecutable()
{
// record that we've reached this step
//记录一下我们已经到达了主可执行程序的初始化
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
//对所有插入的动态库做初始化
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
//获取镜像根文件的数量
const size_t rootCount = sImageRoots.size();
//如果镜像根文件的数量比1多
if ( rootCount > 1 ) {
//循环执行初始化,这里的初始化和下面的主可执行文件及其所有关联文件的初始化函数是同一个
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
//运行主可执行文件和所有关联文件的初始化
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
//注册cxa_atexit()处理程序,在进程退出时在所有加载的镜像(image)中运行静态终止符
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
//如果需要转存信息
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
还是看一下注释
- 执行初始化的真正函数是
runInitializers
。
runInitializers
我就不贴出来了,因为我们就需要那一句processInitializers
函数。
- (1) processInitializers
//这里不用翻译这么多英文了,大概意思就是向上的链接全都放到向下的链接的处理后面
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
//在镜像列表中的所有镜像都利用递归进行init,建立一个没有进行初始化向上依赖关系的列表
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
//如果还存在向上的依赖关系,把它们全部初始化
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
就是对镜像列表中的镜像全部初始化,并且把依赖关系全部变成向下的。
- (2) recursiveInitialization
代码太多,截图看,因为我们最主要的是让dyld
知道了我需要用到你了。
看画红框的,一个通知notifySingle
和一个初始化doInitialization
。
- (3) notifySingle
全局搜索notifySingle(
然后找到获取镜像文件的真实地址的那句代码
一个函数指针,那么继续跟这个函数指针。看看这个指针是在哪里赋值的,
注册Notify。再看谁注册了通知。接着搜索registerObjCNotifiers
。找到了
然后去objc4-781
中搜索_dyld_objc_notify_register
。
本身
*sNotifyObjCInit
就是一个函数回调,这就是数据在这dyld
和objc
中的传递。
本来
dyld
里面的sNotifyObjCInit
是init
,相当于是一个nil
的,只有看objc_init
什么时候调用_dyld_objc_notify_register
,并给他这个load_images
,sNotifyObjCInit
才能执行起来。
所以下面接着看load_images
在objc-781
里面又干了什么,是不是真的调起了+(void)load
。
- (3.1) load_images
下面都是跟着红框往里面进。
这里就行了,因为看到了@selector(load)
。
然后我们对比一下xcode
的堆栈信息。
和上面的流程一模一样。
总结 :
+(void)load
的源码链条 :
_dyld_start
--->dyldbootstrap::start
--->dyld::_main
--->
dyld::initializeMainExecutable
--->ImageLoader::runInitializers
--->
ImageLoader::processInitializers
--->ImageLoader::recursiveInitialization
--->
dyld::notifySingle
(函数指针,利用回调)--->
sNotifyObjCInit
--->load_images(libobjc.A.dylib)
- (4) doInitialization
现在知道它是执行初始化的真正步骤,那么看dyld
的源码
两种初始化方法,一个-init
、一个静态构造器
。分别进去看一下。
(4.1)doImageInit()
两点 :
-
doImageInit()
是通过-init
做的初始化,并且是通过初始化函数的移动完成镜像的初始化。 -
libSystem
必须是第一个进行初始化的库。 - for循环加载方法的调用
(4.2)doModInitFunctions()
你会发现和doImageInit
差不多的内容,但是doModInitFunctions
加载大多都是c++
的文件。
做个验证 :
这是最开始的Project
,在main.m
添加c++
代码
__attribute__((constructor)) void jdFunc() {//挂上断点
printf("%s",__func__);
}
上图3.15就是它的结果,画红框的就是doModInitFunctions
。
另外,可以下载libSystem
的官方源码,搜索_initializer
,会发现
(1). libSystem
也是通过_dyld_initializer
进行的初始化。然后就会进行libdispatch_init(void);
,这一步libdispatch_init
的初始化就通过了libdispatch.dyld
的库。
(2). 其实还可以继续的,这里我就多说一下,因为的确图太多了,我就不全部贴了,感兴趣的可以再下载一份libdispatch
的库,然后搜索libdispatch_init
,会发现它的实际实现是通过_os_object_init
,再进入_os_object_init
,你就会发现_objc_init()
。
结论 :
库的初始化顺序 :
dyld
--->libSystem init
--->libdispatch init
--->objc init
objc_init
的源码链 :
_dyld_start
--->dyldbootstrap::start
--->dyld::_main
--->
dyld::initializeMainExecutable
--->ImageLoader::runInitializers
--->
ImageLoader::processInitializers
--->ImageLoader::recursiveInitialization
--->
doInitialization
--->
libSystem_initializer
(libSystem.B.dylib)--->_os_object_init
(libdispatch.dylib)
--->_objc_init
(libobjc.A.dylib)
附:
一张dyld
的加载流程图。