iOS dyld加载流程

1,818 阅读34分钟

关于程序是否是从 main 函数开始运行的疑问,可以参照 运行库 中的讲解。对于了解之后的内容会很有帮助。

一、dyld初识

1.1. 什么是dyld?

dyld 是英文 the dynamic link editor 的简写,翻译过来就是动态链接器,是苹果操作系统的一个重要的组成部分。在 iOS/Mac OSX 系统中,仅有很少量的进程只需要内核就能完成加载,基本上所有的进程都是动态链接的,所以 Mach-O 镜像文件中会有很多对外部的库和符号的引用,但是这些引用并不能直接用,在启动时还必须要通过这些引用进行内容的填补,这个填补工作就是由 动态链接器dyld 来完成的,也就是符号绑定动态链接器dyld 在系统中以一个用户态的可执行文件形式存在,一般应用程序会在 Mach-O 文件部分指定一个 LC_LOAD_DYLINKER 的加载命令,此加载命令指定了 dyld 的路径,通常它的默认值是 /usr/lib/dyld 。系统内核在加载 Mach-O 文件时,都需要用 dyld(位于 /usr/lib/dyld )程序进行链接。其实 dyld  就是把应用的 Mach-O 文件加载到内存中。在iOS 13系统中,iOS将全面采用新的 dyld 3 以替代之前版本的 dyld 2

dyld 是开源的,我们可以通过 官网 下载它的源码来阅读理解它的运作方式,了解系统加载动态库的细节 。本文是在dyld-750.6版本下的源码进行调试的。

1.2 dyld共享缓存

编译过程.png

1.2.1 预编译

预编译过程主要处理那些源代码文件中的以“#”开始的预编译命令。

  • 将所有的“#define”删除,并且展开所有的宏定义。

  • 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”。

  • 处理“#include”预编译指令,将被包含的文件插入到该编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。

  • 删除所有的注释“//”、“/* */”

  • 添加行号和文件名标识,比如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。

  • 保留所有的#pragma编译器指令,因为编译器须要使用它们。

1.2.2 编译

编译过程一般分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。

编译过程.png

扫描

预处理完成后的源代码被输入到扫描器(Scanner),扫描器的任务很简单,它只是简单地进行语法分析,运用一种类似于 有限状态机(Finite State Machine) 的算法可以很轻松地将源代码的字符序列分割成一系列的记号(Token)。也就是会进行词法分析,这里会把代码切成一个个 Token,比如关键字、大小括号,等于号还有字符串等。

另外对于一些有预处理的语言,比如C语言,它的宏替换和文件包含等工作一般不归入编译器的范围而交给一个独立的预处理器。

语法分析器读入组成的源程序的字符流,并且将它们组织成为有意义的词素的序列。对于每个词素,语法分析器产生如下形式的语法单元(token)作为输出:

<token-name,attribute-value>

这个词法单元被传送给下一个步骤,即语法分析。

语法分析

在这个语法单元中,第一个分量 token-name 是一个由语法分析步骤使用的抽象符号,而第二个分 attribute-value 指向符号表中关于这个词法单元的条目。符号表条目信息会被语义分析和代码生成步骤使用。

验证语法是否正确,如果出现了表达式不合法,比如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。然后将所有节点组成抽象语法树 AST

语义分析

语义分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用。

语义分析的一个重要部分是类型检查。编译器检查每个运算符是否具有匹配的运算分量。比如,很多程序设计语言的定义中要求一个数组的下标必须是整数。如果用一个浮点数作为数组的下标,编译器就必须报告错误。

语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。比如C语言里面两个指针做乘法运算是没有意义的,但是这个语句在语法上是合法的;比如同样一个指针和一个浮点数做乘法运算是否合法等。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译可以确定的语义,与之对应的是动态语义(Dynamic Semantic) 就是只有在运行期才能确定的语义。

静态语义通常包括声明和类型的匹配,类型的转换。动态语义一般指运行期出现的语义相关的问题,比如将0作为除数是一个运行期语义错误。

生成 IR(中间代码)

在把一个源程序翻译成目标代码的过程中,一个编译器可能构造出一个或多个中间表示。这些中间表示可以有多种形式。语法树是一种中间表示形式,它们通常在语法分析和语义分析中使用。

在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。我们可以把这个表示看作是某个抽象机器的程序。该中间表示应该具有两个重要的性质:它应该易于生成,且能够被轻松地翻译为目标机器上的语言。

中间代码使得编译器可以被分为前端和后端。编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。这样对于一些可以跨平台的编译器而言,它们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。

完成这些步骤后就可以开始IR中间代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。

下面是Swift中的编译流程,其中SIL(Swift Intermediate Language),是Swift编译过程中的中间代码,主要用于进一步分析和优化Swift代码。如下图所示,SIL位于在AST和LLVM IR之间

swift编译流程.jpg

注意:这里需要说明一下,Swift与OC的区别在于 Swift生成了高级的SIL

1.2.3 汇编

生成汇编

clang -S -fobjc-arc main.m -o main.

生成目标文件

clang -fmodules -c main.m -o main.o

目标文件

目标文件就是源代码编译后但未进行链接的那些中间文件,它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。

不光是可执行文件按照可执行文件格式存储。动态链接库静态链接库文件都是按照可执行文件格式存储。

