Mach-O 格式

概述

Mach-O 的全称是 Mach Object File Format,它可以用来表示可执行文件、目标代码或共享库、动态库等。Mach 内核的操作系统,如:macOS,iPadOS,iOS 采用的都是 Mach-O。本文我们来学习一下 Mach-O 的内部结构,通过学习 Mach-O,可以了解应用程序是如何加载到系统中的,如何执行的。

基本结构

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

  • Header
    • 每一个 Mach-O 文件的起始部分,用于将文件标识为 Mach-O 文件。Header 还包含关于文件的其他信息,如:文件类型、CPU 架构类型等。
  • Load Commands
    • Load Commands 包含了一系列不同的 加载命令(Load Commands),可用于指导如何设置并加载二进制数据。在 Mach-O 的文件布局中,Load Commands 紧跟在 Header 之后。Load Commands 有诸多功能,比如:
      • 指定文件在虚拟内存中的初始布局
      • 指定动态链接时的符号表位置
      • 指定程序主线程的初始执行状态
      • 指定共享库的名称,共享库包含程序所导入符号的定义。
  • Data
    • Data 包含了多个 segment,每个 segment 包含 0 个或多个 section。每个 segment 中的 section 具有相同的类型,如:代码或数据。每个 segment 定义了一个虚拟内存区域,动态链接器能够将其映射到进程的地址空间。segment 和 section 的数量和布局则是由 load commands 和文件类型指定的。
    • 对于用户级完全链接的 Mach-O 文件,其最后一个 segment 是 LINKEDIT segment。该 segment 包含了符号链接的信息表,如:符号表、字符串表等,动态链接器基于这些信息将可执行程序或 Mach-O bundle 与其依赖的库进行链接接。

下图所示为 Mach-O 文件的基本结构:

下面我们来对 Mach-O 文件 Header 和 Load Commands 进行着重分析。Load Command 中涉及到的一些其他数据结构,如 section 则存储在 Data 之中。

Header

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

字段说明

  • magic
    • 指定文件的类型为 Mach-O。如果文件希望其布局与 CPU 字节序相同,则使用 MH_MAGIC;如果文件希望其布局与 CPU 字节序相反,则使用 MH_CIGAM
  • cputype
    • 指定文件运行的目标架构,可能包含两种类型:
      • CPU_TYPE_POWERPC:基于 PowerPC CPU 的 Mac 计算机。
      • CPU_TYPE_I386:基于 Intel CPU 的 Mac 计算机。
  • cpusubtype
    • 指定 CPU 确切型号。如果希望能够 PowerPC 或 Intel 的所有处理器上执行,可以设置为 CPU_SUBTYPE_POWERPC_ALLCPU_SUBTYPE_I386_ALL
  • filetype
    • 指定文件的类型。其包含以下这些值:
      • MH_OBJECT:表示 中间目标文件。这是一种非常紧凑的格式,它将所有的 section 放在了一个 segment 中。编译器和汇编器通常会为每一个源代码文件生成一个中间目标文件,文件一般以 .o 作为后缀
      • MH_EXECUTE:表示 标准的可执行程序
      • MH_BUNDLE:表示 插件bundle。非独立的二进制文件,由可执行文件显式进行加载文件一般以 .bundle 作为后缀
      • MH_DYLIB:表示 动态共享库。其包含一些额外的表来支持多个模块。文件一般以 .dylib 作为后缀,framework 的主共享库除外,其没有文件后缀名。
      • MH_CORE:表示 核心转储文件。操作系统会在程序崩溃时创建该文件。核心转储文件存储了程序崩溃时的完整地址空间。我们可以通过在核心转储文件上运行 gdb 来判断崩溃的原因。
      • MH_DYLINKER:表示 动态链接器dyld 的文件类型就是 MH_DYLINKER
      • MH_DSYM:表示 存储了二进制符号信息的文件
  • ncmds
    • 指定 load commands 的数量。
  • sizeofcmds
    • 指定 load commands 所占据的字节数。
  • flags
    • 指定 Mach-O 文件的某些可选功能的状态,下面是一些我们可以操作该字段的掩码:
      • MH_NOUNDEFS:表示目标文件在构建时不包含未定义的引用。
      • MH_INCRLNK:表示目标文件是相对于一个基本文件的增量链接输出,无法再次链接。
      • MH_DYLDLINK:表示目标文件是动态链接器的输入,无法再次静态链接。
      • MH_TWOLEVEL:表示镜像采用二级命名空间绑定。
      • MH_BINDATLOAD:表示当文件加载完毕之后,动态链接器应该对未定义引用进行绑定。
      • MH_PREBOUND:表示文件的未定义引用是预先绑定的。
      • MH_PREBINDABLE:表示文件没有预绑定,但是可以重新对其进行预绑定。仅在 MH_PREBOUND 未设置时使用
      • MH_NOFIXPREBINDING:表示动态链接器不会将关于此可执行文件的信息通知预绑定代理。
      • MH_ALLMODSBOUND:表示将此二进制文件绑定到它所依赖的库的所有二级命名空间 module。仅在 MH_PREBINDABLEMH_TWOLEVEL 设置时使用
      • MH_CANONICAL:表示文件已取消预绑定。
      • MH_SPLIT_SEGS:表示文件的只读段和读写段是分离的。
      • MH_FORCE_FLAT:表示可执行文件强制所有镜像使用扁平的命名空间绑定。
      • MH_SUBSECTIONS_VIA_SYMBOLS:表示目标文件的 sections 可以分为独立的块。如果其他代码未使用这些块,则会被清理掉。
      • MH_NOMULTIDEFS:表示确保子镜像没有针对同一符号的多种定义。因此可以使用两级命名空间提示。

