iOS 底层原理探索 之 应用程序加载原理dyld (上)

2,056 阅读6分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程

以上内容的总结专栏


细枝末节整理


前言

本篇内容开始,我们将开启一个新的篇章。探索应用程序加载的一个过程。

  • 在开发过程中,我们会通过我们的代码来实现我们的功能业务,在完成代码的编写之后,是如何写入到系统的内存中执行的呢?
  • 开发过程中,我们都会用到很多的动态库、静态库。比如 UIKitAVFoundationCoreFoundation 等。这些动静态库文件如何加载到内存中的以供我们调用的呢?
  • 之前的内容中,我们主要探索的是 OBJC 的内容, 那么 OBJC 是如何启动的呢? 这都是今天我们探索的内容。

准备

项目文件

image.png

image.png

库:可执行的二进制文件,能够被操作系统加载到内存中。

  • 静态库:链接时,静态库会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝。 常见的格式.a.lib.framework
  • 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。 常见的格式 .dylib.framework

动静态库.001.jpeg

编译过程

源代码文件 -> 预编译 -> 编译 -> 汇编 -> 链接(动静态库) -> 可执行文件

  • 预编译:替换宏,删除注释,展开头文件,产生.i文件
  • 编译:将.i文件转换为汇编语言,产生.s文件
  • 汇编:将汇编文件转换为机器码文件,产生.o文件
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件

编译过程.001.jpeg

那么,程序引用的这些动静态库是如何加载到内存中去的呢?

动态链接器 dyly

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。

加载过程.001.jpeg

dyld流程 探索

下面,我们看一下从app启动,到调起main函数之间,中间系统是如何处理的。我们在main函数开始那一行打一个断点,然后运行项目。

image.png

可以看到在main之前只有一个start。 通过符号断点,我们也并没有断住这个start。 通过日志我们可以知道,ViewControllerload方法在main函数之前调用,那么,打个断点,再看看。

image.png

通过 bt 打印堆栈命令发现最开始 断在了 _dyld_start

捎带着我们整理下堆栈的信息,对于接下来的探索是有引导作用的(这个堆栈信息将是今天探索过程的主线任务):

  • dyld_dyld_start
  • dylddyldbootstrap::start
  • dylddyld::_main
  • dylddyld::initializeMainExecutable()
  • dyldImageLoader::runInitializers
  • dyldImageLoader::processInitializers
  • dyldImageLoader::recursiveInitialization
  • dylddyld::notifySingle
  • libobjc.A.dylibload_images
  • +[ViewController load]

我们从 _dyld_start 开始 image.png

下面我们就下载好一份 dyld 的源码 通过定位_dyld_start 来进一步分析dyld在整个过程中是怎么一个流程并且做了什么事情。

在源码中搜索 _dyld_start, 在dyldStartup.s文件中看到了多条搜索结果,上下翻阅之后,是在不同的环境下的多个结果。 在 __i386____x86_64____arm____arm64__不同的架构下,略微有些区别,但是最终都是会调用到 c++函数 dyldbootstrap::start

// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
	ldr	r0, [r8]	// r0 = mach_header
	ldr	r1, [r8, #4]	// r1 = argc
	add	r2, r8, #8	// r2 = argv
	adr	r3, __dyld_start
	sub	r3 ,r3, #0x1000 // r3 = dyld_mh
	add	r4, sp, #12
	str	r4, [sp, #0]	// [sp] = &startGlue

在全局搜索dyldbootstrap::start 无果的情况下,我们搜索dyldbootstrap试试,结果找到了一个命名空间,整体的代码我们就不贴出来了,直接定位其 start 函数, 其内部最终会调用到了 dyld::_main

//
//  这是代码引导dyld。这个工作通常由dyld和crt来完成。 
//  在dyld中,我们必须手动做这个。
//
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

    // 发出kdebug跟踪点来指示dyld引导已经启动 <rdar://46878536>
    dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

	// 如果内核必须滑动dyld,我们需要修复加载敏感位置
	// 我们必须在使用任何全局变量之前做到这一点
    rebaseDyld(dyldsMachHeader);

	// 内核将env指针设置为刚过agv数组的末端
	const char** envp = &argv[argc+1];
	
	// 内核将apple指针设置为envp数组的末尾
	const char** apple = envp;
	while(*apple != NULL) { ++apple; }
	++apple;

	// 设置堆栈金丝雀的随机值
	__guard_setup(apple);

#if DYLD_INITIALIZER_SUPPORT
	// 在dyld中运行所有c++初始化器
	runDyldInitializers(argc, argv, envp, apple);
#endif

	_subsystem_init(apple);

	//  现在我们已经完成了dyld的引导,调用dyld的main
	uintptr_t appsSlide = appsMachHeader->getSlide();
	return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyld::_main 内部,根据最终返回result我们找到了 是通过 sMainExecutable 有着深深的联系。 再通过 sMainExecutable 接着寻找,

// 为主可执行文件实例化ImageLoader
		sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);         

跟源码是一个慢慢探索未知的过程,接着源码流程进去一层一层探索,我们总结了以下的流程图。

dyld流程图

myNoteBase_副本.001.jpeg

流程分析下来之后可以看到最终是来到了 objc_init,

/***********************************************************************
* _objc_init 
* 引导程序初始化。用dyld注册我们的图像通知程序。 
* 在库初始化时间之前被libSystem调用
**********************************************************************/

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
}

可以看到调用了方法 _dyld_objc_notify_register(&map_images, load_images, unmap_image);

  1. 这个方法中的 map_images是在什么时候调用的呢?
  2. load_images方法执行的时候其内部的一个流程是什么样的?
  3. 最后,我们现在所在的 dyld 阶段是如何进入到 main 函数主程序的呢?

下一篇,我们继续上面流程分析进而来解答上面我们提出的三个问题。