iOS底层探索之dyld(下):动态链接器流程源码分析

50 篇文章 5 订阅
2 篇文章 1 订阅

1.回顾

上一篇博文中介绍了动态库静态库的区别,对dyld动态链接器做了初步的探索分析,本篇博文就进一步的对dyld的源码进行分析。

在这里插入图片描述

2. MachO

上篇文章中,已经找到了dyld的入口了,但是在分析源码之前,还得补充点内容。

在iOS中Mach-O(可执行文件)怎么获取呢?

2.1 macOS工程查看MachO

直接编译运行之后就可以得到Mach-O,就是下面这个黑不溜秋的东西。
在这里插入图片描述

2.2 iOS工程查看MachO

iOS工程的话就需要找到Products里面的.app文件

.app文件

然后Show in Finder找到文件所在位置

查看可执行文件

同样这个黑不溜秋的就是MachO可执行文件

MachO文件

2.3 MachOView查看MachO结构

把这个MachO文件,拖拽到MachOView里面就可以查看MachO的结构。
MachOView查看结构

  • Header 头部,包含可以执行的CPU架构,比如x86,arm64
  • Load commands 加载命令,包含文件的组织架构和在虚拟内存中的布局方式
  • Data,数据,包含load commands中需要的各个段(segment)的数据,每一个Segment都得大小是Page的整数倍。

MachO结构

3. dyld 源码分析

3.1 dyld::_main

dyld的入口main函数,好家伙!我直呼好家伙啊!近千行的代码!

我直呼好家伙啊!dyld::_main

这太长了,代码就不贴出来了,一贴出来,本篇博文基本就结束了,太TM长了😂。

  • 弱水三千,我只取一瓢,
  • 代码千行,我只看几行!

好诗好诗啊!哈哈😁

3.1.1 环境变量设置

从底层源码的注释也能知道,这里是dyld的入口

//
// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
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)
	// Check and see if kernel disabled JOP pointer signing (which lets us load plain arm64 binaries)
	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

上面截取main函数部分代码, 主要是if条件对各种环境变量设置的判断

3.1.2 平台信息设置

在所有镜像文件中设置平台 ID,以便于调试器可以判断进程类型。
注意:这里的image不是图像的意思,是指镜像

在这里插入图片描述

3.1.3 共享缓存

检查是否开启,以及共享缓存是否映射到共享区域,这都是系统级别的,系统控制的,缓存是很宝贵的资源。

  • mapSharedCache
	if ( sJustBuildClosure )
		sClosureMode = ClosureMode::On;

	// load shared cache
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
		if ( sSharedCacheOverrideDir)
			mapSharedCache(mainExecutableSlide);
#else
		mapSharedCache(mainExecutableSlide);
#endif

太难了!这么上千行的代码一行一行的往下看,不说眼睛受不了,人都要疯了!(PS:痛苦)
我太难了

那么博主,你有什么好的探索方式吗?
哎,巧了!靓仔!还真有哦!

反推,直接看最后一行代码。我们从结果反推,看看都是什么条件导致的最后return

在这里插入图片描述
通过搜索在main函数里面定位result,发现和sMainExecutable有关系。


		CRSetCrashLogMessage(sLoadingCrashMessage);
		// instantiate ImageLoader for main executable
		sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
		gLinkContext.mainExecutable = sMainExecutable;
		gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

再搜索看看sMainExecutable是什么关键的东西。

sMainExecutable

貌似找sMainExecutable是找对了,在推导的时候,我们要明确我们的目标是要找什么?我们现在不就是要找images镜像嘛!所以就应该对linkbindload这些词要敏感。

从上图中代码中的一些关键代码sInsertedDylibCount/weakBind /linkingMainExecutable这些也可以验证,我们找sMainExecutable是找对了。

3.1.4 link 链接

从代码中也可以发现,sMainExecutablelink链接的一个参数

// load any inserted libraries 加载任何插入的库
		if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
			for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
				loadInsertedDylib(*lib);
		}
		// record count of inserted libraries so that a flat search will look at 
		// inserted libraries, then main, then others.
		sInsertedDylibCount = sAllImages.size()-1;

		// link main executable 链接main可执行文件
		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 ( sMainExecutable->forceFlat() ) {
			gLinkContext.bindFlat = true;
			gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
		}

  • 遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载动态库。
  • 链接主程序
