一.概述
Mach-O(Mach Object) 是 Apple 操作系统(包括 macOS、iOS、iPadOS、watchOS 和 tvOS)上可执行文件、目标代码、共享库(.dylib)和动态加载器(dyld)的专属文件格式,可由 Clang / LLVM、Swiftc、GCC、Rustc 编译。
常见 Mach-o 文件类型
-
Executable:可执行文件(.out.o) -
Dylib:动态链接库 -
Bundle:不能被链接,只能在运行时使用dlopen()加载 -
Image:包含Executable、Dylib和Bundle -
Framework:包含Dylib、资源文件和头文件的文件夹
二.Mach-O文件结构
使用MachOView看Mach-O,效果如下:
Mach-O文件中包含三个主要的部分:
-
Header(头部):标明文件的基本信息(如架构、文件类型) -
Load Commands(加载命令):指导操作系统如何加载和解析这个文件 -
Data(数据区):存放具体的代码(Code)和数据(Data)
其次还有:
-
Function Starts(函数起始地址表):列出了二进制文件中所有函数的起始偏移量 -
Symbol Table(符号表):存储了程序中定义和引用的所有符号 -
Data in Code Entries(代码中的数据项):记录在代码段(__TEXT)中混杂的非指令数据(即数据常量)的位置和类型 -
Dynamic Symbol Table(动态符号表):对Symbol Table的索引,区分函数是内部定义还是外部引用 -
String Table(字符串表):长字符串缓冲区 -
Code Signature(代码签名):存放开发者证书、签名哈希值以及权限声明(Entitlements)
三.Header
Header 位于文件的最开始。它决定了系统如何看待这个文件。在底层(<mach-o/loader.h>)中,64位架构的头部结构体定义为 mach_header_64。
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
struct mach_header {
uint32_t magic; /* 32位或者64位,系统内核用来判断是否是mach-o格式 */
int32_t cputype; /* CPU架构类型 */
int32_t cpusubtype; /* CPU的具体类型 */
uint32_t filetype; /* mach-o文件类型 */
uint32_t ncmds; /* LoadCommands加载命令的条数 */
uint32_t sizeofcmds; /* 全部LoadCommands加载命令的大小 */
uint32_t flags; /* 标志位标识二进制文件支持的功 */
};
/*
* The 64-bit mach header appears at the very beginning of object files for
* 64-bit architectures.
*/
struct mach_header_64 {
uint32_t magic; /* 32位或者64位,系统内核用来判断是否是mach-o格式 */
int32_t cputype; /* CPU架构类型 */
int32_t cpusubtype; /* CPU的具体类型 */
uint32_t filetype; /* mach-o文件类型 */
uint32_t ncmds; /* LoadCommands加载命令的条数 */
uint32_t sizeofcmds; /* 全部LoadCommands加载命令的大小 */
uint32_t flags; /* 标志位标识二进制文件支持的功能 */
uint32_t reserved; /* 保留字段 */
};
常见的文件类型有以下几种:
#define MH_OBJECT 0x1 /* 中间目标文件 */
#define MH_EXECUTE 0x2 /* 标准可执行文件 */
#define MH_DYLIB 0x6 /* 动态链接库 */
#define MH_DYLINKER 0x7 /* 动态链接器 */
#define MH_DSYM 0xa /* 调试符号文件(.dSYM),用于debug分析 */
#define MH_KEXT_BUNDLE 0xb /* 内核扩展模块 */
四.Data
Load Commands在 Mach-O 文件加载解析时,会被内核(XNU)和动态链接器(dyld)调用。这些指令都采用 Type-Size-Value 这种格式,即:32 位的 cmd 值(表示类型),32 位的 cmdsize 值(32 位二级制位 4 的倍数,64 位位 8 的倍数),以及命令本身(由 cmdsize 指定的长度)。内核加载器使用的命令可以参看 xnu来学习,其他命令则是由动态链接器处理的。
在 Mach-O 中,数据并不是杂乱无章的,而是采用了两级管理结构:Segment(段) 和 Section(节)。Segment 是一个较大的内存管理单位,而每个段内部会包含一个或多个具体的 Section。Segment 是内存对齐和权限控制的最小单位,而 Section 则是根据数据类型进行的更细致的分类。
Segment 的数据结构如下:
struct segment_command_64 { /* 适用于 64 位架构的段命令结构体 */
uint32_t cmd; /* 加载命令类型,对于当前结构体固定为 LC_SEGMENT_64 */
uint32_t cmdsize; /* 当前加载命令的总大小,包含紧随其后的所有 section_64 结构体的大小 */
char segname[16]; /* 段的名称(固定 16 字节,例如 __TEXT, __DATA, __LINKEDIT) */
uint64_t vmaddr; /* 该段在虚拟内存中的起始目标地址 */
uint64_t vmsize; /* 该段在虚拟内存中所占用的内存大小 */
uint64_t fileoff; /* 该段在磁盘文件中的偏移量(从文件开头算起) */
uint64_t filesize; /* 该段在文件中实际占用的大小(需要从文件中映射的数据量) */
int32_t maxprot; /* 该内存段允许的最高虚拟内存保护权限(如读、写、执行权限组合) */
int32_t initprot; /* 该内存段在初始化加载时的默认虚拟内存保护权限 */
uint32_t nsects; /* 该段下包含的 section(节)的数量 */
uint32_t flags; /* 标志位(例如 SG_HIGHVM,用于控制段的加载行为) */
};
Section 的数据结构如下:
struct section_64 { /* 适用于 64 位架构的节结构体 */
char sectname[16]; /* 节的名称(固定 16 字节,例如 __text, __bss, __cstring) */
char segname[16]; /* 该节所属的段的名称(固定 16 字节,例如 __TEXT, __DATA) */
uint64_t addr; /* 该节在虚拟内存中的起始目标地址 */
uint64_t size; /* 该节在内存中所占用的字节大小 */
uint32_t offset; /* 该节在磁盘文件中的偏移量(从文件开头算起) */
uint32_t align; /* 节的内存对齐要求(以 2 的幂次方表示,例如 3 表示 2^3 = 8 字节对齐) */
uint32_t reloff; /* 该节的重定位条目(Relocation Entries)在文件中的偏移量 */
uint32_t nreloc; /* 该节所包含的重定位条目的数量 */
uint32_t flags; /* 标志位,分为两部分:低 8 位表示节的类型(Type),高 24 位表示属性(Attributes) */
uint32_t reserved1; /* 保留字段1(在特定类型的节中用作偏移量或索引,如间接符号表索引) */
uint32_t reserved2; /* 保留字段2(在特定类型的节中用作计数或结构体大小,如 stub 的大小) */
uint32_t reserved3; /* 保留字段3(纯保留字段,目前通常填充为 0) */
};
常见的5个 Segment 如下:
#define SEG_PAGEZERO "__PAGEZERO" /* 空指针捕获段 */
#define SEG_TEXT "__TEXT" /* 传统的 UNIX 代码段(文本段) */
#define SEG_DATA "__DATA" /* 传统的 UNIX 数据段 */
#define SEG_OBJC "__OBJC" /* Objective-C 运行时(Runtime)专用的段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 链接器输出段 */
① __TEXT(代码段)
-
权限:只读、可执行(
r-x)。 -
作用:存放编译后的机器指令和只读数据。因为不可写,所以这一段在内存中可以被多个进程共享。
-
常见 Sections:
-
__text:真正的可执行机器代码(C/OC/Swift 函数主体)。 -
__cstring:硬编码的 C 语言字符串常量(如printf("hello")中的字符串)。 -
__const:只读常量数据(如用const修饰的全局变量)。 -
__stubs:符号桩代码。用于调用外部动态库函数的占位跳转代码(与延迟绑定相关)。 -
__stubs_helper:辅助桩代码。当第一次调用外部函数时,负责通知dyld去寻找真实函数地址。 -
__objc_methname:Objective-C 方法名字符串(如"init","viewDidLoad"),Runtime 注册方法时使用。 -
__objc_methtype:Objective-C 方法的类型编码(Type Encoding)字符串(描述返回值和参数类型)。 -
__objc_classname:Objective-C 类名字符串。
-
② __DATA(数据段)
-
权限:可读、可写、不可执行(
rw-)。 -
作用:存放全局变量、静态变量以及动态链接器(dyld)需要修改的指针。
-
常用 Sections:
-
__data:已初始化的全局变量或静态变量。 -
__la_symbol_ptr:懒加载符号指针表(Lazy Symbol Pointer)。与__stubs对应,第一次调用外部函数时由dyld动态解析并写入真实地址。 -
nl_symbol_ptr:非懒加载符号指针表(Non-Lazy Symbol Pointer)。在程序启动加载时,dyld必须立刻解析出真实地址的外部指针。 -
__const:需要重定位的常量数据(包含需要dyld在启动时修复指针的常量)。 -
__cfstring:Core Foundation 字符串对象(即 Objective-C 的NSString常量对象,内部包含指向__cstring的指针和长度等元数据)。 -
__bss:未初始化的全局变量(在文件中不占空间,加载到内存时才分配并清零)。 -
__common:未初始化的符号定义(通常用于传统的 C 语言公有块变量)。 -
__objc_classlist:Objective-C 的类列表(指针数组,指向程序中定义的所有类结构)。 -
__objc_protolist:Objective-C 协议列表。 -
__objc_imginfo:Objective-C 镜像信息。包含 ObjC 运行时的版本号(如 2.0)和一些编译标记(Flags)。 -
__objc_selfrefs:Objective-C 方法选择器(Selector)引用表。程序中使用的@selector指针列表。 -
__objc_protorefs:Objective-C 协议引用表。程序中用到的外部/内部协议的引用指针。 -
__objc_superrefs:Objective-C 父类引用表。用于子类通过super调用父类方法时快速定位。
-
③ __LINKEDIT(链接段)
-
权限:只读(
r--)。 -
作用:包含动态链接器(dyld)所需的元数据,如符号表、字符串表、代码签名、重定位信息等。没有它,动态链接和调试就无法进行。
④ __PAGEZERO(空页段)
-
权限:不可读、不可写、不可执行(
---)。 -
作用:位于文件和虚拟内存的最开始(在 64 位系统上通常占用 4GB 空间)。它是一个特殊的空白区域,用来捕捉 NULL 指针崩溃。如果程序尝试读写这个区域的地址,系统会直接抛出
EXC_BAD_ACCESS错误。
⑤ __OBJC(ObjC 运行时段)
-
权限:可读、可写、不可执行(
rw-)。 -
作用:存放 Objective-C 语言特有的类结构、方法名、协议、分类(Category)以及实例变量(Ivars)等元数据。


文章评论