0%

iOS底层:应用加载流程 main()函数之前

App从点击图标就开启了生命周期,本文从App启动开始探索。

前导知识

Mach-O文件

Mach-O is a bunch of file types for different run time executables.
Mach-O 是 iOS 系统不同运行时期可执行的文件的文件类型统称。

维基百科上关于 Mach-O 的描述:

Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。作为 a.out 格式的替代品,Mach-O 提供了更好的扩展性,并提升了符号表中信息的访问速度。
大多数基于 Mach 内核的操作系统都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用这种格式作为本地可执行文件、库和对象代码的例子。

Mach-O有三种文件类型:Executable、Dylib、Bundle

  • ExecutableL类型

    So the first executable, that’s the main binary in an app, it’s also the main binary in an app extension.
    Executable 是 app 的二进制主文件,同时也是 app extension 的二进制主文件

    我们一般可以在 Xcode 项目中的 Products 文件夹中找到它.

  • Dylib类型

    A dylib is a dynamic library, on other platforms meet, you may know those as DSOs or DLLs.
    dylib 是动态库,在其他平台也叫 DSO 或者 DLL。

    对于接触 iOS 开发比较早的同学,可能知道我们在 Xcode 7 之前添加一些比如 sqlite 的库的时候,其后缀名为 dylib,而 Xcode 7 之后后缀名都改成了 tbd。

    这里引用 StackoverFlow 上的一篇回答。

    So it appears that the .dylib file is the actual library of binary code that your project is using and is located in the /usr/lib/ directory on the user’s device. The .tbd file, on the other hand, is just a text file that is included in your project and serves as a link to the required .dylib binary. Since this text file is much smaller than the binary library, it makes the SDK’s download size smaller.

    刚刚我们简单介绍了动态库,还有一种库是静态库,他们的区别是什么呢。

    我们先看一下编译的过程:

    compile_process

    当然,这个过程中间其实还设计到编译器前端的 词法分析、语法分析、语义分析、优化 等流程,我们在后面探索 LLVM 和 Clang 的时候会详细介绍。

    回到刚才的话题,静态库和动态库的区别:

    Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime.

    静态库和动态库都是编译好的二进制文件,只是用法不同。那为什么要分动态和静态库呢?

    static_link

    dyld_link

    通过上面两幅图我们可以知道:

    • 静态库表现为:在链接阶段会将汇编生成的目标文件和引用的库一起链接打包到可执行文件中。
    • 动态库的表现为:程序编译并不会链接到目标代码中,在程序可执行文件里面会保留对动态库的引用,其中动态库分为动态链接库和动态加载库。
      • 动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。在Linked Framework and Libraries设置的一些share libraries。【随着程序启动而启动】
      • 动态加载库:当需要的时候再使用dlopen等通过代码或者命令的方式来加载。【在程序启动之后】
  • Bundle类型

    Now a bundle’s a special kind of dylib that you cannot link against, all you can do is load it at run time by an dlopen and that’s used on a Mac OS for plug-ins.
    现阶段 Bundle 是一种特殊类型的 dylib,你是无法对其进行链接的。你所能做的是在 Runtime 运行时去通过 dlopen 来加载它,它可以在 macOS 上用于插件。

  • Image和Framework

    Image refers to any of these three types.
    镜像文件包含了上述的三种文件类型

    a framework is a dylib with a special directory structure around it to holds files needed by that dylib.
    有很多东西都叫做 Framework,但在本文中,Framework 指的是一个 dylib,它周围有一个特殊的目录结构来保存该 dylib 所需的文件。

Mach-O结构分析

segment段

Mach-O

Mach-O镜像文件是由segments段组成的。

  • 段的名称为大写格式

    所有的段都是 page size 的倍数。

  • arm64 上段大小为 16 字节

  • 其它架构为 4 字节

这里再提及一下虚拟内存和内存页的知识:

具有 VM 机制的操作系统,会对每个运行的进程创建一个逻辑地址空间 logical address space 或者叫虚拟地址空间 virtual address space;该空间的大小由操作系统位数决定:32 位的操作系统,其逻辑地址空间的大小为 4GB,64位的操作系统为 2^34 GB(其计算方式是 2^32 || 2^64(理论上是64位,x86 Intel 是48位))。
vitual_memory

虚拟地址空间(或者逻辑地址空间)会被分为相同大小的块,这些块被称为内存页(page)。计算机处理器和它的内存管理单元(MMU - memory management uinit)维护着一张将程序的逻辑地址空间映射到物理地址上的分页表page table。

在masOS和早版本的iOS中,分页的大小为4kB。在之后的基于A7和A8的系统中,虚拟内存(64位的地址空间)地址空间的分页大小变为了16KB,而物理RAM上的内存分页大小仍然维持在4KB;基于A9及之后的系统,虚拟内存和物理内存的分页都是16KB。

section

Mach-O_section

在 segment 段内部还有许多的 section 区。section 名称为小写格式。

But sections are really just a subrange of a segment, they don’t have any of the constraints of being page size, but they are non-overlapping.
但是 sections 节实际上只是一个 segment 段的子范围,它们没有页面大小的任何限制,但是它们是不重叠的。

通过MachOView工具查看App的二进制可执行文件可以看到:

section_show

常见的segments

  • _TEXT:代码段,包括头文件、代码和常量。只读不可修改

    _TEXT_image

  • _DATA:数据段,包括全局变量,静态变量等。可读可写。

    _DATA_image

  • _LINKEDIT:如何加载程序的元数据, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

    _LINKEDIT_image

Mach-O Universal Files

Mach-o_UniversalFiles

Mach-O 通用文件,将多种架构的 Mach-O 文件合并而成。它通过 header 来记录不同架构在文件中的偏移量,segement 占多个分页,header占一页的空间。可能有人会觉得 header 单独占一页会浪费空间,但这有利于虚拟内存的实现。

虚拟内存

virtualMemory_image

虚拟内存是一层间接寻址。

虚拟内存解决的是管理所有进程使用物理 RAM 的问题。通过添加间接层来让每个进程使用逻辑地址空间,它可以映射到 RAM 上的某个物理页上。这种映射不是一对一的,逻辑地址可能映射不到 RAM 上,也可能有多个逻辑地址映射到同一个物理 RAM 上。

  • 针对第一种情况,当进程要存储逻辑地址内容时会触发 page fault。
  • 而第二种情况就是多进程共享内存。
  • 对于文件可以不用一次性读入整个文件,可以使用分页映射 mmap() 的方式读取。也就是把文件某个片段映射到进程逻辑内存的某个页上。当某个想要读取的页没有在内存中,就会触发 page fault,内核只会读入那一页,实现文件的懒加载。也就是说 Mach-O 文件中的 __TEXT 段可以映射到多个进程,并可以懒加载,且进程之间共享内存。
  • __DATA 段是可读写的。这里使用到了 Copy-On-Write 技术,简称 COW。也就是多个进程共享一页内存空间时,一旦有进程要做写操作,它会先将这页内存内容复制一份出来,然后重新映射逻辑地址到新的 RAM 页上。也就是这个进程自己拥有了那页内存的拷贝。这就涉及到了 clean/dirty page 的概念。dirty page 含有进程自己的信息,而 clean page 可以被内核重新生成(重新读磁盘)。所以 dirty page 的代价大于 clean page。

多进程加载Mach-O镜像

Mach-O_image_loading

  • 所以在多个进程加载 Mach-O 镜像时 __TEXT 和 __LINKEDIT 因为只读,都是可以共享内存的,读取速度就会很快。
  • 而 __DATA 因为可读写,就有可能会产生 dirty page,如果检测到有 clean page 就可以直接使用,反之就需要重新读取 DATA page。一旦产生了 dirty page,当 dyld 执行结束后,__LINKEDIT 需要通知内核当前页面不再需要了,当别人需要的使用时候就可以重新 clean 这些页面。