3.1.5 主程序main的入口
	#if SUPPORT_OLD_CRT_INITIALIZATION
		// Old way is to run initializers via a callback from crt1.o
		if ( ! gRunInitializersOldWay ) 
			initializeMainExecutable(); 
	#else
		// run all initializers
		initializeMainExecutable(); 
	#endif

		// notify any montoring proccesses that this process is about to enter main()
		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);

#if TARGET_OS_OSX
		if ( gLinkContext.driverKit ) {
			result = (uintptr_t)sEntryOverride;
			if ( result == 0 )
				halt("no entry point registered");
			*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
		}
		else
#endif
		{
			// find entry point for main executable
			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()
// main 可执行文件使用 LC_MAIN,我们需要使用 libdyld 中的 helper 来调用 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;
			}
		}
	}
  • 执行初始化方法initializeMainExecutable
  • notifyMonitoringDyldMain通知任何监控进程,此进程即将进入main()
  • 通过if判断result寻找主程序的入口点
  • result != 0 时:使用LC_MAIN,我们需要使用 libdyld 中的 helper来调用 main()
  • result == 0: 使用 LC_UNIXTHREADdyld 需要让程序中的“start”main()设置

3.2 initializeMainExecutable

3.2.1 runInitializers

拿到镜像文件的个数,循环开始对镜像进行初始化

// run initialzers for any inserted dylibs
	ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
	initializerTimes[0].count = 0;
	const size_t rootCount = sImageRoots.size();
	if ( rootCount > 1 ) {
		for(size_t i=1; i < rootCount; ++i) {
			sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
		}
	}
	
	// run initializers for main executable and everything it brings up 
	sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
	

初始化进行前期的相关准备 runInitializers -> processInitializers

3.2.2 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);
}

通过recursiveInitialization递归初始化,下面是核心代码部分

3.2.3 recursiveInitialization
// 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);
			
3.2.4 notifySingle

recursiveInitialization可以找到notifySingle

notifySingle

  • notifySingle重点代码

路径加载和镜像文件加载是重点

notifySingle重点代码

全局搜索sNotifyObjCInit,发现是_dyld_objc_notify_init类型

sNotifyObjCInit

3.2.5 objc_init和dyld的联系

registerObjCNotifiers方法的第二个参数赋值给了sNotifyObjCInit

registerObjCNotifiers

全局搜索registerObjCNotifiers看看哪里调用了

发现,在dyldAPIs.cpp文件中找到了registerObjCNotifiers调用

// _dyld_objc_notify_register
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看着好眼熟啊!似曾相识燕归来啊!
这不就是libobjc.dylib源码里面_objc_init方法调用了啊!如下图:

_objc_init

在调用_dyld_objc_notify_register函数时,传入了三个参数(map_images的函数地址 、load_images函数unmap_image函数)

那么回到dyld源码的工程,registerObjCNotifiers里面是这样的

sNotifyObjCMapped	= mapped = &map_images
sNotifyObjCInit    = init   = load_images
sNotifyObjCUnmapped = unmapped = unmap_image

到此也就发现,objc_initdyld关联起来了

objc_init()dyld中注册了三个函数,在dyld进行动静态库加载过程时,当特定环境满足的条件下,这三个函数会调用执行。

实在是妙啊!

之前一直在dyld的源码里面,现在我们回到_objc_init里面在进行正向的猜测探索。
_objc_init函数里面打上断点,查看堆栈信息,发现_oc_object_init是在libdispatch的源码里面。

在这里插入图片描述

那么现在就去libdispatch的源码里面看看

libdispatch-1271.120.2源码
_oc_object_init

libdispatch的源码里面发现了_oc_object_init的调用是在libdispatch_init,那么libdispatch_init是由谁来发起的呢?
从堆栈信息发现,是libSystem_initializer

堆栈信息

那么现在又得去LibSystem源码里面看看,发现libdispatch_init确实调用了

Libsystem-1292.120.1源码
LibSystem

那么libSystem_initializer又是谁来发起的呢?
通过堆栈信息可以发现是doModInitFunctions

在这里插入图片描述

doModInitFunctions又回到了dyld了,这就是反向推导到正向的验证过程。

doModInitFunctionsImageLoaderMachO::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);
}

ImageLoader::recursiveInitialization里面又调用了doInitialization,递归初始化镜像文件

	// 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);

总结一下: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)

_dyld_objc_notify_register 里面的方法是在什么时候调用的呢?

3.3 map_images和load_images

3.3.1 map_images

回到libObjc.dylib也就是objc的源码工程,在map_imagesload_images的方法处分别打上断点,发现是先走到了map_images处,再控制台bt打印堆栈信息