注意:对于所有的文件类型(MH_OBJECT 除外),对于给定的 CPU 架构,segments 必须页对齐:PowerPC 和 x86 处理器的页面大小均为 4KB。这使得内核能够直接为 segment 分配虚拟内存页面。

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_SEGMENT:表示 segment_command 加载命令。将指定的 segment 映射到加载此文件的进程的地址空间中。此外,还定义了 segment 所包含的所有 sections。
      • LC_SYMTAB:表示 symtab_command 加载命令。用于指定此文件的符号表。动态链接或静态链接此文件时都会调用符号表信息,调试器也使用符号表将符号映射到生成符号的原始代码文件
      • LC_DYSYMTAB:表示 dysymtab_command 加载命令。用于指定 动态链接器使用的额外符号表信息
      • LC_THREADLC_UNIXTHREAD:表示 thread_command 加载命令。对于可执行文件,LC_UNIXTHREAD 定义了进程主线程的初始化状态;LC_THREADLC_UNIXTHREAD 类似,但是不会导致内核分配栈区。
      • LC_LOAD_DYLIB:表示 dylib_command 加载命令。用于定义此文件会链接的动态链接库的名称。
      • LC_ID_DYLIB:表示 dylib_command 加载命令。用于定义动态链接器的名称。
      • LC_PREBOUND_DYLIB:表示 prebound_dylib_command 加载命令。对于此文件预先链接到的共享库,通过此加载命令指定共享库中使用的 module。
      • LC_LOAD_DYLINKER:表示 dylinker_command 加载命令。用于指定 内核加载此文件所使用的动态链接器
      • LC_ID_DYLINKER:表示 dylinker_command 加载命令。用于指定此文件是动态链接器。
      • LC_ROUTINES:表示 routines_command 加载命令。包含共享库初始化进程的地址。
      • LC_TWOLEVEL_HINTS:表示 twolevel_hints_command 加载命令。包含两级命名空间查找表。
      • LC_SUB_FRAMEWORK:表示 sub_framework_command 加载命令。将此文件标识为 umbrella framework 的子 framework 的实现。umbrella framework 的名称存储在字符串参数中。
      • LC_SUB_UMBRELLA:表示 sub_umbrella_command 加载命令。将一个文件标记为此 umbrella framework 的子 umbrella。
      • LC_SUB_LIBRARY:表示 sub_library_command 加载命令。用于定义 LC_SUB_LIBRARY 加载命令的属性,标识为此 framework 的子 library,并将该 framework 标记为一个 umbrella framework。
      • LC_SUB_CLIENT:表示 sub_client_command 加载命令。当包含 LC_SUB_CLIENT 加载命令,且包含另一个 framework 或 bundle 的名称时,子 framework 就可以允许被它们所链接。
  • cmdsize
    • 指定数据结构的大小,以字节为单位。不同类型的加载命令,除了包含上述两个基本字段之外,还包含其他的数据字段。

表:

下面依次来介绍各种不同类型的加载命令。

uuid_command

uuid_command 加载命令用于指定镜像或其对应的 dSYM 文件的 128 位通用唯一标识符(UUID)。uuid_command 的数据结构定义如下:

1
2
3
4
5
struct uuid_command {
uint32_t cmd; /* LC_UUID */
uint32_t cmdsize; /* sizeof(struct uuid_command) */
uint8_t uuid[16]; /* the 128-bit uuid */
};

字段说明

  • uuid
    • 128 位的 UUID。

segment_command

segment_command 命令用于指定一个 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 */
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 */
};

字段说明

  • segname
    • 用于描述 segment 名称的 C 字符串。苹果定义 segment 名称以两个下划线为前缀,并由大写字母组成,如:__TEXT__DATA
  • vmaddr
    • 指定 segment 在虚拟内存中的起始地址。
  • vmsize
    • 指定 segment 在虚拟内存中占据的字节数。
  • fileoff
    • 指定要映射到 vmaddr 位置的数据在此文件中的偏移。
  • filesize
    • 指定 segment 在磁盘上占据的字节数。对于运行时比构建时需要更多内存的 segment,vmsize 可以大于 filesize。比如,链接器为 MH_EXECUTABLE 文件生成的 __PAGESIZE segment,其 vmsize0x1000,而 filesize 却是 0。因为 __PAGEZERO 不包含数据,因此在运行前没有必要占据任何空间。类似的,静态链接器通常在 __DATA segment 的末尾分配未初始化的数据。
  • maxprot
    • 指定 segment 的最大允许的虚拟内存保护级别。
  • initprot
    • 指定 segment 的初始化虚拟内存保护级别。
  • nsects
    • 指定紧跟在此加载命令之后的 section 数量。
  • flags
    • 定义一组影响 segment 加载的标志位,如下:
      • SG_HIGHVM:segment 的文件内容占虚拟内存空间的高位部分,低位部分填充零。
      • SG_NORELOC:segment 没有任何重定位的内容,也没有要重定位的内容。

Segment 定义了 Mach-O 中的一段字节,并定义了当这段字节映射到到虚拟内存时的地址和内存保护级别。比如,segment 始终关于虚拟内存页面对齐的。一个 segment 包含 0 个或多个 section。相比加载前,运行时的 segment 可能需要更多的内存。比如:由链接器为 PowerPC 生成的可执行文件中,__PAGEZERO segment 虚拟内存大小为 1 页,但在磁盘上的大小却为 0。由于 __PAGEZERO 不包含任何数据,因此不需要占用可执行文件中的任何空间。

