Mach-O文件详解

Mach-O是什么

Mach-O 是 Mach object 文件格式的缩写,它是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。类似于windows上的PE文件以及linux上的ELF文件。作为 a.out 格式的替代品,Mach-O提供更多的可扩展性和更快的符号表信息存取。大多数基于 Mach 内核的操作系统都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用这种格式作为本地可执行文件、库和对象代码的例子。

Mach-O文件包含固定大小的头部(Header),几个可变大小的加载命令(LoadCommands),一个或多个代码段(Segment),每个代码段可以包含一个或多个代码区(Section)。

胖二进制文件

为了使文件能运行在多个平台,出现了胖二进制文件(Fat Binary),又叫通用二进制文件(Universal Binary)。使用lipo命令可以列出胖文件中的体系结构类型。胖二进制文件实际上是一个包装器(Wrapper),一种将用于多种体系结构的Mach-O文件连接起来的简单存档。

胖二进制文件由胖头部(Fat Header)以及后面的 Mach-O 文件组成。胖头部包含一个幻数,后接一个整数值,表示二进制文件驻留在胖文件中的体系结构数量。

1
2
3
4
5
6
7
#define FAT_MAGIC  0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
uint32_t magic; /* FAT_MAGIC 标记,表示是Fat的文件类型,是固定的0xcafebabe或者是0xbebafeca */
uint32_t nfat_arch; /* number of structs that follow 包含了多个少Mach-O文件 */
};

再后面是一系列胖体系结构指示符(fat_arch),每个指示符用于胖文件中包含的一种体系结构。

1
2
3
4
5
6
7
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) 支持的CPU类型,一般有ARMV7,ARM64,X86,X86_64这几种类型 */
cpu_subtype_t cpusubtype; /* machine specifier (int) 子CPU类型*/
uint32_t offset; /* file offset to this object file 当前架构的 Mach-O 文件的数据相对于文件开头的偏移位置 */
uint32_t size; /* size of this object file 数据的大小*/
uint32_t align; /* alignment as a power of 2 数据的内存对齐边界 */
};

offset 在armv7中是16384(0x4000),arm64中是4294967296 (0x10000 0000)

注意:尽管胖文件中的Mach-O文件遵循其所在体系结构的字节序,但fat_header和fat_arch总是大端字节储存。 MacOS是小端序的。

合并和瘦身

Xcode 在编译 iOS 程序可以选择同时支持 ARMV7 和 ARM64,编译 macOS 程序也可以选择同时支持 x86 和 x86_64,但是如果一个程序需要同时支持 iOS 和 macOS 的时候,Xcode 不能自动生成,可以使用 lipo 命令手动对文件进行合并。

1
$lipo -create test_iPhone test_macOS -output test_all

由于每个 CPU 平台都是单独的一个 Mach-O 文件,然后合成的 Fat 文件,所以体积会变大,比如某个程序我们只需要支持 ARM64,就可以把其他平台给移除掉,这样就能起到 “瘦身” 的作用,使用 lipo 命令移除其他平台。

1
$lipo -thin arm64 ~/debugserver -output ~/debugserver

Mach-O结构

Mach-O 没有类似于 XML、YAML、JSON 等诸如此类的特殊格式,它只是一个二进制字节流,被划分为了有意义的数据块。这些块包含元信息,比如,字节顺序、cpu 类型、块的大小,等等。

典型的 Mach-O 文件包含三个区域:

  • Header-包含该二进制文件的一般信息:字节顺序、(魔数)、cpu 类型、LoadCommands的数量等等。
  • LoadCommands -它是一张包含很多内容的表,内容包括区域的位置、符号表、动态符号表等。每个加载指令都包含一个元信息,比如指令类型、名称、在二进制文件中的位置等等。加载Mach-O文件时会使用这里的数据来确定内存的分布
  • Data-通常是对象文件中最大的部分。每一个segment的具体数据都保存在这里。主要包含代码、数据,例如符号表,动态符号表等等。

