Mach-O

Xcode 构建一个程序的过程中,会把源文件 (.m 和 .h) 文件转换为一个可执行文件(Mach-O executable)。这个可执行文件中包含的字节码将会被 CPU (iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。当然,我们也可以不使用 Xcode,而是通过苹果提供的命令行工具(command line tools)来构建一个程序。

#1 不使用 Xcode 的 Hello World

通过终端 (Terminal),创建一个包含一个 C 文件的文件夹:

1
2
3
$ mkdir command-line
$ cd !$
$ touch helloworld.c

使用文本编辑器来编辑这个文件:

1
open -e helloworld.c

输入如下代码:

1
2
3
4
5
6
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}

保存并返回到终端,然后运行如下命令编译 helloworld.c 文件:

1
$ xcrun clang helloworld.c

这里会产生一个名为 a.out 的文件,使用file命令查看其信息:

1
2
$ file a.out 
a.out: Mach-O 64-bit executable x86_64

这个 a.out 文件就是使用 clang 编译器将 helloworld.c 编译后产生的一个 Mach-O 格式的二进制文件。注意,如果没有指定名字,那么编译器会默认的将其指定为 a.out。

然后我们执行此二进制文件,终端中的输出结果如下:

1
2
$ ./a.out 
Hello World!

#2 Mach-O(可执行文件格式)

Mach-O 是 Mach object 文件格式的缩写,类似于 windows 环境下的 PE 格式、Linux 环境下的 ELF 格式。我们平时见到的可执行文件、dSYM符号文件、Dylib动态库、Framework 等使用的都是这种格式。

Mach-O提供更多的可扩展性和更快的符号表信息存取。

Mach-O应用在基于Mach核心的系统上,目前NeXTSTEP、Darwin、Mac OS X(iPhone)都是使用这种可执行文件格式。

Mach-O 文件的结构由 Header、Load commands、Data(包含Segement的具体数据)组成。

image

上面我们编译出的可执行文件 a.out 使用的也是这种格式。