map_images

map_images方法首先执行的,再查看运行堆栈,其流程为: _dyld_objc_notify_register --> registerObjCNotifiers --> notifyBatchPartial --> map_images
dyld源码中也可以验证,如下:

在这里插入图片描述

3.3.2 load_images

点击继续运行到load_images断点处,再打印堆栈信息

load_images

由此可以知道load_images调用时机:
_dyld_objc_notify_register --> registerObjCNotifiers --> load_images

3.4 main调用时机探索

在上一篇博文iOS底层探索之dyld(上)中,我们有一个测试案例,执行顺序是+ load --> c++ --> main函数
那么就去源码里面探索下,到底是怎么走到main函数的。

3.4.1 load方法调用过程
load_images(const char *path __unused, const struct mach_header *mh)
{
   ....省略代码....

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

这里对所有类的load方法prepare,那么进入prepare_load_methods方法,主要是对所有镜像的懒加载的类、分类prepare,说白了就是找load methods

classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);

schedule_class_load为 类 和任何未加载的superclasses类递归调度 +load

// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

add_class_to_loadable_list把所有的+load收集到一起,先是类,然后再是分类的。

/***********************************************************************
* add_class_to_loadable_list
* Class cls has just become connected. Schedule it for +load if
* it implements a +load method.
**********************************************************************/
void add_class_to_loadable_list(Class cls)
{
    IMP method;

    loadMethodLock.assertLocked();

    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));
    }
    
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

递归获取方法收集在loadable_classes[loadable_classes_used].method,是从通过getLoadMethod方法获取的。

/***********************************************************************
* objc_class::getLoadMethod
* fixme
* Called only from add_class_to_loadable_list.
* Locking: runtimeLock must be read- or write-locked by the caller.
**********************************************************************/
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) {
        for (const auto& meth : *mlist) {
            const char *name = sel_cname(meth.name());
            if (0 == strcmp(name, "load")) {
                return meth.imp(false);
            }
        }
    }

    return nil;
}

递归所有的baseMethods(),通过strcmp匹配"load",这就是load方法的调用过程

3.4.2 C++函数调用时机

C++函数开始处打上断点,然后再bt打印调用堆栈信息查看

bt查看堆栈信息

从堆栈信息可知是按doInitialization -->doModInitFunctions --> C++(JPFunc)调用顺序,然后我们回到dyld源码搜索doInitialization

doInitialization

在上面已经验证过了notifySingle的里面的流程是load_images方法的调用最后再到load,所以上图也验证了load方法是在C++之前的调用的。

那么再根据堆栈的调用进入到doModInitFunctions方法里面。

doModInitFunctions

这个doModInitFunctions里面就是对于macho_header 的处理,包括Load commandsmacho_segment_commandfor循环遍历macho_section里面的函数指针,也就是包括了C++方法。

3.4.3 dyld如何到main.m函数

C++函数后,dyldbootstrap::start后,会寻找main函数。从堆栈可以知道是从dyld_dyld_start开始

__dyld_start

_dyld_start是汇编写的,从汇编可以知道main函数存在rax寄存器里面,最后会jmp跳转到rax执行main函数。
那么现在去工程代码里面验证一下。

代码验证

通过代码的汇编跟踪调试,也可以发现是和dyld源码里面的汇编执行是一模模一样样。

在这里插入图片描述

通过register read打印寄存器信息也可以发现,rax里面存的就是main,最后通过register read rax读取rax验证了rax = main函数。这就是dyldmain的过程。

这就是dyld的探索分析过程,试问还有谁能,把这么复杂的东西,分析的这么有条有理,我这该死的无处安放的魅力啊!哈哈😁
厉害

4. 总结

  • dyld分析反推到正向验证

  • notifySingle单个通知注入

  • _dyld_objc_notify_register注册回调函数,下句柄,类似blockdyld里面实现了map_imagesload_imagesunmap_image之后回调给_objc_init就可以开始正常调用了。

  • libSystem库是最先开始初始化调用的,在ImageLoaderMachO::doModInitFunctionsImageLoaderMachO::doImageInit里面可以验证,因为objc的相关操作依赖系统,dyld也等系统相关库初始化完成,才对镜像进行初始化、映射等操作。
    doModInitFunctions

  • main 、+ load 、C++执行顺序是+ load --> c++ --> main函数

  • 最后奉上dyld流程分析图

dyld流程分析

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卡卡西Sensei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值