Mach-O 文件格式

Mach-O(Mach Object File Format)是 Apple 平台的可执行文件、目标代码或共享库、动态库等文件的格式。

Mach-O 文件的内部组成可以分为三个部分:

  1. Header:每一个 Mach-O 文件的起始部分,包含关于文件的其他信息,如文件类型、CPU 架构类型等。
  2. Load Commands:包含了一系列不同的 加载命令(Load Commands),可用于指导如何设置并加载二进制数据。
  3. Data:包含了多个 segment,每个 segment 包含 0 个或多个 section。每个 segment 定义了一个虚拟内存区域,动态链接器能够将其映射到进程的地址空间。segment 和 section 的数量和布局则是由 load commands 和文件类型指定的。

mach-o 文件格式

Header 的数据结构定义在 /usr/include/mach-o/loader.h 中。

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 (X86、X86_64、ARM)*/
cpu_subtype_t cpusubtype; /* machine specifier (ALL = X86_ALL = i386_ALL = X86_64_ALL)*/
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 */
};
MAGIC 说明
MH_MAGIC 文件布局与 CPU 字节序相同
MH_CIGAM 文件布局与 CPU 字节序相反
FileType 文件类型 说明
MH_OBJECT 中间目标文件 每个源代码文件编译生成的中间目标文件,一般以 .o 作为后缀。静态库为一组目标文件合集。这是一种紧凑格式,所有的 section 放在了一个 segment 中
MH_EXECUTE 可执行程序 app
MH_BUNDLE 插件 或 bundle 非独立的二进制文件,由可执行文件显式进行加载。文件一般以 .bundle 作为后缀。
MH_DYLIB 动态共享库 文件一般以 .dylib 作为后缀,framework 的主共享库没有文件后缀名。
MH_CORE 核心转储文件 操作系统会在程序崩溃时创建该文件。
MH_DYLINKER 动态链接器 dyld
MH_DSYM 符号文件 存储了二进制符号信息。通常会打包到 .dSYM 文件包中

Load Commands

Load commands 位于 Header 之后,有诸多功能,比如:

  • 指定文件在虚拟内存中的初始布局
  • 指定动态链接时的符号表位置
  • 指定程序主线程的初始执行状态
  • 指定共享库的名称,共享库包含程序所导入符号的定义。
1
2
3
4
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
CMD 命令 用途
LC_UUID uuid_command 用于指定一个镜像或其对应的 dSYM 文件的 128 位 UUID。
LC_DYLD_INFO_ONLY dyld_info_command 用于给dyld提供能够加载目标MachO所需要的必要信息。
LC_SEGMENT、LC_SEGMENT_64 segment_command 将指定的 segment 映射到加载此文件的进程的地址空间中。此外,还定义了 segment 所包含的所有 sections。
LC_SYMTAB symtab_command 用于指定此文件的符号表。动态链接或静态链接此文件时都会调用符号表信息,调试器也使用符号表将符号映射到生成符号的原始代码文件。
LC_DYSYMTAB dysymtab_command 用于指定动态链接器使用的额外符号表信息。
LC_LOAD_DYLIB dylib_command 用于定义此文件会链接的动态链接库的名称。
LC_ID_DYLIB dylib_command 用于提供动态库的名称、版本、时间戳等信息。
LC_LOAD_DYLINKER dylinker_command 用于指定 内核加载此文件所使用的动态链接器。
LC_ID_DYLINKER dylinker_command 用于指定此文件是动态链接器。
LC_MAIN 表示主函数地址
LC_FUNCTION_STARTS linkedit_data_command 表示函数入口地址
LC_DATA_IN_CODE linkedit_data_command
LC_CODE_SIGNATURE linkedit_data_command 用于指定当前文件的签名信息
LC_THREAD、LC_UNIXTHREAD thread_command 对于可执行文件,LC_UNIXTHREAD 定义了进程主线程的初始化状态;LC_THREAD 与 LC_UNIXTHREAD 类似,但是不会导致内核分配栈区。
LC_RPATH rpath_command 用于指定动态链接器加载动态库时的查找路径

Data

Mach-O 的 Data 区域,其中由 Segment 段和 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
struct segment_command_64 { 
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* section_64 结构体所需要的空间 */
char segname[16]; /* segment 名字,上述宏中的定义 */
uint64_t vmaddr; /* 所描述段的虚拟内存地址 */
uint64_t vmsize; /* 为当前段分配的虚拟内存大小 */
uint64_t fileoff; /* 当前段在文件中的偏移量 */
uint64_t filesize; /* 当前段在文件中占用的字节 */
vm_prot_t maxprot; /* 段所在页所需要的最高内存保护,用八进制表示 */
vm_prot_t initprot; /* 段所在页原始内存保护 */
uint32_t nsects; /* 段中 Section 数量 */
uint32_t flags; /* 标识符 */
};

