Mach-O 探究

iOS高级开发

Mach-OMach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。
Mach-O格式为大部分基于Mach内核的操作系统所使用的,包括NeXTSTEP, Mac OS XiOS,它们都以Mach-O格式作为其可执行文件,动态库,目标代码的文件格式。

Mach-O简介

在iOS开发中,我们的代码在编译后会生成一个.app的文件(Product文件夹下),而.app文件我们可以把它看做是一个文件夹,内部存放了APP正常运行所需要的文件,通常比较容易识别的是一些资源文件。如图:

我们通过显示包内容看下.app文件夹内都有什么:

当然我们这片文章的主角也在这个文件夹内:

系统识别Mach-O(这个名字是项目的名字)为可执行文件(Mach-O是一种可执行文件格式),我们来看下这个文件,Mach-O文件是无法直接打开或者查看包内容,这里我们需要借助MachOView工具来查看,工具是开源的如果你想看具体的实现,你可以看工具的源码,当然你可以直接下载使用。

打开后的页面是这样的:

Mach-O结构

实际上我们从使用MachOView打开后的文件目录也可以看出,Mach-O的文件结构分为三大部分:Header,Load Commands,Data

下面是官方提供的一张结构图:

根据上图,我们将我们看到的目录大致划分为:

  • Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
  • 加载命令(Load Command):当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。
  • 数据区(Data):Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块,每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。

注意:通过对比我们发现实际上官网给出的结构并不准确,在实际结果中还包含了Dynamic Loader Info,Function Starts,Symbol Table,Data In Code Entries,Dynamic Symbol Table,String Table,Code Signature等。

下面我们来详细看下每部分的内容

Mach64 Header

这里我们可以和苹果开源的Darwin源码一起看方便理解,源码在这里

32位

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};

64位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};

我们看到32位和64位的mach_header基本是一致的,只是在64位中新增了reserved字段,下面我们来看下其中每个字段所表示的意义。

Magic Number

offset data description value
00000000 FEEDFACF Magic Number MH_MAGIC_64

我们可以将其直译为魔数,他的值(Value)有两个:

1
2
#define	MH_MAGIC	0xfeedface	/* the mach magic number */ 32位
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */ 64位

用于这个Mach-O文件的标识,有32位和64位两个值。由此可以看出我们的示例是一个64位的Mach-O文件。

CPU Type ,CPU SubType

offset data description value
00000004 0100000C CPU Type CPU_TYPE_64
00000008 00000000 CPU SubType
00000000 CPU_SubType_ARM64_ALL

CPU Type和CPU SubType 表示支持的CUP架构类型和子类型,如ARM。而具体的类型有哪些我们可以通过查询/mach/machine.h.中的定义查看这里不做过多的扩展,具体可以看这里
我们的示例中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL。

File Type

offset data description value
0000000C 00000002 File Type MH_EXECUTE

File Type 表示 Mach-O的文件类型。包括

1
2
3
4
5
6
7
8
9
10
11
#define	MH_OBJECT	0x1		/* Target 文件:编译器对源码编译后得到的中间结果 */
#define MH_EXECUTE 0x2 /* 可执行二进制文件 */
#define MH_FVMLIB 0x3 /* VM 共享库文件(还不清楚是什么东西) */
#define MH_CORE 0x4 /* Core 文件,一般在 App Crash 产生 */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* 动态库 */
#define MH_DYLINKER 0x7 /* 动态连接器 /usr/lib/dyld */
#define MH_BUNDLE 0x8 /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
#define MH_DYLIB_STUB 0x9 /* 静态链接文件(还不清楚是什么东西) */
#define MH_DSYM 0xa /* 符号文件以及调试信息,在解析堆栈符号中常用 */
#define MH_KEXT_BUNDLE 0xb /* x86_64 内核扩展 */

这里类型均是在loader.h文件中定义的

对于我们示例中的我们的File Type为 MH_EXECUTE表示 可执行的二进制文件。

