前言
前面我们对程序启动之后的许多底层知识进行了探索,但是程序是怎么加载启动的呢,作为一个程序员,应该有相应的了解,现在我们就来探索下程序加载的原理。
准备工作
- dyld源码。
- libdispatch源码。
- libsystem源码。
- objc4-818.2源码。
1: 应用程序加载原理
开始今天话题之前,先对相关知识进行一下简介,以便后续能更好的分析理解。
1.1: 编译流程
编译流程:
- 预编译(Precompile):把宏替换,删除注释,展开头文件,产生
.i
文件。 - 编译(Compliling):把之前的
.i
文件转换成汇编语言,产生.s
文件。 - 汇编(Asembly):把汇编语言文件转换为机器码文件,产生
.o
文件。 - 链接(Link):将所有的
.o
文件以及链接的库,生成一个MachO
类型的可执行文件。
1.2: 编译流程图
1.3: 库的简介
1.3.1: 动态库和静态库的区别
- 静态库在程序编译时会被链接到目标代码中一起打包生成可执行文件,以
.a
和.framework
为文件后缀名。 - 而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,以
.tbd
(之前叫.dylib
) 和.framework
为文件后缀名(系统直接提供给我们的framework
都是动态库!)。
备注:.a
是一个纯二进制文件,.framework
中除了有二进制文件之外还有资源文件。.a
,要有.h
文件以及资源文件配合,.framework
文件可以直接使用。总的来说,.a
+ .h
+ sourceFile
= .framework
。所以创建静态库最好还是用.framework
的形式。
1.3.2: 静态库优缺点
静态库优缺点 优点
- 静态库被打包到可执行文件中,编译成功后可执行文件可以独立运行,不需要依赖外部环境。
缺点
- 编译生成的可执行文件会变大,如果静态库更新必须重新编译。
- 多个
APP
使用同一个静态库,每个APP
都会拷贝一份,浪费内存。
1.3.3: 动态库优缺点
动态库优缺点 优点
- 减小编译生成的可执行文件(也可简单理解为
APP
)的体积。 - 共享内容,节省资源。
- 通过更新动态库,达到更新程序的目的。
缺点
- 可执行文件不可以单独运行,必须依赖外部环境。
补充:
在其它大部分平台上,动态库都可以用于不同应用间共享,这就大大节省了内存。
iOS
平台 在iOS8
之前,苹果不允许第三方框架使用动态方式加载,从iOS8
开始允许开发者有条件地创建和使用动态框架,这种框架叫做Cocoa Touch Framework
。虽然同样是动态框架,但是和系统framework
不同,app
中使用Cocoa Touch Framework
制作的动态库在打包和提交APP
时会被放到app main bundle
的根目录中,运行在沙盒里,而不是系统中。也就是说,不同的app
就算使用了同样的framework
,但还是会有多份的框架被分别签名,打包和加载。不过iOS8
上开放了App Extension
功能,可以为一个应用创建插件,这样主app
和插件之间共享动态库还是可行的。苹果系统专属的
framework
是共享的(如UIKit
), 但是我们自己使用Cocoa Touch Framework
制作的动态库是放到app bundle
中,运行在沙盒里的
1.3.4: 动静态库图解
1.4: dyld
简介
那么在程序启动时,这些库是怎么加载到内存中的呢?
是通过dyld动态链接器加载到内存中的。整个过程大概如下:
dyld(the dynamic link editor)
是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld
负责余下的工作。
本文将详细分析dyld
加载流程。
2: dyld
初探
既然dyld
负责库的加载,那么加载完成之后肯定会进入程序的入口main
函数,那么我们在main
函数打上断点查看下调用栈。
可以看到在main
函数之前只有libdyld.dylib
库的start
调用,下个start
符号断点,发现并不会进入断点,说明在底层的符号根本不是start
。
+load
方法比main
函数先调用,在类文件里实现一个+load
方法并打上断点查看调用栈。
根据调用栈显示+load
方法的调用流程为:_dyld_start
->dyldbootstrap::start
->dyld::_main
->dyld::initializeMainExecutable
->ImageLoader::runInitializers
->ImageLoader::processInitializers
->ImageLoader::recursiveInitialization
->dyld::notifySingle
->load_images
->+[ViewController load]
。
打开dyld
源码,搜索_dyld_start
,最终在dyldStartup.s
文件找到了_dyld_start
的汇编实现(也可直接查看真机汇编)。
静态汇编:
真机汇编:
真机汇编和静态汇编都显示调用了dyldbootstrap::start
函数。
dyldbootstrap
为c++
的命名空间,start
为其中的函数。搜索后发现dyldbootstrap::start
在dyldInitialization.cpp
文件中,接下来结合源码分析dyldbootstrap::start
到+load
方法和main
函数的调用顺序,以及dyld
是如何加载images
的。
3: dyld
源码分析
3.1: dyldbootstrap::start
// 函数所在文件:dyldInitialization.cpp
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// 告诉debug server dyld要开始启动了
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
// 重定位dyld
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;
// 栈溢出保护
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
// 获取虚拟内存偏移
uintptr_t appsSlide = appsMachHeader->getSlide();
// 调用dyld::_main函数,再将其返回值传递给__dyld_start去调用真正的main()函数
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
dyldbootstrap::start
主要做了一下几件事:
- 告诉
debug server``dyld
要开始启动了。 - 重定位
dyld
。 - 栈溢出保护。
- 在
dyld
内运行所有C++
初始化器。 - 调用
dyld::_main
函数,再将其返回值传递给__dyld_start
去调用真正的主程序入口main
函数。
dyldbootstrap::start
只是做了一些配置和初始化的工作,核心逻辑在dyld::_main
函数中。
3.2: dyld::_main
dyld::_main
函数是整个APP
启动的关键函数,有800
多行代码,这里只梳理整理流程,细节部分感兴趣的可以自己探索。
// 函数所在文件:dyld2.cpp
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
//内核检测代码
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
}
……
/* ---第一步:设置运行环境 - begin--- */
//主程序可执行文件 cdHash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
//根据macho头文件配置CPU架构的信息,就是一些文件配置
getHostInfo(mainExecutableMH, mainExecutableSlide);
……
CRSetCrashLogMessage("dyld: launch started");
//配置环境 将信息放入 gLinkContext 中( notifySingle函数 赋值在其中)
setContext(mainExecutableMH, argc, argv, envp, apple);
……
//根据环境变量 envp 配置进程是否受限制,AMFI相关(Apple Mobile File Integrity苹果移动文件保护)
configureProcessRestrictions(mainExecutableMH, envp);
……
#if TARGET_OS_OSX
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
//又设置一次上下文,在文件受限的时候可能更改了envp。
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
//检测环境变量并设置默认值,这个时候还没有加载数据。
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}
……
//打印环境变量,可以在"Scheme -> Arguments -> Environment Variables"中配置
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
……
/* ---第一步:设置运行环境 - end--- */
/* ---第二步:加载共享缓存 - begin--- */
//检查共享缓存是否可用,到了这里只读了主程序还没有加载主程序。iOS必须有共享缓存。
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
//加载共享缓存方法
mapSharedCache(mainExecutableSlide);
#endif
/* ---第二步:加载共享缓存 - end--- */
……
……
/* ---第三步:dyld3或dyld2加载 - begin--- */
#if !TARGET_OS_SIMULATOR
//dyld3 ClosureMode模式,iOS11引入ClosureMode,iOS13后动态库和三方库都使用ClosureMode加载。
if ( sClosureMode == ClosureMode::Off ) {
// dyld2
if ( gLinkContext.verboseWarnings )
dyld::log("dyld: not using closures\n");
} else {
// dyld3
// 启动模式 闭包模式 DYLD_LAUNCH_MODE_USING_CLOSURE
sLaunchModeUsed = DYLD_LAUNCH_MODE_USING_CLOSURE;
const dyld3::closure::LaunchClosure* mainClosure = nullptr;
//主程序 info 和 Header
dyld3::closure::LoadedFileInfo mainFileInfo;
mainFileInfo.fileContent = mainExecutableMH;
mainFileInfo.path = sExecPath;
……
//第一次从共享缓存找闭包
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
//先从共享缓存找实例闭包
mainClosure = sSharedCacheLoadInfo.loadAddress->findClosure(sExecPath);
if ( gLinkContext.verboseWarnings && (mainClosure != nullptr) )
dyld::log("dyld: found closure %p (size=%lu) in dyld shared cache\n", mainClosure, mainClosure->size());
if ( mainClosure != nullptr )
//如果拿到设置状态
sLaunchModeUsed |= DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
}
……
//拿到闭包 && 验证闭包,如果闭包失效
if ( (mainClosure != nullptr) && !closureValid(mainClosure, mainFileInfo, mainExecutableCDHash, true, envp) ) {
mainClosure = nullptr;
//闭包失效设置状态
sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
}
……
//判断mainClosure是否为空
if ( (mainClosure == nullptr) && allowClosureRebuilds ) {
// if forcing closures, and no closure in cache, or it is invalid, check for cached closure
if ( !sForceInvalidSharedCacheClosureFormat )
//缓存中找
mainClosure = findCachedLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure == nullptr ) {
// if no cached closure found, build new one
//缓存中找不到则创建一个,一直拿 mainClosure 是为了拿他创建主程序。
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure != nullptr )
//创建失败则设置状态
sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
}
}
……
// try using launch closure
if ( mainClosure != nullptr ) {
CRSetCrashLogMessage("dyld3: launch started");
if ( mainClosure->topImage()->fixupsNotEncoded() )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
Diagnostics diag;
bool closureOutOfDate;
bool recoverable;
//启动主程序,mainClosure 相当于加载器
bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
//启动失败或者过期 允许重建
if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
// closure is out of date, build new one
//再创建一个
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure != nullptr ) {
diag.clearError();
sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
if ( mainClosure->topImage()->fixupsNotEncoded() )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
else
sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
//启动
launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
}
}
if ( launched ) {
//启动成功保存状态,主程序加载成功
gLinkContext.startedInitializingMainExecutable = true;
if (sSkipMain)
//主程序main函数,dyld的main执行完毕返回主程序的main
result = (uintptr_t)&fake_main;
return result;
}
else {
//失败报错
if ( gLinkContext.verboseWarnings ) {
dyld::log("dyld: unable to use closure %p\n", mainClosure);
}
if ( !recoverable )
halt(diag.errorMessage());
}
}
}
#endif // TARGET_OS_SIMULATOR
// could not use closure info, launch old way
//dyld2模式
sLaunchModeUsed = 0;
// install gdb notifier
//两个回调地址放入stateToHandlers数组中
stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
// make initial allocations large enough that it is unlikely to need to be re-alloced
//分配初始化空间,尽可能大一些保证后面够用。
sImageRoots.reserve(16);
sAddImageCallbacks.reserve(4);
sRemoveImageCallbacks.reserve(4);
sAddLoadImageCallbacks.reserve(4);
sImageFilesNeedingTermination.reserve(16);
sImageFilesNeedingDOFUnregistration.reserve(8);
……
try {
// add dyld itself to UUID list
//dyld加入uuid列表
addDyldImageToUUIDList();
#if SUPPORT_ACCELERATE_TABLES
……
//主程序还没有rebase
bool mainExcutableAlreadyRebased = false;
if ( (sSharedCacheLoadInfo.loadAddress != nullptr) && !dylibsCanOverrideCache() && !sDisableAcceleratorTables && (sSharedCacheLoadInfo.loadAddress->header.accelerateInfoAddr != 0) ) {
struct stat statBuf;
if ( dyld3::stat(IPHONE_DYLD_SHARED_CACHE_DIR "no-dyld2-accelerator-tables", &statBuf) != 0 )
sAllCacheImagesProxy = ImageLoaderMegaDylib::makeImageLoaderMegaDylib(&sSharedCacheLoadInfo.loadAddress->header, sSharedCacheLoadInfo.slide, mainExecutableMH, gLinkContext);
}
//加载所有的可执行文件 image list,这里相当于是个标签。会循环。
reloadAllImages:
#endif
/* ---第三步:dyld3或dyld2加载 - end--- */
……
/* ---第四步:实例化主程序 - begin--- */
//实例化主程序,加入到allImages(第一个靠dyld加载的image就是主程序)
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
//代码签名
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
/* ---第四步:实例化主程序 - end--- */
……
#if defined(__x86_64__) && !TARGET_OS_SIMULATOR
//设置加载动态库版本
if (dyld::isTranslated()) {……}
#endif
// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
//检查版本路径
checkVersionedPaths();
#endif
……
/* ---第五步:加载插入动态库 - begin--- */
//DYLD_INSERT_LIBRARIES 插入动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
//遍历加载插入动态库
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// -1为了排除主程序
sInsertedDylibCount = sAllImages.size()-1;
/* ---第五步:加载插入动态库 - end--- */
// link main executable
/* ---第六步:链接主程序 - begin--- */
gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
//链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
/* ---第六步:链接主程序 - end--- */
……
/* ---第七步:链接动态库 - begin--- */
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
//i+1因为主程序,插入的image在主程序后面
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);
}
}
}
/* ---第七步:链接动态库 - end--- */
……
sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
……
// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
//递归绑定
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
//绑定插入动态库
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}
// <rdar://problem/12186933> do weak binding only after all inserted images linked
/* ---第八步:弱绑定主程序 - begin--- */
//弱引用绑定主程序,所有镜像文件绑定完成后进行。
sMainExecutable->weakBind(gLinkContext);
gLinkContext.linkingMainExecutable = false;
/* ---第八步:弱绑定主程序 - end--- */
sMainExecutable->recursiveMakeDataReadOnly(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
/* ---第九步:执行初始化 - begin--- */
// run all initializers
// 运行所有的初始化
initializeMainExecutable();
/* ---第九步:执行初始化 - end--- */
#endif
……
/* ---第十步:返回main函数 - begin--- */
{
// find entry point for main executable
//找到主程序入口 LC_MAIN
result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
if ( result != 0 ) {
// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
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->getEntryFromLC_UNIXTHREAD();
*startGlue = 0;
}
}
}
……
if (sSkipMain) {
notifyMonitoringDyldMain();
if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
}
ARIADNEDBG_CODE(220, 1);
result = (uintptr_t)&fake_main;
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
}
//返回主程序
return result;
/* ---第十步:返回main函数 - end--- */
}
上面代码中比较重要的地方已经加了一些注释,方法读者查看。整个加载过程可细分为九步:
- 第一步:设置运行环境。
- 第二步:加载共享缓存。
- 第三部:
dyld2/dyld3
(ClosureMode
闭包模式)加载程序。 - 第四步:实例化主程序。
- 第五步:加载插入动态库。
- 第六步:链接主程序和动态库。
- 第七步:弱绑定主程序。
- 第八步:执行初始化。
- 第九步:返回
main
函数。
备注:本文的
image
指动态库的镜像。
3.2.1: 设置运行环境
这一步主要是设置运行参数、环境变量等。代码在开始的时候调用getHostInfo
函数获取当前程序架构,将入参mainExecutableMH
赋值给了sMainExecutableMachHeader
,这是一个macho_header
结构体,表示的是当前主程序的Mach-O
头部信息,加载器依据Mach-O
头部信息就可以解析整个Mach-O
文件信息。接着调用setContext()
设置上下文信息,包括一些回调函数、参数、标志信息等。设置的回调函数都是dyld
模块自身实现的,如loadLibrary()
函数实际调用的是libraryLocator()
,负责加载动态库。代码片断如下:
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.undefinedHandler = &undefinedHandler;
gLinkContext.getAllMappedRegions = &getMappedRegions;
gLinkContext.bindingHandler = NULL;
gLinkContext.notifySingle = ¬ifySingle;
...
}
在这个过程中有一些DYLD_
开头的环境变量,比如:
// 如果设置了DYLD_PRINT_OPTS则调用printOptions()打印参数
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
// 如果设置了DYLD_PRINT_ENV则调用printEnvironmentVariables()打印环境变量
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
其实,只要在Xcode
中配置一下即可让这些环境变量生效,设置路径为Scheme->Arguments->Environment Variables
,按下图所示添加环境变量并设置Value
为1
。
运行Xcode即可看到控制台打印的详细信息:
**opt[0] = "/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug/CategoryDemo"**
**__XCODE_BUILT_PRODUCTS_DIR_PATHS=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**MallocNanoZone=0**
**CA_DEBUG_TRANSACTIONS=0**
**COMMAND_MODE=unix2003**
**LOGNAME=weichunfang**
**USER=weichunfang**
**CA_ASSERT_MAIN_THREAD_TRANSACTIONS=0**
**HOME=/Users/weichunfang**
**PWD=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**DYLD_LIBRARY_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug:/usr/lib/system/introspection**
**__CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34**
**LD_LIBRARY_PATH=/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/**
**__XPC_DYLD_LIBRARY_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**SQLITE_ENABLE_THREAD_ASSERTIONS=1**
**DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/usr/lib/libBacktraceRecording.dylib:/Applications/Xcode.app/Contents/Developer/usr/lib/libMainThreadChecker.dylib:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Debugger/libViewDebuggerSupport.dylib**
**DYLD_PRINT_OPTS=1**
**DYLD_PRINT_ENV=1**
**METAL_DEVICE_WRAPPER_TYPE=1**
**METAL_DEBUG_ERROR_MODE=0**
**DYLD_FRAMEWORK_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**SECURITYSESSIONID=186a6**
**OS_ACTIVITY_DT_MODE=YES**
**SWIFTUI_VIEW_DEBUG=287**
**PATH=/Applications/Xcode.app/Contents/Developer/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin**
**SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.mls7Pr51Bb/Listeners**
**__XPC_DYLD_FRAMEWORK_PATH=/Users/weichunfang/Library/Developer/Xcode/DerivedData/CategoryDemo-evmsrqpceqbdzbdlwhxzunodpqfz/Build/Products/Debug**
**TMPDIR=/var/folders/f5/nnm3g2sd3pl8c6cz5bzpcxw80000gn/T/**
**XPC_FLAGS=0x0**
**SHELL=/bin/zsh**
**GPUProfilerEnabled=YES**
**XPC_SERVICE_NAME=com.apple.xpc.launchd.oneshot.0x10000001.Xcode**
**LaunchInstanceID=61EE7919-BC5B-4B07-8BAB-4505766DEAB9**
**NSUnbufferedIO=YES**
后面还有很多这样的DYLD_
开头的环境变量,感兴趣的读者可以自行测试。
ASLR:
image list
第0
号位主程序地址。
3.2.2: 加载共享缓存
iOS
必须要有共享缓存,共享缓存中存储的都是系统级别的动态库,比如UIKit
,CoreFoundation
等,非系统级别的动态库不会放到共享缓存中,只运行在程序的沙盒中。
checkSharedRegionDisable
函数里很明确的提示了iOS
必须需要共享缓存。
接下来调用mapSharedCache
函数加载共享缓存,而mapSharedCache
函数里实则调用了loadDyldCache
函数。
static void mapSharedCache(uintptr_t mainExecutableSlide)
{
...
// 调用loadDyldCache
loadDyldCache(opts, &sSharedCacheLoadInfo);
...
}
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;
#if TARGET_OS_SIMULATOR
// simulator only supports mmap()ing cache privately into process
return mapCachePrivate(options, results);
#else
// forcePrivate == YES,就是强制私有
if ( options.forcePrivate ) {
// 此时就不会加载共享缓存,而是把你需要的系统库仅缓存到此进程中
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
// 共享缓存已加载,不做任何处理
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
} else {
// slow path: this is first process to load cache
// 当前进程首次加载共享缓存
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}
loadDyldCache
的三种情况:
- 当强制私有时(
forcePrivate == YES
),仅加载到当前进程,不会加载到共享缓存中,调用mapCachePrivate
。 - 共享缓存已加载,不进行任何处理。
- 当前进程首次加载共享缓存,调用
mapCacheSystemWide
。
mapCachePrivate
、mapCacheSystemWide
里面就是具体的共享缓存解析逻辑,感兴趣的读者可以详细分析。
3.3.3: dyld3
或dyld2
(ClosureMode
闭包模式)加载程序
iOS11
引入dyld3
闭包模式,以回调的方式加载,闭包模式加载速度更快,效率更高。iOS13
后动态库和三方库都使ClosureMode
加载。
-
dyld3
:- 使用
mainClosure
来加载。 - 找到/创建
mainClosure
后,通过launchWithClosure
启动主程序,启动失败后会有重新创建mainClosure
重新启动的逻辑。成功后返回result
(主程序入口main
函数)。launchWithClosure
中的逻辑和dyld2
启动主程序逻辑基本相同。
- 使用
-
dyld2
:启动主程序- 实例化主程序
instantiateFromLoadedImage
。sMainExecutable
是通过instantiateFromLoadedImage
赋值的,也就是把主程序加入allImages
中。 - 插入&加载动态库
loadInsertedDylib
。加载在loadInsertedDylib
中调用load
(主程序和动态库都会添加到allImages
中loadAllImages
) - 链接主程序和链接插入动态库(
link
,主程序链接在前)。在这个过程中记录了dyld
加载的时长。可以通过配置环境变量打印出来。 - 绑定符号(非懒加载、弱符号),懒加载在调用时绑定。
- 初始化主程序
initializeMainExecutable
,这个时候还没有执行到主程序中的代码。 - 找到主程序入口
LC_MAIN
(main
函数),然后返回主程序。
- 实例化主程序
3.3.4: 实例化主程序
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
//实例化image(镜像)
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
//将image添加到all Images中
addImage(image);
return (ImageLoaderMachO*)image;
// throw "main executable not a known format";
}
- 传入主程序的
Header
、ASLR
、path
实例化主程序生成image
(镜像)。 - 将
image
加入all images
中(主程序image
是第一个添加到数组中的)。
实例化主程序调用的是ImageLoaderMachO::instantiateMainExecutable
函数。
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
//获取Load Commands
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
//根据 compressed 确定用哪个子类进行加载image,ImageLoader是个抽象类,根据值选择对应的子类实例化image。
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
。 -
根据
compressed
确定用哪个子类进行加载image
,ImageLoader
是个抽象类,根据值选择对应的子类实例化主程序。
sniffLoadCommands
函数。
// 篇幅原因,仅截取部分代码
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
//根据LC_DYLIB_INFO 和 LC_DYLD_INFO_ONLY 来获取的
*compressed = false;
//segment数量
*segCount = 0;
//lib数量
*libCount = 0;
//代码签名和加密
*codeSigCmd = NULL;
*encryptCmd = NULL;
……
// fSegmentsArrayCount is only 8-bits
// segCount 最多 256 个
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);
// fSegmentsArrayCount is only 8-bits
// libCount最多 4096 个
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);
// 确保依赖libsystem库
if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
// dylibs that use LC_DYLD_CHAINED_FIXUPS have that load command removed when put in the dyld cache
if ( !*compressed && (mh->flags & MH_DYLIB_IN_CACHE) )
*compressed = true;
}
compressed
是根据LC_DYLIB_INFO
和LC_DYLD_INFO_ONLY
来获取的。segCount
最多256
个。libCount
最多4096
个。- 确保依赖
libsystem
库。
大家可能对segment
和command
以及macho
头文件还是有点模糊,通过MachOView
工具认识下可执行文件:
图中很清晰明了macho
头文件就是架构信息以及文件类型等。macho
文件主要是3块内容Header
、Commods
、Data
。
3.3.5: 加载插入动态库
static void loadInsertedDylib(const char* path)
{
unsigned cacheIndex;
try {
……
//调用load,加载动态库的真正函数
load(path, context, cacheIndex);
}
……
}
- 根据上下文初始化配置调用
load
函数加载动态库。
3.3.6: 链接主程序和动态库
void link(ImageLoader* image, bool forceLazysBound, bool neverUnload, const ImageLoader::RPathChain& loaderRPaths, unsigned cacheIndex)
{
// add to list of known images. This did not happen at creation time for bundles
if ( image->isBundle() && !image->isLinked() )
addImage(image);
// we detect root images as those not linked in yet
if ( !image->isLinked() )
addRootImage(image);
// process images
try {
const char* path = image->getPath();
#if SUPPORT_ACCELERATE_TABLES
if ( image == sAllCacheImagesProxy )
path = sAllCacheImagesProxy->getIndexedPath(cacheIndex);
#endif
//最终会调用到image的link
image->link(gLinkContext, forceLazysBound, false, neverUnload, loaderRPaths, path);
}
}
link
中调用了ImageLoader::link
方法,ImageLoader
负责加载image
文件(主程序,动态库)每个image
对应一个ImageLoader
类的实例。
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
//起始时间。用于记录时间间隔
uint64_t t0 = mach_absolute_time();
//递归加载主程序依赖的库,完成之后发通知。
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);
……
uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
this->updateDepth(context.imageCount());
__block uint64_t t2, t3, t4, t5;
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
t2 = mach_absolute_time();
//递归重定位,修正ASLR
this->recursiveRebaseWithAccounting(context);
context.notifyBatch(dyld_image_state_rebased, false);
t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
//绑定NoLazy符号
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);
t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
//绑定弱符号
this->weakBind(context);
t5 = mach_absolute_time();
}
// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
//递归应用插入的动态库
this->recursiveApplyInterposing(context);
}
// now that all fixups are done, make __DATA_CONST segments read-only
if ( !context.linkingMainExecutable )
this->recursiveMakeDataReadOnly(context);
if ( !context.linkingMainExecutable )
context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();
if ( context.registerDOFs != NULL ) {
std::vector<DOFInfo> dofs;
this->recursiveGetDOFSections(context, dofs);
//注册
context.registerDOFs(dofs);
}
//计算结束时间.
uint64_t t7 = mach_absolute_time();
// clear error strings
//配置环境变量,就可以看到dyld应用加载的时长。
(*context.setErrorStrings)(0, NULL, NULL, NULL);
fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;
// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}
- 递归加载所有的动态库。
- 递归重定位,修正ASLR。
- 递归绑定非懒加载。
- 绑定弱符号。
- 注册。
- 记录时间,可以通过配置看到
dyld
应用加载时长
recursiveLoadLibraries
方法是递归加载动态库,探究下具体怎么递归加载的(仅截取大致流程)。
void ImageLoader::recursiveLoadLibraries(const LinkContext& context, bool
preflightOnly, const RPathChain& loaderRPaths, const char* loadPath){
...
// get list of libraries this image needs
//获取当前的image依赖的动态库
DependentLibraryInfo libraryInfos[fLibraryCount];
this->doGetDependentLibraries(libraryInfos);
// get list of rpaths that this image adds
//获取当前的image依赖的动态库的文件路径
std::vector<const char*> rpathsFromThisImage;
this->getRPaths(context, rpathsFromThisImage);
const RPathChain thisRPaths(&loaderRPaths, &rpathsFromThisImage);
// 加载image依赖的动态库
for(unsigned int i=0; i < fLibraryCount; ++i){
...
dependentLib = context.loadLibrary(requiredLibInfo.name, true, this->getPath(),
&thisRPaths, cacheIndex);
// 保存加载的动态库
setLibImage(i, dependentLib, depLibReExported, requiredLibInfo.upward);
...
`}`
//告诉image依赖的动态库去加载各自需要的动态库
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
dependentImage->recursiveLoadLibraries(context, preflightOnly,
thisRPaths, libraryInfos[i].name);
}
}
}
- 获取当前
image
(镜像文件)依赖的动态库和动态库的文件路径。 - 加载当前
image
依赖的动态库,并保持起来。 - 告诉当前
image
依赖的动态库去加载各自需要的动态库。
链接动态库和链接主程序的逻辑基本一样,注意下循环取image
文件的时候是从1
开始,因为第0
个位置是主程序,随便运行一个程序,lldb
输出image list
验证下。
lldb
输出image list
第一个就是主程序了。
3.3.7: 弱绑定主程序
大家可能注意到了,链接主程序的时候里面有弱符号绑定。但是在链接主程序的时候linkingMainExecutable
= true
,所以link
里面的弱绑定在主程序时是不调用的,等动态库的都进行了弱绑定,最后对主程序进行弱绑定。
3.3.8: 执行初始化
initializeMainExecutable
函数流程复杂,下文单独解析。
3.3.9: 返回main
函数
获取主程序的入口main
函数,再将其返回传递给__dyld_start
去调用。
4: 初始化流程解析
初始化流程内容复杂,本节单独探索。
4.1: 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 ) {
//从1开始到最后。(第0个为主程序)
for(size_t i=1; i < rootCount; ++i) {
//image初始化,调用 +load 和 构造函数
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]);
}
- 初始化
images
,下标从1
开始,然后再初始化主程序(下标0
)runInitializers
。 - 可以配置环境变量
DYLD_PRINT_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
打印相关信息。
4.2: ImageLoader::runInitializers
函数解析
从上面可以看出initializeMainExecutable
函数里主程序和image
初始化调用的都是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);
}
up.count
值设置为1
然后调用ImageLoader::processInitializers
函数。
4.3: 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;
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.
// 为了保证所有的向上依赖关系都初始化,再次把没有初始化的image去初始化
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
- 调用了
ImageLoader::recursiveInitialization
函数。
4.4: ImageLoader::recursiveInitialization
函数解析
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
……
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 ) {
……
else if ( dependentImage->fDepth >= fDepth ) {
//依赖文件递归初始化
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
……
fState = dyld_image_state_dependents_initialized;
oldState = fState;
//这里调用传递的状态是dyld_image_state_dependents_initialized,image传递的是自己。也就是最后调用了自己的+load。从libobjc.A.dylib开始调用。
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
//初始化镜像文件,调用c++构造函数。libSystem的libSystem_initializer就是在这里调用的。会调用到objc_init中。_dyld_objc_notify_register 中会调用自身的+load方法,然后c++构造函数。
//1.调用libSystem_initializer->objc_init 注册回调。
//2._dyld_objc_notify_register中调用 map_images,load_images,这里是首先初始化一些系统库,调用系统库的load_images。比如libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib。
//3.自身的c++构造函数
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
//这里调用不到+load方法。 notifySingle内部fState==dyld_image_state_dependents_initialized 才调用+load。
context.notifySingle(dyld_image_state_initialized, this, NULL);
……
}
……
}
recursiveSpinUnLock();
}
- 需要初始化的动态库
image
是从libImage()
中获取,而libImage()
的数据是在链接动态库的时recursiveLoadLibraries
中的setLibImage
保存的image
。 - 整个过程是一个递归的过程,先初始化最底层的依赖库,再逐步初始化到自己。
- 调用
notifySingle
最终调用到了objc
中所有的+load
方法。这里第一个notifySingle
调用的是+load
方法,第二个notifySingle
由于参数是dyld_image_state_initialized
不会调用到+load
方法。这里的dyld_image_state_dependents_initialized
意思是依赖文件初始化完毕了,可以初始化自己了。 - 调用
doInitialization
最终调用了c++
的系统构造函数。先调用的是libSystem_initializer -> objc_init
进行注册回调。在回调中调用了map_images
、load_images
(+load
)。这里的load_images
是调用一些加载一些系统库,比如:libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib
。
4.5: notifySingle
函数解析
notifySingle
是一个函数指针,在setContext
函数里赋值。
继续查看notifySingle
函数的实现。
notifySingle
中找不到load_images
的调用(第2
节dyld
初探中探索过,notifySingle
之后调用的就是load_images
,后续探索)。notifySingle
函数里当state == dyld_image_state_dependents_initialized
(依赖库初始化完成)之后执行了一个函数回调sNotifyObjCInit
。
全局搜索sNotifyObjCInit
函数,发现是在registerObjCNotifiers
函数里赋值的。
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
//第一个参数 map_images
sNotifyObjCMapped = mapped;
//第二个参数 load_images
sNotifyObjCInit = init;
//第三个参数 unmap_image
sNotifyObjCUnmapped = unmapped;
// call 'mapped' function with all images mapped so far
try {
//赋值后马上回调 map_images
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}
// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
// 在所有已初始化的映像调用“init”函数
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
//调用一些系统库的 load_images。
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}
- 记录要调用的函数,
sNotifyObjCInit
函数赋值来自于第二个参数。 - 赋值后就调用了
notifyBatchPartial
函数(内部调用了sNotifyObjCMapped
函数)。 - 循环调用
load_images
,这里调用的是依赖的系统库libdispatch.dylib,libsystem_blocks.dylib,libsystem_dnssd.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib
。
全局搜索registerObjCNotifiers
函数,发现是在_dyld_objc_notify_register
函数里调用的。
//_objc_init中调用的。
//单个镜像文件的加载来到了这里->_dyld_objc_notify_register,打符号断点查看被objc-os.mm中 _objc_init 调用。
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
函数,发现在dyld
源码中没有调用这个函数的地方。
为了继续探索,在项目工程中添加_dyld_objc_notify_register
符号断点,查看函数调用栈。
根据函数调用栈可知调用顺序为:dyld-ImageLoaderMachO::doInitialization
->dyld-ImageLoaderMachO::doModInitFunctions
->libSystem.B.dylib-libSystem_initializer
->libdispatch.dylib-libdispatch_init
->libdispatch.dylib-_os_object_init
->libobjc.A.dylib-_objc_init
->libdyld.dylib-_dyld_objc_notify_register
。
libSystem_initializer
函数在libSystem.B.dylib
系统库中。libdispatch_init
函数和_os_object_init
函数在libdispatch.dylib
系统库中。_objc_init
函数在libobjc.A.dylib
系统库中,也就是我们最熟悉的objc
源码。
查看objc
源码里_objc_init
函数的实现。
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();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
//_objc_init 调用dyldAPIs.cpp 中_dyld_objc_notify_register,第二个参数是load_images
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
_objc_init
函数调用了_dyld_objc_notify_register
函数。- 第一个参数是
map_images
,赋值给sNotifyObjCMapped
。 - 第二个参数是
load_images
,赋值给sNotifyObjCInit
。 - 第三个参数是
unmap_image
,赋值给sNotifyObjCUnmapped
。
这三个参数将在后面详细介绍是如何与dyld
进行交互的。
4.6: ImageLoaderMachO::doInitialization
函数解析
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
// 调用image里面的一些初始化方法
doImageInit(context);
// 调用c++构造函数(libSystem_initializer也属于c++构造函数)
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
doImageInit
函数调用Mach-O
里面的一些初始化方法。doModInitFunctions
函数调用libSystem_initializer
在内的c++
构造函数(__attribute__((constructor))
修饰的c
函数)。
- 会对
__mod_init_funcs section
里的c++
构造函数进行验证,然后调用。 - 在所有库加载前必须先加载
libSystem
系统库,调用libSystem_initializer
函数(libSystem_initializer
函数是__attribute__((constructor))
修饰的c++
构造函数)。
Mach-O
验证c++
构造函数分析
在项目中加上一些c++
构造函数然后查看Mach-O
文件。
__attribute__((constructor)) void funcA() {
printf("\n ---funcA--- \n");
}
__attribute__((constructor)) void funcB() {
printf("\n ---funcB--- \n");
}
5:反推objc
与dyld
的关联
从上面的符号断点函数调用栈看到了在_dyld_objc_notify_register
函数和doModInitFunctions
函数之间还有非dyld
库的函数调用,现在就来反推梳理下这个流程。
在最了解的objc
库里的_objc_init
函数里打上断点,查看函数调用栈。
对于doModInitFunctions
函数和_objc_init
函数之间的流程是未知的,最好的方式就是从_objc_init
函数开始反推调用流程。
5.1: _os_object_init
函数解析
从上面的函数调用栈可知_objc_init
函数是被libdispatch.dylib
中的_os_object_init
函数调用的。下载libdispatch
最新的libdispatch-1271.120.2
源码,搜索_os_object_init
函数。
void
_os_object_init(void)
{
//_objc_init调用
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}
- 发现
_objc_init
函数确实是_os_object_init
函数调用的。
5.2: libdispatch_init
函数解析
同时libdispatch_init
函数也调用了_os_object_init
函数。
- 线程相关处理。
- 设定主线程之后调用了
_os_object_init
等一系列函数。
5.3: libSystem_initializer
函数解析
libdispatch_init
函数是被libSystem.dylib
库中的libSystem_initializer
函数调用的。下载libSystem
最新的Libsystem-1292.120.1
源码,搜索libSystem_initializer
函数。
- 其中调用了
libdispatch_init
函数,同样还调用了__malloc_init
、_dyld_initializer
以及_libtrace_init
等函数。
5.4: ImageLoaderMachO::doModInitFunctions
函数解析
libSystem_initializer
函数是dyld
中的ImageLoaderMachO::doModInitFunctions
函数调用的,这样整个流程就连起来了。
在ImageLoaderMachO::doModInitFunctions
中发现了如下代码:
libSystem
库必须第一个被初始化。这也能被理解,因为要初始化dispatch
以及objc
。其它image
都依赖它。- 获取
c++
构造函数,然后调用。
ImageLoaderMachO::doModInitFunctions
函数里并没有libSystem_initializer
函数的明确调用,但是通过断点读取寄存器的值确实可以读取到。
前面已经分析过了doModInitFunctions
中是对c++
构造函数的调用。libSystem_initializer
正好是c++
构造函数:
这样整个调用流程就连起来了。只不过libSystem_initializer
这个c++
构造函数被先调用。
6: dyld
注册objc
回调简单分析
上面的分析在_objc_init
函数中调用了_dyld_objc_notify_register
函数进行回调注册,然后调用到dyld::registerObjCNotifiers
函数中有如下赋值:
//第一个参数 map_images
sNotifyObjCMapped = mapped;
//第二个参数 load_images
sNotifyObjCInit = init;
//第三个参数 unmap_image
sNotifyObjCUnmapped = unmapped;
下面就来详细分析这三个回调的逻辑。
6.1: sNotifyObjCMapped(map_images)
sNotifyObjCMapped
在dyld
中的调用只在notifyBatchPartial
函数中:
而notifyBatchPartial
函数的调用则在notifyBatch
函数、registerImageStateBatchChangeHandler
函数以及registerObjCNotifiers
函数中。根据前面分析的流程是在registerObjCNotifiers
注册回调之后就在里面调用了。
在objc
源码map_images
函数中添加断点:
通过函数调用栈也可以验证在注册回调之后马上就调用了map_images
函数。
map_images
函数中直接加锁调用了map_images_nolock
函数,其中进行了类的加载相关的操作,后续单独发文分析。
6.2: sNotifyObjCInit(load_images)
sNotifyObjCInit
在dyld
中的调用分为以下情况:
notifySingleFromCache
中。notifySingle
中。registerObjCNotifiers
。notifySingleFromCache
与notifySingle
逻辑基本相同,无非就是有没有缓存的区别。registerObjCNotifiers
是在注册回调函数的时候直接进行的回调。
在objc
源码load_images
函数中添加断点:
通过函数调用栈也可以验证系统的基础库在注册回调之后马上就调用了load_images
函数。
从objc
库开始走的是notifySingle
回调逻辑:
6.2.1: load_images
函数
sNotifyObjCInit
其实就是load_images
,它的实现如下:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
//加载所有分类
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
//准备所有load方法
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
//调用 + load 方法
call_load_methods();
}
- 加载所有分类。
- 准备所有
load
方法。 - 调用
call_load_methods
函数。
6.2.2: prepare_load_methods
函数
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t const *classlist = _getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 准备主类的load方法
schedule_class_load(remapClass(classlist[i]));
}
// 获取非懒加载的分类列表
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
// 迫使主类实现(如果没实现的话)
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
// 准备分类的load方法
add_category_to_loadable_list(cat);
}
}
- 准备主类的
load
方法。 - 准备分类的
load
方法。
6.2.3: schedule_class_load
函数
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
// 递归准备类的load方法,直到父类nil
schedule_class_load(cls->getSuperclass());
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
- 递归准备类的load方法,直到父类nil。
6.2.4: add_class_to_loadable_list
和add_category_to_loadable_list
函数
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
// 主类获取load方法
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
// 容量不足,扩容,首次为16字节
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
// loadable_classes里存的是struct loadable_class,里面有2个成员变量,cls和method
// 存一个,已用容量也就是下标++
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
// 分类获取load方法
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
// 容量不足,扩容,首次为16字节
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
// loadable_categories里存的是struct loadable_category,里面有2个成员变量,cat和method
// 存一个,已用容量也就是下标++
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
struct loadable_class {
Class cls; // may be nil
IMP method;
};
struct loadable_category {
Category cat; // may be nil
IMP method;
};
- 获取
load
方法。 - 容量不足,就扩容,首次为
16
字节,后续为现有容量 * 2 + 16
字节。 - 将对应的数据添加进
loadable_classes
与loadable_categories
中。
6.2.5: objc_class::getLoadMethod
和_category_getLoadMethod
函数
IMP
objc_class::getLoadMethod()
{
runtimeLock.assertLocked();
const method_list_t *mlist;
ASSERT(isRealized());
ASSERT(ISA()->isRealized());
ASSERT(!isMetaClass());
ASSERT(ISA()->isMetaClass());
// 获取类方法列表
mlist = ISA()->data()->ro()->baseMethods();
if (mlist) {
// 遍历所有方法,通过字符串比对找到load方法
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
// 匹配load
if (0 == strcmp(name, "load")) {
return meth.imp(false);
}
}
}
return nil;
}
IMP
_category_getLoadMethod(Category cat)
{
runtimeLock.assertLocked();
const method_list_t *mlist;
// 获取分类的类方法列表
mlist = cat->classMethods;
if (mlist) {
// 遍历分类类方法列表,通过字符串比对找到load方法
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
// 匹配load
if (0 == strcmp(name, "load")) {
return meth.imp(false);
}
}
}
return nil;
}
- 通过字符串比对获取
load
方法。
6.2.6: call_load_methods
函数
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
// 循环调用load方法,load方法在这一刻被调用
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
// 调用每个类的load方法
call_class_loads();
}
// 2. Call category +loads ONCE
// 调用分类load,这里也就说明分类的load在所有类的load方法调用后才调用(针对image而言)
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
- 调用
call_class_loads
加载类的+ load
方法。 - 接着调用
call_category_loads
加载分类的+ load
方法。这里也就说明分类的load
在所有类的load
方法调用后才调用(针对image
而言)。
到这里就调用了+ load
方法,这也就是+ load
方法在main
函数之前被调用的原因。
6.2.7: call_class_loads
函数
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
//清空值
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
//从classes中获取method
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
//调用load
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
- 从
loadable_classes
中循环取到load
方法进行调用。
6.2.7: call_category_loads
函数
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
//从cats中取出load
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
……
}
- 分类
load
的调用也是从loadable_categories
循环取load
方法进行调用。分类中内部处理逻辑更多一些。
所以在调用完+ load
方法以及c++
构造函数才进行主程序入口main
函数的调用。可以通过汇编断点验证:
这样就和开头的时候对应上了。那么如果修改main
函数的名称,编译的时候就报错了。
根据以上分析可以看到
dyld
是按image list
顺序从第1
个image
调用runInitializers
(可以看做是以image
分组)。再调用下一个image
的runInitializers
最后再调用主程序(下标为0
)的runInitializers
。在runInitializers
内部先调用所有类的+load
,再调用所有分类的+ load
,最后调用c++
的构造函数。
objc
中调用load
方法,dyld
中调用doModInitFunctions
函数调用c++
构造函数。
6.3: sNotifyObjCUnmapped(unmap_image)
sNotifyObjCUnmapped
在dyld
中只有removeImage
函数进行了调用:
removeImage
函数被checkandAddImage
、garbageCollectImages
、_dyld_link_module
和NSUnLinkModule
函数调用。
checkandAddImage
:检查此加载的image
与任何现有image
的安装路径,相同就直接返回,不同就添加。garbageCollectImages
:在link
等其它异常以及回收的时候调用。_dyld_link_module
:旧版本兼容。NSUnLinkModule
:断开链接时调用。
6.3.1: unmap_image
中调用了unmap_image_nolock
,核心代码如下:
void
unmap_image_nolock(const struct mach_header *mh)
{
……
header_info *hi;
……
//释放类,分类相关资源。
_unload_image(hi);
// Remove header_info from header list
//移除remove Header
removeHeader(hi);
free(hi);
}
- 卸载、释放类、分类相关资源。
- 移除
Header
信息。
7. dyld3
闭包模式分析
7.1: 分析引入
关于闭包模式在开启闭包模式的情况下就直接return
了,所以核心逻辑就在launchWithClosure
中了:
static bool launchWithClosure(const dyld3::closure::LaunchClosure* mainClosure,
const DyldSharedCache* dyldCache,
const dyld3::MachOLoaded* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[], Diagnostics& diag,
uintptr_t* entry, uintptr_t* startGlue, bool* closureOutOfDate, bool* recoverable)
{
……
// run initializers
CRSetCrashLogMessage("dyld3: launch, running initializers");
libDyldEntry->runInitialzersBottomUp((mach_header*)mainExecutableMH);
//dyld::log("returned from runInitialzersBottomUp()\n");
……
}
在launchWithClosure
中发现了runInitialzersBottomUp
的调用:
void AllImages::runInitialzersBottomUp(const closure::Image* topImage)
{
// walk closure specified initializer list, already ordered bottom up
topImage->forEachImageToInitBefore(^(closure::ImageNum imageToInit, bool& stop) {
// get copy of LoadedImage about imageToInit, but don't keep reference into _loadedImages, because it may move if initialzers call dlopen()
uint32_t indexHint = 0;
LoadedImage loadedImageCopy = findImageNum(imageToInit, indexHint);
// skip if the image is already inited, or in process of being inited (dependency cycle)
if ( (loadedImageCopy.state() == LoadedImage::State::fixedUp) && swapImageState(imageToInit, indexHint, LoadedImage::State::fixedUp, LoadedImage::State::beingInited) ) {
// tell objc to run any +load methods in image
if ( (_objcNotifyInit != nullptr) && loadedImageCopy.image()->mayHavePlusLoads() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)loadedImageCopy.loadedAddress(), 0, 0);
const char* path = imagePath(loadedImageCopy.image());
log_notifications("dyld: objc-init-notifier called with mh=%p, path=%s\n", loadedImageCopy.loadedAddress(), path);
// +load
(*_objcNotifyInit)(path, loadedImageCopy.loadedAddress());
}
// run all initializers in image
// c++构造函数
runAllInitializersInImage(loadedImageCopy.image(), loadedImageCopy.loadedAddress());
// advance state to inited
swapImageState(imageToInit, indexHint, LoadedImage::State::beingInited, LoadedImage::State::inited);
}
});
}
_objcNotifyInit
最终调用到了+ load
方法。runAllInitializersInImage
调用c++
构造函数。
void AllImages::runAllInitializersInImage(const closure::Image* image, const MachOLoaded* ml)
{
image->forEachInitializer(ml, ^(const void* func) {
Initializer initFunc = (Initializer)func;
#if __has_feature(ptrauth_calls)
initFunc = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)initFunc, 0, 0);
#endif
{
ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)ml, (uint64_t)func, 0);
//c++构造函数
initFunc(NXArgc, NXArgv, environ, appleParams, _programVars);
}
log_initializers("dyld: called initialzer %p in %s\n", initFunc, image->path());
});
}
真机添加_dyld_objc_notify_register
符号断点调试,发现_dyld_objc_notify_register
函数会先尝试调用dyld3::_dyld_objc_notify_register
,如果条件不满足就查找_dyld_objc_notify_register
函数调用
查看相关源码:
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
if ( gUseDyld3 )
// dyld3,使用这里注册回调
return dyld3::_dyld_objc_notify_register(mapped, init, unmapped);
DYLD_LOCK_THIS_BLOCK;
typedef bool (*funcType)(_dyld_objc_notify_mapped, _dyld_objc_notify_init, _dyld_objc_notify_unmapped);
static funcType __ptrauth_dyld_function_ptr p = NULL;
if(p == NULL)
// 查找_dyld_objc_notify_register函数
dyld_func_lookup_and_resign("__dyld_objc_notify_register", &p);
// 调用_dyld_objc_notify_register函数
p(mapped, init, unmapped);
}
gUseDyld3
不为NULL
就走dyld3
的流程,否则就走dyld2
的流程,真机调试的结果是走了dyld2
的流程,现在来分析下dyld3
的流程。
7.2: dyld3
流程分析
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
log_apis("_dyld_objc_notify_register(%p, %p, %p)\n", mapped, init, unmapped);
gAllImages.setObjCNotifiers(mapped, init, unmapped);
}
- 调用
gAllImages.setObjCNotifiers
函数注册回调。
void AllImages::setObjCNotifiers(_dyld_objc_notify_mapped map, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmap)
{
// dyld3注册的三个回调函数指针与dyld2不同
_objcNotifyMapped = map;
_objcNotifyInit = init;
_objcNotifyUnmapped = unmap;
// We couldn't initialize the objc optimized closure data in init() as that needs malloc but runs before malloc initializes.
// So lets grab the data now and set it up
...
if ( !mhs.empty() ) {
// objc map(映射)的回调
(*map)((uint32_t)mhs.count(), &paths[0], &mhs[0]);
if ( log_notifications("dyld: objc-mapped-notifier called with %ld images:\n", mhs.count()) ) {
for (uintptr_t i=0; i < mhs.count(); ++i) {
log_notifications("dyld: objc-mapped: %p %s\n", mhs[i], paths[i]);
}
}
}
}
- 复制
objc
的三个回调,dyld3
注册的三个回调函数指针与dyld2
不同。 - 进行
objc map
(映射)的回调,即调用_objcNotifyMapped
回调。
7.2.1 _objcNotifyMapped(map_images)
_objcNotifyMapped
函数是被AllImages::runImageCallbacks
函数调用的。
AllImages::runImageCallbacks
函数是被AllImages::applyInitialImages
函数和AllImages::loadImage
函数调用的。
AllImages::applyInitialImages
函数:
AllImages::applyInitialImages
函数是被_dyld_initializer
函数调用的。
- 从
5.3
小节知道_dyld_initializer
函数是被libSystem.dylib
库中的libSystem_initializer
函数调用的。而由于_dyld_initializer
是在libdispatch_init
之前调用的,所以这个时候应该还没有注册回调。
AllImages::loadImage
函数:
- 调用
AllImages::runImageCallbacks
函数。 - 调用
AllImages::runInitialzersBottomUp
函数,间接调用到_objcNotifyInit
函数。
AllImages::loadImage
函数是被AllImages::dlopen
函数调用的。
- 调用
AllImages::loadImage
函数。 - 调用
AllImages::runInitialzersBottomUp
函数,间接调用到_objcNotifyInit
函数。
7.2.2: _objcNotifyInit(load_images)
_objcNotifyInit
函数是被AllImages::runInitialzersBottomUp
函数调用的。
继续查找AllImages::runInitialzersBottomUp
函数的调用者。
- 结合
7.1
和7.2.1
小节得出AllImages::runInitialzersBottomUp
函数是被launchWithClosure
函数、AllImages::loadImage
函数和AllImages::dlopen
函数调用的。
7.2.3: _objcNotifyUnmapped(unmap_image)
_objcNotifyUnmapped
函数是被AllImages::removeImages
函数调用的。
AllImages::removeImages
函数是被AllImages::garbageCollectImages
函数调用的。
AllImages::garbageCollectImages
函数是被AllImages::decRefCount
函数调用的。
AllImages::decRefCount
函数调用分两种情况:
非macOS
情况下是被dlclose
函数调用的。
macOS
情况下是被NSUnLinkModule
函数调用的。
由于真机和模拟器以及Mac
都没有办法进入闭包模式调试验证。并且闭包模式代码逻辑可读性比较差,所以这里只是根据源码得出的结论,不一定成立。
7.3: dyld3
闭包模式流程图
8: dyld简介
启动时间(Startup Time
):main
函数执行之前所用的时间。
启动收尾(Lacunch Closure
):启动应用程序必须的所有信息。
dyld
发展到如今已经有3个大版本了,接下来将对dyld
的演进过程做简单总结
8.1: dyld 1.0 (1996–2004)
- 包含在
NeXTStep 3.3
中一起发布,在这之前NeXT
使用静态二进制数据。 dyld1
的历史早于标准化POSIX dlopen()
调用。dyld1
是在大多数使用c++
动态库的系统之前编写的。
c++
有许多的特性比如其初始化器排序方式等在静态环境中工作良好,但是在动态环境中可能降低性能。因此大型c++
代码库导致dyld
需要完成大量的工作,速度变慢。- 在
macOS Cheetah(10)
中增加了预绑定技术。
预绑定为系统中所有的dylib
和你的程序找到固定地址。dyld
将会加载这些地址的所有内容。加载成功会编辑所有这些二进制数据以获得所有预计算地址。然后下次当它将所有数据放入相同地址时不必进行任何其它额外的工作。这样会大幅提高速度,但是这也意味着每次启动时会编辑你的二进制数据。从安全性来说这样并不是很好的做法。
8.2: dyld2.0 (2004-2007)
- 随着
macOS Tiger
发布。 dyld2
是dyld
的完全重写版本。- 正确支持
c++
初始化器语义,扩展了mach-o
格式并且更新了dyld
。 - 具有完整的本机
dlopen
和dlsym
实现,具有正确的语义,弃用了旧版API
(旧版API
仍然仅位于macOS
中)。 dyld2
的设计目标是提高速度,因此仅进行有限的健全性检查(以前恶意程序并不多)。dyld
有一些安全性问题,对一些功能性改进提高它在现在平台上的安全性。- 由于速度大幅提升可以减少预绑定工作量。不同于
dyld1
编辑你的程序数据,dyld2
仅编辑系统库。可以仅在软件更新时做这些事情。因此在软件更新时可能会看到“优化系统性能”之类的文字,这时就是在更新预绑定。
8.2.1: dyld2.x(2007-2017)
-
增加更多的架构和平台。
x86
、x86_64
、arm64
iOS
、tvOS
、watchOS
-
提升安全性
- 增加代码签名和
ASLR
mach-o header
边界检查,避免恶意二进制数据的加入。
- 增加代码签名和
-
提升性能
- 使用共享缓存替换预绑定。
8.2.2: 共享缓存(shared cache)
共享缓存(dyld
预连接器)最早被引入iOS3.1
& macOS Snow Leopard
,完全取代预绑定。
-
它是一个单文件,含有大多数系统
dylib
。
由于合并成一个文件,因此可以进行优化- 重新调整二进制数据以提高加载速度(重新调整所有文本段和所有数据段重写整个符号表以减小大小)。
- 允许打包二进制数据段节省大量
ram
- 预生成数据结构供
dyld
和objc
使用,在运行时使用让我们不必在应用启动时做这些事情。这样也会节约更多ram
和时间。
-
共享缓存在
macOS
上本地生成运行dyld
共享代码大幅优化系统性能。其它平台由Apple
提供。
8.3: dyld3.0(2017-)
dyld3
是全新的动态连接器,2017(iOS11
)年所有系统程序都默认使用dyld3
,第三方在2019(iOS13
)年完全取代dyld2
。
dyld3
主要做了以下三方面的改进:
- 性能,提高启动速度。
dyld3
可以帮助我们获得更快的程序启动和运行速度。 - 安全性。
dyld2
增加的安全性很难跟随现实情形增强安全性。 - 可测试性和可靠性。
XCTest
依赖于dyld
的底层功能,将它们的库插入进程。因此不能用于测试现有的dyld
代码。这让难以测试安全性和性能水平。
dyld3
将大多数dyld
移出进程,现在大多数dyld
只是普通的后台程序。可以使用标准测试工具进行测试。另外也允许部分dyld
驻留在进程中,驻留部分尽可能小,从而减少程序的受攻击面积。
8.4: dyld2与dyld3加载对比
8.4.1: dyld2流程
Parse mach-o headers & Find dependencies
:分析macho headers
,确认需要哪些库。递归分析依赖的库直到获得所有的dylib库。普通iOS
程序需要3-600
个dylib
,数据庞大需要进行大量处理。Map mach-o files
:映射所有macho
文件将他们放入地址空间(映射进内存)。Perform symbol lookups
:执行符号查找。 比如使用printf
函数,将会查找printf
是否在库系统中,然后找到它的地址,将它复制给应用程序中的函数指针。Bind and rebase
:绑定和基址重置。复制这些指针,所有指针必须使用基地址(ASLR
的存在)。Run initializers
:运行所有初始化器。这之后就开始准备执行main
函数。
8.4.2: dyld3流程
dyld3
整个被分成了3个流程:
-
dyld3
是一个进程外macho
分析器和编译器(对应上图中红色部分)。- 解析所有搜索路径、
rpaths
、环境变量。 - 分析
macho
二进制数据。 - 执行所有符号查找。
- 利用上面的这些结果创建闭包处理。
- 它是一个普通的后台程序,可以进行正常测试。
- 大多数程序启动会使用缓存,始终不需要调用进程外
macho
分析器或编译器。 - 启动闭包比
macho
更简单,它们是内存映射文件,不需要用复杂的方式进行分析,可以简单的验证它们,作用是为了提高速度。
- 解析所有搜索路径、
-
dyld3
也是一个进程内引擎。-
检查闭包是否正确。
-
使用闭包映射所有
dylibs
。 -
绑定和基址重置。
-
运行所有初始化器,然后跳转主程序
main()
⚠️
dyld3
不需要分析macho Headers
或者执行符号查找。在App
启动时没有这个过程,因此极大的提升了程序的启动速度。 -
-
启动闭包缓存服务
- 系统
app
闭包模式构建在共享缓存中。 - 第三方应用在安装时构建,在软件更新时重新构建。
- 在
macOS
上后台进程引擎可以在后台进程被调用,在其它平台上不需要这么做。
- 系统
详细情况参考官方:wwdc2017-413(App Startup Time: Past, Present, and Future)
9: 总结
9.1: dyld
调用流程
9.2: 核心流程文字总结
dyld
动态链接器:记载所有库和可执行文件。dyld
加载流程:- 系统内核调用
_dyld_start
- 重定位
dyld
(rebaseDyld) - 初始化
dyld
(_subsystem_init
) - 调用
dyld::_main
函数(dyld::_main
):- 加载共享缓存(
mapSharedCache
)- 实际调用
loadDyldCache
分为三种情况- 仅加载到当前进程调用
mapCachePrivate
。不放入共享缓存,仅自己使用。 - 已经加载过不进行任何处理。
- 当前进程第一次加载调用
mapCacheSystemWide
- 仅加载到当前进程调用
dyld2/dyld3
(ClosureMode
闭包模式)加载程序(iOS11
引入dyld3
闭包模式,闭包模式加载速度更快,效率更高。iOS13
后动态库和三方库都使用ClosureMode
加载):dyld3
:- 查找/创建
mainClosure
- 通过
launchWithClosure
启动主程序,成功后返回result
(主程序入口main
)。逻辑和dyld2
启动主程序逻辑基本相同。
- 查找/创建
dyld2
:启动主程序- 实例化主程序
instantiateFromLoadedImage
(实际上是创建image
)- 调用
sniffLoadCommands
生成相关信息,比如compressed
。根据compressed
来确定使用哪个类来实例化。compressed
是根据LC_DYLIB_INFO
和LC_DYLD_INFO_ONLY
来获取的。segCount
最多256
个。libCount
最多4096
个。
- 实例化生成
image
,加入all images
中。
- 调用
- 插入&加载动态库
loadInsertedDylib
- 根据上下文初始化配置调用
load
加载动态库
- 根据上下文初始化配置调用
- 链接主程序和链接插入动态库(
link
,主程序链接在前)- 修正
ASLR
、绑定noLazy
符号、绑定弱符号
- 修正
- 初始化主程序
initializeMainExecutable
(核心方法)- 初始化
image
,下标从1
开始,然后再初始化主程序(下标为0
),调用dyld ImageLoader::runInitializers
dyld ImageLoader::processInitializers
:dyld ImageLoader::recursiveInitialization
:dyld dyld::notifySingle
:- 这个函数执行一个回调
- 通过断点调试回调是
_objc_init
初始化赋值时传递的第二个参数load_images
load_images
中调用了call_load_methods
函数call_class_loads
:调用各个类的+load
方法。call_category_loads
:调用各个分类的+load
方法。
doInitialization
- 最终会调用到
doModInitFunctions
- 内部会调用全局
c++
构造函数(attribute((constructor))
修饰的c
函数) - 会首先调用
libsystem_initializer
构造和上进心回调的注册- 回调有三个参数(
map_images
、load_images
、unmap_image
)map_images
进行类的加载,在回调函数注册后就马上调用load_images
在notifySingle
循环中调用unmap_image
在异常/回收/检查镜像文件时调用
- 回调有三个参数(
- 内部会调用全局
- 最终会调用到
- 初始化
- 找到主程序入口
LC_MAIN
,然后返回主程序入口(main
)
- 实例化主程序
- 实际调用
- 加载共享缓存(
- 重定位
- 系统内核调用
load
、c++
构造函数、main
调用总结:dyld
初始化image
是按Link Binary With Libraries
顺序逐个初始化的,从下标1
开始,最后在初始化主程序(下标为0
),可以理解为是按image
进行分组的。image
内部是先加载所有类的+load
方法,再加载分类的+load
方法,最后加载c++
全局构造函数(类load->分类load->c++构造函数)。+load
方法是objc
中调用的,c++
全局构造函数是在dyld
中调用的(在不考虑二进制重排等的优化下,image
内部的顺序默认是按Compile Sources
中顺序进行的)。main
函数是在dyld
返回入口函数(main
)之后才调用的。