Headers的主要作用就是帮助系统迅速的定位Mach-O文件的运行环境,以下是mach_header与mach_header_64的定义

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
/*
* 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 和 fat_hader 里的 magic 类似,也是一个标记,32 位的值是 MH_MAGIC,64位的值是 MH_CIGAM_64。 */
cpu_type_t cputype; /* cpu specifier 与 fat_arch 里的 cputype 的含义一样。*/
cpu_subtype_t cpusubtype; /* machine specifier 与 fat_arch 里的 cpusubtype 的含义一样*/
uint32_t filetype; /* type of file 可执行文件就是 MH_EXECUTE,如果是动态库就是 MH_DYLIB */
uint32_t ncmds; /* number of load commands 表示 Mach-O 文件中 load command (加载命令)的个数 */
uint32_t sizeofcmds; /* the size of all the load commands 表示load command (加载命令) 占用的字节总大小 */
uint32_t flags; /* flags 表示dyld加载时文件的标志信息*/
};

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC 0xfeedface /* the mach magic number */
#define MH_CIGAM 0xcefaedfe /* NXSwapInt(MH_MAGIC) */

/*
* 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 系统保留字段 */
};

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */

Mach-O文件不仅仅用来实现可执行文件,同时还用来实现了其他内容。

filetype:
​```
#define MH_OBJECT 0x1 /* relocatable object file 一个中间的,可重定位的目标文件,也可用于内核扩展(传统具有.o后缀)/
#define MH_EXECUTE 0x2 /
demand paged executable file 一个标准的按需分页的可执行文件*/
#define MH_FVMLIB 0x3 /* fixed VM shared library file CoreDump /
#define MH_CORE 0x4 /
core file 用于存储中止程序的地址空间的文件,包含”核心转储“的核心文件(core file)/
#define MH_PRELOAD 0x5 /
preloaded executable file /
#define MH_DYLIB 0x6 /
dynamically bound shared library 一个动态共享库*/
#define MH_DYLINKER 0x7 /* dynamic link editor 一个特殊共享库,是一个动态链接器 /
#define MH_BUNDLE 0x8 /
dynamically bound bundle file 在运行时以编程方式加载进应用程序中的插件代码*/
#define MH_DYLIB_STUB 0x9 /* shared library stub for static /
/
linking only, no section contents /
#define MH_DSYM 0xa /
companion file with only debug /
/
sections /
#define MH_KEXT_BUNDLE 0xb /
x86_64 kexts 内核扩展文件*/

1
2
3
4

Mach-O headers还包含了一些很重要的dyld的加载参数

flags:

/* Constants for the flags field of the mach_header /
#define MH_NOUNDEFS 0x1 /
the object file has no undefinedreferences 目标文件没有未定义的符号,不存在链接依赖*/
#define MH_INCRLINK 0x2 /* the object file is the output of an
incremental link against a base file
and can’t be link edited again /
#define MH_DYLDLINK 0x4 /
the object file is input for the 目标文件是动态链接输入文件,不能被再次静态链接
dynamic linker and can’t be staticly
link edited again /
#define MH_BINDATLOAD 0x8 /
the object file’s undefined
references are bound by the dynamic
linker when loaded. /
#define MH_PREBOUND 0x10 /
the file has its dynamic undefined
references prebound. /
#define MH_SPLIT_SEGS 0x20 /
the file has its read-only and 只读 segments 和 可读写 segments 分离
read-write segments split /
#define MH_LAZY_INIT 0x40 /
the shared library init routine is
to be run lazily via catching memory
faults to its writeable segments
(obsolete) /
#define MH_TWOLEVEL 0x80 /
the image is using two-level name
space bindings */

/*MH_NO_HEAP_EXECUTION 堆内存不可执行
MH_PIE 允许随机的地址空间
MH_ALLOW_STACK_EXECUTION 栈内存可执行代码,一般是默认关闭的。
MH_NO_HEAP_EXECUTION 堆内存无法执行代码
*/

1
2
3
4
5
6
7
8
9
## LoadCommands
Headers 之后就是 Load Commands,其占用的内存和加载命令的总数在 Headers 中已经指出。

load_command 数据结构:
​```
struct load_command {
uint32_t cmd; /* type of load command 加载命令类型 */
uint32_t cmdsize; /* total size of command in bytes 命令的总大小,以字节为单位*/
};

