写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
以上内容的总结专栏
细枝末节整理
前言
本篇内容开始,我们将开启一个新的篇章。探索应用程序加载的一个过程。
- 在开发过程中,我们会通过我们的代码来实现我们的功能业务,在完成代码的编写之后,是如何写入到系统的内存中执行的呢?
- 开发过程中,我们都会用到很多的动态库、静态库。比如
UIKit
、AVFoundation
、CoreFoundation
等。这些动静态库文件如何加载到内存中的以供我们调用的呢? - 之前的内容中,我们主要探索的是 OBJC 的内容, 那么 OBJC 是如何启动的呢? 这都是今天我们探索的内容。
准备
项目文件
库
库:可执行的二进制文件,能够被操作系统加载到内存中。
- 静态库:链接时,静态库会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝。 常见的格式
.a
、.lib
、.framework
。 - 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。 常见的格式
.dylib
、.framework
。
编译过程
源代码文件 -> 预编译 -> 编译 -> 汇编 -> 链接(动静态库) -> 可执行文件
- 预编译:替换宏,删除注释,展开头文件,产生.i文件
- 编译:将.i文件转换为汇编语言,产生.s文件
- 汇编:将汇编文件转换为机器码文件,产生.o文件
- 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件
那么,程序引用的这些动静态库是如何加载到内存中去的呢?
动态链接器 dyly
dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。而且它是开源的,任何人可以通过苹果官网下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节。
dyld流程 探索
下面,我们看一下从app启动,到调起main函数之间,中间系统是如何处理的。我们在main函数开始那一行打一个断点,然后运行项目。
可以看到在main
之前只有一个start
。
通过符号断点,我们也并没有断住这个start
。 通过日志我们可以知道,ViewController
的load
方法在main
函数之前调用,那么,打个断点,再看看。
通过 bt
打印堆栈命令发现最开始 断在了 _dyld_start
。
捎带着我们整理下堆栈的信息,对于接下来的探索是有引导作用的(这个堆栈信息将是今天探索过程的主线任务
):
- dyld
_dyld_start
- dyld
dyldbootstrap::start
- dyld
dyld::_main
- dyld
dyld::initializeMainExecutable()
- dyld
ImageLoader::runInitializers
- dyld
ImageLoader::processInitializers
- dyld
ImageLoader::recursiveInitialization
- dyld
dyld::notifySingle
- libobjc.A.dylib
load_images
+[ViewController load]
我们从 _dyld_start
开始
下面我们就下载好一份 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流程图
流程分析下来之后可以看到最终是来到了 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);
,
- 这个方法中的
map_images
是在什么时候调用的呢? - load_images方法执行的时候其内部的一个流程是什么样的?
- 最后,我们现在所在的
dyld
阶段是如何进入到main
函数主程序的呢?
下一篇,我们继续上面流程分析进而来解答上面我们提出的三个问题。