struct section_64 {
char sectname[16]; /* Section 名字 */
char segname[16]; /* Section 所在的 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; /* 标志属性 */
uint32_t reserved1; /* 保留字段1 (for offset or index) */
uint32_t reserved2; /* 保留字段2 (for count or sizeof) */
uint32_t reserved3; /* 保留字段3 */
};

从整体上来说,Mach-O 里面包含的段有以下这些:

  • __PAGEZERO 捕获访问 NULL 指针的非法操作的段。
  • __TEXT 代码段/只读数据段。
  • __DATA 数据段。
  • __OBJC 包含会被Objective Runtime使用到的一些数据。
  • __LINKEDIT 包含需要被动态链接器使用的信息,包括符号表、字符串表、重定位项表等。

对于用户级完全链接的 Mach-O 文件,其最后一个 segment 是 LINKEDIT segment。该 segment 包含了符号链接的信息表,如:符号表、字符串表等,动态链接器基于这些信息将可执行程序或 Mach-O bundle 与其依赖的库进行链接接。

通常约定segment的名称为双下划线加全大写字母(如__TEXT),section的名称为双下划线加小写字母(如__text)。

__TEXT 段

1
2
3
4
5
6
7
8
9
10
1. __text: 代码节,存放机器编译后的代码
2. __cstring: 代码运行中包含的字符串常量
3. __ustring:
4. __const: 存储const修饰的常量
5. __stubs: 用于辅助做动态链接代码(dyld).本质上是一小段会直接跳入lazybinding的表对应项指针指向的地址的代码。
6. __stub_helper:用于辅助做动态链接(dyld).上述提到的lazybinding的表中对应项的指针在没有找到真正的符号地址的时候,都指向这。
7. __objc_methname:objc的方法名称
8. __objc_classname:objc类名
9. __objc_methtype:objc方法类型
10. __unwind_info: 用于存储处理异常情况信息

__DATA 段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. __got:存储引用符号的实际地址,类似于动态符号表,存储了`__nl_symbol_ptr`相关函数指针。
2. __la_symbol_ptr:lazy symbol pointers。懒加载的函数指针地址(C代码实现的函数对应实现的地址)。和__stubs和stub_helper配合使用。具体原理暂留。
3. __mod_init_func:模块初始化的方法。
4. __const:存储constant常量的数据。比如使用extern导出的const修饰的常量。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函数列表。
10. __objc_protolist:objc协议列表
11. __objc_imageinfo:objc镜像信息
12. __objc_const:objc常量。保存objc_classdata结构体数据。用于映射类相关数据的地址,比如类名,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc协议
15. __objc_classrefs:引用到的objc类
16. __objc_superrefs:objc超类引用
17. __objc_ivar:objc ivar指针,存储属性。
18. __objc_data:objc的数据。用于保存类需要的数据。最主要的内容是映射__objc_const地址,用于找到类的相关数据。
19. __data:暂时没理解,从日志看存放了协议和一些固定了地址(已经初始化)的静态量。
20. __bss:存储未初始化的静态量。比如:`static NSThread *_networkRequestThread = nil;`其中这里面的size表示应用运行占用的内存,不是实际的占用空间。所以计算大小的时候应该去掉这部分数据。
21. __common:存储导出的全局的数据。类似于static,但是没有用static修饰。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存储在__common里面