有一个问题:如果目标代码中有变量定义在其他模块,该怎么办?事实上,定义在其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。让我们带着这个问题,走进链接的世界。

1.2.4 链接

一、静态链接

比如我们在程序模块main.c中使用另外一个模块func.c中的函数foo()。我们在mian.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器,须要我们手工把每个调用foo的指令进行修正,填入正确的foo函数地址。当func.c模块被重新编译,foo函数的地址有可能改变时,我们在main.c中所有使用到foo的地址指令将要全部重新调整。这些繁琐的工作将成为程序员的噩梦。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时候,会根据你所引用的符号foo,自动去相应 的func.c模块查找foo的地址,然后将main.c模块中所有引用到foo的指令重新修正,让它们的目标地址为真正的foo函数的地址,这就是静态链接的最基本过程和作用。

一般来说,整个链接过程分两步:

第一步 空间与地址分配

扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有到符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。

  • 符号(Symbol) 这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的起始地址。
第二步 符号解析与重定位

使用上面第一步中收集到的所有信息,读取输入文件汇总段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。

  • 重新计算各个目标的地址的过程被叫做重定位(Relocation)

二、动态链接

在Linux下,动态链接器(Dynamic Linker)ld.so实际上是一个共享对象,操作系统通过通过映射的方式将它加载到进程的地址空间中。操作系统在加载完动态链接器之后,就将控制权交给动态链接器的入口地址(与可执行文件一样,共享对象也有入口地址)。当动态链接器得到控制权之后,它开始执行一系列自身初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作。当所有动态链接工作完成以后,动态链接器会将控制权转交到可执行文件的入口地址,程序开始正式执行。

空间浪费是静态链接的一个问题,另一个问题是静态链接对程序的更新、部署和发布也会带来很多麻烦。为了解决这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不将它们静态地链接在一起。简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行的才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接的基本思想。

20170610144939223.png

当程序模块 Program1.c 被编译称为 Program1.o 时,编译器还不知道 foobar() 函数的地址,这个内容我们已在静态链接中解释过了。当链接器将 Program1.o 链接成为可执行文件是,这时候链接器必须确定 Program1.o 中所引用的的 foobar() 函数的性质。如果 foobar() 是一个定义于其他静态目标模块中的函数,那么链接器将会按照静态链接的规则,将 Program1.o 中的 foobar() 地址引用重定位;如果 foobar() 是一个定义在某个动态共享对象中的函数,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,把这个过程留到装载时再进行。

那么链接器如何知道 foobar() 的引用是一个静态符号还是一个动态符号?这实际上就是我们要用到 Lib.so 的原因。 Lib.so 中保存了完整的符号信息(因为运行时进行动态链接还需要使用符号信息),把 Lib.so 也作为链接输入文件之一,链接器在解析符号时就知道: foobar() 是一个定义在 Lib.so 的动态符号。这样链接器就可以对 foobar() 的引用做特殊的处理,使它成为一个对动态符号的引用。

在一个程序被分割成多个模块以后,这些模块之间最后如何组合形成一个单一的程序是须解决的问题。模块之间如何组合的问题可以归结为模块之间如何通信的问题,最常见的属于静态语言的C/C++模块之间的通信有两种方式,一种是模块间的函数调用,另外一种是模块间的变量访问。函数访问须知道目标函数的地址,变量访问也须知道目标变量的地址,所以这两种方式都可以归结为一种方式,那就是模块间的符号的引用。模块间依靠符号来通信类似于拼图版,定义符号的模块多出一块区域,引用符号的模块刚好少了那一块区域,两者一拼接刚好完美组合。这个模块的拼接过程就是链接(Linking)

把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接(Linking)。链接的主要内容就是把各个模块之间的相互引用的部分处理好,使得各个模块之间能够正确地衔接。链接过程主要包括了地址和空间分配(Address and Storge Allocation)、符号决议(Symbol Resolution)和重定位(Relocation) 等这些步骤。

三、生成可执行文件

dyld 链接,将每个模块的源代码经过编译器编译成目标文件(Object File,一般扩展名为.o或.obj)和引用的动态库(.so、.framework、.dylib)静态库(.a、.lib) 一起链接形成最终可执行文件。

1.3 静态库、动态库及可执行文件

不光是可执行文件按照可执行文件格式存储。动态链接库及静态链接库文件都按照可执行文件格式存储。

静态库

静态库可以看成是一堆对象文件 (object files) 的归档(一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件)。当链接这样一个库到应用中时,静态链接器static linker 将会从库中收集这些对象文件并把它们和应用的对象代码一起打包到一个单独的二进制文件中。这意味着应用的可执行文件大小将会随着库的数目增加而增长。另外,当应用启动时,应用的代码(包含库的代码)将会一次性地导入到程序的地址空间中去。

动态库

动态库是可以被多个 app 的进程共用的,所以在内存中只会存在一份;如果是静态库,由于每个 appMach-O 文件中都会存在一份,则会存在多份。相对静态库,使用动态库可以减少 app 占用的内存大小。动态库不能直接运行,而是需要通过系统的 动态链接加载器dyld 进行加载到内存后执行。

dyld 加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被 dyld 映射到内存中,之后,当任何 Mach-O 映像加载时,dyld 首先会检查该 Mach-O 映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。在程序依赖的系统动态库很多的情况下,这种做法对程序启动性能是有明显提升的。

