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

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

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

在运行的时候,咱们通常都已main函数为起点,来进行代码编写,可是咱们发现main函数以前咱们也进行了许多的操做,好比dyld的一系列操做,本章就来详细探究github

装载与动态连接

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

一个App从可执行文件到真正启动运行代码,基本须要通过装载和动态库连接两个步骤缓存

装载

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

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

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

装载的过程也能够理解为进程创建的过程,操做系统只须要作如下三件事情:app

  • 建立一个独立的虚拟地址空间
  • 读取可执行文件头,而且创建虚拟空间与可执行文件的映射关系
  • 将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 函数

层级顺序图

参考

相关文章
相关标签/搜索