为了紧凑起见,中间目标文件仅包含一个 segment。该 segment 没有名称,它包含所有的 section。

以下是 OS X 可执行文件中可能会包含的 segment:

  • __PAGEZERO:静态链接器创建 __PAGEZERO segment 作为可执行文件的第一个 segment。该 segment 的虚拟内存地址为 0,并且未分配保护权限,这些权限的组合会导致对 NULL 的访问立即崩溃。__PAGEZERO segment 的大小为当前体系结构的一个完整 VM 页面的大小。__PAGEZERO segment 中没有数据,所以它在文件中不占空间。
  • __TEXT__TEXT segment 包含可执行代码和其他只读数据。为了允许内核将其直接从可执行文件映射到可共享的内存,静态链接器将此 segment 的虚拟内存权限设置为禁止写入。当该 segment 映射到内存中时,可以在对其内存感兴趣的进程之间进行共享。(这主要用于 framework,bundle 以及共享库,但是可以在 OS X 中运行同一可执行文件的多个副本,在这种情况下也是如此。)只读属性还表示页面组成 __TEXT segment 的文件永远都不需要写回到磁盘。当内核需要释放物理内存时,它可以简单地丢弃一个或多个 __TEXT 页面,并在下次需要它们时从磁盘中重新读取。
  • __DATA_DATA segment 包含可写数据。静态链接器设置此 segment 的虚拟内存权限为允许读写。因为是可写的,所以在逻辑上会为与该库连接的每个进程复制框架或共享库的 __DATA segment。当组成 __DATA segment 的内存页是可读写时,内核将其标记为 写时复制(Copy-on-write)。因此,当一个进程写入这些页面时,该进程将会得到属于自己的页面私有副本。
  • __OBJC__OBJC segment 包含由 Objective-C 语言运行时支持的库所使用的数据。
  • __IMPORTIMPORT segment 包含符号插桩以及指向可执行文件中未定义符号的非惰性指针。只有 IA-32 体系结构的可执行文件会生成此 segment。
  • __LINKEDIT__LINKEDIT segment 包含动态链接器所需要的原始数据,如:符号、字符串、重定位表项。

section

紧跟在 segment_command 数据结构之后的是一个 section 数据结构的数组,section 的数量由 segment_commandnsects 字段确定。多个 section 组成一个 segment。

section 的数据结构定义如下:

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

字段说明

  • sectname
    • 指定 section 的名称。苹果定义 section 的名称以两个下划线开头,并由小写字母组成,如:__text__data
  • segname
    • 指定最终应包含此 section 的 segment 的名称。为了紧凑起见,中间目标文件(MH_OBJECT 类型的文件)仅包含一个 segment,所有的 section 都放在其中。静态链接器在构建最终产物(非 MH_OBJECT 类型的文件)时,会将每个 section 放在指定的 segment 中。
  • addr
    • 指定 section 在虚拟内存中的起始地址。
  • size
    • 指定 section 在虚拟内存中占据的字节数。
  • offset
    • 指定 section 在此文件中的偏移。
  • align
    • 指定 section 的对齐方式。
  • reloff
    • 指定 section 的第一个重定位项在文件中的偏移。
  • nreloc
    • 指定 section 的重定位项的数量。
  • flags
    • 该字段可分为两个部分:低 8 位指定 section 的类型、高 24 位包含指定 section 的其他属性的一组标志。这些类型和标志主要被静态链接器和文件分析工具(如:otool)用来确定如何修改或显示 section。section 的类型有以下这些:
      • S_REGULAR:表示 section 没有特殊类型。标准工具创建的 __TEXT,__text 就是此类 section
      • S_ZEROFILL按需填充零,当首次读取或写入该 section 时,其中的每个页面都会自动填充零。
      • S_CSTRING_LITERALS:表示 section 只包含常量 C 字符传。标准工具创建的 __TEXT,__cstring 就是此类 section
      • S_4BYTE_LITERALS:表示 section 只包含 4 字节长的常量值。标准工具创建的 __TEXT,__literal4 就是此类 section
      • S_8BYTE_LITERALS:表示 section 只包含 8 字节长的常量值。标准工具创建的 __TEXT,__literal8 就是此类 section
      • S_NON_LAZY_SYMBOL_POINTERS:表示 section 只包含符号的非惰性指针。标准工具创建的 __DATA,__nl_symbol_ptrs 就是此类 section
      • S_LAZY_SYMBOL_POINTERS:表示 section 只包含符号的惰性指针。标准工具创建的 __DATA,__la_symbol_ptrs 就是此类 section
      • S_SYMBOL_STUBS:表示 section 只包含符号插桩(stub)。标准工具创建的 __TEXT,__symbol_stub__TEXT,__picsymbol_stub 就是此类 section
      • S_MOD_INIT_FUNC_POINTERS:表示 section 只包含指向模块构建方法的指针。标准工具创建的 __DATA,__mod_init_func 就是此类 section
      • S_MOD_TERM_FUNC_POINTERS:表示 section 只包含指向模块析构方法的指针。标准工具创建的 __DATA,__mod_term_func 就是此类 section
      • S_COALESCED:表示 section 只包含由静态链接器或动态链接器合并的符号。多个文件包含同一符号的合并定义,而不会引起 multiple-defined-symbol 报错。
      • S_GB_ZEROFILL:表示 section 是一个按需填充零的 section。section 可以大于 4 GB。该 section 只能放在仅包含零填充 section 的 segment 中。如果将零填充 section 放在包含非零填充 section 的 segment 中,那么可能会导致这些 section 无法没读取。最终导致静态链接器无法生成输出文件。
    • section 的属性有以下这些:
      • S_ATTR_PURE_INSTRUCTIONS:表示 section 只包含可执行机器码。标准工具会为 __TEXT,__text__TEXT,__symbol_stub__TEXT,__picsymbol_stub 等 section 设置该属性
      • S_ATTR_SOME_INSTRUCTIONS:表示 section 包含一部分可执行机器码。
      • S_ATTR_NO_TOC:表示 section 包含合并符号。
      • S_ATTR_EXT_RELOC:表示 section 包含必须要被重定位的引用。这些引用引用其他文件中的数据(未定义符号)。为了支持外部重定位,包含此 section 的 segment 的最大虚拟内存保护级别必须允许读取和写入。
      • S_ATTR_LOC_RELOC:表示 section 包含的引用必须被重定位。它们引用的是此文件中的数据。
      • S_ATTR_STRIP_STATIC_SYMS:如果镜像的 mach_header 中的 MH_DYLDLINK 标志位被设置了,那么 section 中的静态符号就可以被删除。
      • S_ATTR_NO_DEAD_STRIP:表示 section 的内容如果没有被引用,不能被删除。
      • S_ATTR_LIVE_SUPPORT:如果 section 引用的代码存在,但是无法检测到该引用,那么不能被删除。