ASLR

ASLR (Address Space Layout Randomization) 地址空间布局随机化,镜像会在随机的地址上加载。

Code Signing

可能我们认为 Xcode 会把整个文件都做加密 hash 并用做数字签名。其实为了在运行时验证 Mach-O 文件的签名,并不是每次重复读入整个文件,而是把每页内容都生成一个单独的加密散列值,并存储在 __LINKEDIT 中。这使得文件每页的内容都能及时被校验确并保不被篡改。

exec()

exec_image

Exec is a system call. When you trap into the kernel, you basically say I want to replace this process with this new program.

exec() 是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用 ASLR)。并将起始位置到 0x000000 这段范围的进程权限都标记为不可读写不可执行。如果是 32 位进程,这个范围至少是 4KB;对于 64 位进程则至少是 4GB 。NULL 指针引用和指针截断误差都是会被它捕获。这个范围也叫做 PAGEZERO。

dyld

dyld_image

Unix 的前二十年很安逸,因为那时还没有发明动态链接库。有了动态链接库后,一个用于加载链接库的帮助程序被创建。在苹果的平台里是 dyld,其他 Unix 系统也有 ld.so。 当内核完成映射进程的工作后会将名字为 dyld 的 Mach-O 文件映射到进程中的随机地址,它将 PC 寄存器设为 dyld 的地址并运行。dyld 在应用进程中运行的工作是加载应用依赖的所有动态链接库,准备好运行所需的一切,它拥有的权限跟应用一样。

dyld流程

dyld_process_image

  • 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 的时间线由上图可知为:

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

dyld2 && dyld3

dyld3_image

在 iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程如下:

  • 解析 Mach-O 的 Header 和 Load Commands,找到其依赖的库,并递归找到所有依赖的库
  • 加载 Mach-O 文件
  • 进行符号查找
  • 绑定和变基
  • 运行初始化程序

dyld3 被分为了三个组件:

  • 一个进程外的 MachO 解析器
    • 预先处理了所有可能影响启动速度的 search path、@rpaths 和环境变量
    • 然后分析 Mach-O 的 Header 和依赖,并完成了所有符号查找的工作
    • 最后将这些结果创建成了一个启动闭包
    • 这是一个普通的 daemon 进程,可以使用通常的测试架构
  • 一个进程内的引擎,用来运行启动闭包
    • 这部分在进程中处理
    • 验证启动闭包的安全性,然后映射到 dylib 之中,再跳转到 main 函数
    • 不需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。
  • 一个启动闭包缓存服务
    • 系统 App 的启动闭包被构建在一个 Shared Cache 中, 我们甚至不需要打开一个单独的文件
    • 对于第三方的 App,我们会在 App 安装或者升级的时候构建这个启动闭包。
    • 在 iOS、tvOS、watchOS中,这这一切都是 App 启动之前完成的。在 macOS 上,由于有 Side Load App,进程内引擎会在首次启动的时候启动一个 daemon 进程,之后就可以使用启动闭包启动了。

dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。

好了,先导知识就总结到这里,接下来让我们调整呼吸进入下一章~

App加载分析

我们在探索 iOS 底层的时候,对于对象、类、方法有了一定的认知哦,接下来我们就一起来探索一下应用是怎么加载的。

我们直接新建一个 Single View App 的项目,然后在 main.m 中打一个断点:

demoMain_image

可以看到堆栈信息为:

launch_stack_image

可以看到堆栈栈底是_dyld_start,说明App加载是从_dyld_start开始的

_dyld_start

