ios底层 应用程序加载

504 阅读6分钟

预备知识

在上篇文章中,我们学到应用程序会编译成mach-o可执行文件,而这个可执行文件中只有我们静态文件,并没有动态库,动态库文件就是应用程序在加载过程中通过读取mach-o文件,进行需要加载。

静态库加载
动态库加载

加载分析

1.查看程序加载流程,我们写个简单的程序,打个断点

@implementation ViewController
// 此处打个断点 
+ (void)load{
 NSLog(@"%s",__func__);
}
- (void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view.
}

然后在控制台输入命令 bt,查看调用堆栈

bt输出.png
从输出信息可以知道,整个过程都是在dyld中进行,而dyld正式ios平台的动态简介器,下面进行代码分析。

2.dyld主要做了什么工作呢

Dyld 流程

  • Load dylibs

从主执行文件的 header 获取到需要加载的所依赖动态库列表,而 header 早就被内核映射过。然后它需要找到每个 dylib,然后打开文件读取文件起始位置,确保它是 Mach-O 文件。接着会找到代码签名并将其注册到内核。然后在 dylib 文件的每个 segment 上调用 mmap()。应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以 dyld 所需要加载的是动态库列表一个递归依赖的集合。一般应用会加载 100 到 400 个 dylib 文件,但大部分都是系统 dylib,它们会被预先计算和缓存起来,加载速度很快。

  • Fix-ups

在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是 Fix-ups。代码签名使得我们不能修改指令,那样就不能让一个 dylib 的调用另一个 dylib。这时需要加很多间接层。 现代 code-gen 被叫做动态 PIC(Position Independent Code),意味着代码可以被加载到间接的地址上。当调用发生时,code-gen 实际上会在 __DATA 段中创建一个指向被调用者的指针,然后加载指针并跳转过去。所以 dyld 做的事情就是修正(fix-up)指针和数据。Fix-up 有两种类型,rebasing 和 binding。

  • Rebasing 和 Binding

Rebasing:在镜像内部调整指针的指向 Binding:将指针指向镜像外部的内容

dyld 源码分析

我们从 苹果开源地址,下载源码,找到 
  • _dyld_start,
    通过代码,可以看到,会跳转到dyldbootstrap::start 这个函数中,
  • dyldbootstrap::start 源码如下:
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 dyld's main
	uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
	return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

这里主要做一些mach-O文件初始化的一些操作。然后进入_main函数

  • dyldbootstrap::_main 这里代码比较多,我们只看部分
    判断共享缓存是否开启,如果开启了就将共享缓存映射到当前进程的逻辑内存空间内
  • reloadAllImages

ImageLoader 是一个用于加载可执行文件的基类,它负责链接镜像,但不关心具体文件格式,因为这些都交给子类去实现。每个可执行文件都会对应一个 ImageLoader实例。ImageLoaderMachO 是用于加载 Mach-O 格式文件的 ImageLoader 子类,而 ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都继承于 ImageLoaderMachO,分别用于加载那些 __LINKEDIT 段为传统格式和压缩格式的 Mach-O 文件。

继续跟踪代码instantiateFromLoadedImage

在这里主程序被实例化,而实例化工具就是ImageLoader

继续往下跟踪ImageLoaderMachO::instantiateMainExecutable 方法:

我们再进入 sniiffLoadCommands 方法内部:

通过注释不难看出:sniiffLoadCommands 会确定此 mach-o 文件是否具有原始的或压缩的 LINKEDIT 以及 mach-o 文件的 segement 的个数。

sniiffLoadCommands 完成后,判断 LINKEDIT 是压缩的格式还是传统格式,然后分别调用对应的 instantiateMainExecutable 方法来实例化主程序。


  • 加载任何插入的动态库
  • 链接库
    先是链接主二进制可执行文件,然后链接任何插入的动态库。这里都用到了 link 方法,在这个方法内部会执行递归的 rebase 操作来修正 ASLR 偏移量问题。同时还会有一个 recursiveApplyInterposing 方法来递归的将动态加载的镜像文件插入。

  • 运行所有初始化程序
    完成链接之后需要进行初始化了,这里会来到 initializeMainExecutable:
    这里注意执行顺序:
    • 先为所有插入并链接完成的动态库执行初始化操作
    • 然后再为主程序可执行文件执行初始化操作
      runInitializers 内部我们继续探索到 processInitializers:
      然后我们来到 recursiveInitialization:
      然后我们来到 notifySingle:
      箭头所示的地方是获取镜像文件的真实地址。 我们全局搜索一下 sNotifyObjcInit 可以来到 registerObjCNotifiers:
      接着搜索 registerObjCNotifiers:
      此时,我们打开 libObjc 的源码可以看到:
      上面这一连串的跳转,结果很显然:dyld 注册了回调才使得 libobjc 能知道镜像何时加载完毕
      ImageLoader::recursiveInitialization 方法中还有一个 doInitialization 值得注意,这里是真正做初始化操作的地方。
      doInitialization 主要有两个操作,一个是 doImageInit,一个是 doModInitFunctions:
      doImageInit 内部会通过初始地址 + 偏移量拿到初始化器 func,然后进行签名的验证。验证通过后还要判断初始化器是否在镜像文件中以及 libSystem 库是否已经初始化,最后才执行初始化器。
  • 通知监听dyldmain
    一切工作做完后通知监听 dyldmain,然后为主二进制可执行文件找到入口,最后对结果进行签名

_objc_init

我们直接通过 LLDB 大法来断点调试 libObjc 中的 _objc_init,然后通过 bt 命令打印出当前的调用堆栈,根据上一节我们探索 dyld 的源码,此刻一切的一切都是那么的清晰明了:
我们可以看到 dyld 的最后一个流程是 doModInitFunctions 方法的执行。 我们打开 libSystem 的源码,全局搜索 libSystem_initializer 可以看到
然后我们打开 libDispatch 的源码,全局搜索 libdispatch_init 可以看到:
我们再搜索 _os_object_init:
_objc_init 在这里就被调用了。所以 _objc_init 的流程是

dyld -> libSystem -> libDispatch -> libObc -> _objc_init

总结

app 启动过程中,主要是dyld加载过程中,加载mach-o文件和动态文件的过程,并且在加载过程中,一些容错通知之类的操作。里面代码很多,我也只理解了部分,以后再慢慢研究。

参考资料

iOS 底层探索 - 应用加载