ncmds(Number of Load Commands)

offset data description value
00000010 00000017 Number of Load Commands 23

ncmds表示load command的数量。在我们的示例中表示数量为23个。

sizeofcmds(Size of Load Commands)

offset data description value
00000014 00000AF8 Size of Load Commands 2808

sizeofcmds表示所有load command的总大小。示例中总大小为2808。

Flags

offset data description value
00000018 00200085 Flags
00000001 MH_NOUNDEFS
00000004 MH_DYLDLINK
00000080 MH_TWOLEVEL
00200000 MH_PIE

Flags 是Mach-O文件的标志位。主要作用是告诉系统该如何加载这个Mach-O文件以及该文件的一些特性。有很多值,我们取常见的几种

1
2
3
4
5
6
7
8
9
#define	MH_NOUNDEFS	0x1		/* Target 文件中没有带未定义的符号,常为静态二进制文件 */
#define MH_SPLIT_SEGS 0x20 /* Target 文件中的只读 Segment 和可读写 Segment 分开 */
#define MH_TWOLEVEL 0x80 /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
#define MH_FORCE_FLAT 0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
#define MH_PIE 0x200000 /* 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */

结合我们的示例,我们共有4个Flags:

  • MH_NOUNDEFS
  • MH_DYLDLINK dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存
  • MH_TWOLEVEL 表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。
  • MH_PIE 每次系统加载进程后,都会为其随机分配一个虚拟内存空间(在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件)

注意:flags的值也定义在loader.h文件中 都可以通过源码查看。

Load Commands

Load Commands 紧跟在Header之后,用来告诉内核和dyld,如何将各个Segment加载入内存中。load command被源码表示为struct,有若干种load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:

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
/*
* The load commands directly follow the mach_header. The total size of all
* of the commands is given by the sizeofcmds field in the mach_header. All
* load commands must have as their first two fields cmd and cmdsize.
* The cmd
* field is filled in with a constant for that command type.
* Each command type
* has a structure specifically for it.
* The cmdsize field is the size in bytes
* of the particular load command structure plus anything that follows it that
* is a part of the load command (i.e. section structures, strings, etc.).
* To
* advance to the next load command the cmdsize can be added to the offset or
* pointer of the current load command.
* The cmdsize for 32-bit architectures
* MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
* of 8 bytes (these are forever the maximum alignment of any load commands).
* The padded bytes must be zero. All tables in the object file must also
* follow these rules so the file can be memory mapped. Otherwise the pointers
* to these tables will not work well or at all on some machines. With all
* padding zeroed like objects will compare byte for byte.
*/
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

对应我们示例中的Load Commands

我们在尝试去理解load_command的注释:

1
2
load commands紧跟着mach_header,load commands的总大小由mach_header汇总的sizeofcmds字段给出,所有的load commands都必须以cmd和cmdsize两个字段作为前两个字段(结合我们的示例也得到验证),cmd字段的值为commandtype常量,每一个commandtype都有一种特定的结构。cmdsize字段以字节为单位包含loadcommand结构和额外的其他字段(例如  section structures,strings等)。要前进到下一个加载命令,可以将cmdsize添加到当前加载命令的偏移量或指针。对于32位体系结构的cmdsize
必须是4字节的倍数,并且对于64位架构,必须是8字节的倍数(这些永远是所有装入命令的最大对齐),填充字节必须为零。

Segment

在这么多的load command中,需要我们重点关注的是segment load command,segment command解释了该如何将Data中的各个Segment加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment中。

而和我们的Run time相关的Segment,则位于__DATA类型Segment下。

Segment load command也分为32位和64位:

32位

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
/*
* The segment load command indicates that a part of this file is to be
* mapped into the task's address space. The size of this segment in memory,
* vmsize, maybe equal to or larger than the amount to map from this file,
* filesize. The file is mapped starting at fileoff to the beginning of
* the segment in memory, vmaddr. The rest of the memory of the segment,
* if any, is allocated zero fill on demand. The segment's maximum virtual
* memory protection and initial virtual memory protection are specified
* by the maxprot and initprot fields. If the segment has sections then the
* section structures directly follow the segment command and their size is
* reflected in cmdsize.
*/
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