dyld 3dyld 2 不同点在 main 方法中可以看出,在老的 main 方法中,完成第一步以后会初始化主 App ,然后加载共享缓存。到了 dyld 3 ,对他们的顺序做了调整:
dyld 3 中,会先执行 mapSharedCache (加载共享缓存): 系统应用的launch closure直接加入到共享缓存 share cache,在应用安装或者更新期间构建launch closure,因为此时 system library已发生更改,然后加载主 Appdyld 3 把很多耗时的操作都提前处理好了,极大提升了启动速度。

这样就能够执行看到输出结果

clang main.o -o main
执行
./main
输出
starming rank 14

可执行文件 Mach-O

目标文件就是源代码编译后但未进行链接的那些中间文件,它跟可执行文件的内容和结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。

目标文件中的内容至少有编译后的机器指令代码、数据,还包括了链接时所需要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按照不同的属性,以的形式存储。如:代码段、数据段、只读数据段、其他段、文件头、段表、重定位表、字符串表、符号表、调试表等。

ELF Header(ELF 文件头):

它包含了描述整个文件的基本属性,比如 ELF文件版本、目标机器型号、程序入口地址等

ELF文件头中定义了ELF 魔数、文件机器字节长度、数据存储方式。版本。运行平台、ABI版本、ELF 重定位类型、硬件平台、硬件平台版本。入口地址。程序头入口和长度、段表的位置和长度及段的数量

ELF 魔数

查看ELF文件头信息–readelf -h

➜  ~ readelf -h test
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400430
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6624 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 28

最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASCII码。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。比如a.out格式最开始两个自己为0x01、0x07;PE/COFF文件最开始的两个字节为0x4d、0x5a,及ASCII字符MZ。这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。

接下来的一个字节是用来标识ELF文件类的,0x01表示是32位的,0x02表示是64位的;第6个字节是字节序,规定该ELF文件是大端还是小端的。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版以后就再也没有更新了。后面9个字节ELF标准灭有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

Section Header Table(段表):

该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。

重定位表

链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段和数据段,都会有一个相应的重定位表。

字符串表

ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。

符号表

ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf32_Sym结构的数组,每个Elf32_Sym结构对应一个符号。这个数组的第一个元素,也就是下标0的元素为无效的“未定义”符号。Elf32_Sym的结构定义如下:

typedef struct {
    Elf32_Word st_name; // 符号名
    Elf32_Addr st_value; // 符号相对应的值
    Elf32_Word st_size; // 符号大小
    unsigned char st_info; // 符号类型和绑定信息
    unsigned char st_other; // 该成员目前为0,没用
    Elf32_Half st_shndx; // 符号所在的段
}Elf32_Sym;

调试信息

目标文件里面还有可能保存的是调试信息。几乎所有现代的编译器都支持源代码级别的调试,比如我们可以在函数里面设置断点,可以监视变量变化,可以单步行进等,前提是编译器必须提前将源代码与目标代码之间的关系等,比如目标代码中的地址对应源代码中的哪一行、函数和变量的类型、结构体的定义、字符串保存到目标文件里面。甚至有些高级的编译器和调试器支持查看STL容器的内容,即程序员在调试过程中可以直接观察STL容器中的成员的值。

但是值得一提的是,调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据本身大好几倍,所以当我们开发完程序并要将它发布的时候,须要把这些对于用户没有用的调试信息去掉,以节省大量的空间。在Linux下,我们可以使用“strip”命令来去掉ELF文件中的调试信息

$strip foo

Mach-O.png

可执行文件包含了可以直接直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名。

COFF 是由 Unix System V Release 3 首先提出并且使用的格式规范,后来微软公司基于 COFF 格式,制定了 PE 格式标准,并且将其用于当时的 Windows NT 系统System V Release 4COFF 的基础上引入了 ELF 格式,目前流行的 Linux 系统 也以 ELF 作为基本可执行文件格式。这也就是为什么目前 PEELF 如此相似的主要原因,因为它们都是源于同一种可执行文件格式 COFF

Unix 最早的可执行文件格式为 a.out 格式,它的设计非常地简单,以至于后来共享库这个概念出现的时候,a.out 格式 就变得捉襟见肘了。

那么对于 OS XiOS 来说 Mach-O 是其可执行文件的格式。

Mach-O 为 Mach Object 文件格式的缩写,是 iOS 系统不同运行时期 可执行文件 的文件类型统称。它是一种用于 可执行文件、目标代码、动态库、内核转储的文件格式。

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

  • Executable

Executableapp 的二进制主文件,我们可以在 Xcode 项目中的 products 文件中找到它:

截屏2021-04-27 上午12.28.06.png

截屏2021-04-27 上午12.28.55.png

截屏2021-04-27 上午12.29.18.png

  • Dylib

Dylib 是动态库,动态库分为 动态链接库 和 动态加载库。

动态链接库:在没有被加载到内存的前提下,当可执行文件被加载,动态库也随着被加载到内存中。(随着程序启动而启动)

动态加载库:当需要的时候再使用 dlopen 等通过代码或者命令的方式加载。(程序启动之后)

  • Bundle