我们在源码里全局搜索_dyld_start。源码下载地址,我这里下载的版本是832.7.3,我们可以来到 dyldStartup.s 这个汇编文件,然后我们聚焦于 arm64 架构下的汇编代码:

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
#if __arm64__ && !TARGET_OS_SIMULATOR
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
#if __LP64__
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
#else
ldr w0, [x28] // get app's mh into x0
ldr w1, [x28, #4] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add w2, w28, #8 // get argv into x2
#endif
adrp x3,___dso_handle@page
add x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
mov x4,sp // x5 has &startGlue

// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
#if __LP64__
ldr x1, [sp]
#else
ldr w1, [sp]
#endif
cmp x1, #0
b.ne Lnew

对于这里的汇编代码,我们肯定也没必要逐行分析,我们直接定位到 bl 语句后面(bl 在汇编层面是跳转的意思):

1
2
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm

看注释可以知道,调用位于 dyldbootstrap 命名空间下的 start 方法,我们继续搜索一下这个 start 方法,结果位于 dyldInitialization.cpp 文件(从文件名我们可以看出该文件主要是用来初始化 dyld),这里查找 start 的时候可能会有很多结果,我们其实可以先搜索命名空间,再搜索 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
//
// 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 dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{

// Emit kdebug tracepoint to indicate dyld bootstrap has started <rdar://46878536>
dyld3::kdebug_trace_dyld_marker(DBG_DYLD_TIMING_BOOTSTRAP_START, 0, 0, 0, 0);

// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
// dyld 重定向
rebaseDyld(dyldsMachHeader);

// 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
// 初始化dyld
runDyldInitializers(argc, argv, envp, apple);
#endif

_subsystem_init(apple);

// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

start方法主要做了很多dyld的初始化工作:

  • rebaseDyld dyld重定位
  • __guard_setup 栈溢出保护

在结束dyld初始化工作后,函数调用 dyld::_main() 函数,再将返回值传递给__dyld_start去调用真正的main函数。

dyld::_main

1
2
3
4
5
6
7
8
9
10
11
12
13
//
// Entry point for dyld. The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
///省略代码
}

_main方法 官方的注释如下:

dyld 的入口。内核加载了 dyld 然后跳转到 __dyld_start 来设置一些寄存器的值然后调用到了这个方法。
返回 __dyld_start 所跳转到的目标程序的 main 函数地址。

dyld流程可总结为九个步骤:

  • 第一步:设置运行环境
  • 第二步:加载共享缓存
  • 第三步:实例化主程序
  • 第四步:加载插入的动态库
  • 第五步:链接主程序
  • 第六步:链接插入的动态库
  • 第七步:执行弱引用绑定
  • 第八步:执行初始化方法
  • 第九步:查找程序入口main然后返回
第一步:设置运行环境

这一步主要是设置程序的运行环境,运行条件等准备工作,包括环境,平台,版本,路径,主机信息,设置程序上下文信息等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取主程序的hash
mainExecutableCDHash = mainExecutableCDHashBuffer;
// 获取主程序的macho_header结构
sMainExecutableMachHeader = mainExecutableMH;
// 获取主程序的slide值
sMainExecutableSlide = mainExecutableSlide;
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// 获取主程序路径
sExecPath = _simple_getenv(apple, "executable_path");
// 进程的头环境配置
configureProcessRestrictions(mainExecutableMH, envp);
// 检测环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
// 获取主机信息 可理解为 程序结构
getHostInfo(mainExecutableMH, mainExecutableSlide);
第二步:加载共享缓存

首先检查dyld共享缓存区是否禁用,iOS必须开启,在checkSharedRegionDisable里面iOS环境下注释:

// iOS cannot run without shared region

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
47
48
49
50
51
52
53
54
55
56
57
58
59
// 检查缓存共享区域是否开启
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
// 共享缓存加载
mapSharedCache();
// mapSharedCache 函数
static void mapSharedCache()
{
dyld3::SharedCacheOptions opts;
opts.cacheDirOverride = sSharedCacheOverrideDir;
opts.forcePrivate = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);


#if __x86_64__ && !TARGET_OS_SIMULATOR
opts.useHaswell = sHaswell;
#else
opts.useHaswell = false;
#endif
opts.verbose = gLinkContext.verboseMapping;
//--- 加载dyld缓存主函数
loadDyldCache(opts, &sSharedCacheLoadInfo);

