这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战
1.应用程序加载原理
在分析dyld
加载应用程序之前,先清楚以下基本概念。
库:可执行的二进制文件,可以被系统加载到内存。库分为静态库和动态库,动态和静态库的区别是链接的区别。
编译过程
源文件->预编译->编译->汇编->链接->可执行文件(
MachO
格式)。
动态库:动态链接。只会存在一份,在内存中共享。减少了包的体积大小。这里有完全动态特性的就是系统的动态库了。 静态库:静态链接。静态库在装载的时候会重复,浪费了空间。 那么这些库是怎么加载到内存中的呢?
是通过dyld
动态链接器加载到内存中的。整个过程大概如下:
dyld
(the dynamic link editor
)动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后交由dyld
负责余下的工作。
这篇文章将详细分析整个dyld
加载过程。
2. 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
的。
3. 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);
}
}
// <rdar://problem/12186933> 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
个主程序第一个地址。