Bundle 是一种特殊类型的Dylib,你无法对其进行链接。所能做的是在Runtime运行时通过dlopen来加载它,它可以在macOS 上用于插件。

  • ImageFramework

Image (镜像文件)包含了上述的三种类型;

Framework 可以理解为动态库。

Mach-O的结构

MachO结构.png

Mach-O 是一个以数据块分组的二进制字节流,每个 Mach-O 文件包括一个 Mach-O头,然后是一系列的载入命令,再是一个或多个段,每个段包括0到255个块。

Header结构

保存 Mach-O 的一些基本信息,包括运行平台、文件类型、LoadCommands指令 的个数、指令总大小,dyld标记Flags 等等。

Load Commands

紧跟 Header,这些加载指令清晰地告诉加载器如何处理二进制数据,有些命令是由内核处理的,有些是由动态链接器处理的。加载 Mach-O文件 时会使用这部分数据确定内存分布以及相关的加载命令,对系统内核加载器和动态连接器起指导作用。比如我们的 main()函数 的加载地址、程序所需的 dyld 的文件路径、以及相关依赖库的文件路径。

Data

每个 segment 的具体数据保存在这里,包含具体的代码、数据等等。

二、dyld加载流程

程序的一些特定的操作必须在 main函数 之前被执行,还有一些操作必须在 main函数 之后被执行,其中很具有代表性的就是 C++ 的全局对象的构造(main函数之前)和析构函数(main函数之后)。

2.1 找程序入口:_dyld_start

下载 dyld 最新版源码 dyld-750.6

新建工程在 ViewController 文件中添加 load 方法,在 main.m 文件中添加一个 C++ 方法:

 __attribute__((constructor)) void kcFunc(){
    printf("来了 : %s \n",__func__);
}

load 方法添加断点。 运行程序 。LLDB 调试指令 bt 查看函数调用堆栈:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100661d5c 002-应用程加载分析`+[ViewController load](self=0x00000001007252d0, _cmd=<no value available>) at ViewController.m:16
    frame #1: 0x00000001984b53bc libobjc.A.dylib`load_images + 944
    frame #2: 0x00000001006b221c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
    frame #3: 0x00000001006c35e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
    frame #4: 0x00000001006c1878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
    frame #5: 0x00000001006c1940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
    frame #6: 0x00000001006b26d8 dyld`dyld::initializeMainExecutable() + 216
    frame #7: 0x00000001006b7928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
    frame #8: 0x00000001006b1208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
    frame #9: 0x00000001006b1038 dyld`_dyld_start + 56

到这里,我们已经找到了程序的入口函数:_dyld_start。然后我们就可以去源码中找一下 _dyld_start 函数来一探究竟。发现这个文件中按照不同架构分别做了逻辑处理,比如 i386、x86_64、arm64、arm 。下面就以 x86_64 为例来分析一下 dyld

一看源码全是汇编,看不懂怎么办?我们可以借助注释来了解其流程。其实在我们LLDB 调试堆栈的时候已经看到 _dyld_start 函数之后走的是 dyldbootstrap::start 函数。

call 就是调用函数的指令 , ( 同 bl ) . 这个函数也就是我们 app 开始的地方

#if __x86_64__ && !TARGET_OS_SIMULATOR
	...
__dyld_start:
	...

	# call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
    movl	8(%rbp),%esi	# param2 = argc into %esi
	leaq	16(%rbp),%rdx	# param3 = &argv[0] into %rdx
	leaq	___dso_handle(%rip),%rcx # param4 = dyldsMachHeader into %rcx
	leaq	-8(%rbp),%r8    # param5 = &glue into %r8
	call	__ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
	movq	-8(%rbp),%rdi
	cmpq	$0,%rdi
	jne	Lnew
	...

2.2 dyldbootstrap :: start

dyldbootstrap::start 就是指 dyldbootstrap 这个命名空间作用域里的 start 函数。 来到源码中 , 搜索 dyldbootstrap , 然后找到 start 函数。

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
				const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
...
	return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

还是借用 LLDB 调试堆栈结果, dyldbootstrap::start 函数之后走的是 dyld::_main 函数。

2.3 dyld::_main

_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
...省略600多行代码

        sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
...
		{
			// find entry point for main executable
			result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
			if ( result != 0 ) {
				// main executable uses LC_MAIN, we need to use helper in libdyld to call into main()
				if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
					*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
				else
					halt("libdyld.dylib support not present for LC_MAIN");
			}
			else {
				// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
				result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();
				*startGlue = 0;
			}
		}
...
	
	return result;
}

还是借用 LLDB 调试堆栈结果, dyld::_main 函数之后走的是 dyld::initializeMainExecutable 函数。

通过注释 // find entry point for main executable(查找函数的入口) 我们可以看到是借助 sMainExecutable 这个函数查找的。我们在找一下 sMainExecutable 这个函数,发现确实找到了 instantiateFromLoadedImage 函数。

2.4 instantiateFromLoadedImage

这个函数内容比较少,只有一个函数跳转,接着我们跳转到 instantiateMainExecutable 函数。

通过函数注释我们了解到这个函数的主要目的:dyld 获得控制权之前,内核会映射到可执行文件,这一步正是创建了可执行文件的映射 ImageLoader, 返回给我们的主程序 sMainExecutable , 加在了我们的镜像 image 里面。

// 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";
}