// update global state
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
gLinkContext.dyldCache = sSharedCacheLoadInfo.loadAddress;
dyld::gProcessInfo->processDetachedFromSharedRegion = opts.forcePrivate;
dyld::gProcessInfo->sharedCacheSlide = sSharedCacheLoadInfo.slide;
dyld::gProcessInfo->sharedCacheBaseAddress = (unsigned long)sSharedCacheLoadInfo.loadAddress;
sSharedCacheLoadInfo.loadAddress->getUUID(dyld::gProcessInfo->sharedCacheUUID);
dyld3::kdebug_trace_dyld_image(DBG_DYLD_UUID_SHARED_CACHE_A, sSharedCacheLoadInfo.path, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
}
}
// loadDyldCache函数
bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results)
{
results->loadAddress = 0;
results->slide = 0;
results->errorMessage = nullptr;

#if TARGET_OS_SIMULATOR
// simulator only supports mmap()ing cache privately into process
return mapCachePrivate(options, results);
#else
if ( options.forcePrivate ) {
// mmap cache into this process only
return mapCachePrivate(options, results);
}
else {
// fast path: when cache is already mapped into shared region
bool hasError = false;
if ( reuseExistingCache(options, results) ) {
hasError = (results->errorMessage != nullptr);
} else {
// slow path: this is first process to load cache
hasError = mapCacheSystemWide(options, results);
}
return hasError;
}
#endif
}

mapSharedCache函数主要调用loadDyldCache 函数,loadDyldCache函数主要有三种方式加载共享缓存:

  • mapCachePrivate() 仅加载到当前进程
  • 共享缓存已经加载过,不做任何处理
  • mapCacheSystemWide() 未加载过,首次加载
第三步:实例化主程序

这一步主要是将主程序的Mach-O加载进内存,并实例化一个ImageLoader。instantiateFromLoadedImage() 首先调用isCompatibleMachO() 函数检测当前进程的magic、cputype、cpusubtype 等相关属性,判断Mach-O文件的兼容性,如果兼容性满足,就调用ImageLoaderMachO::instantiateMainExecutable()实例化主程序的ImageLoader:

1
2
3
4
// 第三步:实例化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 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";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// create image for main executable
ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context)
{
//dyld::log("ImageLoader=%ld, ImageLoaderMachO=%ld, ImageLoaderMachOClassic=%ld, ImageLoaderMachOCompressed=%ld\n",
// sizeof(ImageLoader), sizeof(ImageLoaderMachO), sizeof(ImageLoaderMachOClassic), sizeof(ImageLoaderMachOCompressed));
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}

instantiateMainExecutable() 函数中通过sniffLoadCommands() 判断这个mach-o文件是普通的还是压缩的LINKEDIT,以及它有多少段。
最后根据compressed 是否压缩来实例化最后返回的ImageLoader。

第四步:加载插入的动态库
1
2
3
4
5
// 第四步:加载插入的动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

loadInsertedDylib() 函数中设置了一个LoadContext,并为它配置一些参数后,调用load() 方法:

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
ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex)
{
......
// try all path permutations and check against existing loaded images

ImageLoader* image = loadPhase0(path, orgPath, context, cacheIndex, NULL);
if ( image != NULL ) {
CRSetCrashLogMessage2(NULL);
return image;
}

// try all path permutations and try open() until first success
std::vector<const char*> exceptions;
image = loadPhase0(path, orgPath, context, cacheIndex, &exceptions);
#if !TARGET_OS_SIMULATOR
// <rdar://problem/16704628> support symlinks on disk to a path in dyld shared cache
if ( image == NULL)
image = loadPhase2cache(path, orgPath, context, cacheIndex, &exceptions);
#endif
CRSetCrashLogMessage2(NULL);
if ( image != NULL ) {
// <rdar://problem/6916014> leak in dyld during dlopen when using DYLD_ variables
for (std::vector<const char*>::iterator it = exceptions.begin(); it != exceptions.end(); ++it) {
free((void*)(*it));
}
// if loaded image is not from cache, but original path is in cache
// set gSharedCacheOverridden flag to disable some ObjC optimizations
if ( !gSharedCacheOverridden && !image->inSharedCache() && image->isDylib() && dyld3::MachOFile::isSharedCacheEligiblePath(path) && inSharedCache(path) ) {
gSharedCacheOverridden = true;
}
return image;
}
......
}
第五步:链接主程序
1
2
3
// 第五步:链接主程序        Executable:可执行的意思
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH,
true, ImageLoader::RPathChain(NULL, NULL), -1);