cmd结构类型:

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
/* Constants for the cmd field of all load commands, the type */
#define LC_SEGMENT 0x1 /* segment of this file to be mapped */
#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */
#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */
#define LC_IDFVMLIB 0x7 /* fixed VM shared library identification */
#define LC_IDENT 0x8 /* object identification info (obsolete) */
#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */
#define LC_PREPAGE 0xa /* prepage command (internal use) */
#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */
#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */
#define LC_ID_DYLIB 0xd /* dynamically linked shared lib ident */
#define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */
#define LC_ID_DYLINKER 0xf /* dynamic linker identification */
#define LC_PREBOUND_DYLIB 0x10 /* modules prebound for a dynamically */
/* linked shared library */
#define LC_ROUTINES 0x11 /* image routines */
#define LC_SUB_FRAMEWORK 0x12 /* sub framework */
#define LC_SUB_UMBRELLA 0x13 /* sub umbrella */
#define LC_SUB_CLIENT 0x14 /* sub client */
#define LC_SUB_LIBRARY 0x15 /* sub library */
#define LC_TWOLEVEL_HINTS 0x16 /* two-level namespace lookup hints */
#define LC_PREBIND_CKSUM 0x17 /* prebind checksum */

/*
* load a dynamically linked shared library that is allowed to be missing
* (all symbols are weak imported).
*/
#define LC_LOAD_WEAK_DYLIB (0x18 | LC_REQ_DYLD)

#define LC_SEGMENT_64 0x19 /* 64-bit segment of this file to be
mapped */
#define LC_ROUTINES_64 0x1a /* 64-bit image routines */
#define LC_UUID 0x1b /* the uuid */
#define LC_RPATH (0x1c | LC_REQ_DYLD) /* runpath additions */
#define LC_CODE_SIGNATURE 0x1d /* local of code signature */
#define LC_SEGMENT_SPLIT_INFO 0x1e /* local of info to split segments */
#define LC_REEXPORT_DYLIB (0x1f | LC_REQ_DYLD) /* load and re-export dylib */
#define LC_LAZY_LOAD_DYLIB 0x20 /* delay load of dylib until first use */
#define LC_ENCRYPTION_INFO 0x21 /* encrypted segment information */
#define LC_DYLD_INFO 0x22 /* compressed dyld information */
#define LC_DYLD_INFO_ONLY (0x22|LC_REQ_DYLD) /* compressed dyld information only */
#define LC_LOAD_UPWARD_DYLIB (0x23 | LC_REQ_DYLD) /* load upward dylib */
#define LC_VERSION_MIN_MACOSX 0x24 /* build for MacOSX min OS version */
#define LC_VERSION_MIN_IPHONEOS 0x25 /* build for iPhoneOS min OS version */
#define LC_FUNCTION_STARTS 0x26 /* compressed table of function start addresses */
#define LC_DYLD_ENVIRONMENT 0x27 /* string for dyld to treat
like environment variable */
#define LC_MAIN (0x28|LC_REQ_DYLD) /* replacement for LC_UNIXTHREAD */
#define LC_DATA_IN_CODE 0x29 /* table of non-instructions in __text */
#define LC_SOURCE_VERSION 0x2A /* source version used to build binary */
#define LC_DYLIB_CODE_SIGN_DRS 0x2B /* Code signing DRs copied from linked dylibs */
#define LC_ENCRYPTION_INFO_64 0x2C /* 64-bit encrypted segment information */
#define LC_LINKER_OPTION 0x2D /* linker options in MH_OBJECT files */
#define LC_LINKER_OPTIMIZATION_HINT 0x2E /* optimization hints in MH_OBJECT files */
#ifndef __OPEN_SOURCE__
#define LC_VERSION_MIN_TVOS 0x2F /* build for AppleTV min OS version */
#endif /* __OPEN_SOURCE__ */
#define LC_VERSION_MIN_WATCHOS 0x30 /* build for Watch min OS version */
结构 介绍
LC_SEGMENT、LC_SEGMENT_64 将 segment 映射到进程的内存空间,
LC_ID_DYLIB 动态库
LC_UUID 二进制文件 id,与符号表 uuid 对应,可用作符号表匹配
LC_LOAD_DYLINKER 启动动态加载器,
LC_SYMTAB 描述在 __LINKEDIT 段的哪找字符串表、符号表
LC_CODE_SIGNATURE 代码数字签名等
LC_THREAD 开启一个MACH线程,但是不分配栈空间
LC_UNIXTHREAD 开启一个UNIX线程
LC_ENCRYPTION_INFO 加密二进制文件
LC_RPATH 程序运行时的查找路径
LC_VERSION_MIN_IPHONEOS 支持最低的 iOS 版本号
LC_MAIN 记录了可执行文件的主函数main()的位置

