iOS底层学习 - 从编译到启动的奇幻旅程(二)

3,635 阅读12分钟

上一章我们经过编译的旅程,我们的App已经成功编译完成,生成了对应的Mach-O可执行文件,那么我们之后要进行启动的相关操作了,启动的时候,我们是如何加载的动态库,如果执行类似objc_init这些代码的呢

编译过程传送门☞iOS底层学习 - 从编译到启动的奇幻旅程(一)

在运行的时候,我们一般都已main函数为起点,来进行代码编写,但是我们发现main函数之前我们也进行了许多的操作,比如dyld的一系列操作,本章就来详细探究

装载与动态链接

首先安利一本书《程序员的自我修养--链接、装载与库》,看完神清气爽。

一个App从可执行文件到真正启动运行代码,基本需要经过装载和动态库链接两个步骤

装载

可执行文件(程序)是一个静态的概念,在运行之前它只是硬盘上的一个文件;而进程是一个动态的概念,它是程序运行时的一个过程,我们知道每个程序被运行起来后,它会拥有自己独立的虚拟地址空间,这个地址空间大小的上限是由计算机的硬件(CPU的位数)决定的。

进程的虚拟空间都在操作系统的掌握之中,且在操作系统中会同时运行着多个进程,它们彼此之间的虚拟地址空间是隔离的,如果进程访问了操作系统分配给该进程以外的地址空间,会被系统当做非法操作而强制结束进程。

装载就是将硬盘上的可执行文件映射到虚拟内存中的过程,但内存是昂贵且稀有的,所以将程序执行时所需的指令和数据全部装载到内存中显然是行不通的,于是人们研究发现了程序运行时是有局部性原理的,可以只将最常用的部分驻留在内存中,而不太常用的数据存放在磁盘里,这也是动态装载的基本原理

装载的过程也可以理解为进程建立的过程,操作系统只需要做以下三件事情:

  • 创建一个独立的虚拟地址空间
  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
  • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行

动态库链接

概念

链接的共用库分为静态库和动态库:静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新;而动态库是运行时链接的库,使用 dyld 就可以实现动态加载。

在真实的 iOS 开发中,你会发现很多功能都是现成可用的,不光你能够用,其他 App 也在用,比如 GUI 框架、I/O、网络等。链接这些共享库到你的Mach-O文件,也是通过链接器来完成的。

iOS 中用到的所有系统framework(UIKit,Foundation等)都是动态链接的,类比成插头和插排,静态链接的代码在编译后的静态链接过程就将插头和插排一个个插好,运行时直接执行二进制文件;而动态链接需要在程序启动时去完成“插插销”的过程,所以在我们写的代码执行前,动态连接器需要完成准备工作。

共享缓存

为了节约空间 , 苹果将这些系统库放在了一个地方 : 动态库共享缓存区 (dyld shared cache)

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以Mach-O文件中并没有包含动态库里的符号定义

也就是说,这些符号会显示为未定义,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopendlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

dlopen会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。dlopen也可以选择是立刻解析所有引用还是滞后去做。dlopen 打开动态库后返回的是引用的指针,dlsym的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。

优点

系统使用动态库链接的好处如下:

  • 代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中中只有一份
  • 易于维护:由于被依赖的 lib 是程序执行时才 link 的,所以这些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成 libSystem.C.dylib 然后再替换替身就行了
  • 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多

从dyld看程序启动

简介

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

dyld的相关代码是开源的☞源码地址

启动流程

创建一个空工程,我们知道load函数是优于main函数来调用的,所以将断点打在load方法里,看一下函数的调用堆栈。

我们可以看到load方法钱,几乎全是dyld动态链接器的调用,从_dyld_start开始

dyldbootstrap::start

dyldbootstrap::start 就是指 dyldbootstrap 这个命名空间作用域里的 start 函数 。来到源码中,搜索 dyldbootstrap,然后找到 start 函数。

//
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
//  In dyld we have to do this manually.
//
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
				intptr_t slide, const struct macho_header* dyldsMachHeader,
				uintptr_t* startGlue)
{
	// if kernel had to slide dyld, we need to fix up load sensitive locations
	// we have to do this before using any global variables
    slide = slideOfMainExecutable(dyldsMachHeader);
    bool shouldRebase = slide != 0;
#if __has_feature(ptrauth_calls)
    shouldRebase = true;
#endif
    if ( shouldRebase ) {
        rebaseDyld(dyldsMachHeader, slide);
    }

	// allow dyld to use mach messaging
	mach_init();

	// 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(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif

	// now that we are done bootstrapping dyld, call dylds main
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

start函数主要的调用流程为:

1.首先进行bootstrap自举操作,因为dyld本身也是一个动态库,但是由于它需要链接其他动态库,所以它不依赖其他库,且本身所需要的全局和静态变量的重定位工作由它本身完成,这样就防止了“蛋生鸡,鸡生蛋”的问题

  • const struct macho_header这个指Mach-O文件里的header
  • intptr_t slide这个其实就是 ALSR , 说白了就是通过一个随机值 ( 也就是我们这里的 slide ) 来实现地址空间配置随机加载 ,防止被攻击
  • rebaseDyld是dyld的重定向

2.开放函数消息使用:mach_init()

3.设置堆栈保护:__guard_setup

4.开始链接共享对象:dyld::_main

dyld::_main

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链接的主要函数,代码太长,逐步分析...
}