otool 命令

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
-f print the fat headers
-a print the archive header
-h print the mach header
-l print the load commands
-L print shared libraries used
-D print shared library id name
-t print the text section (disassemble with -v)
-p <routine name> start dissassemble from routine name
-s <segname> <sectname> print contents of section
-d print the data section
-o print the Objective-C segment
-r print the relocation entries
-S print the table of contents of a library
-T print the table of contents of a dynamic shared library
-M print the module table of a dynamic shared library
-R print the reference table of a dynamic shared library
-I print the indirect symbol table
-H print the two-level hints table
-G print the data in code table
-v print verbosely (symbolically) when possible
-V print disassembled operands symbolically
-c print argument strings of a core file
-X print no leading addresses or headers
-m don't use archive(member) syntax
-B force Thumb disassembly (ARM objects only)
-q use llvm's disassembler (the default)
-Q use otool(1)'s disassembler
-mcpu=arg use `arg' as the cpu for disassembly
-j print opcode bytes
-P print the info plist section as strings
-C print linker optimization hints
--version print the version of /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/otool

查看头部信息(CPU、FileType)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看 Header 信息(-h 或 -f)
$ otool -h YourMacho
YourMacho:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 2 26 3832 0x00200085

# 查看 Header 信息(符号化)
otool -hv YourMacho
YourMacho:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 26 3832 NOUNDEFS DYLDLINK TWOLEVEL PIE

查看加载库信息(Path、Version)

1
2
3
4
5
6
7
8
9
10
11
$ otool -L YourMacho
libGCloud.dylib:
@rpath/libGCloud.dylib (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 58286.51.6)
/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork (compatibility version 1.0.0, current version 897.15.0)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1452.23.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 1452.23.0)

查看加载命令(Load Commands)

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
$ otool -l YourMacho
EmptyProject:
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
...
Load command 73
cmd LC_RPATH
cmdsize 40
path @executable_path/Frameworks (offset 12)
Load command 74
cmd LC_FUNCTION_STARTS
cmdsize 16
dataoff 76088088
datasize 387672
Load command 75
cmd LC_DATA_IN_CODE
cmdsize 16
dataoff 76475760
datasize 3976
Load command 76
cmd LC_CODE_SIGNATURE
cmdsize 16
dataoff 76741840
datasize 619168

查看动态库加载路径(install name)

1
2
3
$ otool -D YourMacho
libGCloud.dylib:
@rpath/libGCloud.dylib

获取 Macho 文件的字符串表

1
$ otool -VX -s __TEXT __cstring YourMacho

获取 Macho 中的 objc 信息(类名、方法名等)

1
$ otool -ov YourMacho

获取 Macho 文件引用的oc方法名

1
2
3
4
5
# 查看方法名
$ otool -VX -s __TEXT __objc_methname YourMacho

# 查看引用到的方法
$ otool -VX -s __DATA __objc_selrefs YourMacho

获取 Macho 文件的oc类

1
2
3
4
5
6
7
8
# 查看类名
$ otool -VX -s __TEXT __objc_classname YourMacho

# 查看所有类
$ otool -VX -s __DATA __objc_classlist YourMacho

# 查看引用到的类
$ otool -VX -s __DATA __objc_classrefs YourMacho

获取 Macho 集成的 .o (静态库)

1
$ otool -a YourMacho

llvm-nm 命令

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
USAGE: nm [options] <input files>

OPTIONS:
llvm-nm Options:

-B - Alias for --format=bsd
-P - Alias for --format=posix
--add-dyldinfo - Add symbols from the dyldinfo not already in the symbol table, Mach-O only
--arch=<string> - architecture(s) from a Mach-O file to dump
--debug-syms - Show all symbols, even debugger only
--defined-only - Show only defined symbols
--demangle - Demangle C++ symbol names
--dyldinfo-only - Show only symbols from the dyldinfo, Mach-O only
--dynamic - Display the dynamic symbols instead of normal symbols.
--extern-only - Show only external symbols
--format=<value> - Specify output format
=bsd - BSD format
=sysv - System V format
=posix - POSIX.2 format
=darwin - Darwin -m format
--just-symbol-name - Print just the symbol's name
-m - Alias for --format=darwin
--no-demangle - Don't demangle symbol names
--no-dyldinfo - Don't add any symbols from the dyldinfo, Mach-O only
--no-llvm-bc - Disable LLVM bitcode reader
--no-sort - Show symbols in order encountered
--no-weak - Show only non-weak symbols
--numeric-sort - Sort symbols by address
--portability - Alias for --format=posix
--print-armap - Print the archive map
--print-file-name - Precede each symbol with the object file it came from
--print-size - Show symbol size as well as address
--radix=<value> - Radix (o/d/x) for printing symbol Values
=d - decimal
=o - octal
=x - hexadecimal
--reverse-sort - Sort in reverse order
--size-sort - Sort symbols by size
--undefined-only - Show only undefined symbols
-x - Print symbol entry in hex, Mach-O only
常用选项 说明
-B (default) 使用BSD输出格式 ,同 -format=bsd
-P 使用POSIX输出格式, 通过 -format=posix
-debug-syms,-a 显示所有符号
–dynamic,-D 显示动态链接符号
-defined-only 仅显示在本文件中定义的符号
–extern-only,g 仅显示外部符号
–undefined-only,-u 仅显示引用但未在此文件中定义的符号
–no-weak,-W 不显示弱引用符号
–format=format, -f format 选择一个输出格式 sysv,posix,bsd, 默认值是bsd
–no-sort,-p 符号按顺序显示
–numeric-sort,-n,-v 符号按地址进行排序
–size-sort 符号按地址进行排序
–print-file-name,-A,-o 在每个符号前面加上它来自的文件

llvm-nm 与 gnu-nm 命令并不完全相同,llvm-nm 不支持 GNU nm所做的全部参数。

MachO Install Name

MachO 格式的动态库生成时,会在其 LC_ID_DYLIB 写入动态库的信息,其中会有一个 name 字段,包括路径和动态库的名称。这个 name 即为动态库的 Install Name,。路径可以是绝对路径,也可以是相对路径。当可执行文件或其他库文件链接此动态库时,链接器会把这个 Install Name 写入到该文件中的 LC_LOAD_DYLIB 中。dyld 会根据这个 Install Name,去查找并加载该动态库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 某库文件依赖动态库 libc++.1.dylib,其 LC_LOAD_DYLIB 包含 libc++ 的 Install name (id name)
Load command 15
cmd LC_LOAD_DYLIB
cmdsize 48
name /usr/lib/libc++.1.dylib (offset 24)
time stamp 2 Thu Jan 1 08:00:02 1970
current version 400.9.0
compatibility version 1.0.0

# libc++.1.dylib 的 LC_ID_DYLIB 信息中包含 Install name (id name)
Load command 3
cmd LC_ID_DYLIB
cmdsize 48
name /usr/lib/libc++.1.dylib (offset 24)
time stamp 1 Thu Jan 1 08:00:01 1970
current version 902.1.0
compatibility version 1.0.0

相对路径

在开发过程中,动态库可能被放置在不同的路径下(如:系统 Library 目录、嵌入 App、嵌入 Bundle 等),因此 Install Name 设置为相对路径,更便于开发者使用。

MachO中有三种相对路径:

  • @executable_path,表示可执行程序所在的目录。(例如:xxx.app, @executable_path 为 xxx.app/Contents/MacOS)
  • @loader_path 表示被加载的 binary 所在的目录。(例如:xxx.bundle,@loader_path 为 xxx.bundle/Contents/MacOS。如果是.app,则等同于 @executable_path)
  • @rpath RPATH 是由用户额外设置的一个或者一组路径。(类似于 Windows下的PATH变量)

在一个进程中, 对于每一个模块, @loader_path 会解析成不用的路径, 而 @executable_path 总是被解析为同一个路径。
在 app 中,@rpath 默认会被设置为 Frameworks 路径,即 @loader_path/../Frameworks。

可以通过以下方式设置 RPATH:

  1. xcode 通过 Build Settings -> Linking -> Runpath Search Paths 来设置。
  2. 链接时,在 “Other Linker Flags” 中添加 * -rpath @executable_path/*。
  3. 编译时,添加 *-Wl, -rpath, @executable_path/*。

辅助命令

install_name_tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 修改依赖库的加载路径 INSTALL_PATH
install_name_tool -change {old} {new} {YourMacho}

# 修改动态库的加载路径 INSTALL_NAME
install_name_tool -id {new} {YourMacho}

# 修改可执行文件的 RPath
install_name_tool -rpath {old} {new} {YourMacho}

# 增加可执行文件的 RPath
install_name_tool -add_rpath {new} {YourMacho}

# 删除可执行文件的 RPath
install_name_tool -delete_rpath {old} {YourMacho}

lipo

主要用来管理Fat File的工具, 可以查看cpu架构, 提取特定架构,整合和拆分库文件。

1
2
3
4
5
6
7
8
9
10
11
# 查看信息,支持的cpu架构列表
lipo -info xxxx.framework/xxxx或/xxxx.a

# 整合成Fat文件
lipo -create xxxx xxxx -output xxxx

# 提取特定的cpu架构的thin文件
lipo xxxx -thin cpu(armv7/arm64等) -output xxxx

# 移除掉特定的cpu架构的文件
lipo -remove cpu(armv7/arm64等) xxxx -output xxxx

App Store 关于 Mach-O 的限制

  • App minVersion < 7.0,二进制代码段(__TEXT sections)总和最大 80 MB。
  • App minVersion 7.0 ~ 8.x,二进制代码段(__TEXT sections) 每个片段最大 60 MB。
  • App minVersion >= 9.0,二进制代码段(__TEXT sections)总和最大 500 MB。

参考

https://llvm.org/doxygen/structllvm_1_1MachO_1_1mach__header__64.html
https://dishibolei.github.io/2017/10/26/mach-o-parser/
https://www.mikeash.com/pyblog/friday-qa-2009-11-06-linking-and-install-names.html
https://llvm.liuxfe.com/docs/man/llvm-nm