调用link()函数将实例化后的主程序进行动态修正,让二进制变为可正常执行的状态。link()函数内部调用了ImgaeLoader::link() 函数,主要做了下面几件事:

  • this->recursiveLoadLibraries() 递归加载依赖库进内存
  • this->recursiveUpdateDepth() 递归更新依赖库的路径
  • this->recursiveRebaseWithAccounting() 递归重定位主程序和依赖库
  • this->recursiveBindWithAccounting() 递归将主程序和依赖库执行符号表绑定(链接动态库使用)
  • this->weakBind() 如果不是正在链接主程序二进制,那就主程序弱符号绑定(链接动态库使用)
  • this->recursiveApplyInterposing() 递归申请可插入依赖库权限
  • this->recursiveMakeDataReadOnly() 递归设置所有信息只读(链接动态库使用)
  • this->recursiveGetDOFSections() 注册DOF节

代码如下所示:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths, const char* imagePath)
{
// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);
uint64_t t0 = mach_absolute_time();
// 递归加载依赖库进内存
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths, imagePath);
context.notifyBatch(dyld_image_state_dependents_mapped, preflightOnly);

// we only do the loading step for preflights
if ( preflightOnly )
return;

uint64_t t1 = mach_absolute_time();
// 递归更新依赖库的路径
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());

__block uint64_t t2, t3, t4, t5;
{
dyld3::ScopedTimer(DBG_DYLD_TIMING_APPLY_FIXUPS, 0, 0, 0);
t2 = mach_absolute_time();
// 递归重定位主程序和依赖库
this->recursiveRebaseWithAccounting(context);
context.notifyBatch(dyld_image_state_rebased, false);

t3 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// 递归将主程序和依赖库执行符号表绑定
this->recursiveBindWithAccounting(context, forceLazysBound, neverUnload);

t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
// 如果不是正在链接主程序二进制,那就主程序弱符号绑定
this->weakBind(context);
t5 = mach_absolute_time();
}

// interpose any dynamically loaded images
if ( !context.linkingMainExecutable && (fgInterposingTuples.size() != 0) ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_APPLY_INTERPOSING, 0, 0, 0);
// 递归申请可插入依赖库权限
this->recursiveApplyInterposing(context);
}

// now that all fixups are done, make __DATA_CONST segments read-only
if ( !context.linkingMainExecutable )
// 递归设置所有信息只读
this->recursiveMakeDataReadOnly(context);

if ( !context.linkingMainExecutable )
context.notifyBatch(dyld_image_state_bound, false);
uint64_t t6 = mach_absolute_time();

if ( context.registerDOFs != NULL ) {
std::vector<DOFInfo> dofs;
// 注册DOF节
this->recursiveGetDOFSections(context, dofs);
context.registerDOFs(dofs);
}
uint64_t t7 = mach_absolute_time();

// clear error strings
(*context.setErrorStrings)(0, NULL, NULL, NULL);

fgTotalLoadLibrariesTime += t1 - t0;
fgTotalRebaseTime += t3 - t2;
fgTotalBindTime += t4 - t3;
fgTotalWeakBindTime += t5 - t4;
fgTotalDOF += t7 - t6;