2.5 instantiateMainExecutable

instantiateMainExecutable 里 , 真正实例化主程序是用 sniffLoadCommands 这个函数去做的。

// determine if this mach-o file has classic or compressed LINKEDIT and number of // 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
}

2.6 sniffLoadCommands

还是 ImageLoaderMachO 这个作用域里的 sniffLoadCommands 函数。我们稍微看一下:

// determine if this mach-o file has classic or compressed LINKEDIT and number of segments it has
void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
											unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
											const linkedit_data_command** codeSigCmd,
											const encryption_info_command** encryptCmd)
{
    *compressed = false;
	*segCount = 0;
	*libCount = 0;
	*codeSigCmd = NULL;
	*encryptCmd = NULL;
...
switch (cmd->cmd) {
			case LC_DYLD_INFO:
			case LC_DYLD_INFO_ONLY:
			
			case LC_LOAD_DYLIB:
			case LC_LOAD_WEAK_DYLIB:
			case LC_REEXPORT_DYLIB:
			case LC_LOAD_UPWARD_DYLIB
...		   
			
}

这个函数就是根据 Load Commands 来加载主程序 . MachOView解析 这里几个参数我们稍微说明下 :

  • compressed -> 根据 LC_DYLD_INFO_ONLY 来决定 。
  • segCount 段命令数量 , 最大不能超过 255 个。
  • libCount 依赖库数量 , LC_LOAD_DYLIB (Foundation / UIKit ..) , 最大不能超过 4095 个。
  • codeSigCmd , 应用签名 。
  • encryptCmd , 应用加密信息 , ( 我们俗称的应用加壳 , 我们非越狱环境重签名都是需要砸过壳的应用才能调试) 。

经过以上步骤 , 主程序的实例化就已经完成了 。

2.7 重回 dyld::_main

然后我们再回到 dyld::_main 函数分析一下主程序。

  1. 配置环境变量
configureProcessRestrictions(mainExecutableMH, envp);

Linux 系统中,LD_LIBRARY_PATH 是一个由若干个路径组成的环境变量,每个路径之间有冒号隔开。默认情况下,LD_LIBRARY_PATH 为空。如果我们为某个进程设置了 LD_LIBRARY_PATH ,那么程序在启动时,动态链接器在查找共享库时,会首先查找由 LD_LIBRARY_PATH 指定的目录。这个环境变量可以很方便地让我们测试新的共享库或使用非标准的共享库。比如我们希望使用修改过的 libc.so.6,可以将这个新版的 libc 放到我们的目录 /home/user 中,然后指定 LD_LIBRARY_PATH

$ LD_LIBRARY_PATH=/home/user /bin/ls

2. 共享缓存

// load shared cache
    checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
    if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
        if ( sSharedCacheOverrideDir)
            mapSharedCache();  // 共享缓存
#else
        mapSharedCache(); // 共享缓存
#endif
    }

对于共享缓存的理解:dyld 加载时,为了优化程序启动,启用了共享缓存(shared cache)技术。共享缓存会在进程启动时被 dyld 映射到内存中,之后,当任何 Mach-O 映像加载时,dyld 首先会检查该 Mach-O 映像与所需的动态库是否在共享缓存中,如果存在,则直接将它在共享内存中的内存地址映射到进程的内存地址空间。 mapSharedCache 参考如下:


static void mapSharedCache()
{
    dyld3::SharedCacheOptions opts;
    opts.cacheDirOverride   = sSharedCacheOverrideDir;
    opts.forcePrivate       = (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion);
#if __x86_64__ && !TARGET_IPHONE_SIMULATOR
    opts.useHaswell         = sHaswell;
#else
    opts.useHaswell         = false;
#endif
    opts.verbose            = gLinkContext.verboseMapping;
    loadDyldCache(opts, &sSharedCacheLoadInfo);

    // update global state
    if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
        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, (const uuid_t *)&dyld::gProcessInfo->sharedCacheUUID[0], {0,0}, {{ 0, 0 }}, (const mach_header *)sSharedCacheLoadInfo.loadAddress);
    }
}

这里会执行核心方法 loadDyldCache , 对该方法简要说下:

bool loadDyldCache(const SharedCacheOptions& options, SharedCacheLoadInfo* results) {
/**
* 省略
**/
#if TARGET_IPHONE_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
        if ( reuseExistingCache(options, results) )
            return (results->errorMessage != nullptr);
        // slow path: this is first process to load cache
        return mapCacheSystemWide(options, results);
    }
#endif
}

loadDyldCache 会有关键判断:

  • 是否运行在模拟器,模拟器有单独处理

  • 如果缓存已经映射到了共享区域下,就把其在共享区域的地址映射到本进程的地址空间。感兴趣的可以深入研究 reuseExistingCache。如果没有,就加载缓存,并映射。

  • 被加载的缓存位于 /System/Library/Caches/com.apple.dyld 下的若干组,每个 cpu 架构代表一组。

  1. 主程序初始化
#if SUPPORT_OLD_CRT_INITIALIZATION
        // 初始化程序
        // Old way is to run initializers via a callback from crt1.o
        if ( ! gRunInitializersOldWay ) 
            initializeMainExecutable(); 
    #else
        // run all initializers
        initializeMainExecutable(); 
    #endif
  1. 加入动态库