Mach-O 文件中的每个 section 都具有类型和一组属性标志位。在中间目标文件中,静态链接器通过 section 的类型和属性决定如何将这些 section 拷贝到最终产物中。目标文件分析工具(如:otool)使用 section 的类型和属性来确定如何读取和显示 section。动态链接器也会用到某些 section 类型和属性。

以下是一些重要的符号类型和属性的静态链接变体:

  • regular section:在一个 regular section 中,一个外部符号只能有一个定义存在于中间目标文件。如果静态链接器找到两个外部符号的定义,则会报错。
  • coalesced section:在最终的产物中,静态链接器对于 coalesced section 中的每一个符号只保留一个定义。为了支持复杂的语言特性(如 C++ 的虚表和 RTTI),编译器可以在每个中间目标文件中为一个特定的符号创建一个定义。之后,静态链接器和动态链接器会将重复的定义减少为单个定义。
  • 带弱定义的 coalesced section:弱符号定义只可能出现在 coalesced section 中。当静态链接器找到符号的重复定义时,它会丢弃任何具有弱定义属性的合并符号定义。如果没有非弱定义,则是会用第一个弱定义。此功能旨在支持 C++ 模板。它允许显式模板实例覆盖隐式模板实例。C++ 编译器将显式定义放在 regular section 中,并将隐式定义放在 coalesced section 中,并标记为弱定义。用弱定义构建的的中间目标文件(以及静态归档库)只能与 OX X v10.2 及更高版本中的静态链接器一起使用。如果最终产物(应用程序和共享库)应该在 OS X 的早期版本中使用,则不应包含较弱的定义。

以下是 __TEXT segment 中的几种 section:

Segment and Section content
__TEXT,__text 可执行的机器码。编译器通常在此部分中放置可执行代码,而不放置任何形式的表或数据。
__TEXT,__cstring 常量 C 字符串。C 字符串是一系列非空字节,以空字节 \0 结尾。静态链接器会在构建最终产物时合并 C 字符串常量,并删除重复项。
__TEXT,__picsymbol_stub 与位置无关的间接符号插桩。
__TEXT,__symbol_stub 间接符号插桩。
__TEXT,__const 初始化的常量。编译器将所有声明为 const 的不可重定位数据放在此 section 中。
__TEXT,__literal4 4 字节文本值。编译器在此 section 中放置单精度浮点常量。在构建最终产物时,静态链接器会合并这些值,并删除重复项。在某些架构中,编译器使用即时加载指令比添加到此 section 中更加高效。
__TEXT,__literal8 8 字节文本值。同上

以下是 __DATA segment 中的几种 section:

Segment and Section content
__DATA,__data 初始化的可变变量,例如可写的 C 字符串和数据数组。
__DATA,__la_symbol_ptr 惰性符号指针,它们是对从其他文件导入的函数的间接引用。
__DATA,__nl_symbol_ptr 非惰性符号指针,它们是对从其他文件导入的数据项的间接引用。
__DATA,__dyld 动态链接器使用的占位 section。
__DATA,__const 初始化的可重定位常量变量。
__DATA,__mod_init_func 模块构造函数。C++ 编译器将静态构造函数放在此处。
__DATA,__mod_term_func 模块析构函数。
__DATA,__bss 未初始化的静态变量的数据,例如 static int i;
__DATA,__common 位于全局范围内(函数声明之外)的未初始化的导入符号定义,例如,int i;

以下是 __IMPORT segment 中的几种 section: |Segment and Section | content | |:---|:---| |__IMPORT,__jump_table|动态库中的函数调用插桩。| |__IMPORT,__pointers|非惰性符号指针,它们是对从其他文件中导入的函数的直接引用。|

twolevel_hints_command

twolevel_hints_command 的数据结构定义如下:

1
2
3
4
5
6
struct twolevel_hints_command {
uint32_t cmd; /* LC_TWOLEVEL_HINTS */
uint32_t cmdsize; /* sizeof(struct twolevel_hints_command) */
uint32_t offset; /* offset to the hint table */
uint32_t nhints; /* number of hints in the hint table */
};

字段说明

  • offset
    • 指定 twolevel_hint 数组在文件中的偏移。
  • nhints
    • 指定 twolevel_hint 的数量。

