【IOS开发高级系列】dyld专题
1 dyld
1.1 dyld简介
在iOS系统中,几乎所有的程序都会用到动态库,而动态库在加载的时候都需要用dyld(位于/usr/lib/dyld)程序进行链接。很多系统库几乎都是每个程序都要用到的,与其在每个程序运行的时候一个一个将这些动态库都加载进来,还不如先把它们打包好,一次加载进来来的快。
动态库不能直接运行,而是需要通过系统的动态链接加载器进行加载到内存后执行,动态链接加载器在系统中以一个用户态的可执行文件形式存在,一般应用程序会在Mach-O文件部分指定一个LC_LOAD_DYLINKER的加载命令,此加载命令指定了dyld的路径,通常它的默认值是“/usr/lib/dyld”。系统内核在加载Mach-O文件时,会使用该路径指定的程序作为动态库的加载器来加载dylib。
1.2 Dyld缓存
dyld加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被dyld映射到内存中,之后,当任何Mach-O映像加载时,dyld首先会检查该Mach-O映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。
update_dyld_shared_cache程序确保了dyld的共享缓存是最新的,它会扫描 /var/db/dyld/shared_region_roots/目录下paths路径文件,这些paths文件包含了需要加入到共享缓存的Mach-O文件路径列表,update_dyld_shared_cache()会挨个将这些Mach-O文件及其依赖的dylib都加共享缓存中去。
共享缓存是以文件形式存放在/var/db/dyld/目录下的,生成共享缓存的update_dyld_shared_cache程序位于是/usr/bin/目录下,该工具会为每种系统加构生成一个缓存文件与对应的内存地址map表,如下所示:
ls -l/var/db/dyld/
total1741296
-rw-r--r-- 1root wheel 333085108 Apr 22 15:02 dyld_shared_cache_i386
-rw-r--r-- 1 root wheel 65378 Apr 22 15:02dyld_shared_cache_i386.map
-rw-r--r-- 1 root wheel 558259294 Apr 25 16:18dyld_shared_cache_x86_64h
-rw-r--r-- 1 root wheel 129633 Apr 25 16:18dyld_shared_cache_x86_64h.map
drwxr-xr-x 10 root wheel 340 Apr 7 09:19 shared_region_roots
生成的共享缓存可以使用工具dyld_shared_cache_util查看它的信息,该工具位于dyld源码中的launch-cache\dyld_shared_cache_util.cpp文件,需要自己手动编译。另外,也可以使用dyld提供的两个函数dyld_shared_cache_extract_dylibs()与dyld_shared_cache_extract_dylibs_progress()来自己解开cache文件,代码位于dyld源码的launch-cache\dsc_extractor.cpp文件中。
update_dyld_shared_cache通常它只在系统的安装器安装软件与系统更新时调用,当然,可以手动运行“sudoupdate_dyld_shared_cache”来更新共享缓存。新的共享缓存会在系统下次启动后自动更新。
dyld缓存在系统中位于“/System/Library/Caches/com.apple.dyld/”目录下,文件名是以“dyld_shared_cache_”开头,再加上这个dyld缓存文件所支持的指令集。在这个目录下,有可能有多个dyld缓存文件,对应所支持的不同指令集。比如,在iPadAir 2上,该目录下就存在两个缓存文件:
因为iPad Air 2是64位的ARM(ARM v8)处理器,同时它也兼容32位的ARM应用,所以就要有两个缓存文件。dyld_shared_cache_arm64对应64位的版本,而dyld_shared_cache_armv7s对应32位的版本。到目前为止,所有iOS支持的ARM指令集有以下四种:
1)armv6
2)armv7
3)armv7s
4)arm64
1.3 dyld缓存提取工具
没有了系统库的原始二进制版本是不是就没法分析了呢?当然不是,我们还可以从dyld缓存文件中将系统库的原始二进制文件给解出来。目前,有两个工具可以做到这点,一是dyld_decache,还有一个就是jtool。
使用dyld_decache可以整体提取dyld缓存文件中的所有库原始二进制文件:
dyld_decache [-o folder] [-f name [-f name] ...]path/to/dyld_shared_cache_armvX
-o用来指定提取出来的文件所要保存的路径,如果不指定,默认就在当前目录下创建一个叫做“library”的目录保存。-f用来说明要提取库的名字,如果要提取的库不止一个,那么每个库的名字前面都要带上-f。如果不指定默认行为就是把缓存文件中所有的库文件全部都提取出来。例如,如果想要解压Security库,可以使用下面的命令:
dyld_decache –o./Security -f Security ./dyld_shared_cache_armv7s
前面也提到了,还可以用jtool来达到提取指定库文件的目的:
jtool –extractname path/to/dyld_shared_cache_armvx
-extract用来指定要提取库的名字。jtool默认不支持提取全部缓存中库文件的功能,只能一个一个提取。
以上就是iOS中dyld缓存的相关使用方法,大家可以在系统中去找找对应的缓存文件,结合本文的分享,深入研究下。
相关文章:《iOSCornerstone工具操作方法详解》
2 IOS程序启动过程
系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dyld,dyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。
2.1 Mach-O文件加载
这里先说下Mach-O文件。
Mach-O文件格式是OS X与iOS系统上的可执行文件格式,像我们编译过程产生的.O文件,以及程序的可执行文件,动态库等都是Mach-O文件。它的结构如下:
Mach-O文件结构mach-o文件有如下几个部分组成:
Header:保存了一些基本信息,包括了该文件运行的平台、文件类型、LoadCommands的个数等等。
LoadCommands:可以理解为加载命令,在加载Mach-O文件时会使用这里的数据来确定内存的分布以及相关的加载命令。比如我们的main函数的加载地址,程序所需的dyld的文件路径,以及相关依赖库的文件路径。
Data: 这里包含了具体的代码、数据等等。
我们可以通过Mach-O文件查看器MachOView查看一个测试项目(这里放上地址)编译后的可执行文件内容:
Mach-O文件内容这里可以看到,程序需要的dyld的路径在LC_LOAD_DYLINKER命令里,一般都是在/usr/lib/dyld路径下。这里的LC_MAIN指的是程序main函数加载地址,下面还有写LC_LOAD_DYLIB指向的都是程序依赖库加载信息,如果我们程序里使用到了AFNetworking,这里就会多一条名为LC_LOAD_DYLIB(AFNetworking)的命令,如下图:
三方库
这里可以看到一些我们比较常用的三方库:AFNetworking,IQKeyboard等。
系统加载程序可执行文件后,通过分析文件来获得dyld所在路径来加载dyld,然后就将后面的事情甩给dyld了。
2.2 dyld加载
dyld: (the dynamiclink editor)动态链接器,其源码是开源的。
ImageLoader:用于辅助加载特定可执行文件格式的类,程序中对应实例可简称为image(如程序可执行文件,Framework库,bundle文件)。
dyld接手后得做很多事情,主要负责初始化程序环境,将可执行文件以及相应的依赖库与插入库加载进内存生成对应的ImageLoader类的image(镜像文件)对象,对这些image进行链接,调用各image的初始化方法等等(注:这里多数事情都是递归的,从底向上的方法调用),其中runtime也是在这个过程中被初始化,这些事情大多数在dyld:_mian方法中被发生,我们可以看段简洁的代码:
2.2.1 dyld::_main函数代码
这里的_main函数是dyld的函数,并非我们程序里的main函数。
1. sMainExecutable = instantiateFromLoadedImage(....)与loadInsertedDylib(...)
这一步dyld将我们可执行文件以及插入的lib加载进内存,生成对应的image。
sMainExecutable对应着我们的可执行文件,里面包含了我们项目中所有新建的类。
InsertDylib一些插入的库,他们配置在全局的环境变量sEnv中,我们可以在项目中设置环境变量DYLD_PRINT_ENV为1来打印该sEnv的值。
环境变量设置运行程序Log如下:
打印出插入库的log
可以看到插入的库为:libBacktraceRecording.dylib和libViewDebuggerSupport.有时我们会在三方App的Mach-O文件中通过修改DYLD_INSERT_LIBRARIES的值来加入我们自己的动态库,从而注入代码,hook别人的App(相关资料)。
2.2.2 link(sMainExecutable,...)和link(image,....)
2. link(sMainExecutable,...)和link(image,....)
对上面生成的Image进行链接。其主要做的事有对image进行load(加载), rebase(基地址复位),bind(外部符号绑定),我们可以查看源码:
link方法
recursiveLoadLibraries(context, preflightOnly, loaderRPaths)
递归加载所有依赖库进内存。
recursiveRebase(context)
递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法、变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间配置随机加载),程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问。
recursiveBind(context, forceLazysBound, neverUnload)
对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。
2.2.3 initializeMainExecutable方法
3. initializeMainExecutable()
这一步主要是调用所有image的Initalizer方法进行初始化。这里的Initalizers方法并非名为Initalizers的方法,而是C++静态对象初始化构造器,atribute((constructor))进行修饰的方法,在LmageLoader类中initializer函数指针所指向该初始化方法的地址。
initiallizer函数指针
我们可以在程序中设置环境变量DYLD_PRINT_INITIALIZERS为1来打印出程序的各种依赖库的initializer方法:
可以打印出调用了Initalizers的image的运行程序,系统Log打印如下:
lnitializer调用log(由于打印的比较长,这样就截取开头的log)可以看到每个依赖库对应着一个初始化方法,名称各有不同。
这里最开始调用的libSystem.dylib的initializer function比较特殊,因为runtime初始化就在这一阶段,而这个方法其实很简单,我们可以在这里看到init.c源码,主要方法如下:
2.2.4 libSystem_initializer方法
其中libdispatch_init里调用了到了runtime初始化方法_objc_init.我们可以、在程序中打个符号断点来验证:
_objc_init符号断点
运行程序,然后断点命中,我们来看下调用栈:
2.2.5 objc_init调用栈
这里可以看到_objc_init调用的顺序,先libSystem_initializer调用libdispatch_init再到_objc_init初始化runtime。
runtime初始化后不会闲着,在_objc_init中注册了几个通知,从dyld这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的load方法。
就拿sMainExcuatable来说,它的initializer方法是最后调用的,当initializer方法被调用前dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行bind,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。
2.3 main函数被调用
当所有的依赖库库的lnitializer都调用完后,dyld::main函数会返回程序的main函数地址,main函数被调用,从而代码来到了我们熟悉的程序入口。
main函数入口结语
这里只是简单概括了从程序启动->dyld加载依赖库->runtime初始化->main的过程。但这阶段还有很多事情未讲,如果想深入了解还得结合源码来学习,这里我已经将dyld和runtime源码都放在这了,大家可直接下载,也可以从opensource-apple下载。
2.4 源码解析
2.4.1 主函数解析
dyld是苹果操作系统一个重要组成部分,而且令人兴奋的是,它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式(下载地址:Source Browser),了解系统加载动态库的细节。
系统内核在加载动态库前,会加载dyld,然后调用去执行__dyld_start(),该函数会执行dyldbootstrap::start(),后者会执行_main()函数,dyld的加载动态库的代码就是从_main()开始执行的。下面以dyld源码的360.18版本为蓝本进行分析:
uintptr_t_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
//第一步,设置运行环境,处理环境变量
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
......
CRSetCrashLogMessage("dyld: launch started");
......
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path.
sExecPath = _simple_getenv(apple, "executable_path");
// Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
......
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
sProcessIsRestricted = processRestricted(mainExecutableMH, &ignoreEnvironmentVariables, &sProcessRequiresLibraryValidation);
if ( sProcessIsRestricted ) {
#if SUPPORT_LC_DYLD_ENVIRONMENT
checkLoadCommandEnvironmentVariables();
#endif
pruneEnvironmentVariables(envp, &apple);
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else {
if ( !ignoreEnvironmentVariables )
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
getHostInfo(mainExecutableMH, mainExecutableSlide);
......
//第二步,初始化主程序
try {
// add dyld itself to UUID list
addDyldImageToUUIDList();
CRSetCrashLogMessage(sLoadingCrashMessage);
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
......
//第三步,加载共享缓存
checkSharedRegionDisable();
#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
mapSharedCache();
#endif
// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
checkVersionedPaths();
#endif
//第四步,加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
sInsertedDylibCount = sAllImages.size()-1;
//第五步,链接主程序
gLinkContext.linkingMainExecutable = true;
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
//第六步,链接插入的动态库
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));
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing();
}
}
// dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (int i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}
// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
//第七步,执行弱符号绑定
gLinkContext.linkingMainExecutable = false;
// do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);
//第八步,执行初始化方法
CRSetCrashLogMessage("dyld: launch, running initializers");
#if SUPPORT_OLD_CRT_INITIALIZATION
// Old way is to run initializers via a callback from crt1.o
if ( ! gRunInitializersOldWay )
initializeMainExecutable();
#else
// run all initializers
initializeMainExecutable();
#endif
//第九步,查找入口点并返回
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}
CRSetCrashLogMessage(NULL);
return result;
}
整个方法的代码比较长,将它按功能分成九个步骤进行讲解:
2.4.2 第一步,设置运行环境,处理环境变量
代码在开始时候,将传入的变量mainExecutableMH赋值给了sMainExecutableMachHeader,这是一个macho_header类型的变量,其结构体内容就是本章前面介绍的mach_header结构体,表示的是当前主程序的Mach-O头部信息,有了头部信息,加载器就可以从头开始,遍历整个Mach-O文件的信息。
接着执行了setContext(),此方法设置了全局一个链接上下文,包括一些回调函数、参数与标志设置信息,代码片断如下:
static void setContext(const macho_header* mainExecutableMH, int argc, const char* argv[], const char* envp[], const char* apple[])
{
gLinkContext.loadLibrary = &libraryLocator;
gLinkContext.terminationRecorder = &terminationRecorder;
gLinkContext.flatExportFinder = &flatFindExportedSymbol;
gLinkContext.coalescedExportFinder = &findCoalescedExportedSymbol;
gLinkContext.getCoalescedImages = &getCoalescedImages;
......
gLinkContext.bindingOptions = ImageLoader::kBindingNone;
gLinkContext.argc = argc;
gLinkContext.argv = argv;
gLinkContext.envp = envp;
gLinkContext.apple = apple;
gLinkContext.progname = (argv[0] != NULL) ? basename(argv[0]) : "";
gLinkContext.programVars.mh = mainExecutableMH;
gLinkContext.programVars.NXArgcPtr = &gLinkContext.argc;
gLinkContext.programVars.NXArgvPtr = &gLinkContext.argv;
gLinkContext.programVars.environPtr = &gLinkContext.envp;
gLinkContext.programVars.__prognamePtr=&gLinkContext.progname;
gLinkContext.mainExecutable = NULL;
gLinkContext.imageSuffix = NULL;
gLinkContext.dynamicInterposeArray = NULL;
gLinkContext.dynamicInterposeCount = 0;
gLinkContext.prebindUsage = ImageLoader::kUseAllPrebinding;
#if TARGET_IPHONE_SIMULATOR
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
#else
gLinkContext.sharedRegionMode = ImageLoader::kUseSharedRegion;
#endif
}
设置的回调函数都是dyld本模块实现的,如loadLibrary方法就是本模块的libraryLocator()方法,负责加载动态库。
在设置完这些信息后,执行processRestricted()方法判断进程是否受限。代码如下:
static bool processRestricted(const macho_header* mainExecutableMH, bool* ignoreEnvVars, bool* processRequiresLibraryValidation)
{
#if TARGET_IPHONE_SIMULATOR
gLinkContext.codeSigningEnforced = true;
#else
// ask kernel if code signature of program makes it restricted
uint32_t flags;
if ( csops(0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1 ) {
if (flags & CS_REQUIRE_LV)
*processRequiresLibraryValidation = true;
#if __MAC_OS_X_VERSION_MIN_REQUIRED
if ( flags & CS_ENFORCEMENT ) {
gLinkContext.codeSigningEnforced = true;
}
if ( ((flags & CS_RESTRICT) == CS_RESTRICT) && (csr_check(CSR_ALLOW_TASK_FOR_PID) != 0) ) {
sRestrictedReason = restrictedByEntitlements;
return true;
}
#else
if ((flags & CS_ENFORCEMENT) && !(flags & CS_GET_TASK_ALLOW)) {
*ignoreEnvVars = true;
}
gLinkContext.codeSigningEnforced = true;
#endif
}
#endif
// all processes with set uid or set gid bit set are restricted
if ( issetugid() ) {
sRestrictedReason = restrictedBySetGUid;
return true;
}
// Respect __RESTRICT,__restrict section for root processes
if ( hasRestrictedSegment(mainExecutableMH) ) {
// existence of __RESTRICT/__restrict section make process restricted
sRestrictedReason = restrictedBySegment;
return true;
}
return false;
}
进程受限会是以下三种可能:
restrictedByEntitlements:在macOS系统上,在需要验证代码签名(Gatekeeper开启)的情况下,且csr_check(CSR_ALLOW_TASK_FOR_PID)返回为真(表示Rootless开启了TASK_FOR_PID标志)时,进程才不会受限,在macOS版本10.12系统上,默认Gatekeeper是开启的,并且Rootless是关闭了CSR_ALLOW_TASK_FOR_PID标志位的,这意味着,默认情况下,系统上运行的进程是受限的。
restrictedBySetGUid:当进程的setuid与setgid位被设置时,进程会被设置成受限。这样做是出于安全的考虑,受限后的进程无法访问DYLD_开头的环境变量,一种典型的系统攻击就是针对这种情况而发生的,在macOS版本10.10系统上,一个由DYLD_PRINT_TO_FILE环境变量引发的系统本地提权漏洞,就是通过向DYLD_PRINT_TO_FILE环境变量传入拥有SUID权限的受限文件,而系统没做安全检测,而这些文件是直接有向系统创建与写入文件权限的。关于漏洞的具体细节可以参看:OS X 10.10 DYLD_PRINT_TO_FILE
Local Privilege Escalation Vulnerability
restrictedBySegment:段名受限。当Mach-O包含一个__RESTRICT/__restrict段时,进程会被设置成受限。
在进程受限后,执行了以下三个方法:
checkLoadCommandEnvironmentVariables():遍历Mach-O中所有的LC_DYLD_ENVIRONMENT加载命令,然后调用processDyldEnvironmentVariable()对不同的环境变量做相应的处理。
pruneEnvironmentVariables():删除进程的LD_LIBRARY_PATH与所有以DYLD_开头的环境变量,这样以后创建的子进程就不包含这些环境变量了。
setContext():重新设置链接上下文。这一步执行的主要目的是由于环境变量发生变化了,需要更新进程的envp与apple参数。
2.4.3 第二步,初始化主程序
这一步主要执行了instantiateFromLoadedImage()。它的代码如下:
static ImageLoader* 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 image;
}
throw "main executable not a known format";
}
isCompatibleMachO()主要检查Mach-O的头部的cputype与cpusubtype来判断程序与当前的系统是否兼容。如果兼容接下来就调用instantiateMainExecutable()实例化主程序,代码如下:
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(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of 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()主要获取了加载命令中的如下信息:
compressed:判断Mach-O的Compressed还是Classic类型。判断的依据是Mach-O是否包含LC_DYLD_INFO或LC_DYLD_INFO_ONLY加载命令。这2个加载命令记录了Mach-O的动态库加载信息,使用结构体dyld_info_command表示:
struct dyld_info_command {
uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */
uint32_t cmdsize; /* sizeof(struct dyld_info_command) */
uint32_t rebase_off; /* file offset to rebase info */
uint32_t rebase_size; /* size of rebase info */
uint32_t bind_off; /* file offset to binding info */
uint32_t bind_size; /* size of binding info */
uint32_t weak_bind_off; /* file offset to weak binding info */
uint32_t weak_bind_size; /* size of weak binding info */
uint32_t lazy_bind_off; /* file offset to lazy binding info */
uint32_t lazy_bind_size; /* size of lazy binding infs */
uint32_t export_off; /* file offset to lazy binding info */
uint32_t export_size; /* size of lazy binding infs */
};
rebase_off与大小rebase_size存储了rebase(重设基址)相关信息,当Mach-O加载到内存中的地址不是指定的首选地址时,就需要对当前的映像数据进行rebase(重设基址)。
bind_off与bind_size存储了进程的符号绑定信息,当进程启动时必须绑定这些符号,典型的有dyld_stub_binder,该符号被dyld用来做迟绑定加载符号,一般动态库都包含该符号。
weak_bind_off与weak_bind_size存储了进程的弱绑定符号信息。弱符号主要用于面向对旬语言中的符号重载,典型的有c++中使用new创建对象,默认情况下会绑定ibstdc++.dylib,如果检测到某个映像使用弱符号引用重载了new符号,dyld则会重新绑定该符号并调用重载的版本。
lazy_bind_off与lazy_bind_size存储了进程的延迟绑定符号信息。有些符号在进程启动时不需要马上解析,它们会在第一次调用时被解析,这类符号叫延迟绑定符号(LazySymbol)。
export_off与export_size存储了进程的导出符号绑定信息。导出符号可以被外部的Mach-O访问,通常动态库会导出一个或多个符号供外部使用,而可执行程序由导出_main与_mh_execute_header符号供dyld使用。
segCount:段的数量。sniffLoadCommands()通过遍历所有的LC_SEGMENT_COMMAND加载命令来获取段的数量。
libCount:需要加载的动态库的数量。Mach-O中包含的每一条LC_LOAD_DYLIB、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB、LC_LOAD_UPWARD_DYLIB加载命令,都表示需要加载一个动态库。
codeSigCmd:通过解析LC_CODE_SIGNATURE来获取代码签名的加载命令。
encryptCmd:通过LC_ENCRYPTION_INFO与LC_ENCRYPTION_INFO_64来获取段加密信息。
获取compressed后,根据Mach-O是否compressed来分别调用ImageLoaderMachOCompressed::instantiateMainExecutable()与ImageLoaderMachOClassic::instantiateMainExecutable()。ImageLoaderMachOCompressed::instantiateMainExecutable()代码如下:
// create image for main executable
ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path,
unsigned int segCount, unsigned int libCount, const LinkContext& context)
{
ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
// set slide for PIE programs
image->setSlide(slide);
// for PIE record end of program, to know where to start loading dylibs
if ( slide != 0 )
fgNextPIEDylibAddress = (uintptr_t)image->getEnd();
image->disableCoverageCheck();
image->instantiateFinish(context);
image->setMapped(context);
if ( context.verboseMapping ) {
dyld::log("dyld: Main executable mapped %s\n", path);
for(unsigned int i=0, e=image->segmentCount(); i < e; ++i) {
const char* name = image->segName(i);
if ( (strcmp(name, "__PAGEZERO") == 0) || (strcmp(name, "__UNIXSTACK") == 0) )
dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segPreferredLoadAddress(i), image->segPreferredLoadAddress(i)+image->segSize(i));
else
dyld::log("%18s at 0x%08lX->0x%08lX\n", name, image->segActualLoadAddress(i), image->segActualEndAddress(i));
}
}
return image;
}
ImageLoaderMachOCompressed::instantiateStart()使用主程序Mach-O信息构造了一个ImageLoaderMachOCompressed对象。disableCoverageCheck()禁用覆盖率检查。instantiateFinish()调用parseLoadCmds()解析其它所有的加载命令,后者会填充完ImageLoaderMachOCompressed的一些保护成员信息,最后调用setDyldInfo()设置动态库链接信息,然后调用setSymbolTableInfo()设置符号表信息。
instantiateFromLoadedImage()调用完了ImageLoaderMachO::instantiateMainExecutable()后,接着调用addImage(),代码如下:
static void addImage(ImageLoader* image)
{
// add to master list
allImagesLock();
sAllImages.push_back(image);
allImagesUnlock();
// update mapped ranges
uintptr_t lastSegStart = 0;
uintptr_t lastSegEnd = 0;
for(unsigned int i=0, e=image->segmentCount(); i < e; ++i) {
if ( image->segUnaccessible(i) )
continue;
uintptr_t start = image->segActualLoadAddress(i);
uintptr_t end = image->segActualEndAddress(i);
if ( start == lastSegEnd ) {
// two segments are contiguous, just record combined segments
lastSegEnd = end;
}
else {
// non-contiguous segments, record last (if any)
if ( lastSegEnd != 0 )
addMappedRange(image, lastSegStart, lastSegEnd);
lastSegStart = start;
lastSegEnd = end;
}
}
if ( lastSegEnd != 0 )
addMappedRange(image, lastSegStart, lastSegEnd);
if ( sEnv.DYLD_PRINT_LIBRARIES || (sEnv.DYLD_PRINT_LIBRARIES_POST_LAUNCH && (sMainExecutable!=NULL) && sMainExecutable->isLinked()) ) {
dyld::log("dyld: loaded: %s\n", image->getPath());
}
}
这段代码将实例化好的主程序添加到全局主列表sAllImages中,最后调用addMappedRange()申请内存,更新主程序映像映射的内存区。做完这些工作,第二步初始化主程序就算完成了。
2.4.4 第三步,加载共享缓存
这一步主要执行mapSharedCache()来映射共享缓存。该函数先通过_shared_region_check_np()来检查缓存是否已经映射到了共享区域了,如果已经映射了,就更新缓存的slide与UUID,然后返回。反之,判断系统是否处于安全启动模式(safe-bootmode)下,如果是就删除缓存文件并返回,正常启动的情况下,接下来调用openSharedCacheFile()打开缓存文件,该函数在sSharedCacheDir路径下,打开与系统当前cpu架构匹配的缓存文件,也就是/var/db/dyld/dyld_shared_cache_x86_64h,接着读取缓存文件的前8192字节,解析缓存头dyld_cache_header的信息,将解析好的缓存信息存入mappings变量,最后调用_shared_region_map_and_slide_np()完成真正的映射工作。部分代码片断如下:
static void mapSharedCache()
{
uint64_t cacheBaseAddress = 0;
if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
sSharedCache = (dyld_cache_header*)cacheBaseAddress;
#if __x86_64__
const char* magic = (sHaswell ? ARCH_CACHE_MAGIC_H : ARCH_CACHE_MAGIC);
#else
const char* magic = ARCH_CACHE_MAGIC;
#endif
if ( strcmp(sSharedCache->magic, magic) != 0 ) {
sSharedCache = NULL;
if ( gLinkContext.verboseMapping ) {
dyld::log("dyld: existing shared cached in memory is not compatible\n");
return;
}
}
const dyld_cache_header* header = sSharedCache;
......
// if cache has a uuid, copy it
if ( header->mappingOffset >= 0x68 ) {
memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
}
......
}
else {
#if __i386__ || __x86_64__
uint32_t safeBootValue = 0;
size_t safeBootValueSize = sizeof(safeBootValue);
if ( (sysctlbyname("kern.safeboot", &safeBootValue, &safeBootValueSize, NULL, 0) == 0) && (safeBootValue != 0) ) {
struct stat dyldCacheStatInfo;
if ( my_stat(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME, &dyldCacheStatInfo) == 0 ) {
struct timeval bootTimeValue;
size_t bootTimeValueSize = sizeof(bootTimeValue);
if ( (sysctlbyname("kern.boottime", &bootTimeValue, &bootTimeValueSize, NULL, 0) == 0) && (bootTimeValue.tv_sec != 0) ) {
if ( dyldCacheStatInfo.st_mtime < bootTimeValue.tv_sec ) {
::unlink(MACOSX_DYLD_SHARED_CACHE_DIR DYLD_SHARED_CACHE_BASE_NAME ARCH_NAME);
gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
return;
}
}
}
}
#endif
// map in shared cache to shared region
int fd = openSharedCacheFile();
if ( fd != -1 ) {
uint8_t firstPages[8192];
if ( ::read(fd, firstPages, 8192) == 8192 ) {
dyld_cache_header* header = (dyld_cache_header*)firstPages;
#if __x86_64__
const char* magic = (sHaswell ? ARCH_CACHE_MAGIC_H : ARCH_CACHE_MAGIC);
#else
const char* magic = ARCH_CACHE_MAGIC;
#endif
if ( strcmp(header->magic, magic) == 0 ) {
const dyld_cache_mapping_info* const fileMappingsStart = (dyld_cache_mapping_info*)&firstPages[header->mappingOffset];
const dyld_cache_mapping_info* const fileMappingsEnd = &fileMappingsStart[header->mappingCount];
shared_file_mapping_np mappings[header->mappingCount+1]; // add room for code-sig
......
if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
sSharedCacheSlide = cacheSlide;
dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
......
}
else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED
throw "dyld shared cache could not be mapped";
#endif
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file could not be mapped\n");
}
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file is invalid\n");
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be read\n");
}
close(fd);
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be opened\n");
}
}
......
}
共享缓存加载完毕后,接着进行动态库的版本化重载,这主要通过函数checkVersionedPaths()完成。该函数读取DYLD_VERSIONED_LIBRARY_PATH与DYLD_VERSIONED_FRAMEWORK_PATH环境变量,将指定版本的库比当前加载的库的版本做比较,如果当前的库版本更高的话,就使用新版本的库来替换掉旧版本的。
2.4.5 第四步,加载插入的动态库
这一步循环遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表,并调用loadInsertedDylib()将其加载。该函数调用load()完成加载工作。load()会调用loadPhase0()尝试从文件加载,loadPhase0()会向下调用下一层phase来查找动态库的路径,直到loadPhase6(),查找的顺序为DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路径->DYLD_FALLBACK_LIBRARY_PATH,找到后调用ImageLoaderMachO::instantiateFromFile()来实例化一个ImageLoader,之后调用checkandAddImage()验证映像并将其加入到全局映像列表中。如果loadPhase0()返回为空,表示在路径中没有找到动态库,就尝试从共享缓存中查找,找到就调用ImageLoaderMachO::instantiateFromCache()从缓存中加载,否则就抛出没找到映像的异常。部分代码片断如下:
ImageLoader* load(const char* path, const LoadContext& context)
{
......
if ( context.useSearchPaths && ( gLinkContext.imageSuffix != NULL) ) {
if ( realpath(path, realPath) != NULL )
path = realPath;
}
ImageLoader* image = loadPhase0(path, orgPath, context, NULL);
if ( image != NULL ) {
CRSetCrashLogMessage2(NULL);
return image;
}
......
image = loadPhase0(path, orgPath, context, &exceptions);
#if __IPHONE_OS_VERSION_MIN_REQUIRED && DYLD_SHARED_CACHE_SUPPORT && !TARGET_IPHONE_SIMULATOR
// support symlinks on disk to a path in dyld shared cache
if ( (image == NULL) && cacheablePath(path) && !context.dontLoad ) {
......
if ( (myerr == ENOENT) || (myerr == 0) )
{
const macho_header* mhInCache;
const char* pathInCache;
long slideInCache;
if ( findInSharedCacheImage(resolvedPath, false, NULL, &mhInCache, &pathInCache, &slideInCache) ) {
struct stat stat_buf;
bzero(&stat_buf, sizeof(stat_buf));
try {
image = ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
image = checkandAddImage(image, context);
}
catch (...) {
image = NULL;
}
}
}
}
#endif
......
else {
const char* msgStart = "no suitable image found. Did find:";
......
throw (const char*)fullMsg;
}
}
2.4.6 第五步,链接主程序
这一步执行link()完成主程序的链接操作。该函数调用了ImageLoader自身的link()函数,主要的目的是将实例化的主程序的动态数据进行修正,达到让进程可用的目的,典型的就是主程序中的符号表修正操作,它的代码片断如下:
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
......
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
......
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());
this->recursiveRebase(context);
......
this->recursiveBind(context, forceLazysBound, neverUnload);
if ( !context.linkingMainExecutable )
this->weakBind(context); //现在是链接主程序,这里现在不会执行
......
std::vector dofs;
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
......
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
this->recursiveApplyInterposing(context); //现在是链接主程序,这里现在不会执行
}
......
}
recursiveLoadLibraries()采用递归的方式来加载程序依赖的动态库,加载的方法是调用context的loadLibrary指针方法,该方法在前面看到过,是setContext()设置的libraryLocator(),该函数只是调用了load()来完成加载,load()加载动态库的过程在上一步已经分析过了。
接着调用recursiveUpdateDepth()对映像及其依赖库按列表方式进行排序。recursiveRebase()则对映像完成递归rebase操作,该函数只是调用了虚函数doRebase(),doRebase()被ImageLoaderMachO重载,实际只是将代码段设置成可写后调用了rebase(),在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的rebase_off与rebase_size来确定需要rebase的数据偏移与大小,然后挨个修正它们的地址信息。
recursiveBind()完成递归绑定符号表的操作。此处的符号表针对的是非延迟加载的符号表,它的核心是调用了doBind(),在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的bind_off与bind_size来确定需要绑定的数据偏移与大小,然后挨个对它们进行绑定,绑定操作具体使用bindAt()函数,它主要通过调用resolve()解析完符号表后,调用bindLocation()完成最终的绑定操作,需要绑定的符号信息有三种:
BIND_TYPE_POINTER:需要绑定的是一个指针。直接将计算好的新值屿值即可。
BIND_TYPE_TEXT_ABSOLUTE32:一个32位的值。取计算的值的低32位赋值过去。
BIND_TYPE_TEXT_PCREL32:重定位符号。需要使用新值减掉需要修正的地址值来计算出重定位值。
recursiveGetDOFSections()与registerDOFs()主要注册程序的DOF节区,供dtrace使用。
2.4.7 第六步,链接插入的动态库
链接插入的动态库与链接主程序一样,都是使用的link(),插入的动态库列表是前面调用addImage()保存到sAllImages中的,之后,循环获取每一个动态库的ImageLoader,调用link()对其进行链接,注意:sAllImages中保存的第一项是主程序的映像。接下来调用每个映像的registerInterposing()方法来注册动态库插入与调用applyInterposing()应用插入操作。registerInterposing()查找__DATA段的__interpose节区,找到需要应用插入操作(也可以叫作符号地址替换)的数据,然后做一些检查后,将要替换的符号与被替换的符号信息存入fgInterposingTuples列表中,供以后具体符号替换时查询。applyInterposing()调用了虚方法doInterpose()来做符号替换操作,在ImageLoaderMachOCompressed中实际是调用了eachBind()与eachLazyBind()分别对常规的符号与延迟加载的符号进行应用插入操作,具体使用的是interposeAt(),该方法调用interposedAddress()在fgInterposingTuples中查找要替换的符号地址,找到后然后进行最终的符号地址替换。
2.4.8 第七步,执行弱符号绑定
weakBind()函数执行弱符号绑定。首先通过调用context的getCoalescedImages()将sAllImages中所有含有弱符号的映像合并成一个列表,合并完后调用initializeCoalIterator()对映像进行排序,排序完成后调用incrementCoalIterator()收集需要进行绑定的弱符号,后者是一个虚函数,在ImageLoaderMachOCompressed中,该函数读取映像动态链接信息的weak_bind_off与weak_bind_size来确定弱符号的数据偏移与大小,然后挨个计算它们的地址信息。之后调用getAddressCoalIterator(),按照映像的加载顺序在导出表中查找符号的地址,找到后调用updateUsesCoalIterator()执行最终的绑定操作,执行绑定的是bindLocation(),前面有讲过,此处不再赘述。
2.4.9 第八步,执行初始化方法
执行初始化的方法是initializeMainExecutable()。该函数主要执行runInitializers(),后者调用了ImageLoader的runInitializers()方法,最终迭代执行了ImageLoaderMachO的doInitialization()方法,后者主要调用doImageInit()与doModInitFunctions()执行映像与模块中设置为init的函数与静态初始化方法,代码如下:
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
2.4.10 第九步,查找入口点并返回
这一步调用主程序映像的getThreadPC()函数来查找主程序的LC_MAIN加载命令获取程序的入口点,没找到就调用getMain()到LC_UNIXTHREAD加载命令中去找,找 到后就跳到入口点指定的地址并返回了。
到这里,dyld整个加载动态库的过程就算完成了。
另外再讨论下延迟符号加载的技术细节。在所有拥有延迟加载符号的Mach-O文件里,它的符号表中一定有一个dyld_stub_helper符号,它是延迟符号加载的关键!延迟绑定符号的修正工作就是由它完成的。绑定符号信息可以使用XCode提供的命令行工具dyldinfo来查看,执行以下命令可以查看python的绑定信息:
xcrun dyldinfo -bind /usr/bin/python
for arch i386:
bind information:
segment section address type addend dylib symbol
__DATA __cfstring 0x000040F0 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __cfstring 0x00004100 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __nl_symbol_ptr 0x00004010 pointer 0 CoreFoundation _kCFAllocatorNull
__DATA __nl_symbol_ptr 0x00004008 pointer 0 libSystem ___stack_chk_guard
__DATA __nl_symbol_ptr 0x0000400C pointer 0 libSystem _environ
__DATA __nl_symbol_ptr 0x00004000 pointer 0 libSystem dyld_stub_binder
bind information:
segment section address type addend dylib symbol
__DATA __cfstring 0x1000031D8 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __cfstring 0x1000031F8 pointer 0 CoreFoundation ___CFConstantStringClassReference
__DATA __got 0x100003010 pointer 0 CoreFoundation _kCFAllocatorNull
__DATA __got 0x100003000 pointer 0 libSystem ___stack_chk_guard
__DATA __got 0x100003008 pointer 0 libSystem _environ
__DATA __nl_symbol_ptr 0x100003018 pointer 0 libSystem dyld_stub_binder
所有的延迟绑定符号都存储在_TEXT段的stubs节区(桩节区),编译器在生成代码时创建的符号调用就生成在此节区中,该节区被称为“桩”节区,桩只是一小段临时使用的指令,在stubs中只是一条jmp跳转指令,跳转的地址位于__DATA段__la_symbol_ptr节区中,指向的是一段代码,类似于如下的语句:
push xxx
jmp yyy
其中xxx是符号在动态链接信息中延迟绑定符号数据的偏移值,yyy则是跳转到_TEXT段的stub_helper节区头部,此处的代码通常为:
lea r11, qword [ds:zzz]
push r11
jmp qword [ds:imp___nl_symbol_ptr_dyld_stub_binder]
jmp跳转的地址是__DATA段中__nl_symbol_ptr节区,指向的是符号dyld_stub_binder(),该函数由dyld导出,实现位于dyld源码的“dyld_stub_binder.s”文件中,它调用dyld::fastBindLazySymbol()来绑定延迟加载的符号,后者是一个虚函数,实际调用ImageLoaderMachOCompressed的doBindFastLazySymbol(),后者调用bindAt()解析并返回正确的符号地址,dyld_stub_binder()在最后跳转到符号地址去执行。这一步完成后,__DATA段__la_symbol_ptr节区中存储的符号地址就是修正后的地址,下一次调用该符号时,就直接跳转到真正的符号地址去执行,而不用dyld_stub_binder()来重新解析该符号了。
3 load函数
3.1 为什么要分析Load函数
通过一个简单的图,可以看到Load函数的调用者都是哪些函数。
除了前面提到的loadInsertedDylib函数之外接触的比较多的就是dlopen了,所以,可以看出load是dyld动态加载一个Mach-O文件的重要接口。在动态加载一个Mach-o文件的时候,最终都调用了load这个API。
3.2 Load函数分析
函数的实现为一系列的loadPhase*函数,主要可以分为这几个部分:
1、处理环境变量,生成各种搜索路径。
2、如果该lib已经加载过,则利用share_cache中已经存在的imageloader实例。
3、如果该lib没有加载过,通过读取文件,将mach-o文件映射到内存中,生成imageloader的实例。
3.2.1 load
通过一幅图可以简单的理解load函数的流程。load函数主要做的这几件事情:
1、处理suffix字段。
2、通过loadPhase0函数从share_cache中加载image。
3、如果share_cache中不存在image,则再使用不同的参数调用loadPhase0函数,通过open函数读取文件并加载image到内存中。
4、函数调用结束后的内存管理。
//根据所有的环境变量生成路径,去加载一个ImageLoader
ImageLoader*load(const char* path, const LoadContext& context)
{
CRSetCrashLogMessage2(path);
const char* orgPath = path;
//dyld::log("%s(%s)\n",
__func__ , path);
charrealPath[PATH_MAX];
// when DYLD_IMAGE_SUFFIX is in used,
do a realpath(), otherwise a load of "Foo.framework/Foo" will not
match
//当设置了DYLD_IMAGE_SUFFIX字段,需要使用realpath来加载
if( context.useSearchPaths &&( gLinkContext.imageSuffix != NULL) ) {
if( realpath(path, realPath) != NULL )
path =realPath;
}
// try all path permutations and
check against existing loaded images
//尝试各种路径组合去加载image
ImageLoader* image =loadPhase0(path, orgPath, context, NULL);
if( image != NULL ) {
CRSetCrashLogMessage2(NULL);
returnimage;
}
// try all path permutations and try
open() until first success
std::vector exceptions;
image = loadPhase0(path, orgPath,context, &exceptions);
/*...*/
CRSetCrashLogMessage2(NULL);
if( image != NULL ) {
/*加载成功内存处理*/
returnimage;
}
else if ( exceptions.size() == 0) {
/*出错处理*/
}
else{
/*出错处理*/
}
}
3.2.2 loadPhase0函数
函数逻辑比较简单:
1、遍历DYLD_ROOT_PATH环境变量,生成加载路径,调用loadPhase1。
2、如果不存在DYLD_ROOT_PATH环境变量,则使用原始的路径_
// try root substitutions
//主要处理DYLD_ROOT_PATH环境变量的功能,修饰Loadimage时候的path
//运行完结之后执行loadPhase1
static ImageLoader* loadPhase0(const char* path, const char* orgPath, const LoadContext& context, std::vector* exceptions)
{
//dyld::log("%s(%s, %p)\n", __func__ , path, exceptions);
// handle DYLD_ROOT_PATH which forces absolute paths to use a new root
if ( (gLinkContext.rootPaths != NULL) && (path[0] == '/') ) {
for(const char* const* rootPath = gLinkContext.rootPaths ;*rootPath != NULL; ++rootPath) {
char newPath[strlen(*rootPath) + strlen(path)+2];
strcpy(newPath, *rootPath);
strcat(newPath, path);
ImageLoader*image = loadPhase1(newloadPhase1Path, orgPath, context, exceptions);
if( image != NULL )
returnimage;
}
}
// try raw path
return loadPhase1(path, orgPath, context, exceptions);
}
3.2.3 loadPhase1
主要处理内容:
1、通过LD_LIBRARY_PATH参数组成的所有路径,通过loadPhase2尝试加载image。
2、当无法通过LD_LIBRARY_PATH获取image时,则通过DYLD_FRAMEWORK_PATH与DYLD_LIBRARY_PATH组成的路径,通过loadPhase2尝试加载image。
3、如果上面两个流程都无法加载到image则通过原始路径通过loadPhase3尝试加载image。
4、如果依然无法加载到image则通过DYLD_FALLBACK_FRAMEWORK_PATH环境变量,组成路径最后尝试加载image。
这里要注意一下,因为不同的分支使用的Phase函数有可能是不同的。同_时该函数也确定了环境变量在动态加载时的优先级。
static ImageLoader* loadPhase1(const char* path, const char* orgPath, const LoadContext& context, std::vector* exceptions)
{
//dyld::log("%s(%s, %p)\n", __func__ , path, exceptions);
ImageLoader* image = NULL;
// handle LD_LIBRARY_PATH environment variables that force searching
//如果存在LD_LIBRARY_PATH变量,优先通过LD_LIBRARY_PATH中设置的路径进行搜索和加载
if( context.useLdLibraryPath&& (sEnv.LD_LIBRARY_PATH != NULL) ) {
image = loadPhase2(path, orgPath, context, NULL, sEnv.LD_LIBRARY_PATH, exceptions);
if( image != NULL )
return image;
}
// handle DYLD_ environment variables that force searching
//如果使用了DYLD_FRAMEWORK_PATH或者sEnv.DYLD_LIBRARY_PATH,则使用这两个环境变量去加载
if(context.useSearchPaths &&((sEnv.DYLD_FRAMEWORK_PATH != NULL) || (sEnv.DYLD_LIBRARY_PATH != NULL)) ) {
image = loadPhase2(path, orgPath, context, sEnv.DYLD_FRAMEWORK_PATH, sEnv.DYLD_LIBRARY_PATH, exceptions);
if( image != NULL )
return image;
}
// try raw path
//如果上面的环境变量都没有设置,就使用原始地址去加载
//函数是loadphase3
image = loadPhase3(path, orgPath, context, exceptions);
if( image != NULL )
return image;
// try fallback paths during second time (will open file)
const char* const* fallbackLibraryPaths =sEnv.DYLD_FALLBACK_LIBRARY_PATH;
if( (fallbackLibraryPaths != NULL)&& !context.useFallbackPaths )
fallbackLibraryPaths =NULL;
if( !context.dontLoad && (exceptions != NULL) &&((sEnv.DYLD_FALLBACK_FRAMEWORK_PATH != NULL) || (fallbackLibraryPaths != NULL))) {
image= loadPhase2(path, orgPath, context, sEnv.DYLD_FALLBACK_FRAMEWORK_PATH, fallbackLibraryPaths, exceptions);
if( image != NULL )
return image;
}
return NULL;
}
3.2.4 loadPhase2, loadPhase3, loadPhase4
这两个函数纯粹的实现了路径修改的逻辑,通过不同的方式去生成最终的加载路径,逻辑与loadPhase0基本类似,有兴趣可以自行查看代码。
3.2.5 loadPhase5
loadPhase5根据参数exceptions的不同形成了两个不同的分支。
1、loadPhase5load:通过读取文件,加载文件到内存中,实例化ImageLoader。
2、loadPhase5check:通过遍历已经加载的ImageLoader的容器,获取已经加载的ImageLoader。
// open or check existing
//检测是否有覆盖的,修正path,最后调用loadPhase5Load或者check
static ImageLoader* loadPhase5(const char* path, const char* orgPath, const LoadContext& context, std::vector* exceptions)
{
//dyld::log("%s(%s, %p)\n", __func__ , path, exceptions);
// check for specific dylib overrides
for (std::vector::iterator it = sDylibOverrides.begin(); it != sDylibOverrides.end(); ++it) {
if ( strcmp(it->installName, path) == 0) {
path= it->override;
break;
}
}
if( exceptions != NULL )
return loadPhase5load(path, orgPath, context, exceptions);
else
return loadPhase5check(path, orgPath, context);
}
loadPhase5check的逻辑非常简单,就是遍历容器,取出相同名字的imageLoader对象。有兴趣的可以自己查看loadPhase5check。
3.2.6 loadPhase5load
这里是真正的加载逻辑:
1、防止Image改名,在Image的容器里面遍历,检查是否已经加载
2、在SharedCache寻找是否存在Image的缓存,如果存在的使用ImageLoaderMachO::instantiateFromCache来实例化ImageLoader。
3、如果上面两个都没有找到的话,就通过loadPhase5open打开文件,并读取到内存。
static ImageLoader* loadPhase5load(const char* path, const char* orgPath, const LoadContext& context, std::vector* exceptions)
{
//dyld::log("%s(%s, %p)\n", __func__ , path, exceptions);
ImageLoader* image = NULL;
// just return NULL if file not found, but record any other errors
structstat stat_buf;
if ( my_stat(path, &stat_buf) == -1) {
int err = errno;
if( err != ENOENT ) {
exceptions->push_back(dyld::mkstringf("%s:stat() failed with errno=%d", path, err));
}
return NULL;
}
// in case image was renamed or found via symlinks, check for inlode match
image = findLoadedImage(stat_buf);
if( image != NULL )
return image;
// do nothing if not already loaded and if RTLD_NOLOAD or NSADDIMAGE_OPTION_RETURN_ONLY_IF_LOADED
//RTLD_NOLOAD或者NSADDIMAGE_OPTION_RETURN_ONLY_IF_LOADED字段设置了则不进行加载
if( context.dontLoad )
return NULL;
#if DYLD_SHARED_CACHE_SUPPORT
// see if this image is in shared cache
const macho_header* mhInCache;
const char* pathInCache;
long slideInCache;
//如果在sharedCacheImage中找到了,则通过cache来加载
if( findInSharedCacheImage(path, false, &stat_buf, &mhInCache, &pathInCache, &slideInCache) ) {
image =ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
return checkandAddImage(image, context);
}
#endif
// file exists and is not in dyld shared cache, so open it
// shared_cache中不存在image,则通过LoadPhase5open来加载image
return loadPhase5open(path, context, stat_buf, exceptions);
}
3.2.7 loadPhase5open与loadPhase6
loadPhase5open只是一个简单的封装。
//根据路径打开文件
//调用loadPhase6
static ImageLoader* loadPhase5open(const char* path, const LoadContext& context, const struct stat& stat_buf, std::vector* exceptions)
{
//dyld::log("%s(%s, %p)\n", __func__ , path, exceptions);
// open file (automagically closed when this function exits)
FileOpener file(path);
// just return NULL if file not found, but record any other errors
if ( file.getFileDescriptor() == -1) {
int err = errno;
if( err != ENOENT ) {
const char* newMsg = dyld::mkstringf("%s:open() failed with errno=%d", path, err);
exceptions->push_back(newMsg);
}
return NULL;
}
try{
return loadPhase6(file.getFileDescriptor(), stat_buf, path, context);
}
catch (const char* msg) {
const char* newMsg = dyld::mkstringf("%s: %s",path, msg);
exceptions->push_back(newMsg);
free((void*)msg);
return NULL;
}
}
做了错误提示之后,调用了loadPhase6。
static ImageLoader* loadPhase6(int fd, const structstat& stat_buf, const char* path, const LoadContext& context)
{
//dyld::log("%s(%s)\n", __func__ , path);
uint64_t fileOffset = 0;
uint64_t fileLength = stat_buf.st_size;
// validate it is a file (not directory)
if((stat_buf.st_mode & S_IFMT) !=S_IFREG )
throw"not a file";
uint8_t firstPage[4096];
bool shortPage = false;
// min mach-o file is 4K
if ( fileLength < 4096) {
if ( pread(fd, firstPage, fileLength, 0) != (ssize_t)fileLength )
throwf("preadof short file failed: %d", errno);
shortPage = true;
}
else{
if ( pread(fd, firstPage, 4096,0) != 4096)
throwf("preadof first 4K failed: %d", errno);
}
// if fat wrapper, find usable sub-file
//如果是一个fat格式的文件,找到对应的子文件
//从fat文件中找到对应子文件的代码。
const fat_header* fileStartAsFat = (fat_header*)firstPage;
if( fileStartAsFat->magic == OSSwapBigToHostInt32(FAT_MAGIC)) {
if( fatFindBest(fileStartAsFat,&fileOffset, &fileLength) ) {
if ( (fileOffset+fileLength) > (uint64_t)(stat_buf.st_size) )
throwf("truncated fat file. file length=%llu, but needed slice goes to %llu", stat_buf.st_size, fileOffset+fileLength);
if (pread(fd, firstPage, 4096, fileOffset) != 4096)
throwf("preadof fat file failed: %d", errno);
}
else{
throw"no matching architecture in universal wrapper";
}
}
// try mach-o loader
if( shortPage )
throw"file too short";
//检测运行平台是否正确
if( isCompatibleMachO(firstPage, path)) {
// only MH_BUNDLE, MH_DYLIB, and some MH_EXECUTE can be dynamically loaded
//只有MH_EXECUTE,MH_DYLIB,MH_BUNDLE三种文件才可被动态加载
switch( ((mach_header*)firstPage)->filetype) {
case MH_EXECUTE:
case MH_DYLIB:
case MH_BUNDLE:
break;
default:
throw"mach-o, but wrong filetype";
}
#if TARGET_IPHONE_SIMULATOR
#if TARGET_OS_WATCH || TARGET_OS_TV
// disable error during bring up of these simulators
#else
// dyld_sim should restrict loading os x binaries
if( !isSimulatorBinary(firstPage, path) ) {
throw"mach-o, but not built for iOS simulator";
}
#endif
#endif
// instantiate an image
ImageLoader* image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPage, fileOffset, fileLength, stat_buf, gLinkContext);
// validate
return checkandAddImage(image, context);
}
// try other file formats here...
// throw error about what was found
switch (*(uint32_t*)firstPage) {
case MH_MAGIC:
case MH_CIGAM:
case MH_MAGIC_64:
case MH_CIGAM_64:
throw"mach-o, but wrong architecture";
default:
throwf("unknown file type, first eight bytes: 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X0x%02X", firstPage[0], firstPage[1], firstPage[2], firstPage[3], firstPage[4], firstPage[5], firstPage[6],firstPage[7]);
}
}
1、做了Fat格式的检测,子类型文件提取。
2、检测Mach-O类型,只有MH_EXECUTE,MH_DYLIB,MH_BUNDLE三种文件才可被动态加载。
3、通过ImageLoaderMachO::instantiateFromFile生成ImageLoader的实例。
3.3 小结
Load的函数调用流程就是一系列的loadPhase*函数的调用,在load最后都会通过ImageLoader的构造函数,实例化ImageLoader,接下来 就需要分析ImageLoader的几个不同的构造函数。
4 参考链接
iOS程序启动->dyld加载->runtime初始化 过程
http://www.cocoachina.com/ios/20170811/20228.html
iOS中的dyld缓存是什么?
https://blog.csdn.net/gaoyuqiang30/article/details/52536168
https://segmentfault.com/a/1190000007769327
dyld源码分析-动态加载main的流程和load函数执行的流程
https://blog.csdn.net/fishmai/article/details/51419824
dyld源码分析-动态加载main的流程和load函数执行的流程
https://blog.csdn.net/fishmai/article/details/51419824
【性能优化】今日头条iOS客户端启动速度优化