#示例1:使用 otool 命令查看 a.out 文件的内部结构

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
$ otool -l a.out 
a.out:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x80 2 15 1200 0x00200085
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0
Load command 1
cmd LC_SEGMENT_64
cmdsize 472
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000001000
fileoff 0
filesize 4096
maxprot 0x00000007
initprot 0x00000005
nsects 5
flags 0x0
Section
sectname __text
segname __TEXT
addr 0x0000000100000f50
size 0x0000000000000034
offset 3920
align 2^4 (16)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
Section
sectname __stubs
segname __TEXT
addr 0x0000000100000f84
size 0x0000000000000006
offset 3972
align 2^1 (2)
reloff 0
nreloc 0
flags 0x80000408
reserved1 0 (index into indirect symbol table)
reserved2 6 (size of stubs)
Section
sectname __stub_helper
segname __TEXT
addr 0x0000000100000f8c
size 0x000000000000001a
offset 3980
align 2^2 (4)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
Section
sectname __cstring
segname __TEXT
addr 0x0000000100000fa6
size 0x000000000000000e
offset 4006
align 2^0 (1)
reloff 0
nreloc 0
flags 0x00000002
reserved1 0
reserved2 0
Section
sectname __unwind_info
segname __TEXT
addr 0x0000000100000fb4
size 0x0000000000000048
offset 4020
align 2^2 (4)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Load command 2
cmd LC_SEGMENT_64
cmdsize 232
segname __DATA
vmaddr 0x0000000100001000
vmsize 0x0000000000001000
fileoff 4096
filesize 4096
maxprot 0x00000007
initprot 0x00000003
nsects 2
flags 0x0
Section
sectname __nl_symbol_ptr
segname __DATA
addr 0x0000000100001000
size 0x0000000000000010
offset 4096
align 2^3 (8)
reloff 0
nreloc 0
flags 0x00000006
reserved1 1 (index into indirect symbol table)
reserved2 0
Section
sectname __la_symbol_ptr
segname __DATA
addr 0x0000000100001010
size 0x0000000000000008
offset 4112
align 2^3 (8)
reloff 0
nreloc 0
flags 0x00000007
reserved1 3 (index into indirect symbol table)
reserved2 0
Load command 3
cmd LC_SEGMENT_64
cmdsize 72
segname __LINKEDIT
vmaddr 0x0000000100002000
vmsize 0x0000000000001000
fileoff 8192
filesize 240
maxprot 0x00000007
initprot 0x00000001
nsects 0
flags 0x0
Load command 4
cmd LC_DYLD_INFO_ONLY
cmdsize 48
rebase_off 8192
rebase_size 8
bind_off 8200
bind_size 24
weak_bind_off 0
weak_bind_size 0
lazy_bind_off 8224
lazy_bind_size 16
export_off 8240
export_size 48
Load command 5
cmd LC_SYMTAB
cmdsize 24
symoff 8296
nsyms 4
stroff 8376
strsize 56
Load command 6
cmd LC_DYSYMTAB
cmdsize 80
ilocalsym 0
nlocalsym 0
iextdefsym 0
nextdefsym 2
iundefsym 2
nundefsym 2
tocoff 0
ntoc 0
modtaboff 0
nmodtab 0
extrefsymoff 0
nextrefsyms 0
indirectsymoff 8360
nindirectsyms 4
extreloff 0
nextrel 0
locreloff 0
nlocrel 0
Load command 7
cmd LC_LOAD_DYLINKER
cmdsize 32
name /usr/lib/dyld (offset 12)
Load command 8
cmd LC_UUID
cmdsize 24
uuid 6122E2B2-9651-3991-B2BA-15B45ADC0C6B
Load command 9
cmd LC_VERSION_MIN_MACOSX
cmdsize 16
version 10.13
sdk 10.13
Load command 10
cmd LC_SOURCE_VERSION
cmdsize 16
version 0.0
Load command 11
cmd LC_MAIN
cmdsize 24
entryoff 3920
stacksize 0
Load command 12
cmd LC_LOAD_DYLIB
cmdsize 56
name /usr/lib/libSystem.B.dylib (offset 24)
time stamp 2 Thu Jan 1 08:00:02 1970
current version 1252.50.4
compatibility version 1.0.0
Load command 13
cmd LC_FUNCTION_STARTS
cmdsize 16
dataoff 8288
datasize 8
Load command 14
cmd LC_DATA_IN_CODE
cmdsize 16
dataoff 8296
datasize 0

##2.1.Header

头部主要用来规定这个文件是什么,以及文件是如何被加载的(通过 otool -h 可以打印出头信息)。

1
2
3
4
$ otool -h a.out 
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x80 2 15 1200 0x00200085
  • 32 位架构下 Header 的数据结构:
1
2
3
4
5
6
7
8
9
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 位架构下 Header 的数据结构:
1
2
3
4
5
6
7
8
9
10
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 */
uint32_t reserved; /* reserved */
};

64 位架构只比 32 位架构多了一个 reserved 字段。

1
参数:magic

表示当前二进制文件的类型,不同类型的二进制文件有自己独特的”魔数值”。加载器通过这个魔数值来判断这是什么样的文件,例如32位 Mach-O 文件的魔术值是 0xfeedface、64位 Mach-O 文件的魔术值是 0xfeedfacf、胖二进制文件的魔数值是 0xcafebabe。所以这个字段也可以用于快速确认该文件用于64位还是32位。

1
参数:cputype

cup的类型(如 iOS设备的ARM、Mac上的Intel)。

1
参数:cpusubtype

cup的子类型(如armv7、arm64等)。

1
参数:filetype

文件类型(如可执行文件、库文件、dSYM文件等),具体枚举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
* Constants for the filetype field of the mach_header
*/
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* 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 */
#define MH_DSYM 0xa /* companion file with only debug */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
1
参数:ncmds

指的是加载命令(Load Commands)的数量,如上面#示例1中 a.out 文件的 ncmds 是 15 个,编号为 0-14。

1
参数:sizeofcmds

表示 N 个 Load Commands 的总字节大小, Load Commands 区域是紧接着 Header 区域的。

1
参数:flags

标志位,具体枚举值定义如下:

1
2
3
4
5
#define MH_NOUNDEFS  0x1 //目前没有未定义的符号,不存在链接依赖
#define MH_DYLDLINK 0x4 //该文件是dyld的输入文件,无法被再次静态链接
#define MH_PIE 0x200000 //加载程序在随机地址空间(ASLR),只在 MH_EXECUTE中使用
#define MH_TWOLEVEL 0x80 // 两级名称空间
...

##2.2. Load commands

Load commands 跟在 Header 部分的后面,这些加载指令负责在 Mach-O 文件加载解析时告诉加载器如何处理二进制数据:有些命令是由内核处理的,有些是由动态链接器处理的,最终会根据 cmd 字段的不同类型,使用不同的函数来加载。

Load commands 的结构定义如下:

1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
1
参数:cmd

代表 Load commands 的类型,如 LC_SEGMENT_64、LC_UUID、LC-CODE-SIGNATURE 等等。

1
参数:cmdsize

代表 Load commands 的大小。

下面列举几个看上去比较熟悉的 Load commands…

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将文件的32位 或 64位的段映射到进程地址空间
#define LC_SEGMENT 0x1
#define LC_SEGMENT_64 0x19

// 唯一的 UUID,标示二进制文件
#define LC_UUID 0x1b /* the uuid */

// 启动动态加载连接器
#define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */

// 代码签名和加密
#define LC_CODE_SIGNATURE 0x1d /* local of code signature */
#define LC_ENCRYPTION_INFO 0x21 /* encrypted segment information */

以 LC_SEGMENT_64 类型为例,它代表将文件中64位的段映射到进程的地址空间。它的结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 */
};

下面解释一下各参数的作用。。

1
参数:segname

段的名称,下面的##2.3小结中有更详细的介绍。

1
参数:vmaddr

段的虚拟内存地址。

1
参数:vmsize

段的虚拟内存大小

1
参数:fileoff

段在文件中偏移量。

1
参数:filesize

段在文件中的大小。

1
参数:nsects

段中有多少 secetion。

以上面 #示例1 中的 Load command 0 为例:

1
2
3
4
5
6
7
8
9
10
11
12
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot 0x00000000
initprot 0x00000000
nsects 0
flags 0x0

它就是告诉加载器将该段文件的内容加载到内存中:从 fileoff 处加载 filesize 大小到虚拟内存 vmaddr 处。由于这里在内存地址空间中是 _PAGEZERO 段(这个段不具有访问权限,用来处理空指针)所以各参数都是零。

##2.3. Segments(段数据)

Segments 包含了很多 segment,每一个 segment 定义了一些 Mach-O 文件的数据、地址和内存保护属性,这些数据在动态链接器加载程序时被映射到了虚拟内存中。每个段都有不同的功能,一般包括:

1
__PAGEZERO

空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对NULL指针的引用;

1
__TEXT

包含了执行代码以及其他只读数据。 为了让内核将它直接从可执行文件映射到共享内存, 静态连接器设置该段的虚拟内存权限为不允许写。当这个段被映射到内存后可以被所有进程共享(这主要用在 Frameworks、 Bundles和共享库等程序中,也可以为同一个可执行文件的多个进程拷贝使用)。

1
__DATA

包含了程序数据,该段可写;

1
__OBJC

Objective-C运行时支持库;

1
__LINKEDIT

含有为动态链接库使用的原始数据,比如符号,字符串,重定位表条目等等。

##2.4. section(区数据)

一般段又会按不同的功能划分为几个区(section)。区的字母为小写,同样加两个下划线作为前缀。下面列出段中可能包含的 section:

  • __TEXT段
1
__text, __cstring, __picsymbol_stub, __symbol_stub, __const, __litera14, __litera18;
  • __DATA段
1
2
__data, __la_symbol_ptr, __nl_symbol_ptr, __dyld, __const, 
__mod_init_func, __mod_term_func, __bss, __commom;
  • __IMPORT段
1
__jump_table, __pointers;

其中 __TEXT 段中的 __text 是实际上的代码部分;__DATA 段的 __data 是实际的初始数据。

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 */
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) */
};

其中部分参数的介绍:

1
参数:sectname

section 的名字,如上面提到的 __text、__cstring等。