静态链接器在编译一个两级命名空间镜像时,会将 LC_TWOLEVEL_HINTS 加载命令、两级命名空间提示表写入输出文件中。

默认,ld 不会在 MH_BUNDLE 中写入 LC_TWOLEVEL_HINTS 加载命令和两级命名空间提示表。

twolevel_hint

twolevel_hint 用于表示两级命名空间提示表中的一个项。twolevel_hint 的数据结构定义如下:

1
2
3
4
5
struct twolevel_hint {
uint32_t
isub_image:8, /* index into the sub images */
itoc:24; /* index into the table of contents */
};

字段说明

  • isub_image
    • 表示符号已定义的子镜像(subimage)。用来索引组成综合镜像(umbrella image)的子镜像。如果该字段为 0,则表示符号位于综合镜像中。如果镜像不是一个 umbrella framework 或 library,那么该字段也是 0。
  • itoc
    • isub_image 所指定镜像的符号索引。

两级命名空间提示表为动态链接器提供了建议的位置,从而在当前镜像所链接的库中搜索符号。

两级命名空间镜像中每一个未定义的符号在二级命名空间提示表中有着对应的索引项。

静态链接器在构建两级命名空间镜像时将 LC_TWOLEVEL_HINTS 加载命令和两级命名空间提示表写入到输出文件中。

默认情况下,链接器不会在 MH_BUNDLE 文件中写入 LC_TWOLEVEL_HINTS 加载命令和两级命名空间提示表。

lc_str

lc_str 用于定义一个可变长度的字符串。lc_str 的数据结构定义如下:

1
2
3
4
5
6
union lc_str {
uint32_t offset; /* offset to the string */
#ifndef __LP64__
char *ptr; /* pointer to the string */
#endif
};

字段说明

  • offset
    • 表示从包含此字符串的加载命令的开始到字符串数据开始的偏移。
  • ptr
    • 表示指向字节数组的指针。在运行时,该指针包含字符串数据的虚拟内存地址。在 Mach-O 文件中没有使用 ptr 字段。

加载命令使用 lc_str 数据结构存储可变长度的数据,如库名。除非另有说明,否则数据由 C 字符串组成。

指针指向的数据存储在加载命令之后,并且将数据大小存储在加载命令之中。该字符串应该以 null 终止。我们还可以通过从加载命令数据结构的 cmdsize 字段减去加载命令数据结构大小来确定字符串的大小。

dylib_command

dylib_command 的数据结构定义如下:

1
2
3
4
5
struct dylib_command {
uint32_t cmd; /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB, LC_REEXPORT_DYLIB */
uint32_t cmdsize; /* includes pathname string */
struct dylib dylib; /* the library identification */
};

字段说明

  • dylib
    • 用于描述共享库的属性。

对于文件所链接的每一个共享库,静态链接器都会创建一个 LC_LOAD_DYLIB 加载命令,并且将其 dylib 字段设置为目标库的 LC_ID_DYLD 加载命令的 dylib 字段值。所有 LC_LOAD_DYLIB 加载命令组成一个列表,该列表根据文件中的位置进行排序,最早的 LC_LOAD_DYLIB 命令在列表的最前面。对于两级命名空间文件,符号表中未定义的符号项通过索引此列表来引用其父级共享库。该索引称之为 library ordinal,它存储在 nlist 数据结构的 n_desc 字段中。

在运行时,动态链接器使用 LC_LOAD_DYLIB 加载命令的 dyld 字段中的名称来查找共享库。如果找到了库,则动态链接器会将 LC_LOAD_DYLIB 加载命令的版本信息与库的版本信息进行比较。为了使动态链接器能够成功链接共享库,共享库的兼容版本必须小于等于 LC_LOAD_DYLIB 加载命令中的兼容版本。

动态链接器使用时间戳来确定它是否可以使用预绑定信息。当前版本由 NSVersionOfRunTimeLibrary 函数返回,从而允许你确定程序正在使用的库的版本。

dylib

动态链接器使用 dylib 来匹配要链接的共享库。仅在 dylib_command 加载命令中被用到。

dylib 的数据结构定义如下:

1
2
3
4
5
6
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};

字段说明

  • name
    • lc_str 类型的数据结构。用于指定共享库的名称。
  • timestamp
    • 表示共享库构建的时间戳。
  • current_version
    • 表示共享库的当前版本。
  • compatibility_version
    • 表示共享库的兼容版本。

dylinker_command

dylinker_command 的数据结构定义如下:

1
2
3
4
5
struct dylinker_command {
uint32_t cmd; /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or LC_DYLD_ENVIRONMENT */
uint32_t cmdsize; /* includes pathname string */
union lc_str name; /* dynamic linker's path name */
};

字段说明

  • name
    • lc_str 类型的数据结构。用于指定动态链接器的名称。

动态链接的每个可执行文件都包含一个 LC_LOAD_DYLINKER 加载命令,该命令指定内核必须加载指定名称的动态链接器。动态链接器本身使用 LC_ID_DYLINKER 加载命令指定其名称

prebound_dylib_command

对于可执行文件链接到的每一个预绑定的库,静态链接器都会添加一个 LC_PREBOUND_DYLIB 命令。

prebound_dylib_command 的数据结构定义如下:

1
2
3
4
5
6
7
struct prebound_dylib_command {
uint32_t cmd; /* LC_PREBOUND_DYLIB */
uint32_t cmdsize; /* includes strings */
union lc_str name; /* library's path name */
uint32_t nmodules; /* number of modules in library */
union lc_str linked_modules; /* bit vector of linked modules */
};

