八:底层探索-应用程序的加载之dyld

2020/3/30 23:02:37

本文主要是介绍八:底层探索-应用程序的加载之dyld,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

在之前的篇章中,我们了解的方法调用的本质,知道了消息查找以及消息转发,今天我们来研究一个新的话题,应用程序的加载。

  曾经我在面试的时候被问过这么一个问题:点击Runmain函数之间发生了什么?在此也是借这个问题来展开我们的分析。当我们点击Run之后,会经历一个编译到执行的过程,本篇暂时对编译部分不做分析(正在看《程序员的自我修养》这本书,对理解编译很有帮助,后期也会写一篇文章来进行总结和串联),因此本文将通过start函数并结合Dyld来进行分析。

一:知识前导

1. Dyld是什么

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在应用被编译打包成可执行文件格式的 Mach-O 文件之后,交由 dyld 负责链接,加载程序 。

dyld源码是开源的,可以到 官网 进行下载

2. 共享缓存

在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着。

没有动态库缓存的情况

由图可以看出:如果没有缓存库存在的话,那么我们手机上的每一个App,如果要用到系统动态库的话,是需要每一个App都要去加载一次的,一样的资源被加载多次,无论是空间还是执行效率,都是造成了浪费。

有动态库缓存的情况

如果有缓存库存在的话,那么我们手机上的每一个App,如果要用到系统动态库的话,都去加载缓存库就好了,加载缓存库里的动态库会通过dyld这个动态连接器,dyld在加载动态库会做些优化。

3. 延伸

既然动态库是运行时才加载到内存的,那么意味着Mach-O中没有这些内容,那么系统是如何找到外部函数的地址并进行调用的呢?

  1. 在工程编译时 , 所产生的 Mach-O 可执行文件中会预留出一段空间 , 这个空间其实就是符号表 , 存放在 _DATA 数据段中 ( 因为 _DATA 段在运行时是可读可写的 )
  2. 编译时 : 工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址 , ( 例如工程中有一个 NSLog , 那么编译时就会在 Mach-O 中创建一个 NSLog 的符号 , 工程中的 NSLog 就指向这个符号 )
  3. 运行时 : 当 dyld将应用进程加载到内存中时 , 根据 load commands 中列出的需要加载哪些库文件 , 去做绑定的操作 ( 以 NSLog 为例 , dyld 就会去找到 FoundationNSLog 的真实地址写到 _DATA 段的符号表中 NSLog 的符号上面 )

这个过程我们称之为:PIC技术(Position Independent Code : 位置代码独立 )

二:dyld加载过程

1. main函数

我们在main函数中打上断点,可以看到在main函数之前还调用了start

打开混编,可以发现start的调用位于libdyld.dylib这个库中

也就是说主程序的main函数是由dyld调起的。下面我们将对dyld源码 进行解读

2. start函数

dyld的源码中,在dyldInitialization.cpp文件中找到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);

	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    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;

	// set up random value for stack canary
	__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
	// run all C++ initializers inside dyld
	runDyldInitializers(argc, argv, envp, apple);
#endif

	// now that we are done bootstrapping dyld, call dyld's main
	uintptr_t appsSlide = appsMachHeader->getSlide();
	return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
复制代码

在此对函数中两个重要参数稍作解释

  1. const struct macho_header* appsMachHeader , 这个参数就是 Mach-Oheader.
  2. intptr_t slide , 这个其实就是 ALSR, 说白了就是通过一个随机值 ( 也就是我们这里的 slide ) 来实现地址空间配置随机加载
  3. 物理地址 = ALSR + 虚拟地址 ( 偏移 )

那么在这个函数中到底做了什么呢?

  • 根据计算出来的 ASLRslide 来重定向 macho .

  • 初始化 , 允许 dyld 使用 mach 消息传递 .

  • 栈溢出保护 .

  • 初始化完成后调用 dyldmain 函数 ,dyld::_main

3. dyld::_main

//
// 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)
{
    ......讲真的,代码太长,下面会挑重点进行分析
}
复制代码

老规矩先看注释:dyld的入口。内核程序加载dyld并跳转到__dyld_start__dyld_start是个函数,它会对寄存器进行设置后调用此函数。返回目标程序的main()函数地址。

接下来我将挑重点进行分析

3.1 前期准备

3.1.1 环境变量的配置

这部分代码篇幅较长就不贴代码了,大家可以自己去看一下,当然我们也可以在Xcode中设置环境变量

3.1.2 设置上下文信息 setContext

setContext(mainExecutableMH, argc, argv, envp, apple);
复制代码

3.1.3 检测线程是否受限,并做相关处理 configureProcessRestrictions

configureProcessRestrictions(mainExecutableMH, envp);
复制代码

3.1.4 检查环境变量 checkEnvironmentVariables

{
	checkEnvironmentVariables(envp);
	defaultUninitializedFallbackPaths(envp);
}
复制代码

3.1.5 获取程序架构getHostInfo

{
	getHostInfo(mainExecutableMH, mainExecutableSlide);
}
复制代码

3.2 加载共享缓存

3.2.1 检测共享缓存禁用状态

checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
复制代码