LC_MAIN加载命令中的Entry Offset字段+基地址(RVA选项下的文件头部地址)= IDA中左侧函数_main的地址。

1
2
3
4
5
6
struct entry_point_command {
uint32_t cmd; /* LC_MAIN only used in MH_EXECUTE filetypes */
uint32_t cmdsize; /* 24 */
uint64_t entryoff; /* file (__TEXT) offset of main() main() 函数的文件偏移*/
uint64_t stacksize;/* if not zero, initial stack size */
};

Segment & Section

加载数据时,主要加载的就是LC_SEGMETLC_SEGMENT_64LC_SEGMENT的数据结构是这样的。这里大部分的数据是用来帮助内核将Segment映射到虚拟内存的

segment_command:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name 段的名称,常见的段名称有 __PAGEZERO、__LINKEDIT、__TEXT、__DATA*/
uint32_t vmaddr; /* memory address of this segment 段要加载的虚拟内存地址 未偏移),由于 ALSR,程序会在进程加上一段偏移量(slide),真实的地址 = vm address + slide*/
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 标示了Segment中有多少secetion */
uint32_t flags; /* flags 段的标志信息*/
};
1
2
3
4
5
__PAGEZERO : 可执行文件有的,动态库里没有。这个段开始地址为0(NULL指针指向的位置),是一个不可读、不可写、不可执行的空间,能够在空指针访问时抛出异常。这个段的大小,32位上是 0x4000,64位上是 4G。
__TEXT:代码段,里面主要是存放代码的,该段是可读可执行,但是不可写。
__DATA :数据段,里面主要是存放数据,该段是可读可写,但不可执行。
__LINKEDIT :用于存放签名信息,该段是只可读,不可写不可执行。 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等
__OBJC 包含会被Objective Runtime使用到的一些数据

section:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section section的名称,最长 16 字节大小*/
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) 节区的标志属性 如果是 SG_PROTECTED_VERSION_1,表示该段是经过加密的*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};

TEXT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_text:主程序代码
__cstring: 硬编码的字符串 去重后的C字符串
const 初始化过的常量
__stubs 桩代码 符号桩。本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。
__stub_helper:辅助函数。上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。
__symbolstub1:用于动态链接的存根
unwind_info:用于存储处理异常情况信息
__objc_methname:Objective-C的方法名
__objc_classname:Objective-C的类名
__cstring:硬编码的字符串
_DATA
__data 初始化可变的数据
const 没有初始化过的常量
__objc_imageinfo 镜像信息 ,在运行时初始化时 objc_init,调用 load_images 加载新的镜像到 infolist 中
__lazy_symbol:懒加载,延迟加载节,通过dyld_stub_binder辅助链接
_got:存储引用符号的实际地址,类似于动态符号表
__la_symbol_ptr: lazy-binding的指针表,每个表项中的指针一开始指向stub_helper
__nl_symbol_ptr:非lazy-binding的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
__mod_init_func:初始化的全局函数地址,在主要之前被调用 初始化函数,在main之前调用
__mod_term_func:结束函数地址 终止函数,在main返回之后调用
__cfstring:Core Foundation用到的字符串(OC字符串)
__objc_classlist 类列表
__objc_classrefs 引用的类

Symbol Table & String Table

Symbol 表的头信息是在 LoadCommand 里的 LC_SYMTAB,其中 symoff 表示符号表的偏移。符号表的结构是一个连续的列表,其中的每一项都是一个 struct nlist。

nlist:

1
2
3
4
5
6
7
8
9
struct nlist {
union {
uint32_t n_strx; /*符号名在字符串表中的偏移量 */
} n_un;
uint8_t n_type;
uint8_t n_sect; /*节的索引*/
int16_t n_desc;
uint32_t n_value; /*函数对应的地址*/
};
  • 动态库载入信息 Dynamic Loader Info
  • 入口函数 Function Starts
  • 符号表 Symbol Table
  • 动态库符号表 Dynamic Symbol Table
  • 字符串表 String Table

Mach-O执行

当你点击一个icon启动应用程序的时候,系统在内部大致做了如下几件事:

  • 内核(OS Kernel)创建一个进程,分配虚拟的进程空间等等,加载动态链接器。
  • 通过动态链接器加载主二进制程序引用的库、绑定符号。
  • 启动程序