字段说明

  • name
    • lc_str 类型的数据结构。用于指定预绑定共享库的名称。
  • nmodules
    • 指定预绑定共享库所包含模块的数量。
  • linked_modules
    • lc_str 类型的数据结构。它是一个可变长度的位集,每个位表示相应的模块是否已经链接到当前文件中,1 表示是,0 表示否。第一个模块是第一个字节的低位。

thread_command

thread_command 的数据结构定义如下:

1
2
3
4
5
6
7
8
struct thread_command {
uint32_t cmd; /* LC_THREAD or LC_UNIXTHREAD */
uint32_t cmdsize; /* total size of this command */
/* uint32_t flavor flavor of thread state */
/* uint32_t count count of uint32_t's in thread state */
/* struct XXX_thread_state state thread state for this flavor */
/* ... */
};

routines_command

routines_command 描述共享库构建函数的位置,动态链接器会在调用任何程序之前调用该函数。routines_command 的数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct routines_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_ROUTINES */
uint32_t cmdsize; /* total size of this command */
uint32_t init_address; /* address of initialization routine */
uint32_t init_module; /* index into the module table that */
/* the init routine is defined in */
uint32_t reserved1;
uint32_t reserved2;
uint32_t reserved3;
uint32_t reserved4;
uint32_t reserved5;
uint32_t reserved6;
};

字段说明

  • init_address
    • 表示构建函数在虚拟内存中的地址。
  • init_module
    • 表示包含构建函数的模块在模块表中的索引。

当使用 -init 选项指定一个共享库构建函数时,静态链接器会添加一个 LC_ROUTINES 命令。

sub_framework_command

sub_framework_command 加载命令用于指定 subframework 所属的 umbrella framework。sub_framework_command 的数据结构定义如下:

1
2
3
4
5
struct sub_framework_command {
uint32_t cmd; /* LC_SUB_FRAMEWORK */
uint32_t cmdsize; /* includes umbrella string */
union lc_str umbrella; /* the umbrella framework name */
};

字段说明

  • umbrella
    • 表示此文件所属的 umbrella framework 的名称。

sub_umbrella_command

sub_umbrella_command 加载命令用于指定 framework 的一个 subumbrella。与 subframework 不同,任何客户端都可以链接到一个 subumbrella。

sub_umbrella_command 的数据结构定义如下:

1
2
3
4
5
struct sub_umbrella_command {
uint32_t cmd; /* LC_SUB_UMBRELLA */
uint32_t cmdsize; /* includes sub_umbrella string */
union lc_str sub_umbrella; /* the sub_umbrella framework name */
};

字段说明

  • sub_umbrella
    • lc_str 类型的数据结构。用于指定 subumbrella 的名称。

sub_library_command

指定此 framework 的一个 sublibrary,并将此 framework 标记为 umbrella framework。与 subframework 不同,任何客户端都可以链接到一个 sublibrary。

sub_library_command 的数据结构定义如下:

1
2
3
4
5
struct sub_library_command {
uint32_t cmd; /* LC_SUB_LIBRARY */
uint32_t cmdsize; /* includes sub_library string */
union lc_str sub_library; /* the sub_library name */
};

字段说明

  • sub_library
    • lc_str 类型的数据结构。用于指定此文件所属的 sublibrary 的名称。

sub_client_command

sub_client_command 加载命令用于指定允许链接到此 subframework 的文件的名称。否则,需要该文件链接到该文件所在的 umbrella framework。

sub_client_command 的数据结构如下所示:

1
2
3
4
5
struct sub_client_command {
uint32_t cmd; /* LC_SUB_CLIENT */
uint32_t cmdsize; /* includes client string */
union lc_str client; /* the client name */
};

字段说明

  • client
    • lc_str 类型的数据结构。用于指定允许链接到此 library 的客户端的名称。

如果在调用 ld 时带上 -allowable_client <name> 选项,其中 <name> 可以是一个 framework 的安装名称,也可以是一个 bundle 的客户端名称,那么 ld 工具会在产物中生成一个 sub_client_command 加载命令。

symtab_command

symtab_command 加载命令用于描述符号表的数据结构以及位置和大小信息。symtab_command 的数据结构定义如下:

1
2
3
4
5
6
7
8
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};

字段说明

  • symoff
    • 指定符号表表项在文件中的起始位置。符号表是一组 nlist 数据结构组成的一个表
  • nsyms
    • 指定符号表中的表项数量。
  • stroff
    • 指定字符串表在文件中的起始位置。
  • strsize
    • 指定字符串表的大小。单位为字节。

nlist