3.2.2 加载共享缓存库 mapSharedCache

	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
		if ( sSharedCacheOverrideDir)
			mapSharedCache();
#else
		mapSharedCache();
#endif
复制代码

3.3 添加dyld到UUID列表

将dyld本身添加到UUID列表addDyldImageToUUIDList

// add dyld itself to UUID list
		addDyldImageToUUIDList();
复制代码

3.4 reloadAllImages

3.4.1 实例化主程序 instantiateFromLoadedImage

// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
复制代码
static ImageLoaderMachO* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path)
{
	// try mach-o loader
	if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
		ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
		addImage(image);
		return (ImageLoaderMachO*)image;
	}
	
	throw "main executable not a known format";
}
复制代码
  1. 在判断条件中,isCompatibleMachO会去Mach-O的head去检测兼容情况

  1. 通过instantiateMainExecutable中的sniffLoadCommands加载主程序其实就是对MachO文件中LoadCommons段的一些列加载
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)
{
    *compressed = false;
	*segCount = 0;
	*libCount = 0;
	*codeSigCmd = NULL;
	*encryptCmd = NULL;
	/*
	...省略掉.
	*/
	// fSegmentsArrayCount is only 8-bits
	if ( *segCount > 255 )
		dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

	// fSegmentsArrayCount is only 8-bits
	if ( *libCount > 4095 )
		dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

	if ( needsAddedLibSystemDepency(*libCount, mh) )
		*libCount = 1;
}
复制代码

这里几个参数我们稍微说明下 :

  • compressed -> 根据 LC_DYLD_INFO_ONYL 来决定 .
  • segCount 段命令数量 , 最大不能超过 255 个.
  • libCount 依赖库数量 , LC_LOAD_DYLIB (Foundation / UIKit ..) , 最大不能超过 4095 个.
  • codeSigCmd , 应用签名
  • encryptCmd , 应用加密信息
  1. 生成镜像文件后,添加到sAllImages全局镜像中
static void addImage(ImageLoader* image)
{
	// add to master list
    allImagesLock();
        sAllImages.push_back(image);
    allImagesUnlock();
    ......
}
复制代码

经过以上步骤 , 主程序的实例化就已经完成了

3.4.2 加载插入动态库

// load any inserted libraries
if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
	for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
	loadInsertedDylib(*lib);
}
复制代码

3.4.3 链接主程序

// 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 ( sMainExecutable->forceFlat() ) {
			gLinkContext.bindFlat = true;
			gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
		}
复制代码

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1)中链接主程序中各动态库,进行符号绑定

到了这里,配置环境变量 -> 加载共享缓存 -> 实例化主程序 -> 加载动态库 -> 链接动态库 就已经完成了

3.5 运行所有初始化程序initializeMainExecutable()

在这里将会为主要可执行文件及其带来的一切运行初始化程序
调用顺序 initializeMainExecutableinitializeMainExecutable -> runInitializers -> processInitializers -> 递归调用 recursiveInitialization

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 ) {
		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]);
	
	// 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]);
}
复制代码

3.5.1 初始化准备runInitializers

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
	uint64_t t1 = mach_absolute_time();
	mach_port_t thisThread = mach_thread_self();
	ImageLoader::UninitedUpwards up;
	up.count = 1;
	up.images[0] = this;
	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);
}
复制代码

3.5.2 processInitializers遍历image.count,递归开始初始化镜像`

// <rdar://problem/14412057> upward dylib initializers can be run too soon
// To handle dangling dylibs which are upward linked but not downward, all upward linked dylibs
// have their initialization postponed until after the recursion through downward dylibs
// has completed.
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);
}
复制代码

3.5.3 recursiveInitialization获取到镜像的初始化

void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
										  InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
    ...
    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);
    ...
}
复制代码

notifySingle获取到镜像的回调

void (*notifySingle)(dyld_image_states, const ImageLoader* image, InitializerTimingList*);
复制代码

根据调用堆栈,我们知道下一步就是调用load_images

但是在回调里压根看不到相关的内容,其实这是一个回调函数的调用

在if判断条件中对sNotifyObjCInit进行了非空判断,也就是有值,在本文件搜索,发现它在registerObjCNotifiers中被赋值

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}
复制代码

全局搜索,发现只在_dyld_objc_notify_register调用了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);
}
复制代码

在此对其中的参数稍作解释:

  • map_images : dyld 将 image 加载进内存时 , 会触发该函数.
  • load_images : dyld 初始化 image 会触发该方法. ( 我们所熟知的 load 方法也是在此处调用 ) .
  • unmap_image : dyld 将 image 移除时 , 会触发该函数 .

通过符号断点的方式,我们发现在_objc_init调用了_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();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
复制代码

3.5.4 doInitialization

doModInitFunctions 中 , 值得一提的是会调用 c++ 的构造方法 .

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);
}
复制代码

3.6 notifyMonitoringDyldMain监听dyld的main

3.7 找到主程序的入口

// find entry point for main executable
  result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
复制代码

到此,dyld的加载流程结束

四:总结

流程简图

流程细分

五:参考

iOS逆向- 动态库共享缓存(dyld shared cache)

iOS 底层 - 从头梳理 dyld 加载流程



这篇关于八:底层探索-应用程序的加载之dyld的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程