1.配置环境变量等

1.1 从环境变量中主要可执行文件的cdHash。其中环境变量是系统定义的,可以再Xcode中进行配置

1.2 设置上下文信息setContext

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

1.4 检查环境变量checkEnvironmentVariables

1.5 获取程序架构getHostInfo

2.加载共享缓存

2.1 验证共享缓存路径:checkSharedRegionDisable

2.2 加载共享缓存:mapSharedCache

3. 添加dyld到UUID列表

将dyld本身添加到UUID列表addDyldImageToUUIDList

4.reloadAllImages

4.1 实例化主程序instantiateFromLoadedImage

sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

内核会映射到主可执行文件中。我们需要已经映射到主可执行文件中的文件创建一个ImageLoader

// The kernel maps in main executable before dyld gets control.  We need to 
// make an ImageLoader* for the already mapped in main executable.
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";
}

通过instantiateMainExecutable中的sniffLoadCommands加载主程序其实就是对MachO文件中LoadCommons段的一些列加载

  • 最大的segment数量为256个!
  • 最大的动态库(包括系统的个自定义的)个数为4096个!
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)
{
    ...
    for (uint32_t i = 0; i < cmd_count; ++i) {
    ...
}

生成镜像文件后,添加到sAllImages全局镜像中,主程序永远是sAllImages的第一个对象

static void addImage(ImageLoader* image)
{
	// add to master list
    allImagesLock();
        sAllImages.push_back(image);
    allImagesUnlock();
    ...
}

4.2 加载插入动态库loadInsertedDylib

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

4.3 链接主程序link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -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 ( sMainExecutable->forceFlat() ) {
			gLinkContext.bindFlat = true;
			gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
		}

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

5.运行所有初始化程序

函数调用为initializeMainExecutable();。为主要可执行文件及其带来的一切运行初始化程序

5.1 runInitializers->processInitializers初始化准备

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

5.2 遍历image.count,递归开始初始化镜像,

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.images[i]->recursiveInitialization(context, thisThread, images.images[i]->getPath(), timingInfo, ups);
	}
	// If any upward dependencies remain, init them.
	if ( ups.count > 0 )
		processInitializers(context, thisThread, timingInfo, ups);
}

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

5.3.1 notifySingle获取到镜像的回调

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{ ... }

重头戏来了 . 根据函数调用栈我们发现 , 下一步是调用load_images , 可是这个 notifySingle 里并没有找到 load_images,其实这是一个回调函数的调用

5.3.2 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());
		}
	}
}

5.3.3 registerObjCNotifiers的调用在_dyld_objc_notify_register函数中

这个函数是用来给外部共享动态库调用的,比如runtime中需要加载的objc

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

我们可以看到源码中在_objc_init调用了_dyld_objc_notify_register

3个参数的含义如下:

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

5.4 doInitialization这是一个系统特定的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);
}

这种C++构造函数有特定的写法,在MachO文件中找到对应的方法,如下

__attribute__((constructor)) void CPFunc(){
    printf("C++Func1");
}

6.notifyMonitoringDyldMain监听dyld的main

7.找到main函数的调用

找到真正 main 函数入口 并返回.

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

小结

至此,整个启动流程结束了

大体runtime的加载流程如下

  • dyld 开始将程序二进制文件初始化
  • 交由 ImageLoader 读取 image,其中包含了我们的类、方法等各种符号
  • 由于 runtime 向 dyld 绑定了回调,当 image 加载到内存后,dyld 会通知 runtime 进行处理
  • runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用call_load_methods方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 +load 方法和其 Category 的 +load 方法

总结

流程简图

dyld调用顺序

1.从 kernel 留下的原始调用栈引导和启动自己

2.将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制

3.non-lazy 符号立即 link 到可执行文件,lazy 的存表里

4.Runs static initializers for the executable

5.找到可执行文件的 main 函数,准备参数并调用

6.程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口

7.程序main函数 return 后执行 static terminator

8.某些场景下 main 函数结束后调 libSystem 的 _exit 函数

层级顺序图

参考