nlist 是表示符号表中的表项的数据结构。nlist 的数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct nlist {
union {
#ifndef __LP64__
char *n_name; /* for use when in-core */
#endif
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
int16_t n_desc; /* see <mach-o/stab.h> */
uint32_t n_value; /* value of this symbol (or stab offset) */
};

字段说明

  • u_un
    • 保存了指向字符串表的索引,n_str。如果希望指定一个空字符串(""),可以将字段设置为 0。联合体中的 n_name 字段并不会用在 Mach-O 文件中。
  • n_type
    • 一个字节值,包含使用四个位掩码访问的数据。
      • N_STAB(0xE0):如果设置了这 3 位,则表示该符号是符号调试表(stab)表项。在这种情况下,整个 n_type 字段都被解释为 stab 值。
      • N_PEXT(0x10):如果设置了此位,则表示该符号被标记为具有有限的全局作用域。静态链接器处理该文件时会将每个设置了 N_PEXT 位的符号清除 N_EXT 位(ld-keep_private_externs 选项关闭了此行为)。对于 OS X GCC,我们可以使用 __private_extern__ 方法属性来设置此位。
      • N_TYPE(0x0E):这些位定义了符号的类型。
      • N_EXT(0x01):如果设置了这 3 位,则表示该符号是一个外部符号,该符号可以在此文件外部定义,也可以在此文件中定义并可以被外部文件引用。
    • 上述提到的 N_TYPE 字段包含以下这些值:
      • N_UNDF(0x0):表示符号未定义。未定义符号是指在此模块中引用但是在其他模块中定义的符号。此时,n_sect 字段为 NO_SECT
      • N_ABS(0x2):表示符号是绝对的。链接器不会修改绝对符号的值。此时,n_sect 字段为 NO_SECT
      • N_SECT(0xE):表示符号定义在 n_sect 所指定的 section 中。
      • N_PBUD(0xC):表示符号未定义,并且镜像为该符号使用了一个预绑定的值。此时,n_sect 字段为 NO_SECT
      • N_INDR(0xA):表示符号的定义与另一个符号相同。n_value 字段是一个指向字符串表的索引,用于指定另一个符号的名称。链接该符号时,此符号与另一个符号具有相同定义的类型和值。
  • n_sect
    • 表示 section 的编号,在对应 section 中可以找到该符号;如果该字段的值为 NO_SECT,则表示在该镜像中找不到该符号。section 在 segment 中是按顺序排列的,从 1 开始连续编号。
  • n_desc
    • 一个 16 位的值,为非稳定符号提供关于该符号性质的附加信息。可以使用 REFERENCE_TYPE(0xF)掩码来访问其值。值的定义有以下这些:
      • REFERENCE_FLAG_UNDEFINED_NON_LAZY(0x0):表示该符号引用了外部非惰性(数据)符号。
      • REFERENCE_FLAG_UNDEFINED_LAZY(0x1):表示该符号引用了外部惰性符号。比如一个函数调用。
      • REFERENCE_FLAG_DEFINED(0x2):表示符号定义在本模块内。
      • REFERENCE_FLAG_PRIVATE_DEFINED(0x3):表示符号定义在本模块内,并且只对本共享库内的模块可见。
      • REFERENCE_FLAG_PRIVATE_UNDEFINED_NON_LAZY(0x4):表示符号定义在了此文件中的其他模块内,是一个非惰性(数据)符号,并且只对本共享库内的模块可见。
      • REFERENCE_FLAG_PRIVATE_UNDEFINED_LAZY(0x5):表示符号定义在了此文件中的其他模块内,是一个惰性(函数)符号,并且只对本共享库内的模块可见。
    • 掩码以外的位的值的定义有以下这些:
      • REFERENCED_DYNAMICALLY(0x10):对于被动态加载器 API (如:dlsymNSLookupSymbolInImage)引用的已定义符号,或者该位必须被设置。strip 工具使用该位来避免删除必须存在的符号:如果符号设置了该位,则 strip 不会删除它。
      • N_DESC_DISCARDED(0x20):在一个完全链接的镜像中,动态链接器会在运行时用到该位。因此,不要在完全链接的图像中设置此位。
      • N_NO_DEAD_STRIP(0x20):当在一个可重定位目标文件(文件类型为 MH_OBJECT)中对一个已定义符号进行设置时,表示静态链接器用于不会对符号进行 dead-strip 处理。
      • N_WEAK_REF(0x40):表示此未定义符号是一个弱引用。如果动态链接器找不到该符号的定义,那么将会符号地址位置为 0。静态链接器在给定合适的弱链接标志的情况下会设置此符号。
      • N_WEAK_DEF(0x80):表示此符号是一个弱定义。如果静态链接器或动态链接器为该符号找到了另一个(非弱)定义,那么弱定义会被忽略。只有在已合并的 section 中的符号可以被标记为弱定义。
    • 如果文件是一个两级命名空间镜像(即,如果设置了 mach_headerMH_TWOLEVEL 标志位),那么 n_desc 的高 8 位则指向未定义符号的定义所位于的 library。使用 GET_LIBRARY_ORDINAL 宏 来读取此值,使用 SET_LIBRARY_ORIDINAL 宏来设置此值。0 表示指定当前镜像。1 到 253 则是根据此文件中的 LC_LOAD_COMMAND 加载命令的顺序指定 library 的编号。254 用于会被动态查找的未定义的符号。对于能够从可执行程序中加载符号的插件,会将其链接至 255,从而指定可执行镜像。对于扁平命名空间镜像,高 8 位必须为 0。
  • n_value
    • 即符号的值。符号表表项的类型(n_type 字段指定)不同,那么值的形式也不同。对于 N_SECT 符号类型,n_value 是符号的地址。

公共符号必须是 N_UNDF 类型,并且必须设置 N_EXT 位。公共符号的 n_value 是符号的数据的大小(以字节为单位)。在 C 语言中,公共符号是在此文件中声明但未初始化的变量。公共符号只能出现在 MH_OBJECT 类型的 Mach-O 文件中。

dysymtab_command

dysymtab_command 加载命令描述了用于动态链接的符号表的各部分的大小和位置。dysymtab_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
27
28
29
30
31
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */

uint32_t ilocalsym; /* index to local symbols */
uint32_t nlocalsym; /* number of local symbols */

uint32_t iextdefsym; /* index to externally defined symbols */
uint32_t nextdefsym; /* number of externally defined symbols */

uint32_t iundefsym; /* index to undefined symbols */
uint32_t nundefsym; /* number of undefined symbols */

uint32_t tocoff; /* file offset to table of contents */
uint32_t ntoc; /* number of entries in table of contents */

uint32_t modtaboff; /* file offset to module table */
uint32_t nmodtab; /* number of module table entries */

uint32_t extrefsymoff; /* offset to referenced symbol table */
uint32_t nextrefsyms; /* number of referenced symbol table entries */

uint32_t indirectsymoff;/* file offset to the indirect symbol table */
uint32_t nindirectsyms; /* number of indirect symbol table entries */

uint32_t extreloff; /* offset to external relocation entries */
uint32_t nextrel; /* number of external relocation entries */

uint32_t locreloff; /* offset to local relocation entries */
uint32_t nlocrel; /* number of local relocation entries */
};