load 方法不仅被 loadInsertedDylib 调用,也会被 dlopen 等运行时加载动态库的方法使用。

// load any inserted libraries
if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
	for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
        // 动态库的加入
	loadInsertedDylib(*lib); 
}
  1. 链接 link 主程序
      // 先链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
        sMainExecutable->setNeverUnloadRecursive();
  if ( sMainExecutable->forceFlat() ) {
       gLinkContext.bindFlat = true;
       gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
  1. 链接 link 动态库
// 再链接插入的库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted 
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
    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();
    }
    if ( gLinkContext.allowInterposing ) {
        // only INSERTED libraries can interpose
        // register interposing info after all inserted libraries are bound so chaining works
        for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
            ImageLoader* image = sAllImages[i+1];
            image->registerInterposing(gLinkContext);
        }
    }
}
  1. 初始化程序:initializeMainExecutable
    // Old way is to run initializers via a callback from crt1.o
    if ( ! gRunInitializersOldWay ) 
        initializeMainExecutable(); 
#else
    // run all initializers
    initializeMainExecutable(); 
#endif

    // notify any montoring proccesses that this process is about to enter main()
    notifyMonitoringDyldMain();

dyld::_main 函数


_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
		int argc, const char* argv[], const char* envp[], const char* apple[], 
		uintptr_t* startGlue)
{
	if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
		launchTraceID = dyld3::kdebug_trace_dyld_duration_start(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, (uint64_t)mainExecutableMH, 0, 0);
	}

	//Check and see if there are any kernel flags
	dyld3::BootArgs::setFlags(hexToUInt64(_simple_getenv(apple, "dyld_flags"), nullptr));
    // 配置签名信息
    // Grab the cdHash of the main executable from the environment
	uint8_t mainExecutableCDHashBuffer[20];
	const uint8_t* mainExecutableCDHash = nullptr;
	if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
		mainExecutableCDHash = mainExecutableCDHashBuffer;

        ...
#endif

	CRSetCrashLogMessage("dyld: launch started");
    // 设置上下文路径
	setContext(mainExecutableMH, argc, argv, envp, apple);

	// Pickup the pointer to the exec path.
	sExecPath = _simple_getenv(apple, "executable_path");

	...
    // 配置环境变量
    configureProcessRestrictions(mainExecutableMH, envp);

	...

	if ( sJustBuildClosure )
		sClosureMode = ClosureMode::On;
	getHostInfo(mainExecutableMH, mainExecutableSlide);

	// load shared cache
	checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
	if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
		if ( sSharedCacheOverrideDir)
			mapSharedCache();  // 共享缓存
#else
		mapSharedCache(); // 共享缓存
#endif
	}

	...
	// install gdb notifier
	stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
	stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
...
		// load any inserted libraries
		if	( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
			for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                // 动态库的加入
				loadInsertedDylib(*lib); 
		}
		// record count of inserted libraries so that a flat search will look at 
		// inserted libraries, then main, then others.
		sInsertedDylibCount = sAllImages.size()-1;

		// link main executable
		gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
		if ( mainExcutableAlreadyRebased ) {
			// previous link() on main executable has already adjusted its internal pointers for ASLR
			// work around that by rebasing by inverse amount
			sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
		}
#endif
        // 先链接主程序
		link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
		sMainExecutable->setNeverUnloadRecursive();
		if ( sMainExecutable->forceFlat() ) {
			gLinkContext.bindFlat = true;
			gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
		}
        // 再链接插入的库
		// link any inserted libraries
		// do this after linking main executable so that any dylibs pulled in by inserted 
		// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
		if ( sInsertedDylibCount > 0 ) {
			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();
			}
			if ( gLinkContext.allowInterposing ) {
				// only INSERTED libraries can interpose
				// register interposing info after all inserted libraries are bound so chaining works
				for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
					ImageLoader* image = sAllImages[i+1];
					image->registerInterposing(gLinkContext);
				}
			}
		}

		if ( gLinkContext.allowInterposing ) {
			// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
			for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
				ImageLoader* image = sAllImages[i];
				if ( image->inSharedCache() )
					continue;
				image->registerInterposing(gLinkContext);
			}
		}
	...
        // 初始化程序
		// Old way is to run initializers via a callback from crt1.o
		if ( ! gRunInitializersOldWay ) 
			initializeMainExecutable(); 
	#else
		// run all initializers
		initializeMainExecutable(); 
	#endif

		// notify any montoring proccesses that this process is about to enter main()
		notifyMonitoringDyldMain();
		if (dyld3::kdebug_trace_dyld_enabled(DBG_DYLD_TIMING_LAUNCH_EXECUTABLE)) {
			dyld3::kdebug_trace_dyld_duration_end(launchTraceID, DBG_DYLD_TIMING_LAUNCH_EXECUTABLE, 0, 0, 2);
		}
		ARIADNEDBG_CODE(220, 1);

        ...
	
	return result;
}

现在还是根据LLDB 调试堆栈结果来进行分析,接下来将会进入 初始化方法 initializeMainExecutable 函数

2.8 initializeMainExecutable

根据 LLDB 调试堆栈结果。我们知道接下来我们要进入 runInitializers 函数,看名字也很明显知道,这是与初始化相关的函数。