64位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};

32位和64位的segment_command基本一致,只是在64位的结构中把和寻址相关的数据类型由uint32_t改为uint64_t

我们先看下示例中,和Segment相关的Command:

结合源码我们可以看到:

1
2
3
4
#define    SEG_PAGEZERO    "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,捕获到空指针 */
#define SEG_TEXT "__TEXT" /* 代码/只读数据段 */
#define SEG_DATA "__DATA" /* 数据段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */

根据前面结构图我们知道Load Commands实际上是一个二级结构:Segment->Section,正如示例中所示

因此,下面我们在看下section的结构

Section

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
/*
* A segment is made up of zero or more sections. Non-MH_OBJECT files have
* all of their segments with the proper sections in each, and padded to the
* specified segment alignment when produced by the link editor. The first
* segment of a MH_EXECUTE and MH_FVMLIB format file contains the mach_header
* and load commands of the object file before its first section. The zero
* fill sections are always last in their segment (in all formats). This
* allows the zeroed segment padding to be mapped into memory where zero fill
* sections might be. The gigabyte zero fill sections, those with the section
* type S_GB_ZEROFILL, can only be in a segment with sections of this type.
* These segments are then placed after all other segments.
*
* The MH_OBJECT format has all of its sections in one segment for
* compactness. There is no padding to a specified segment boundary and the
* mach_header and load commands are not part of the segment.
*
* Sections with the same section name, sectname, going into the same segment,
* segname, are combined by the link editor. The resulting section is aligned
* to the maximum alignment of the combined sections and is the new section's
* alignment. The combined sections are aligned to their original alignment in
* the combined section. Any padded bytes to get the specified alignment are
* zeroed.
*
* The format of the relocation entries referenced by the reloff and nreloc
* fields of the section structure for mach object files is described in the
* header file <reloc.h>.
*/
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section Section 名字 */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};

struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* Section 名字 */
char segname[16]; /* 所在的Segment名称*/
uint64_t addr; /* Section 所在的内存地址 */
uint64_t size; /* Section 的大小 */
uint32_t offset; /* Section 所在的文件偏移 */
uint32_t align; /* Section 的内存对齐边界 (2 的次幂) */
uint32_t reloff; /* 重定位信息的文件偏移 */
uint32_t nreloc; /* 重定位条目的数目 */
uint32_t flags; /* 标志属性 (section type and attributes)*/
uint32_t reserved1; /* 保留字段1 (for offset or index) */
uint32_t reserved2; /* 保留字段2 (for count or sizeof) */
uint32_t reserved3; /* 保留字段3 */
};

在64位和32位的section定义中,64位新增了一个reserved3保留字段,以及将section的addr和size字段由原来的uint32_t类型升级为uint64_t。

在Data中,程序的逻辑和数据是按照Segment(段)存储,在Segment中,又分为0或多个section,每个section中在存储实际的内容。而之所以这么做的原因在于,在section中,可以不用内存对齐达到节约内存的作用,而所有的section作为整体的Segment,又可以整体的内存对齐。

结合我们示例中的一个section结构如下图:

DATA(数据)

Mach-O的Data部分,其实是真正存储APP二进制数据的位置,前面的header和load command,仅是提供文件的说明以及加载信息的功能。

前面我们介绍过,我们通过Load Commands从DATA中读取数据,而Load Commands被划分成了多个Segment,也就是说 我们通过不同的Load Commands从DATA中读取不同的数据。

在介绍Segment的时候我们说过Segment被划分成__PAGEZERO,__TEXT,__DATA,__LINKEDIT这几段。

结合我们的示例,我们发现DATA被划分为:__TEXT,__DATA

