一、dyld初识
1.1. 什么是dyld?
dyld
是英文 the dynamic link editor
的简写,翻译过来就是动态链接器,是苹果操作系统的一个重要的组成部分。在 iOS/Mac OSX
系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O
镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由 动态链接器dyld
来完成的,也就是符号绑定。动态链接器dyld
在系统中以一个用户态的可执行文件形式存在,一般应用程序会在 Mach-O
文件部分指定一个 LC_LOAD_DYLINKER
的加载命令,此加载命令指定了 dyld
的路径,通常它的默认值是 /usr/lib/dyld
。系统内核在加载 Mach-O
文件时,都需要用 dyld
(位于 /usr/lib/dyld
)程序进行链接。
dyld
是开源的,我们可以通过 官网 下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节 。本文是在dyld-750.6版本下的源码进行调试的。
1.2. dyld共享缓存
上图展示了应用程序的编译流程,在链接阶段会将应用程序的动态库(.so、.framework、.dylib)
和静态库(.a、.lib)
和我们的应用程序进行链接。
静态库可以看成是一堆对象文件 (object files)
的归档。当链接这样一个库到应用中时,静态链接器static linker
将会从库中收集这些对象文件并把它们和应用的对象代码一起打包到一个单独的二进制文件中。这意味着应用的可执行文件大小将会随着库的数目增加而增长。另外,当应用启动时,应用的代码(包含库的代码)将会一次性地导入到程序的地址空间中去。
动态库是可以被多个 app
的进程共用的,所以在内存中只会存在一份;如果是静态库,由于每个 app
的 Mach-O
文件中都会存在一份,则会存在多份。相对静态库,使用动态库可以减少 app
占用的内存大小。动态库不能直接运行,而是需要通过系统的 动态链接加载器dyld
进行加载到内存后执行。
dyld
加载时,为了优化程序启动,启用了共享缓存(shared cache)
技术。共享缓存会在进程启动时被 dyld
映射到内存中,之后,当任何 Mach-O
映像加载时,dyld
首先会检查该 Mach-O
映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。
二、应用程序加载
在应用程序的入口 main()
函数之前断点,查看堆栈信息
可以看到,先于main
函数调用的是 start
,同时,这一流程是由libdyld.dylib
库执行的。dyld
是开源库,可以下载源码探索。点击下载dyld 源码
为了看到更详细的调用过程,我们在项目中的 ViewController 的 + (void) load
方法打断点。详细堆栈信息如下
2.1 _dyld_start
可见,调用流程是从 _dyld_start
开始的,我们在下载好的源码中搜索 _dyld_start
。在 dyldStartup.s
文件中找到了入口,这里是用汇编实现的,尽管在不同架构下有所区别,但都是会调用 dyldbootstrap
命名空间下的start
方法,这和上面的堆栈顺序也是相同的。
call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
2.2 dyldbootstrap::start
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
rebaseDyld(dyldsMachHeader);
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
// 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);
}
dyldbootstrap::start中,主要过程为:
①使用全局变量之前,对dyld
进行rebase
操作,以修复为 real pointer
来运行;
②设置参数和环境变量;
③读取 app
二进制文件 Mach-O
的header
得到偏移量 appSlide
,然后调用dyld
命名空间下的_main
方法。
2.3 dyld::_main
这里是dyld
的入口。内核加载了dyld
然后跳转到 _dyld_start
来设置一些寄存器的值之后 进入这个方法。返回 _dyld_start
所跳转到的目标程序的main
函数地址。精简的代码如下:
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
......
// 设置运行环境,可执行文件准备工作
......
// load shared cache 加载共享缓存
mapSharedCache();
......
reloadAllImages:
......
// instantiate ImageLoader for main executable 加载可执行文件并生成一个ImageLoader实例对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
......
// load any inserted libraries 加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// link main executable 链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
......
// link any inserted libraries 链接所有插入的动态库
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
// 注册符号插入
image->registerInterposing(gLinkContext);
}
}
}
......
//弱符号绑定
sMainExecutable->weakBind(gLinkContext);
sMainExecutable->recursiveMakeDataReadOnly(gLinkContext);
......
// run all initializers 执行初始化方法
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
return result;
}
复制代码
主要过程:
①第一步: 设置运行环境,为可执行文件的加载做准备工作;
②第二步: 映射共享缓存到当前进程的逻辑内存空间;
③第三步: 实例化主程序;
④第四步: 加载插入的动态库;
⑤第五步: 链接主程序;
⑥第六步: 链接插入的动态库;
⑦第七步: 执行弱符号绑定(weakBind);
⑧第八步: 执行初始化方法;
⑨第九步: 查找程序入口并返回main( ).
- 注1: sMainExecutable = instantiateFromLoadedImage(....) 与 loadInsertedDylib(...)
这一步 dyld
将我们可执行文件以及插入的 lib
加载进内存,生成对应的image
。 sMainExecutable
对应着我们的可执行文件,里面包含了我们项目中所有新建的类。 InsertDylib
一些插入的库,他们配置在全局的环境变量 sEnv 中,我们可以在项目中设置环境变量 DYLD_PRINT_ENV 为1来打印该 sEnv 的值。
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";
}
isCompatibleMachO
是检查Mach-O的subtype是否是当前cpu可以支持; 内核会映射到主可执行文件中,我们需要为映射到主可执行文件的文件,创建ImageLoader。
instantiateMainExecutable 就是实例化可执行文件, 这个期间会解析LoadCommand
, 这个之后会发送 dyld_image_state_mapped
通知; 在此方法中,读取image,然后addImage()
到镜像列表。
- 注2: link(sMainExecutable,...) 和 link(image,....)
对上面生成的 Image
进行链接。这个过程就是将加载进来的二进制变为可用状态的过程。其主要做的事有对image
进行 load
(加载),rebase
(基地址复位),bind
(外部符号绑定),我们可以查看源码:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
......
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
......
this->recursiveRebaseWithAccounting(context);
......
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
}
注2.1: recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
递归加载
所有依赖库
进内存。注2.2:recursiveRebase(context)
递归
对自己以及依赖库进行rebase操作
。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现 ASLR(Address space layout randomization,地址空间布局随机化),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。注2.3:recursiveBindWithAccounting(context, forceLazysBound, neverUnload); 对库中所有nolazy的符号进行
bind
,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。
2.4 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();
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
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]);
}
这一步主要是调用所有image
的Initalizer
方法进行初始化。先为所有插入并链接完成的动态库执行初始化操作
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
再为主程序可执行文件执行初始化操作
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
具体流程为: ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization
详细代码如下:
2.5 ImageLoader::runInitializers
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
// 重点
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
复制代码
调用 processInitializers
2.6 ImageLoader::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.
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);
}
在这里,对镜像表中的所有镜像执行recursiveInitialization
,创建一个未初始化的向上依赖新表。如果依赖中未初始化完毕,则继续执行processInitializers
,直到全部初始化完毕。
2.7 ImageLoader::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);
// 重点 1: 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);
// 重点 2: initialize this image
bool hasInitializers = this->doInitialization(context);
// 重点 3: 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();
}
在 recursiveInitialization 函数中,我们重点关注
- context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);,
- doInitialization(context)
- context.notifySingle(dyld_image_state_initialized, this, NULL);
- context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
通知objc我们要初始化这个镜像,这里 通过 notifySingle
函数对sNotifyObjCInit
进行函数调用。
2.7.1 context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo)
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();
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
......
}
获取镜像文件的真实地址 【*sNotifyObjCInit)(image->getRealPath(), image->machHeader() 】,而 sNotifyObjCInit
是 通过 registerObjCNotifiers
中传递的参数(_dyld_objc_notify_init)进行赋值的。
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;
......
}
继而找到,registerObjCNotifiers
的 拉起函数 _dyld_objc_notify_register
.
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}
_dyld_objc_notify_register
函数是供 objc runtime 使用的,当objc镜像被映射,取消映射,和初始化时 被调用的注册处理器。我们可以在 libobjc.A.dylib 库里,_objc_init
函数中找到其调用。
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init(); // 环境变量
tls_init();
static_init(); // C++
runtime_init(); // runtime 初始化
exception_init(); // 异常初始化
cache_init(); // 缓存初始化
_imp_implementationWithBlock_init(); //
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
runtime初始化后,在_objc_init
中注册了几个通知,从dyld
这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的load方法等。
就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld
会通知runtime进行类结构初始化,然后再通知调用load
方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind
,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。
当所有的依赖库的lnitializer都调用完后,dyld::main 函数会返回程序的main()
函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。
那么 _objc_init
又是如何被调用的呢?
看调用堆栈,在 ImageLoader::recursiveInitialization
函数中,我们之前关注的重点2: doInitialization
- this->doInitialization(context);
// 重点 2: initialize this image
bool hasInitializers = this->doInitialization(context);
2.7.2 ImageLoaderMachO::doInitialization
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
在 doModInitFunctions之后 会 先执行 libSystem_initializer
,保证系统库优先初始化完毕,在这里初始化 libdispatch_init
,进而在_os_object_init
中 调用 _objc_init
。
由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
runtime 接手后调用 map_images
做解析和处理,接下来 load_images
中调用 call_load_methods
方法,遍历
所有加载进来的 Class,按继承
层级依次调用 Class 的 +load
方法和其 Category 的 +load
方法。
至此,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime
所管理,在这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)
总结:
APP是由内核引导启动的,kernel内核做好所有准备工作后会得到线程入口及main入口,但是线程不会马上进入main入口,因为还要加载动态链接器(dyld),dyld会将入口点保存下来,等dyld加载完所有动态链接库等工作之后,再开始执行main函数。
系统kernel做好启动程序的初始准备后,交给dyld负责。
dyld接手后,系统先读取 App 的可执行文件(Mach-O
文件),从里面获取dyld
的路径,然后加载dyld
,dyld
去初始化运行环境,开启缓存策略,配合 ImageLoader 将二进制文件按格式加载到内存,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接
,最后调用每个依赖库的初始化
方法,在这一步,runtime
被初始化。当所有依赖库初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化
,然后调用所有的load
方法。最后dyld
返回main()
函数地址,main()
函数被调用。
这个过程远比写出来的要复杂,这里只提到了 runtime 这个分支,还有像 GCD、XPC 等重头的系统库初始化分支没有提及(当然,有缓存机制在,它们也不会玩命初始化),总结起来就是 main 函数执行之前,系统做了茫茫多的加载和初始化工作,最终引入那个熟悉的main函数。