一、应用程序加载原理
在分析dyld
加载应用程序之前,先清楚以下基本概念。
库:可执行的二进制文件,可以被系统加载到内存。库分为静态库和动态库,动态和静态库的区别是链接的区别。
编译过程
源文件->预编译->编译->汇编->链接->可执行文件(MachO
格式)。
动态库:动态链接。只会存在一份,在内存中共享。减少了包的体积大小。这里有完全动态特性的就是系统的动态库了。
静态库:静态链接。静态库在装载的时候会重复,浪费了空间。
那么这些库是怎么加载到内存中的呢?
是通过dyld
动态链接器加载到内存中的。整个过程大概如下:
dyld
(the dynamic link editor
)动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后交由dyld
负责余下的工作。
这篇文章将详细分析整个dyld
加载过程。
二、dyld 初探
既然是dyld
加载的库,那么在加载完成后肯定会进入main
函数,那么在main
函数上打个断点看下调用:
可以看到是libdyld.dylib start:
调用的main
函数。给start
下个符号断点并没有进入断点。那么证明在底层的符号不是start
。实现一个+ load
方法打个断点发现如下调用栈:
可以看到是
dyld _dyld_start
发起的调用。opensoure
上直接下载dyld-852
源码。
搜索_dyld_start
发现这个入口是在汇编中,其中主要是调用了dyldbootstrap::start
:
最终跳转了返回的
LC_MAIN
。
也可以通过断点查看汇编调用确定:
dyldbootstrap
是c++
的命名空间,start
是其中的函数。搜索后发现dyldbootstrap::start
在dyldInitialization.cpp
中,这也就是函数开始的地方。接下来结合源码分析怎么从start
调用到load
和main
方法,以及dyld
是如何加载images
的。
三、dyld源码分析
3.1 dyldbootstrap::start(dyldInitialization.cpp
)
可以通过搜索dyldbootstrap
命名空间找到start
源码。核心代码如下:
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);
//栈溢出保护
__guard_setup(apple);
//初始化dyld
_subsystem_init(apple);
//偏移
uintptr_t appsSlide = appsMachHeader->getSlide();
//调用dyld main函数
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
start
主要做了以下几件事:
- 告诉
debug server
dyld
启动 - 重定位
dyld
- 栈溢出保护
- 初始化
dyld
- 调用
dyld main
函数
其中start
只做了一些配置和初始化的工作,核心逻辑在main
函数中,start
返回了main
函数的返回值。
3.2 dyld::_main(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);
}
……
//主程序可执行文件 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;
}
//获取主程序Header,Slide(ASLR的偏移值)
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);
……
//检查共享缓存是否可用,到了这里只读了主程序还没有加载主程序。iOS必须有共享缓存。
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
//加载共享缓存方法
mapSharedCache(mainExecutableSlide);
#endif
……
}
……
#if !TARGET_OS_SIMULATOR
//dyld3 ClosureMode模式,iOS11引入ClosureMode,iOS13后动态库和三方库都使用ClosureMode加载。
if ( sClosureMode == ClosureMode::Off ) {
//ClosureMode off打印log,往 if-else 后面走了
if ( gLinkContext.verboseWarnings )
dyld::log("dyld: not using closures\n");
} else {
//启动模式 闭包模式 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
……
//实例化主程序,加入到allImages(第一个靠dyld加载的image就是主程序)
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
//代码签名
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
#if TARGET_OS_SIMULATOR
// check main executable is not too new for this OS
//检查主程序是否属于当前系统
{……}
#endif
……
#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
……
//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;
// link main executable
//记录链接主程序
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();
……
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 SUPPORT_ACCELERATE_TABLES
//判断条件不满足,持续 goto reloadAllImages
if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
// Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
ImageLoader::clearInterposingTuples();
// unmap all loaded dylibs (but not main executable)
for (long i=1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image == sMainExecutable )
continue;
if ( image == sAllCacheImagesProxy )
continue;
image->setCanUnload();
ImageLoader::deleteImage(image);
}
// note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
sAllImages.clear();
sImageRoots.clear();
sImageFilesNeedingTermination.clear();
sImageFilesNeedingDOFUnregistration.clear();
sAddImageCallbacks.clear();
sRemoveImageCallbacks.clear();
sAddLoadImageCallbacks.clear();
sAddBulkLoadImageCallbacks.clear();
sDisableAcceleratorTables = true;
sAllCacheImagesProxy = NULL;
sMappedRangesStart = NULL;
mainExcutableAlreadyRebased = true;
gLinkContext.linkingMainExecutable = false;
resetAllImages();
goto reloadAllImages;
}
#endif
……
//绑定主程序
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);
}
}
// do weak binding only after all inserted images linked
//弱引用绑定主程序,所有镜像文件绑定完成后进行。
sMainExecutable->weakBind(gLinkContext);
gLinkContext.linkingMainExecutable = false;
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
// run all initializers
//初始化主程序,到目前为止还没有执行到主程序中的代码。
initializeMainExecutable();
……
{
// 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
函数主要是返回了主程序的函数入口(main
),主要流程如下:
- 配置环境,获取主程序
Header
,Slide
(ASLR
) - 加载共享缓存:
mapSharedCache
。这个时候只读了主程序还没有加载主程序。iOS必须有共享缓存。
在checkSharedRegionDisable
方法中说明了iOS
必须有共享缓存:
-
dyld2/dyld3
(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
,然后返回主程序。
- 实例化主程序
-
DYLD_PRINT_OPTS
,DYLD_PRINT_ENV
环境变量配置,可以打印环境变量配置(在"Scheme -> Arguments -> Environment Variables"中配置):
ASLR
:image list
第0
个主程序第一个地址。
关于dyld2/dyld3
更多信息将在后面做进一步总结。
3.3 mapSharedCache加载共享缓存
共享缓存专门缓存系统动态库,如:UIKit
、Foundation
等。(自己的库、三方库不行)
mapSharedCache
真正调用的是loadDyldCache
:
static void mapSharedCache(uintptr_t mainExecutableSlide)
{
……
//真正调用的是loadDyldCache
loadDyldCache(opts, &sSharedCacheLoadInfo);
……
}
3.3.1 loadDyldCache
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
if ( options.forcePrivate ) {
// mmap cache into this process only
//仅加载到当前进程
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
有3
个逻辑:
1.仅加载到当前进程调用mapCachePrivate
。不放入共享缓存,仅自己使用。
2.已经加载过不进行任何处理。
3.当前进程第一次加载调用mapCacheSystemWide
动态库的共享缓存在整个应用的启动过程中是最先被加载的。
3.4 instantiateFromLoadedImage 实例化主程序(创建image
)
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
中。
实际上实例化真正调用的是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);
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
个。
3.5 loadInsertedDylib 插入&加载动态库
static void loadInsertedDylib(const char* path)
{
unsigned cacheIndex;
try {
……
//调用load,加载动态库的真正函数
load(path, context, cacheIndex);
}
……
}
- 根据上下文初始化配置调用
load
加载动态库。
3.6 ImageLoader::link链接主程序/动态库
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::link
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();
//Rebase修正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 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
。 - 绑定
NoLazy
符号。 - 绑定弱符号。
- 注册。
- 记录时间,可以通过配置看到
dyld
应用加载时长。
3.7 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
打印相关信息。
dyld ImageLoader::runInitializers(ImageLoader.cpp
)
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
然后调用processInitializers
。
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.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
- 最终调用了
recursiveInitialization
。
ImageLoader::recursiveInitialization(ImageLoader.cpp
)
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
//先初始化下级lib
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();
}
- 整个过程是一个递归的过程,先调用依赖库的,再调用自己的。
- 调用
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
。
c++
系统构造函数__attribute__((constructor)) void func() { printf("\n ---func--- \n"); }
⚠️这里也就说明了对于同一个
image
而言,+ load
方法是比c++
构造函数更早调用的。
dyld::notifySingle(dyld2.cpp
)
notifySingle
对应一个函数,在setContext
的时候赋值:
//调用到objc里面去
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
……
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
//回调指针 sNotifyObjCInit 是在 registerObjCNotifiers 中赋值的。这里执行会跑到objc的load_images中
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
……
}
-
notifySingle
中找不到load image
的调用(从堆栈信息中可以看到notifySingle
之后是load image
)。 - 这个函数执行一个回调
sNotifyObjCInit
,条件是state
为dyld_image_state_dependents_initialized
。
搜索下回调sNotifyObjCInit
的赋值操作,发现是在registerObjCNotifiers
中赋值的
registerObjCNotifiers
//谁调用的 registerObjCNotifiers ? _dyld_objc_notify_register。这里赋值了三个参数 _dyld_objc_notify_mapped,_dyld_objc_notify_init,_dyld_objc_notify_unmapped
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
}
// call 'init' function on all images already init'ed (below libSystem)
for (std::vector::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());
}
}
}
-
registerObjCNotifiers
赋值来自于第二个参数_dyld_objc_notify_init
。 - 赋值后里面调用了
notifyBatchPartial
(内部调用了sNotifyObjCMapped
)。 - 循环调用
load_images
,这里调用的是依赖的系统库的libdispatch.dylib,libsystem_featureflags.dylib,libsystem_trace.dylib,libxpc.dylib
。
搜索发现是_dyld_objc_notify_register
调用的registerObjCNotifiers
。
_dyld_objc_notify_register(dyldAPIs.cpp
)
//_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
排查调用情况:
可以看到是被
_objc_init
调用的。
_objc_init
的调用在objc-os.mm
中,查看源码:
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
进行交互的。
ImageLoaderMachO::doInitialization(ImageLoaderMachO.cpp
)
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
//加载c++构造函数
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
加上以下代码查看MachO
文件:
__attribute__((constructor)) void func1() {
printf("\n ---func1--- \n");
}
__attribute__((constructor)) void func2() {
printf("\n ---func2--- \n");
}
会发现MachO
中多了__mod_init_func
- 调用
doModInitFunctions
函数加载c++
构造函数(__attribute__((constructor))
修饰的c
函数)
ImageLoaderMachO::doModInitFunctions
- 内部是对
macho
文件的一些读取操作。 - 会进行
__mod_init_func
section
的确认,与上面的验证符合。 - 加载前必须加载完
libSystem
库。
四、反推objc与dyld的关联
在上面的符号断点过程中可以看到在_dyld_objc_notify_register
与doModInitFunctions
之间还有非dyld
的库。
在_objc_init
中打个断点有如下调用栈:
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
* frame #0: 0x00000001002d2d44 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
frame #1: 0x000000010044b0bc libdispatch.dylib`_os_object_init + 13
frame #2: 0x000000010045bafc libdispatch.dylib`libdispatch_init + 282
frame #3: 0x00007fff69543791 libSystem.B.dylib`libSystem_initializer + 220
frame #4: 0x000000010002f1d3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
frame #5: 0x000000010002f5de dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #6: 0x0000000100029ffb dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
frame #7: 0x0000000100029f66 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #8: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #9: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #10: 0x0000000100016662 dyld`dyld::initializeMainExecutable() + 129
frame #11: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #12: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #13: 0x0000000100015025 dyld`_dyld_start + 37
对于doModInitFunctions
后面的流程是未知的。从doModInitFunctions->_objc_init
流程是未知的。那么最好的方式就是从_objc_init
反推调用到它的整个流程。
4.1 _os_object_init
_objc_init
是被_os_object_init
调用的,这个函数在libdispatch.dylib
中。下载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
}
发现确实是在_os_object_init
中直接调用了_objc_init()
。
接着在libdispatch_init
中发现了_os_object_init
的调用:
- 其中进行了
TLS
键值处理以及线程处理。
4.2 libSystem_initializer
libSystem_initializer
是在libSystem
库中,下载libSystem
最新源码1292.120.1
。
同样直接搜索libSystem_initializer
:
- 其中直接调用了
libdispatch_init
,同样还调用了__malloc_init
、_dyld_initializer
以及_libtrace_init
。
libSystem_initializer
是ImageLoaderMachO::doModInitFunctions
调用的,这样整个流程就回到了dyld
中。整个流程就串起来了。
在doModInitFunctions
中发现了如下代码:
-
libSystem
库必须第一个被初始化。这也能被理解,因为要初始化dispatch
以及objc
。其它image
都依赖它。 -
func
是对c++
构造函数的调用。
那么libSystem_initializer
是在哪里调用的呢?在doModInitFunctions
中并没有看到libSystem_initializer
的调用。但是断点读取确实读取到了:
前面已经分析过了
doModInitFunctions
中是对c++
构造函数的调用。libSystem_initializer
正好是c++
构造函数:
这样整个流程就通了。只不过
libSystem_initializer
这个c++
构造函数被先调用。
libSystem
的c++
构造函数在dyld
、libobjc
、Foundation
的c++
构造函数之后,主程序之前执行。
五、 dyld注册objc回调简单分析
通过上面的分析在_objc_init
中调用了_dyld_objc_notify_register
进行回调注册,有如下赋值:
//第一个参数 map_images
sNotifyObjCMapped = mapped;
//第二个参数 load_images
sNotifyObjCInit = init;
//第三个参数 unmap_image
sNotifyObjCUnmapped = unmapped;
接下来将详细分析这3
个回调的逻辑。
5.1 sNotifyObjCMapped(map_images)
sNotifyObjCMapped
在dyld
中的调用只在notifyBatchPartial
中:
而notifyBatchPartial
的调用是在registerObjCNotifiers
、registerImageStateBatchChangeHandler
、以及notifyBatch
中。那么根据之前的分析这里的调用就是registerObjCNotifiers
注册回调后就在里面调用了。
在objc
源码map_images
中打个断点:
可以验证在注册回调后立马调用了
map_images
。
map_images
中直接加锁调用了map_images_nolock
,其中进行了类的加载相关的操作。这块逻辑将单独写篇文章进行分析。
5.2 sNotifyObjCInit(load_images)
sNotifyObjCInit
在dyld
中的调用分为以下情况:
1.notifySingleFromCache
中。
2.notifySingle
中。
3.registerObjCNotifiers
。
notifySingleFromCache
与notifySingle
逻辑基本相同,无非就是有没有缓存的区别。
registerObjCNotifiers
是在注册回调函数的时候直接进行的回调。直接在load_images
中打个断点可以跟踪到如下信息:
可以看到系统的基础库在注册回调后就马上进行了
load_images
的调用。
而对于其他库是通过notifySingle
走的回调逻辑:
5.2.1 load_images(objc-runtime-new.mm
)
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
。
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
方法。
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
。
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());
}
//空间不足开辟空间
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));
}
//将load方法添加到loadable_classes中。相当于一个下标中存储的是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));
}
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中
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
- 通过字符出那比较获取
load
方法。 - 空间不足的情况下开辟空间吗,每次开辟的空间大小为(
2
倍+16
)* 16 字节。
struct loadable_class { Class cls; // may be nil IMP method; };
- 将对应的数据添加进
loadable_classes
与loadable_categories
中。
⚠️加载过程中类和分类是有区分的。为什么区分将在后续的文章中详细分析。
getLoadMethod
IMP
objc_class::getLoadMethod()
{
runtimeLock.assertLocked();
const method_list_t *mlist;
//递归所有的baseMethods,查找load方法。
mlist = ISA()->data()->ro()->baseMethods();
if (mlist) {
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) {
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
if (0 == strcmp(name, "load")) {
return meth.imp(false);
}
}
}
return nil;
}
-
load
方法获取是通过字符出那比较获取的。
5.2.2 call_load_methods (objc-loadmethod.mm
)
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();
//循环调用 call_class_loads,类的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
之前被调用的原因。
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
方法进行调用。
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++
构造函数才返回LC_MAIN
进行main
函数的调用。可以通过汇编断点验证:
这样就和开头的时候对应上了。那么如果修改
main
函数的名称,编译的时候就报错了。主程序的入口main
是写死的,可以通过Hook
去操作main
隐藏自己的逻辑。
根据以上分析可以看到
dyld
是按image list
顺序从第1
个image
调用runInitializers
(可以看做是以image
分组)。再调用下一个image
的runInitializers
最后再调用主程序(下标为0
)的runInitializers
。在runInitializers
内部先调用所有类的+load
,再调用所有分类的+ load
,最后调用c++
的构造函数。
objc
中调用load
,dyld
中调用doModInitFunctions
。
⚠️如果在+ load
中做了防护,那么可以通过在+ load
执行前断住外部符号做处理。这样就可以绕过防护了。
防护最重要的就是不让别人找到防护的逻辑,只要能找到那么破解就很容易了。
案例分析:你真的了解dyld么?
5.3 sNotifyObjCUnmapped(unmap_image)
sNotifyObjCUnmapped
在dyld
中只有removeImage
进行了调用:
removeImage
被checkandAddImage
、garbageCollectImages
、_dyld_link_module
调用。
-
garbageCollectImages
:在link
等其它异常以及回收的时候调用。 -
checkandAddImage
:检测加载的image
不在镜像列表中的时候直接删除。 -
_dyld_link_module
:暂时不确定是哪里调用的。
5.3.1 unmap_image
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
信息。
六 、dyld3闭包模式分析
关于闭包模式在开启闭包模式的情况下就直接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)
{
……
libDyldEntry->runInitialzersBottomUp((mach_header*)mainExecutableMH);
……
}
在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
调用的:
但是最终的回调以及调用方确是
dyld2
的逻辑。看下源码:
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
if ( gUseDyld3 )
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_func_lookup_and_resign("__dyld_objc_notify_register", &p);
p(mapped, init, unmapped);
}
那么就说明gUseDyld3
为NULL
,走了dyld2
的逻辑。但是如果走dyld3
可以得到以下信息,注册的三个回调函数指针与dyld2
名称不同:
_objcNotifyMapped = map;
_objcNotifyInit = init;
_objcNotifyUnmapped = unmap;
-
_objcNotifyInit
已经清楚了是在runInitialzersBottomUp
中调用的。 -
_objcNotifyUnmapped
是在garbageCollectImages ->removeImages
中调用的。 -
_objcNotifyMapped
是在runImageCallbacks
中调用的,它有两个调用方applyInitialImages
以及loadImage
。-
applyInitialImages
是被_dyld_initializer
调用的。_dyld_initializer
在第四部分已经明确了是在libSystem_initializer
中调用的。而由于_dyld_initializer
是在libdispatch_init
之前调用的,所以这个时候应该还没有注册回调。 -
loadImage
是在dlopen
中调用的。
-
由于真机和模拟器以及mac
都没有办法进入闭包模式调试验证。并且闭包模式代码逻辑可读性比较差,所以这里只是根据源码得出的结论,不一定成立。
七、dyld简介
启动时间(Startup Time
):main
函数执行之前所用的时间。
启动收尾(Lacunch Closure
):启动应用程序必须的所有信息。
dyld
发展到如今已经有3个大版本了,接下来将对dyld
的演进过程做简单总结。
7.1 dyld 1.0 (1996–2004)
- 包含在
NeXTStep 3.3
中一起发布,在这之前NeXT
使用静态二进制数据。 -
dyld1
的历史早于标准化POSIX dlopen()
调用。 -
dyld1
是在大多数使用c++
动态库的系统之前编写的。
c++
有许多的特性比如其初始化器排序方式等在静态环境中工作良好,但是在动态环境中可能降低性能。因此大型c++
代码库导致dyld
需要完成大量的工作,速度变慢。 - 在
macOS Cheetah(10)
中增加了预绑定技术。
预绑定为系统中所有的dylib
和你的程序找到固定地址。dyld
将会加载这些地址的所有内容。加载成功会编辑所有这些二进制数据以获得所有预计算地址。然后下次当它将所有数据放入相同地址时不必进行任何其它额外的工作。这样会大幅提高速度,但是这也意味着每次启动时会编辑你的二进制数据。从安全性来说这样并不是很好的做法。
7.2 dyld2.0 (2004-2007)
- 随着
macOS Tiger
发布。 -
dyld2
是dyld
的完全重写版本。 - 正确支持
c++
初始化器语义,扩展了mach-o
格式并且更新了dyld
。 - 具有完整的本机
dlopen
和dlsym
实现,具有正确的语义,弃用了旧版API
(旧版API
仍然仅位于macOS
中)。 -
dyld2
的设计目标是提高速度,因此仅进行有限的健全性检查(以前恶意程序并不多)。 -
dyld
有一些安全性问题,对一些功能性改进提高它在现在平台上的安全性。 - 由于速度大幅提升可以减少预绑定工作量。不同于
dyld1
编辑你的程序数据,dyld2
仅编辑系统库。可以仅在软件更新时做这些事情。因此在软件更新时可能会看到“优化系统性能”之类的文字,这时就是在更新预绑定。
7.2.1dyld2.x(2007-2017)
- 增加更多的架构和平台。
-
x86
、x86_64
、arm64
-
iOS
、tvOS
、watchOS
-
- 提升安全性
- 增加代码签名和
ASLR
-
mach-o header
边界检查,避免恶意二进制数据的加入。
- 增加代码签名和
- 提升性能
- 使用共享缓存替换预绑定。
7.2.2 共享缓存(shared cache)
共享缓存(dyld
预连接器)最早被引入iOS3.1
& macOS Snow Leopard
,完全取代预绑定。
-
它是一个单文件,含有大多数系统
dylib
。
由于合并成一个文件,因此可以进行优化- 重新调整二进制数据以提高加载速度(重新调整所有文本段和所有数据段重写整个符号表以减小大小)。
- 允许打包二进制数据段节省大量
ram
- 预生成数据结构供
dyld
和objc
使用,在运行时使用让我们不必在应用启动时做这些事情。这样也会节约更多ram
和时间。
共享缓存在
macOS
上本地生成运行dyld
共享代码大幅优化系统性能。其它平台由Apple
提供。
7.3 dyld3.0(2017-)
dyld3
是全新的动态连接器,2017(iOS11
)年所有系统程序都默认使用dyld3
,第三方在2019(iOS13
)年完全取代dyld2
。
dyld3
主要做了以下三方面的改进:
- 性能,提高启动速度。
dyld3
可以帮助我们获得更快的程序启动和运行速度。 - 安全性。
dyld2
增加的安全性很难跟随现实情形增强安全性。 - 可测试性和可靠性。
XCTest
依赖于dyld
的底层功能,将它们的库插入进程。因此不能用于测试现有的dyld
代码。这让难以测试安全性和性能水平。
dyld3
将大多数dyld
移出进程,现在大多数dyld
只是普通的后台程序。可以使用标准测试工具进行测试。另外也允许部分dyld
驻留在进程中,驻留部分尽可能小,从而减少程序的受攻击面积。
7.4 dyld2与dyld3加载对比
7.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
函数。
7.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)