void initializeMainExecutable()
{
	// record that we've reached this step
	gLinkContext.startedInitializingMainExecutable = true;

	// run initialzers for any inserted dylibs
	ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
	initializerTimes[0].count = 0;
	const size_t rootCount = sImageRoots.size();
	if ( rootCount > 1 ) {
		for(size_t i=1; i < rootCount; ++i) {
			sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
		}
	}
	
	// run initializers for main executable and everything it brings up 
	sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
	
	// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
	if ( gLibSystemHelpers != NULL ) 
		(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

	// dump info if requested
	if ( sEnv.DYLD_PRINT_STATISTICS )
		ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
	if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
		ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

2.9 runInitializers

发现 runInitializers 函数跳不进去了,那么我们全局搜索,找到其定义:

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
	uint64_t t1 = mach_absolute_time();
	mach_port_t thisThread = mach_thread_self();
	ImageLoader::UninitedUpwards up;
	up.count = 1;
	up.imagesAndPaths[0] = { this, this->getPath() };
	processInitializers(context, thisThread, timingInfo, up);
	context.notifyBatch(dyld_image_state_initialized, false);
	mach_port_deallocate(mach_task_self(), thisThread);
	uint64_t t2 = mach_absolute_time();
	fgTotalInitTime += (t2 - t1);
}

根据 LLDB 调试堆栈结果。我们知道接下来我们要进入 processInitializers 函数

2.10 processInitializers

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
									 InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
	uint32_t maxImageCount = context.imageCount()+2;
	ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
	ImageLoader::UninitedUpwards& ups = upsBuffer[0];
	ups.count = 0;
	// Calling recursive init on all images in images list, building a new list of
	// uninitialized upward dependencies.
	for (uintptr_t i=0; i < images.count; ++i) {
		images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
	}
	// If any upward dependencies remain, init them.
	if ( ups.count > 0 )
		processInitializers(context, thisThread, timingInfo, ups);
}

根据 LLDB 调试堆栈结果。我们知道接下来我们要进入 recursiveInitialization 函数

2.11 recursiveInitialization

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();
}

根据 LLDB 调试堆栈结果。我们知道接下来我们要进入 notifySingle 函数

2.12 notifySingle

static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
	//dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
	std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
	if ( handlers != NULL ) {
		dyld_image_info info;
		info.imageLoadAddress	= image->machHeader();
		info.imageFilePath		= image->getRealPath();
		info.imageFileModDate	= image->lastModified();
		for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
			const char* result = (*it)(state, 1, &info);
			if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
				//fprintf(stderr, "  image rejected by handler=%p\n", *it);
				// make copy of thrown string so that later catch clauses can free it
				const char* str = strdup(result);
				throw str;
			}
		}
	}
	if ( state == dyld_image_state_mapped ) {
		// <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
		if ( !image->inSharedCache() ) {
			dyld_uuid_info info;
			if ( image->getUUID(info.imageUUID) ) {
				info.imageLoadAddress = image->machHeader();
				addNonSharedCacheImageUUID(info);
			}
		}
	}
	if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
		uint64_t t0 = mach_absolute_time();
		dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
		(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		uint64_t t1 = mach_absolute_time();
		uint64_t t2 = mach_absolute_time();
		uint64_t timeInObjC = t1-t0;
		uint64_t emptyTime = (t2-t1)*100;
		if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
			timingInfo->addTime(image->getShortName(), timeInObjC);
		}
	}
    // mach message csdlc about dynamically unloaded images
	if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
		notifyKernel(*image, false);
		const struct mach_header* loadAddress[] = { image->machHeader() };
		const char* loadPath[] = { image->getPath() };
		notifyMonitoringDyld(true, 1, loadAddress, loadPath);
	}
}

接下来 LLDB 调试堆栈就没有了,我们先来找一下 sNotifyObjCInit 这个函数,发现它在 registerObjCNotifiers 这个函数里面

2.13 registerObjCNotifiers

void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
	// record functions to call
	sNotifyObjCMapped	= mapped;
	sNotifyObjCInit		= init;
	sNotifyObjCUnmapped = unmapped;

	// call 'mapped' function with all images mapped so far
	try {
		notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
	}
	catch (const char* msg) {
		// ignore request to abort during registration
	}

	// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
	for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
		ImageLoader* image = *it;
		if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
			dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
			(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
		}
	}
}

发现就只是个 init 方法,跟不进去了,怎么办呢?我们先从源码中找一下 registerObjCNotifiers 这个函数,会发现它在 _dyld_objc_notify_register 这个函数里面。

2.14 _dyld_objc_notify_register

void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{
	dyld::registerObjCNotifiers(mapped, init, unmapped);
}

接着我们去 objc源码 中找一下 _dyld_objc_notify_register 这个函数,我们发现在 _objc_init 函数调用的时候,会调用 _dyld_objc_notify_register 函数

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();
    cache_init();
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

来到这里 , 我们就看到了 _dyld_objc_notify_register 被调用 , 传递了三个参数

  • map_images : dyldimage 加载进内存时 , 会触发该函数。
  • load_images : dyld 初始化 image 会触发该方法。( 我们所熟知的 load 方法也是在此处调用 )
  • unmap_image : dyldimage 移除时 , 会触发该函数。

也就是说 _objc_init 中注册并保存了 map_images , load_images , unmap_image 函数地址。