// done with initial dylib loads
fgNextPIEDylibAddress = 0;
}
第六步:链接插入的动态库

这一步跟链接主程序一样,将sAllImages中的ImageLoader遍历出来,然后调用link()进行链接,需要注意的是,sAllImages中保存的第一个是主程序的镜像,所以要获取所有的动态库的ImageLoader,就必须从i + 1 开始遍历:

1
2
3
4
5
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
第七步:执行主程序弱符号绑定

weakBind()首先通过getCoalescedImages()合并所有动态库的弱符号到一个列表里,然后调用initializeCoalIterator()对需要绑定的弱符号进行排序,接着调用incrementCoalIterator()读取dyld_info_command结构的weak_bind_off和weak_bind_size字段,确定弱符号的数据偏移与大小,最终进行弱符号绑定

第八步:执行初始化方法

这一步就开始进行初始化工作了,initializeMainExecutable() 函数中调用 runInitializers() 函数,接着依次调用 processInitializers() 函数进行一些初始化准备工作,接着recursiveInitialization() 函数调用进行初始化工作,接着全局搜索recursiveInitialization(,找到ImageLoader.cpp中的此方法定义,看重点,函数里面我们看到noffitySingle()单个通知注入:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);

if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
// 先初始化底层依赖库
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}

// record termination order
// 记录终端序号
if ( this->needsTermination() )
context.terminationRecorder(this);

// let objc know we are about to initialize this image
// 单个通知注入 通知告知大家我要开始初始化啦,你们赶紧去做初始化的工作
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);

if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}

recursiveSpinUnLock();
}

探索 _objc_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}

通过LLDB来断掉调试_objc_init,然后通过bt命令打印出当前的调用堆栈,根据上一节我们探索dyld源码,感觉很清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001002d29c4 libobjc.A.dylib`_objc_init at objc-os.mm:925:9
frame #1: 0x000000010044f0bc libdispatch.dylib`_os_object_init + 13
frame #2: 0x000000010045fafc libdispatch.dylib`libdispatch_init + 282
frame #3: 0x00007fff6a99c791 libSystem.B.dylib`libSystem_initializer + 220
frame #4: 0x000000010002f1d3 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 535
frame #5: 0x000000010002f5de dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
frame #6: 0x0000000100029ffb dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 493
frame #7: 0x0000000100029f66 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 344
frame #8: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
frame #9: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
frame #10: 0x0000000100016662 dyld`dyld::initializeMainExecutable() + 129
frame #11: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
frame #12: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
frame #13: 0x0000000100015025 dyld`_dyld_start + 37

我们可以看到 dyld 的最后一个流程是 doModInitFunctions 方法的执行。

我们打开 libSystem 的源码,全局搜索 libSystem_initializer 可以看到:

1
2
3
4
5
6
7
8
9
10

_dyld_initializer();
_libSystem_ktrace_init_func(DYLD);

libdispatch_init();
_libSystem_ktrace_init_func(LIBDISPATCH);

#if !TARGET_OS_DRIVERKIT
_libxpc_initializer();
_libSystem_ktrace_init_func(LIBXPC);

然后我们打开 libDispatch 的源码,全局搜索 libdispatch_init 可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
	void
libdispatch_init(void)
{
//省略代码
_dispatch_hw_config_init();
_dispatch_time_init();
_dispatch_vtable_init();
//调用
_os_object_init();
_voucher_init();
_dispatch_introspection_init();
}

我们再搜索_os_object_init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
_os_object_init(void)
{
_objc_init();
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}

完美~,_objc_init 在这里就被调用了。所以 _objc_init 的流程是

dyld -> libSystem -> libDispatch -> libObjc -> _objc_init

总结

本文主要探索了 app 启动之后 dyld 的流程,整个分析过程确实比较复杂,但在探索的过程中,我们不仅对底层源码有了新的认知,同时对于优化我们 app 启动也是有很多好处的。下一章,我们会对 objc_init 内部的 map_images 和 load_images 进行更深入的分析。