下面我们来看下这几个数据段(section):

__TEXT段

__TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。

下面是几个我们常见的section:

Section 存储内容
__TEXT.__text 主程序代码
__TEXT.__cstring C 语言字符串
__TEXT.__const const 关键字修饰的常量
__TEXT.__stubs 用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper 当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methname Objective-C 方法名称
__TEXT.__objc_methtype Objective-C 方法类型
__TEXT.__objc_classname Objective-C 类名称

我们来结合示例看下这几个section的内容:

cstring

我们可以从中看到lw_property,lw_publicproperty这两个属性名。以及我们打印的NSLog中的内容,同时我们发现,我们可能定义的某些三方key或者appid在这里都暴露在外部。

1
static const NSString *lw_constsecretKey = @"11234455556";

objc_methname

我们可以看到我们自定义的方法名lw_publicMethod,lw_privateMethod以及lw_property,lw_publicproperty重写的setter和getter方法。

classname

我们可以看到我们自定义的类的类:LWCustomClass

_DATA

__DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的sectin有:

下面我们看下常见的__DATA下的section:

Section 用途
__DATA.__data 初始化过的可变数据
__DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__DATA.__const 没有初始化过的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
__DATA.__common 没有初始化过的符号声明
__DATA.__objc_classlist Objective-C 类列表
__DATA.__objc_protolist Objective-C 协议列表
__DATA.__objc_imginfo Objective-C 镜像信息
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超类引用

这些以objc开头的DATA字段都是跟runtime有关的,后面我们会详细分析。

__objc_imageinfo

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
typedef struct objc_image_info {
uint32_t version; // currently 0
uint32_t flags;

#if __cplusplus >= 201103L
private:
// 位移枚举
enum : uint32_t {
IsReplacement = 1<<0, // used for Fix&Continue, now ignored
SupportsGC = 1<<1, // 是否支持垃圾回收
RequiresGC = 1<<2, // 镜像是否需要回收
OptimizedByDyld = 1<<3, // image is from an optimized shared cache
CorrectedSynthesize = 1<<4, // used for an old workaround, now ignored
IsSimulated = 1<<5, // image compiled for a simulator platform
HasCategoryClassProperties = 1<<6, // class properties in category_t

SwiftVersionMaskShift = 8,
SwiftVersionMask = 0xff << SwiftVersionMaskShift // Swift ABI version

};
public:
enum : uint32_t {
SwiftVersion1 = 1,
SwiftVersion1_2 = 2,
SwiftVersion2 = 3,
SwiftVersion3 = 4
};

public:
bool isReplacement() const { return flags & IsReplacement; }
bool supportsGC() const { return flags & SupportsGC; }
bool requiresGC() const { return flags & RequiresGC; }
bool optimizedByDyld() const { return flags & OptimizedByDyld; }
bool hasCategoryClassProperties() const { return flags & HasCategoryClassProperties; }
bool containsSwift() const { return (flags & SwiftVersionMask) != 0; }
uint32_t swiftVersion() const { return (flags & SwiftVersionMask) >> SwiftVersionMaskShift; }
#endif
} objc_image_info;

我们发现objc_image_info中主要是有version字段和flag字段,

__objc_classlist

这个section列出了所有的class,包括meta class。

图中的value值是就是这个类结构体的地址(包括元类),类结构体的结构为objc中的objc_class结构体,结构如下:

1
2
3
4
5
6
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
__objc_catlist

这里可以查看代码中的所有分类,其value的值为指向分类结构体的指针

对应oc中的结构为category_t,具体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct category_t {
// 是指 class_name 而不是 category_name
const char *name;
// 要扩展的类对象,编译期间是不会定义的,而是在运行时通过 * name 对应到对应的类对象。
classref_t cls;
// 对象方法列表
struct method_list_t *instanceMethods;
// 类方法列表
struct method_list_t *classMethods;
// 协议列表
struct protocol_list_t *protocols;
// 实例属性
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
// 类属性(这个结构体以_开头命名???)
struct property_list_t *_classProperties;
// methodsForMeta 返回类方法列表或者对象方法列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
// 属性列表返回方法
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
__objc_protolist

该Section中记录了项目中所有的协议。 其value值为指向协议的指针

协议的结构体为protocol_t,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
}
__objc_classrefs