内核处理流程

二进制文件加载过程

下面列出来在加载二进制文件过程中依次调用的函数

1
2
3
4
5
6
7
execve
__mac_execve
exec_activate_image
exec_mach_imgact
load_machfile
parse_machfile
load_dylinker

加载dyld过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
load_dylinker{
get_macho_vnode{
//读取dyld的fat_header
vn_rdwr{}
//读取dyld的mach_header
vn_rdwr{}
}
parse_machfile{
//Map the load commands into kernel memory.
vn_rdwr{}
load_segment{
//这里进行了slide偏移, 并且在对_TEXT segment 进行映射时重新定位了, result->mach_header, 这个的原理像elf的segment加载时, 把elf-header算在第一个segment上.
map_segment{}
}
}
}

dyld处理流程

dyld 的处理过程在 dyld.cpp, 从 LC_MAIN 拿到地址后转到 dyld.cpp/_main() 执行

一切的开始是dyldbootstrap::start这个函数.它去调用了dyld::main函数.这个函数从外部传入Mach-O的header,在dyld::main中,dyld会去设置运行环境,配置相关的环境变量.

处理环境变量

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
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
[...]
configureProcessRestrictions(mainExecutableMH);
#if __MAC_OS_X_VERSION_MIN_REQUIRED
if ( gLinkContext.processIsRestricted ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}



configureProcessRestrictions //对 ios 和 osx 做了区分, ios 默认不支持任何环境变量
|
checkEnvironmentVariables //检查环境变量, 之后调用下一个函数做处理
|
processDyldEnvironmentVariable //处理环境变量, 设置gLinkContext

这里有一个关键的过程 setContext(mainExecutableMH, argc, argv, envp, apple); 设置上下文需要使用到的全局变量.

解析macho执行文件

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
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
[...]

// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
从 instantiateFromLoadedImage 开始解析

instantiateFromLoadedImage
// 检查文件格式, 加载主可执行文件, 记录该image到全局环境变量
instantiateFromLoadedImage:dyld.cpp
{
// 处理加载命令, 根据加载命令处理加载可执行文件
ImageLoaderMachO::instantiateMainExecutable:ImageLoaderMachO.cpp
{
// 处理, 区分加载命令
ImageLoaderMachO::sniffLoadCommands:ImageLoaderMachO.cpp
// 根据加载命令, 开始加载可执行文件 ImageLoaderMachOCompressed::instantiateMainExecutable:ImageLoaderMachOCompressed.cpp
{
// 创建ImageLoaderMachOCompressed对象
ImageLoaderMachOCompressed::instantiateStart:ImageLoaderMachOCompressed.cpp
// 根据加载命令填充ImageLoaderMachOCompressed对象
ImageLoaderMachOCompressed::instantiateFinish:ImageLoaderMachOCompressed.cpp
}
}
// 记录image到全局变量
addImage:dyld.cpp
}

加载共享动态库

在环境变量配置完毕后,dyld会去加载共享缓存

加载的步骤是先通过checkShareRegionDisable函数检查是否被关闭,iOS下必须开启共享缓存,如果没有被禁用,那么就会调用mapSharedCache函数去加载,当然实际加载是在该函数内调用的loadDyldCache函数,加载共三种,fast Path(已经加载的不需要再加载),slow path(第一次调用则去加载.mapCacheSystemWide),还有一种是模拟器下(simulator)的.

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
uintptr_t

_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,

int argc, const char* argv[], const char* envp[], const char* apple[],

uintptr_t* startGlue)