走到这里我们还是没有找到堆栈的闭环。那么我们回到 2.11 recursiveInitialization 当中的这行代码进行查看 doInitialization 方法

// initialize this image
bool hasInitializers = this->doInitialization(context);

2.15 doInitialization

根据 LLDB 调试堆栈我们就差 load images 这一步了,很明显,我们需要进入 doImageInit 函数看看里面做了什么。

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
	CRSetCrashLogMessage2(this->getPath());

	// mach-o has -init and static initializers
	doImageInit(context);
	doModInitFunctions(context);
	
	CRSetCrashLogMessage2(NULL);
	
	return (fHasDashInit || fHasInitializers);
}

2.16 doImageInit

简单分析一下这个函数,其实就是将函数方法的指针进行平移,最终找到其初始化的实现。

我们通过 // <rdar://problem/17973316> libSystem initializer must run first 这里可以看到,这个函数第一次执行 , 进行 libsystem 的初始化 . 会走到 doInitialization -> doModInitFunctions -> libSystemInitialized

Libsystem 的初始化 , 它会调用起 libdispatch_init , libdispatchinit 会调用 _os_object_init , _os_object_init 这个函数里面调用了 _objc_init

runtime 初始化后,在 _objc_init 中注册了几个通知,从 dyld 这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的 load 方法等。

_objc_init

前面我们说过, _objc_init 中注册并保存了 map_images , load_images , unmap_image 函数地址,到这里终于 找到了 load images 调用的地方,LLDB 调用堆栈的结果终于形成闭环了。

  • map_imagesdyldimage(镜像文件)加载(映射)进内存时,会触发该函数
  • load imagesdyld 初始化 image 会触发该函数。当镜像文件映射完毕后,会继而执行 load_images ,处理在 dyld 中已映射完毕的镜像的 +load 方法.
  • unmap_imagedyldimage 移除时,会触发该函数

runtime 接手后调用 map_images 做解析和处理,接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用类的 +load 方法和其 元类 的 +load 方法。

其实根据注释提醒 libSystem initializer must run first(系统库必须先初始化) ,系统库也是以镜像的形式被 dyld 加载,所以 load images 也可以形成闭环的。

void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
	if ( fHasDashInit ) {
		const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
		const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
		const struct load_command* cmd = cmds;
		for (uint32_t i = 0; i < cmd_count; ++i) {
			switch (cmd->cmd) {
				case LC_ROUTINES_COMMAND:
					Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
#if __has_feature(ptrauth_calls)
					func = (Initializer)__builtin_ptrauth_sign_unauthenticated((void*)func, ptrauth_key_asia, 0);
#endif
					// <rdar://problem/8543820&9228031> verify initializers are in image
					if ( ! this->containsAddress(stripPointer((void*)func)) ) {
						dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
					}
					if ( ! dyld::gProcessInfo->libSystemInitialized ) {
						// <rdar://problem/17973316> libSystem initializer must run first
						dyld::throwf("-init function in image (%s) that does not link with libSystem.dylib\n", this->getPath());
					}
					if ( context.verboseInit )
						dyld::log("dyld: calling -init function %p in %s\n", func, this->getPath());
					{
						dyld3::ScopedTimer(DBG_DYLD_TIMING_STATIC_INITIALIZER, (uint64_t)fMachOData, (uint64_t)func, 0);
						func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
					}
					break;
			}
			cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
		}
	}
}

总结

dyld 原理

dyld 从主可执行文件开始,解析 mach-o 找依赖动态库,对动态库进行 mmap()。然后对每个动态库进行遍历并解析 mach-o 结构,根据需要加载其它动态库。加载完毕,dyld 会查找所有需要绑定符号,并在修改时使用这些地址。最后修改完,dyld 自下而上运行初始化程序。先前做的优化是只要程序和动态库,dyld 很多步骤都可以在首次启动时被缓存。

dyld 总结

  • 本文通过从 APP 的启动时的 start函数 引入到了对 dyld 的探索
  • dyld 汇编中的 _dyld_start 找到 dyldbootstrap::start
  • dyldbootstrap::start 调用了 rebaseDyld函数Mach-O 内部的 DATA段 指针进行了重设基址和绑定操作,并且初始化了mach的系统调用。接下来 start函数 还做了很多初始化操作
  • 下面从 dyldbootstrap::start 中调用了 dyld::_main
  • dyld::_main 中,
    1. 环境变量配置
    2. 共享缓存
    3. 主程序的初始化
    4. 加入动态库
    5. link 主程序
    6. link 动态库
    7. 初始化主程序initializeMainExexutable()
  • 加载镜像文件首先实例化一个 ImageLoader,通过 ImageLoader 加载动态库,记录并插入库的数量,先试引用的库后主库,最后是其他库,加载完毕就是链接库
  • 链接库首先是链接主二进制文件,其次是引入的动态库,链接时是递归操作的,通过递归来实现库里面引用库的连接操作,链接完毕后循环插入初始镜像集
  • 然后我们通过获取镜像文件的真实地址对其进行初始化,初始化的时候通过各种调用的分析,我们找到了它在 objc:_objc_init函数 的具体初始化操作。
  • 最后通过 notifyMonitoringDyldMain函数 ,通知监听所有监听着该进程即将进入 main()函数