该section记录了哪些class被引用了,这里记录了所有被实例化的class,有些类虽然在包里,但是我们并未使用,因此这里不会有。

__objc_selrefs

这section记录哪些SEL对应的字符串被引用了,有系统方法,也有自定义方法:

__objc_superrefs

该section记录了调用super方法的类。

比如,在子类方法中,我们调用了父类的方法,就会将子类记录在这里。

__objc_const

该section用来记录在OC内存初始化过程中的不可变内容。这里所谓的不可变内容并不是我们在程序中所写的const NSInteger k = 5这种常量数据(它存在__TEXT的const section中),而是在OC内存布局中不可变得部分。

应用启动

根据上面介绍的在应用启动期间,dyld和kern会读取Mach-O文件中的Load Command去读取和加载_DATA数据段下的内容,而这一切都发生在main函数之前。所以我们看下main函数之前都发生了什么?

启动调用堆栈

添加一个符号断点(Symbolic BreakPoint)让应用在执行到_objc_init方法是断点执行。

这样我们就能看到下面的这个调用栈:

因为_objc_init方法是runtime的入口,因此在这之前调用的方法都是dyld和ImageLoader的操作

dyld

dyld(the dynamic link editor)动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责,dyld的主要工作内容为(参考 dyld: Dynamic Linking On OS X ):

  • 从 kernel 留下的原始调用栈引导和启动自己
  • 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
  • non-lazy 符号立即 link 到可执行文件,lazy 的存表里
  • Runs static initializers for the executable
  • 找到可执行文件的 main 函数,准备参数并调用
  • 程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
  • 程序main函数 return 后执行 static terminator
  • 某些场景下 main 函数结束后调 libSystem 的 _exit 函数

ImageLoader

这里的image不是图片的意思,它是一个二进制文件,你可以把他理解为一个镜像文件。内部是被编译过的符号、代码等,因此ImageLoader作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。

他的主要工作为:

  • 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)
  • 再从可执行文件 image 递归加载所有符号

ImageLoaderMachO

顾名思义这里应该是去加载MachO文件,从堆栈中我们可以看到主要跟doInitialization方法和doModInitFunctions方法。

doInitialization

这个方法的主要作用是:获取Mach-O的init方法的地址并调用

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
// mach-o文件中指令的个数
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
// 遍历指令
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
// 获取macho_routines_command的init_address
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
// 执行-init方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
break;
}
// 计算下一个指令((char*)cmd)+cmd->cmdsize
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
doModInitFunctions

这个方法的主要作用是:获取Mach-O的static initializer的地址并调用

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

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
// mach-o文件中指令的个数
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
// 遍历所有的指令
for (uint32_t i = 0; i < cmd_count; ++i) {
// 如果指令是Mach-o中的LC_SEGMENT_COMMAND
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
// 从sectionsStart到sectionsEnd
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {

for (size_t i=0; i < count; ++i) {
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
// 执行initializer方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
}
// 根据指令的地址+指令大小获取到下一个指令
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}

总结

上述我们介绍了Mach-O文件的主要结构,以及每个segment和section的功能和字段的作用,结尾处我们通过查看应用启动调用堆栈来确认Mach-O文件何时被ImageLoader解析并加载到内存中,提供给后续的runtime使用。鉴于main函数之前系统内核,dyld,ImageLoader,rumtime做了很多准备,我们决定新开一篇文章来讲述这个过程发生了什么,敬请期待!

参考文章

XNU源码
探秘 Mach-O 文件
Mach-O文件结构理解
Mach-O 与动态链接
iOS 程序 main 函数之前发生了什么