字段说明

  • ilocalsym
    • 指定本地符号组中第一个符号的索引。
  • nlocalsym
    • 指定本地符号组中所有符号的数量。
  • iextdefsym
    • 指定已定义的外部符号组中第一个符号的索引。
  • nextdefsym
    • 指定已定义的外部符号组中所有符号的数量。
  • iundefsym
    • 指定未定义的外部符号组中第一个符号的索引。
  • nundefsym
    • 指定未定义的外部符号组中所有符号的数量。
  • tocoff
    • 指定内容数据表在文件中的偏移。
  • ntoc
    • 指定内容数据表的表项数量
  • modtaboff
    • 指定模块表数据在文件中的偏移。
  • nmodtab
    • 指定模块表的表项数量。
  • extrefsymoff
    • 指定外部引用表数据在文件中的偏移。
  • nextrefsyms
    • 指定外部引用表数据的表项数量。
  • indirectsymoff
    • 指定间接符号表数据在文件中的偏移。
  • nindirectsyms
    • 指定间接符号表的表项数量。
  • extreloff
    • 指定外部重定位表数据在文件中的偏移。
  • nextrel
    • 指定外部重定位表的表项数量。
  • locrelloff
    • 指定本地重定位表数据在文件中的偏移。
  • nlocrel
    • 指定本地重定位表的表项数量。

LC_DYSYMTAB 加载命令包含符号表的一组索引和一组文件偏移量,这些文件偏移量定义了其他几个表的位置。文件中未使用的表的字段应设置为 0。

dylib_table_of_contents

dylib_table_of_contents 用于描述一个动态共享库内容表的表项。dylib_table_of_contents 的数据结构定义如下:

1
2
3
4
struct dylib_table_of_contents {
uint32_t symbol_index; /* the defined external symbol (index into the symbol table) */
uint32_t module_index; /* index into the module table this symbol is defined in */
};

字段说明

  • symbol_index
    • 一个符号表表项的索引,该表项引用了这个已定义的外部符号。
  • module_index
    • 一个模块表表项的索引,表示这个已定义的外部符号所在的模块。

dylib_module

dylib_module 用于描述一个动态共享库的模块表表项。dylib_module 的数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct dylib_module {
uint32_t module_name; /* the module name (index into string table) */

uint32_t iextdefsym; /* index into externally defined symbols */
uint32_t nextdefsym; /* number of externally defined symbols */
uint32_t irefsym; /* index into reference symbol table */
uint32_t nrefsym; /* number of reference symbol table entries */
uint32_t ilocalsym; /* index into symbols for local symbols */
uint32_t nlocalsym; /* number of local symbols */

uint32_t iextrel; /* index into external relocation entries */
uint32_t nextrel; /* number of external relocation entries */

uint32_t iinit_iterm; /* low 16 bits are the index into the init section, high 16 bits are the index into the term section */
uint32_t ninit_nterm; /* low 16 bits are the number of init section entries, high 16 bits are the number of term section entries */

uint32_t /* for this module address of the start of */
objc_module_info_addr; /* the (__OBJC,__module_info) section */
uint32_t /* for this module size of */
objc_module_info_size; /* the (__OBJC,__module_info) section */
};

字段说明

  • module_name
    • 一个字符串表的表项索引,该表项描述了这个模块的名称。
  • iextdefsym
    • 表示该模块的第一个已定义外部符号在符号表中的索引。
  • nextdefsym
    • 表示该模块的所有已定义外部符号的数量。
  • irefsym
    • 表示该模块在外部引用表中的第一个表项的索引。
  • nrefsym
    • 表示该模块在外部引用表中的占有表项的数量。
  • ilocalsym
    • 表示该模块的第一个本地符号在符号表中的索引。
  • nlocalsym
    • 表示该模块的所有本地符号的数量。
  • iexterel
    • 表示该模块在外部重定位表中的第一个表项的索引。
  • nextrel
    • 表示该模块在外部重定位表中占有表项的数量。
  • iinit_iterm
    • 指定了模块构造 section 的索引(低 16 位);指定了模块析构 section 的索引(高 16 位)。
  • ninit_nterm
    • 指定了模块构造 section 中的指针数量(低 16 位);指定了模块析构 section 中的指针数量(高 16 位)。
  • objc_module_info_addr
    • 表该模块数据部分的起始静态链接地址,位于 __module_info section __OBJC segment。
  • objc_module_info_size
    • 表示 __OBJC segment 的 __module_info section 使用该模块的数据的字节数。

dylib_reference

dylib_reference 为共享库中的模块提供的外部引用表项定义属性。dylib_reference 的数据结构定义如下:

1
2
3
4
struct dylib_reference {
uint32_t isym:24, /* index into the symbol table */
flags:8; /* flags to indicate the type of reference */
};

字段说明

  • isym
    • 表示被引用符号在符号表中的索引。
  • flags
    • 用于表示引用的类型。

参考

  1. Mach-O_File_Format.pdf