{

[...]

// load shared cache

checkSharedRegionDisable();

#if DYLD_SHARED_CACHE_SUPPORT

if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {

mapSharedCache(); // 判断是否存在共享动态库, 如果存在直接使用, 否则进行加载, gLinkContext记录共享库地址

} else {

dyld_kernel_image_info_t kernelCacheInfo;

bzero(&kernelCacheInfo.uuid[0], sizeof(uuid_t));

kernelCacheInfo.load_addr = 0;

kernelCacheInfo.fsobjid.fid_objno = 0;

kernelCacheInfo.fsobjid.fid_generation = 0;

kernelCacheInfo.fsid.val[0] = 0;

kernelCacheInfo.fsid.val[0] = 0;

task_register_dyld_shared_cache_image_info(mach_task_self(), kernelCacheInfo, true, false);

}

#endif

当共享缓存被加载后,接下来,dyld就会继续在main函数中加载我们的主程序也就是我们的可执行文件。我们在方法中找到instantiateFromLoadedImage这个函数,在这个函数里,dyld会实例化我们的可执行文件。它实际上是通过我们传进来的machO的header判断当前cpu是否支持当前我们的machO的架构.如果支持则调用instantiateMainExecutable函数去实例化我们的可执行文件,并添加到imageList中。

当然可执行文件的实例化是在instantiateMainExecutable函数内部实现的,在该函数内部先调用了sniffLoadCommands,这个函数通过读取loadCommand段内的信息去加载。在sniffLoadCommands中严格判断了loadCommands的条数,不能超过255条,依赖的库不能超过4095个。最后该函数会修改Compress值,外部的instantiateMainExecutable函数会通过这个值来决定加载主程序的方式。在主程序被实例化加载后,接下来dyld就会继续在main函数中去加载我们插入的动态库,具体加载函数在loadInsertedDylib里进行。

加载DYLD_INSERT_LIBRARIES的动态库

1
2
3
4
5
6
7
8
9
10
11
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
[...]
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

loadInsertedDylib:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
loadInsertedDylib:dyld.cpp
{
load:dyld.cpp
{
loadPhase0:dyld.cpp
loadPhase1:dyld.cpp
loadPhase2:dyld.cpp
loadPhase3:dyld.cpp
loadPhase4:dyld.cpp
loadPhase5:dyld.cpp
loadPhase5check:dyld.cpp
loadPhase5load:dyld.cpp
loadPhase5stat:dyld.cpp
loadPhase5load:dyld.cpp
loadPhase5open:dyld.cpp
loadPhase6
{
checkandAddImage::dyld.cpp
}

}
}

加载依赖动态库

1
2
3
4
5
6
7
8
9
10
11
12
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
[...]
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 函数开始加载依赖动态库,在link方法中不光是链接我们的插入动态库,还会在函数内通过recursiveLoadLibraries函数循环加载我们的所有的依赖库.在加载后再Rebase每一个都添加上偏移值以得到真正的依赖库的地址也就是重定位。在链接定位后,还是在这个函数中继续对依赖库进行符号绑定,弱绑定等一系列操作,当这些都做完了主程序也就被加载链接完成。跟主程序加载链接一致,dyld当得知插入依赖库长度大于0会遍历加载链接这些库.链接完毕后就会将主程序与这些库绑定起来。

link

1
2
3
4
5
6
7
8
9
10
11
12
13
14
link:dyld.cpp
{
ImageLoader::link:ImageLoader.cpp
{
ImageLoader::recursiveLoadLibraries:ImageLoader.cpp
{
ImageLoaderMachO::doGetDependentLibraries:ImageLoader.cpp
libraryLocator:dyld.cpp
{
load:dyld.cpp
}
}
}
}

在这些依赖库的操作全部完成,就会调用initializeMainExecutable函数来初始化我们的主程序!

接下来会调用loadImages函数

load_images调用了call_load_methods,函数内就是循环调用我们的Objc类的load方法。接下来dyld就会调用doModInitFunctions这个函数会调用执行我们程序的特殊函数,比如全局的C++的构造方法.其实实质上就是dyld会读取Mach-O里DATA段中的init_func这个字段进行调用里面的函数。

最终一系列的操作完毕后,dyld就会去查找我们主程序的入口,对应我们Mach-O的LC_MAIN.在找到后返回一个result结果,也就调起了我们主程序的main函数,结束掉dyld_start整个流程。

参考文献:

[1]解读Mach-O文件格式

[2]mach-o格式分析

[3]Mach-O 文件格式解析

[4]探秘 Mach-O 文件

[5]http://www.bubuko.com/infodetail-2243416.html

[6]https://www.jianshu.com/p/8498cec10a41

[7]https://satanwoo.github.io/2017/06/13/Macho-1/

[8]https://jmpews.github.io/2017/02/27/darwin/PWN%E4%B9%8Bmacho%E5%8A%A0%E8%BD%BD%E8%BF%87%E7%A8%8B/

[9]https://www.jianshu.com/p/7ad7b3ba7985

[10]https://www.jianshu.com/p/4d86de908721

[11]https://juejin.im/post/5c8e6f5c518825458b3ba6a4这个写的比较清楚