1
参数:segname

section 所属的 segment,比如 __TEXT。

1
参数:addr

section 在内存的起始位置。

1
参数:size

section 的大小。

1
参数:offset

section 的文件偏移量。

#示例2:通过 otool –s查看某个 section 的信息

1
2
3
4
5
6
7
$ otool -s __TEXT __text a.out
a.out:
Contents of (__TEXT,__text) section
0000000100000f50 55 48 89 e5 48 83 ec 20 48 8d 05 47 00 00 00 c7
0000000100000f60 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7
0000000100000f70 b0 00 e8 0d 00 00 00 31 c9 89 45 ec 89 c8 48 83
0000000100000f80 c4 20 5d c3

由于-s __TEXT __text 很常见,otool 对其设置了一个缩写 -t 。我们还可以通过添加 -v 来查看反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ otool -t -v a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000f50 pushq %rbp
0000000100000f51 movq %rsp, %rbp
0000000100000f54 subq $0x20, %rsp
0000000100000f58 leaq 0x47(%rip), %rax
0000000100000f5f movl $0x0, -0x4(%rbp)
0000000100000f66 movl %edi, -0x8(%rbp)
0000000100000f69 movq %rsi, -0x10(%rbp)
0000000100000f6d movq %rax, %rdi
0000000100000f70 movb $0x0, %al
0000000100000f72 callq 0x100000f84
0000000100000f77 xorl %ecx, %ecx
0000000100000f79 movl %eax, -0x14(%rbp)
0000000100000f7c movl %ecx, %eax
0000000100000f7e addq $0x20, %rsp
0000000100000f82 popq %rbp
0000000100000f83 retq

#3 Mach-O & 胖文件

OS X 有两种类型的目标文件:Mach-O 和 universal binary ,后者就是通用二进制文件,也叫胖文件。区别在于:

  • Mach-O 文件只包含一种架构(i386、x86_64、arm64 等等)的对象代码;

  • 胖文件可能同时包含若干“包含不同架构(i386、arm、arm64 等等)对象代码”的对象文件。

实际上 universal binary 文件只不过是将支持不同架构的 Mach-O 文件打包在一起,再在文件起始位置加上 fat_header 来说明所包含的 Mach-O 文件支持的架构和偏移地址信息。

胖文件的结构︰

fat_header
fat_arch
fat_arch
。。。

其中 fat_header 的数据结构在 mach-o/fat.h 头文件上定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define FAT_MAGIC   0xcafebabe
#define FAT_CIGAM 0xbebafeca /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};

struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};

参数说明:

  • magic字段

就是上面说的魔数,加载器通过这个魔数值来判断这是什么样的文件,胖二进制文件的魔数值是0xcafebabe;

  • nfat_arch字段

是指当前的胖二进制文件包含了多少个不同架构的 Mach-O 文件。有多少个不同架构的 Mach-O 文件,就有多少个 fat_arch,用于说明对应 Mach-O 文件大小、支持的CPU架构、偏移地址等;

大部分情况下,xxx.app/xxx 文件并不是 Mach-O 格式文件,由于现在需要支持不同 CPU 架构的 iOS 设备,我们编译打包出来的执行文件一般都是胖文件。当然,支持的架构越多,最终打出来的包就会越大,所以项目里的 Architectures 和 Valid Architectures 选项要根据需求来配置哟。

#示例3:WeChat.app/WeChat 文件的信息

1
2
3
4
5
$ file WeChat
WeChat: Mach-O universal binary with 2 architectures:
[arm_v7:Mach-O executable arm_v7] [arm64]
WeChat (for architecture armv7): Mach-O executable arm_v7
WeChat (for architecture arm64): Mach-O 64-bit executable arm64

最后,了解 Mach-O 文件对于使用 dSYM 解析崩溃日志、逆向工程、bitcode、应用瘦身等会有很大的帮助!更多相关的内容之后继续慢慢研究。。


相关参考:

#©WWDC 2016

#©极客学院

#©简书1

#©简书2


Mach-O
https://davidlii.cn/2019/03/16/macho.html
作者
Davidli
发布于
2019年3月16日
许可协议