iOS 逆向07 -- Dyld动态加载器
- 在上篇中我们知道App经过
编译之后
最终生成一个Mach-O格式的可执行文件
,在运行之前它只是硬盘上的一个静态文件,现在我们需要将它加载进入内存,运行形成一个独立的进程,主要包含下面两个步骤:- 装载:将磁盘上的Mach-O可执行文件映射到虚拟内存当中;
- 动态链接(Dynamic Linking):App应用程序启动时,会加载Dyld动态链接器程序,Dyld会加载链接App运行时所需要的系统动态库的过程称为动态链接;
Dyld
- Dyld:全称为dynamic loader即
动态加载器
,是苹果系统提供的一个加载与链接动态库
的程序,是iOS系统的一个非常重要组成部分; - Dyld可加载的Mach-O格式的文件类型有:
MH_OBJECT
,MH_DYLIB
,MH_BUNDLE
等等; - Dyld文件路径位
/usr/lib/dyld
,其就是用来加载动态库的程序,同理在移动设备下也有这个程序,如下所示:
共享缓存技术
- 首先App程序启动运行时,会依赖很多的系统动态库,例如UIKit,Foundation等等;
- Dyld程序开始运行,通过Dyld程序去
加载链接App所依赖的所有的系统动态库
,由于每个App进程启动时都需要使用这些系统动态库,为了优化程序的启动速度,iOS系统采用了共享缓存技术
,即将所有的系统动态库编译成一个大的缓存文件,就是 dyld_shared_cache,该缓存文件存储在iOS系统下的/System/Library/Caches/com.apple.dyld/
目录下; - 然后App启动时,Dyld直接去
动态库共享缓存中
去加载链接
这些系统动态库
;
动态链接的优势
- 共享动态库资源,节省磁盘空间与内存空间;
- 动态库升级时,App应用程序可做到无感知升级动态库;
- 更方便程序插件(Plug-in)的制作,为程序带来更好的可扩展性和兼容性;
Dyld源码分析
- 源码地址: https://opensource.apple.com/tarballs/dyld/
- 我下载的是 [dyld-832.7.1.tar.gz]版本;
- 在工程文件中的load类方法中加入断点;
- 运行工程我们发现类Class的load函数调用,是在main函数之前,为什么会出现这种情况?下面我们来详细分析App运行加载的流程,就能解开其中的秘密;
- 运行工程,进入断点处可以看到汇编代码如下:
- 在控制台上输入bt命令可以看到函数的调用堆栈:
- 打开源码工程,在
dyldStartup.s
汇编文件中可以看到:
#if __arm64__ && !TARGET_OS_SIMULATOR
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
#if __LP64__
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8]
add x2, x28, #16 // get argv into x2
#else
ldr w0, [x28] // get app's mh into x0
ldr w1, [x28, #4]
add w2, w28, #8 // get argv into x2
#endif
adrp x3,___dso_handle@page
add x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
mov x4,sp // x5 has &startGlue
//call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
#if __LP64__
ldr x1, [sp]
#else
ldr w1, [sp]
#endif
cmp x1, #0
b.ne Lnew
- 在dyld源码工程中全局搜索
dyldbootstrap
定位到dyldInitialization.cpp
文件中,然后 在dyldInitialization.cpp
文件中搜索start(
,找到start的函数实现如下:
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);
//1.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;
//2.栈溢出保护
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
//3.初始化Dyld
runDyldInitializers(argc, argv, envp, apple);
#endif
_subsystem_init(apple);
//4.进入dyld::main() 主函数 (核心)
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
- 在start方法中主要做了以下操作:
-
Rebase重定位
; -
栈溢出保护
; -
初始化Dyld
; -
调用dyld::main() 主函数
,其中传参macho_header是Mach-O文件的头部,由此可见dyld是针对Mach-O文件进行相关处理的;
-
为什么要执行Rebase重定位
- App程序启动时,Dyld会将生成的Mach-O文件加载进入内存,Mach-O文件在内存中的默认起始地址为0,苹果为了防止他人破解App的源码,会
采用ASLR技术(地址空间布局随机化)
,给Mach-O文件载入内存的起始地址新增一个随机偏移量
; - Rebase重定位就是用来
纠正Mach-O文件在内存中的起始地址的
; - 执行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);
}
//Check and see if there are any kernel flags
dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
#if __has_feature(ptrauth_calls)
if ( const char* disableStr = _simple_getenv(apple, "ptrauth_disabled") ) {
if ( strcmp(disableStr, "1") == 0 )
sKeysDisabled = true;
}else {
// needed until kernel passes ptrauth_disabled for arm64 main executables
if ( (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_V8) || (mainExecutableMH->cpusubtype == CPU_SUBTYPE_ARM64_ALL) )
sKeysDisabled = true;
}
#endif
//第一步:环境变量的配置
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))
//获取主程序的hash
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
//获取主机信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
#if !TARGET_OS_SIMULATOR
// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
uintptr_t result = 0;
//获取主程序的macho_header
sMainExecutableMachHeader = mainExecutableMH;
//获取主程序的slide值
sMainExecutableSlide = mainExecutableSlide;
CRSetCrashLogMessage("dyld: launch started");
//设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
//获取主程序路径
sExecPath = _simple_getenv(apple, "executable_path");
//进程的名字
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
//进程的头环境配置
configureProcessRestrictions(mainExecutableMH, envp);
#if TARGET_OS_OSX
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);
setContext(mainExecutableMH, argc, argv, envp, apple);
}else
#endif
{
//检测环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}
//第二步:加载共享缓存
//检查缓存共享区域是否开启,iOS必须开启
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
//共享缓存加载
mapSharedCache(mainExecutableSlide);
#endif
}
//第三步:实例化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
//load any inserted libraries
//第四步:插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
//记录插入的动态库的数量
sInsertedDylibCount = sAllImages.size()-1;
//link main executable
gLinkContext.linkingMainExecutable = true;
//第五步:链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
//第六步:链接插入的动态库
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}
//第七步:弱引用绑定主程序
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
//第八步:执行所有初始化(动态库+主程序)
initializeMainExecutable();
#endif
//第九步:查找程序入口函数 main并返回
notifyMonitoringDyldMain();
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}
CRSetCrashLogMessage("dyld2 mode");
#if !TARGET_OS_SIMULATOR
if (sLogClosureFailure) {
// We failed to launch in dyld3, but dyld2 can handle it. synthesize a crash report for analytics
dyld3::syntheticBacktrace("Could not generate launchClosure, falling back to dyld2", true);
}
#endif
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;
}
- 其流程可以总结为以下9个步骤:
- 【第一步:
环境变量的配置
】:检查设置的环境变量以及获取运行文件的主机信息;
- 【第二步:
加载共享缓存中的系统动态库
】
- 【第三步:
实例化主程序
】:调用instantiateFromLoadedImage
函数,返回了ImageLoaderMachO对象
- 【第四步:
插入系统动态库
】:遍历DYLD_INSERT_LIBRARIES环境变量
,然后调用loadInsertedDylib
加载,插入动态库;sInsertedDylibCount
用来记录插入动态库的数量。
- 【第五步:
链接主程序
】主程序sMainExecutable本质是ImageLoader,会将主程序的ImageLoader添加到sAllImages
集合中去;
- 【第六步:
链接动态库
】:会将各动态库的ImageLoader添加到sAllImages
集合中去;
- 【第七步:
Rebase重定位与符号的绑定bind
】:
-
Rebase重定位:镜像文件内部指针的重定位,修复正确的指向;
-
符号的绑定:镜像文件之间存在相互依赖,符号的绑定就是修正镜像外部指针的指向;
-
【第八步:
执行所有初始化 包括动态库与主程序
】:
-
dyld会优先初始化动态库,其次初始化主程序;
-
执行初始化的主要内容包括以下两个方面:主要涉及objc运行时
- 执行map_images函数,完成所有类与分类的注册,实现与初始化,
- 执行load_images函数,完成所有类与分类的load方法的调用,以及调用C++中的构造函数,创建C++的全局变量等等;
-
【第九步:
返回主程序入口即main函数
】:
下面重点分析第八步执行所有初始化,即调用initializeMainExecutable()
函数
image.png
- 其内部调用
runInitializers()
函数,注意调用者为ImageLoader
; - 函数实现如下:
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}
- 核心步骤为
processInitializers()
函数,其函数实现如下:
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}
- 看到循环遍历ImageLoader执行
recursiveInitialization
函数,其实现如下:
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
//核心步骤
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);
// initialize this image
bool hasInitializers = this->doInitialization(context);
// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);
if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}
- 其核心步骤有两个:
-
notifySingle()
,发现notifySingle()
先后调用了两次,第一次在doInitialization()
函数之前,第二次在doInitialization()
之后。 doInitialization()
-
notifySingle()函数的核心逻辑如下:
Snip20210304_16.png-
关键代码
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader())
-
第一次调用
notifySingle()
函数时,sNotifyObjCInit
是空的,因为sNotifyObjCInit
是在doInitialization()
调用-->_objc_init()
-->_dyld_objc_notify_register
-->registerObjCNotifiers
中赋值的; -
全局搜索
sNotifyObjCInit
,定位如下:
- 看到只有赋值操作,然后全局搜索
registerObjCNotifiers
,定位如下:
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);
}
- 最终定位到
void _dyld_objc_notify_register()
这个函数,此函数不是在dyld中调用的,而是在objc中调用的,这就涉及到两个不同底层库之间的通信; - objc调用
void _dyld_objc_notify_register()
注册了三个回调函数,当dyldimage镜像载入内存时
,load加载image镜像时
,image镜像从内存中移除时
,分别调用相应的回调函数; - 在objc4_781源码工程中全局搜索
_dyld_objc_notify_register(
,定位如下:
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();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
-
看到objc 在
_objc_init
中调用了_dyld_objc_notify_register
传入的三个参数就是三个回调函数,dyld会保存这三个回调函数,且在合适的时机去调用这三个回调函数; -
objc中的
load_images
方法实现如下:
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);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
- 核心逻辑
call_load_methods()
,其实现如下:
-
call_class_loads
实现如下:
- 流程到了这里,也就解释了为什么类的load类方法会比入口main函数率先调用;
- 同时也验证了上面控制台打印的函数调用堆栈;
- 先调用类的load方法,然后再调用分类的load方法;
总结:类的load方法调用堆栈:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> dyld::notifySingle --> sNotifyObjCInit --> load_images(libobjc.A.dylib)
- 在Xcode中加入符号断点
_objc_init
- 当工程执行到符号断点_objc_init 时在控制器台中输入bt,打印出系统函数的调用堆栈:
- 从函数堆栈我们看到中途调用的是
recursiveInitialization()
函数中的doInitialization()
也就是上面所说的核心步骤的第二个步骤,doInitialization()
函数实现如下:
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
其内部也包含两个核心步骤:
-
doImageInit()
:镜像初始化 doModInitFunctions()
- 其中
doImageInit()
与doModInitFunctions()
实现分别如下所示:
- 从上面的两个函数实现并没有看到
_objc_init()
的调用; - 看到注释在初始化时必须要先初始化
libSystem
,猜测是否在libSystem
的初始化中存在_objc_init()
的调用; - 下载Libsystem-1292.60.1源码工程,全局搜索
libSystem_initializer
函数,其实现如下所示:
- 其内部也存在dyld的初始化
_dyld_initializer()
,其次是libdispatch_init()
,根据_objc_init()
的调用堆栈,知道libdispatch_init()
是在libdispatch
开源库中,下载它的开源库 libdispatch-1173.0.3打开工程全局搜索libdispatch_init
- 全局搜索
_os_object_init(
,找到其函数实现如下所示:
- 到了这里终于看到了
_objc_init()
的调用。
总结_objc_init
的调用堆栈:
_dyld_start --> dyldbootstrap::start --> dyld::_main --> dyld::initializeMainExecutable --> ImageLoader::runInitializers --> ImageLoader::processInitializers --> ImageLoader::recursiveInitialization --> doInitialization -->libSystem_initializer(libSystem.B.dylib) --> _os_object_init(libdispatch.dylib) --> _objc_init(libobjc.A.dylib)
总结:
- Mach-O文件装载进入内存,创建App进程,接下来的工作交给Dyld,其链接动态库与主应用程序,最后返回Application的Main函数入口,其整体的流程如下图所示:
- objc调用
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
函数,然后dyld接收到这三个方法参数dyld::registerObjCNotifiers(mapped, init, unmapped)
,并用全局变量保存,对应关系如下: -
map_images
<==>sNotifyObjCMapped
-
load_images
<==>sNotifyObjCInit
-
unmap_images
<==>sNotifyObjCUnmapped
- 最后dyld在合适的时机,分别调用这三个函数方法;