dyld的那些事

前几天学习了MachO文件的基本结构,简单了解了MathO的工作原理,但是里面经常会涉及到dyld加载器,本篇主要学习dyld加载应用程序中的那些事。

dyld

每个程序我们看到的入口函数都是main函数,但是程序并不是从该函数开始执行的,在执行mian函数之前就已经执行了+load和constructor构造函数,首先看看main函数在执行之前都发生了什么。

dyld简介

程序在运行时会依赖很多系统动态库,系统动态库会通过动态库加载器dyld加载到内存中,在操作系统内核做好程序的准备工作之后,后续工作就交给dyld。

dyld加载流程

首先编写一个demo,定义一个+ load函数,下断点,同时在main函数下断点,运行发现确实是在main函数之前断下的,接着来研究一下,加载main函数之前都干啥了。

dyldbootstrap::start

系统启动应用的入口是_dyld_start,首先dyld会调用dyldbootstrap::start,该方法会返回main函数指针,看一下dyldbootstrap的start做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 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
// 滑块,ASLR技术,地址偏移,是MachO文件在内存中的地址重定向
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
// 正在的启动函数,在dyld中的_main函数中
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

从start函数的源码可知:dlyd会内存中找到一块地址给MachO使用,也就是ASLR,内存偏移。
最后start函数调用了dyld::_main并返回程序main函数的指针。

dyld::_main

main函数很多,这里只分析了一些关键的代码

配置环境变量

从main函数的初始,到函数getHostInfo()之前都是在配置一些环境变量,贴出部分代码功能。

加载共享缓存

在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到/System/Library/Caches/com.apple.dyld/目录下,按不同的架构保存分别保存着。其中包括UIKit,Foundation等基础库。

实例化主程序

主程序的实例化主要是将MachO文件中LoadCommons段内信息放入内存中。

加载动态链接库

加载外部的动态链接库,注入涉及到的库也是通过这种方式添加。

链接主程序

链接其他动态链接库

加载load方法&寻找main函数

从加载load方法到main函数中间这个过程相对比较重要,由于再三还是花一些功夫去研究一下,跳过这个复杂的过程总觉得这波白搞了,那么继续搞。

  • 首先进入initializeMainExecutable源码,主要是循环遍历,都会执行runInitializers方法

  • 进入runInitializers方法,这个防护核心是processInitializers函数的调用

  • 进入processInitializers函数,其中对镜像列表调用recursiveInitialization函数进行递归实例化

  • 全局搜索recursiveInitialization函数

  • 首先分析notifySingle函数,定位到加载image->getRealPath(), image->machHeader()的sNotifyObjCInit

  • 全局搜索sNotifyObjCInit,找到其赋值操作

  • 搜索registerObjCNotifiers在哪里调用了,发现在_dyld_objc_notify_register进行了调用

  • 在dyld源码中搜索_dyld_objc_notify_register函数的调用点,无法找到,我们用符号断点的方法定位其调用

  • 符号断点断下之后,lldb输入finish命令,发现是objc_init调用了_dyld_objc_notify_register函数

  • 在objc源码中搜索_dyld_objc_notify_register的调用位置

-进入load_images函数,查看load函数调用

  • 进入call_load_methods,发现其核心是通过do-while循环调用+load方法

  • 最终在call_class_loads方法中找到了调用load方法的位置

  • 另外在notifySingle方法下面紧跟着doInitialization方法是调用系统C++构造函数的方法

总结

至此dyld加载main函数的基本流程大致分析完了,有幸找了一位大佬总结的dyld流程图做个总结

参考链接

不知MachO怎敢说自己懂DYLD

OC底层探索(十一)dyld流程

有钱的捧个钱场
0%