Mach-O
胖文件
胖二进制(Fat Binaries)或通用二进制(Universal Binaries)的文件头结构非常简单,其核心目的是在一个文件中封装多个不同架构的Mach-O切片(Slices),即将多个不同架构版本的二进制文件打包在一起。起初是为了解决从PowerPC向Intel(以及后续架构)迁移时的兼容性问题,后来也在Intel往Arm迁移时发挥余热。它的具体结构定义在<mach-o/fat.h>中,由两部分组成:一个全局头部结构体以及紧随其后的架构描述符数组)

全局头部结构(struct fat_header)
这是文件的最开始部分,包含以下两个关键字段:
- magic(魔数):占用4个字节,其标准值为
0xcafebabe,需要注意的是这个值在磁盘上通常以大端(Big-Endian)格式存储,如果以小端格式读取则显示为0xbebafeca。该魔数与Java的.class文件魔数恰好冲突) - nfat_arch(架构数量):占用4个字节,它定义了该通用二进制文件中包含的架构切片的总数,常见情况如:
nfat_arch 的值 |
包含的架构切片示例 | 适用场景与背景 |
|---|---|---|
| 1 | x86_64 或 arm64 |
单架构(Thin)文件,在iOS 11之后,由于彻底弃用32位支持,许多系统库已变为单架构) |
| 2 | x86_64 + i386 |
经典macOS通用二进制,为了在Intel平台上同时兼容32位和64位程序,/usr/lib/dyld等文件常采用此组合) |
| 2 | arm64 + armv7 |
旧版iOS通用二进制,在iOS 11之前,第三方App常包含此两类切片以支持旧设备) |
| 3 | x86_64 + i386 + x86_64h |
macOS共享库缓存(SLC),在某些macOS版本中,缓存会同时包含通用Intel 64位、32位以及针对Haswell架构优化的版本) |
| N (>3) | 多种架构组合 | 在 NeXTSTEP 或早期的 Mac OS X 迁移期,一个胖二进制可能同时包含 PowerPC、PowerPC64、i386、m68k 等多个切片) |
架构描述符数组(struct fat_arch)
紧跟在fat_header之后,是一个包含nfat_arch个元素的数组,每个元素都是一个struct fat_arch结构体,用于描述特定切片在文件中的位置和属性:
- cputype(CPU类型):指定该切片的处理器家族(如x86_64或ARM64))
- cpusubtype(CPU子类型):指定该处理器的具体变体(如ARM64_V8或X86_64_ALL))
- offset(偏移量):该架构切片的Mach-O数据相对于整个文件起始位置的字节偏移量)
- size(大小):该架构切片所占用的字节大小)
- align(对齐):该切片在内存中的对齐要求,通常以2的幂次方表示(如2^12表示4096字节对齐)。对齐值必须是对应架构页面大小的整数倍,这使得内核能够使用
mmap映射该文件并丢弃不需要的架构切片,从而保证通用二进制文件的内存占用与单架构二进制文件基本一致)
当内核加载胖二进制文件时,它会识别FAT_MAGIC并通过“评分”(Grading)机制遍历这些描述符,寻找与硬件最匹配的架构切片。关于如何查看和操作胖文件,参看文末工具部分的otool -f、jtool -f和lipo)
Mach-O文件头
先来个文件总览,它的格式如下:

这里看头部,它的大小是固定的,定义了该对象的类型、目标架构、加载命令的数量以及影响链接器行为的标志,下面逐个说明:
| 字段 | 大小 | 说明 |
|---|---|---|
| magic | 4字节 | 魔数(如0xfeedfacf) |
| cputype | 4字节 | CPU架构类型(如ARM64) |
| cpusubtype | 4字节 | 具体机器型号子类型 |
| filetype | 4字节 | 文件用途(如EXECUTE, DYLIB) |
| ncmds | 4字节 | 加载命令的数量 |
| sizeofcmds | 4字节 | 加载命令的总大小 |
| flags | 4字节 | 链接/运行标志位 |
| reserved | 4字节 | 64位对齐保留位 |
魔数(magic)
位于文件头的最开始,用于标识文件的位数和字节序(Endianness))
- 32位小端(Little Endian):
0xfeedface(MH_MAGIC)) - 64位小端(Little Endian):
0xfeedfacf(MH_MAGIC_64)) - 大端(Big Endian): 在hex dump中会显示为字节翻转的形式,如
0xcefaedfe(MH_CIGAM)或0xcffaedfe(MH_CIGAM_64))
CPU 类型 (cputype) 与子类型 (cpusubtype)
cputype标识了处理器家族,而cpusubtype则指定了该家族中的具体架构变体。64位架构的类型通常通过将32位值与CPU_ARCH_ABI64 (0x01000000) 进行按位或运算得到):
| CPU 类型 (cputype) | 常量值 (Hex) | 常见 CPU 子类型 (cpusubtype) | 架构描述 |
|---|---|---|---|
| I386 / X86 | 0x00000007 | I386_ANY | Intel 32位处理器(10.8起弃用) |
| X86_64 | 0x01000007 | X86_64_ALL (3) / X86_64_H (8) | Intel 64位通用处理器Haswell 或更高版本的处理器 |
| ARM | 0x0000000C | ARM(v6)/ARM_V7/ARM_V7S/ARM_V7K | 初始 ARM 支持A4 处理器A5, A6 处理器Apple Watch S1-3 使用的架构 |
| ARM_64 | 0x0100000C | ARM64_ALL (0)/ARM64_E (2) | A7-A11 处理器 (ARMv8支持)/A12+ 处理器 (ARMv8.3支持) |
| ARM_64_32 | 0x0200000C | ARM64_32_V8 | Apple Watch S4 使用的架构:32位寻址,但使用64位指令集 |
文件类型 (filetype)
该字段定义了Mach-O文件的用途,目前定义了11种类型:
| 常量 ID | 常量名称 (MH_*) | 文件类型描述 | 备注与用途 |
|---|---|---|---|
| 1 | MH_OBJECT | 可重定位目标文件 | 由编译器生成的中间产物;也曾用于旧版 32 位内核扩展) |
| 2 | MH_EXECUTE | 可执行文件 | 应用程序的主程序,如 /usr/bin 下的工具或 App 的二进制文件) |
| 3 | MH_FVMLIB | 固定 VM 库文件 | 不可重定位的库文件(已过时)) |
| 4 | MH_CORE | 核心转储文件 | 用于进程崩溃或系统恐慌(Panic)时的内存快照) |
| 5 | MH_PRELOAD | 预加载可执行文件 | XNU 不再支持,但仍用于某些固件镜像) |
| 6 | MH_DYLIB | 动态库 | 共享库文件,如 /usr/lib 中的 .dylib 或 Frameworks) |
| 7 | MH_DYLINKER | 动态链接器 | 专门用于动态链接器,如 /usr/lib/dyld) |
| 8 | MH_BUNDLE | 插件/束文件 | 由 dlopen() 或 NSBundle 显式加载的插件(如 QuickLook 插件)) |
| 9 | MH_DYLIB_STUB | 动态链接存根 | 仅包含符号而不含代码的存根,曾简要用于 SDK 早期版本) |
| 10 | MH_DSYM | 伴随符号文件 | 包含 DWARF 调试信息的文件,通常以 .dSYM 文件夹形式存在) |
| 11 | MH_KEXT_BUNDLE | 内核扩展束 | 现代 64 位系统中的内核驱动程序二进制文件) |
Darwin系统中的特殊文件类:
* 可执行性:在定义的11种类型中,只有MH_EXECUTE和MH_DYLINKER是可以直接执行的。其中MH_DYLINKER特指动态链接器,虽然系统默认使用/usr/lib/dyld,但Mach-O格式允许指定自定义链接器
-
加载要求(Loading Requirements):
MH_DYLIB和MH_BUNDLE类型不能独立运行,必须被加载到可执行文件中。动态库(Dylib)可以通过加载命令隐式加载,也可以通过dlopen()显式加载;而插件(Bundle)通常必须通过dlopen()或NSBundleAPI显式加载 -
调试与符号关联(DSYM):
MH_DSYM是Apple处理调试信息的独特方式。与ELF格式将调试信息嵌入节区导致文件体积剧增不同,Mach-O将调试信息分离到独立的.dSYM文件中。这种伴随文件通过128位的UUID(由LC_UUID命令定义)与主可执行文件保持耦合,确保调试器(如LLDB)能准确匹配符号 -
内核扩展(KEXT):文件类型的选择也随架构演进:旧的32位内核扩展使用
MH_OBJECT或MH_BUNDLE类型,而现代的64位内核扩展统一使用MH_KEXT_BUNDLE类型
加载命令信息(ncmds & sizeofcmds)
- ncmds: 紧跟在头部之后的加载命令(Load Commands)的数量
- sizeofcmds: 所有加载命令所占用的总字节大小
下面会详细说明常见的加载命令
标志位(flags)
flags字段是一个32位的字段,其中的每一位都代表了链接器(ld)或动态链接器(dyld)关心的特定属性:
| 标志常量 (Flags) | 十六进制值 | 用途与说明 |
|---|---|---|
| MH_NOUNDEFS | 0x00000001 | 没有未定义的引用,文件已完全链接。 |
| MH_DYLDLINK | 0x00000004 | 该对象文件是为了通过dyld链接而构建的。 |
| MH_PREBOUND | 0x00000010 | 动态引用已预绑定(已弃用,由共享缓存取代)。 |
| MH_SPLIT_SEGS | 0x00000020 | 文件将只读段和可写段分开。 |
| MH_TWOLEVEL | 0x00000080 | 使用两级命名空间链接。 |
| MH_FORCE_FLAT | 0x00000100 | 强制使用平坦命名空间(较罕见)。 |
| MH_WEAK_DEFINES | 0x00008000 | 二进制文件包含弱定义符号。 |
| MH_BINDS_TO_WEAK | 0x00010000 | 二进制文件使用了弱引用符号。 |
| MH_ALLOW_STACK_EXECUTION | 0x00020000 | 允许栈执行(通常被视为安全风险)。 |
| MH_NO_REEXPORTED_DYLIBS | 0x00100000 | 动态库不包含重新导出的库命令。 |
| MH_PIE | 0x00200000 | 位置无关可执行文件,允许加载时应用ASLR。 |
| MH_HAS_TLV_DESCRIPTORS | 0x00800000 | 对象包含一个带有线程本地变量(TLV)的节。 |
| MH_NO_HEAP_EXECUTION | 0x01000000 | 激活平台NX保护,禁止堆/数据页执行。 |
| MH_APP_EXTENSION_SAFE | 0x02000000 | 二进制文件已链接,可安全用于应用程序扩展。 |
| MH_NLIST_OUTOFSYNC_WITH_DYLDINFO | 0x04000000 | nlist符号表与dyldinfo信息不同步(dyld-625)。 |
| MH_SIM_SUPPORT | 0x08000000 | 包含模拟器支持信息。 |
| MH_HAS_OBJC | 0x40000000 | 二进制文件包含Objective-C节。 |
| MH_DYLIB_IN_CACHE | 0x80000000 | 指示该动态库目前位于共享库缓存中。 |
在大多数现代可执行文件中,默认常见的标志组合是NOUNDEFS, DYLDLINK, TWOLEVEL和PIE (0x200085)
保留字段 (reserved)
仅限 64 位文件头,用于保证 64 位对齐
加载命令
Mach-O格式目前(截至Darwin 17)定义了大约50种加载命令,可大致分为如下三类:
内核主要负责定义虚拟内存布局、程序入口点及安全校验。其关注点在于权限划分、签名核验和启动入口,而具体的符号解析则交给dyld
LC_SEGMENT或LC_SEGMENT_64
这是Mach-O二进制文件中最重要的加载命令,它们定义了文件的某一部分如何映射到进程的虚拟内存空间中,并设置相应的保护权限。包含段名(segname)、虚拟内存地址(vmaddr)、大小(vmsize)、文件偏移(fileoff)以及初始保护位(initprot)和最大保护位(maxprot)。内核通过mmap(2)按照这些指令将文件内容分页映射。例如,__PAGEZERO段被定义为不可访问(---/---)以捕获空指针解引用,而__TEXT段则通常映射为可读可执行(r-x)
LC_MAIN 与 LC_UNIXTHREAD(入口点命令)
内核必须知道从哪里开始执行代码。LC_MAIN是现代Mach-O使用的架构无关入口点命令,它直接指定了程序的入口偏移。LC_UNIXTHREAD是传统的加载命令,它定义了主线程启动时完整的CPU寄存器状态,在OS X 10.8后被LC_MAIN取代,但目前仍用于dyld自身和内核二进制文件的加载
LC_LOAD_DYLINKER
该命令指示内核定位并加载动态链接器(通常是/usr/lib/dyld)。除了静态链接的程序外,内核在完成初步映射后,实际上会将控制权交给此命令指定的dyld入口点__dyld_start,由dyld完成后续的动态库链接工作
LC_CODE_SIGNATURE
这是Darwin系统实现代码签名的基础。该命令属于linkedit_data_command,指向__LINKEDIT段中的代码签名数据块,内核在加载镜像时会感知此命令并缓存代码签名,这是强制执行代码完整性校验和处理“权利”(Entitlements)声明的依据,后续会有专门的一篇来分析代码签名
LC_ENCRYPTION_INFO或LC_ENCRYPTION_INFO_64
用于原生支持二进制加密(即FairPlay),当内核遇到此命令时,会标记指定的内存页范围(从cryptoff到cryptoff + cryptsize)为加密状态。如果加密ID(cryptid)为1,内核会将这些页面的控制权交给Apple Protect分页器处理,以实现透明解密加载。我们平常说的砸壳就是解密它所指定范围的内存
LC_UUID
包含一个随机生成的128位通用唯一标识符,XNU虽然不关心UUID的具体值,但会将其与进程信息一同缓存,以便在生成崩溃报告(Crash reports)或低内存报告(Jetsam reports)时精确定位二进制版本
LC_THREAD
主要用于核心转储(Core dumps)文件,包含线程信息
LC_IDENT
在官方文档中被标记为“已弃用”,它是在内核发生恐慌(Panic)生成转储文件时,XNU使用它来嵌入内核版本信息
dyld处理
dyld(动态链接器)负责处理与库依赖、符号解析以及链接元数据相关的加载命令。与内核关心的内存布局不同,dyld处理的命令涉及如何将不同的二进制镜像“缝合”在一起。由于内容很杂下面将按功能分类说明:
库依赖与路径识别 (Library Dependencies & Paths)
这类命令告诉 dyld 程序运行需要哪些外部支持,以及去哪里寻找这些库
LC_LOAD_DYLIB:最基本的命令,指定程序启动时必须加载的动态库。它由链接器为每一个由-l参数指定的库生成,该命令包含一个struct dylib结构,记录了库的路径、时间戳、当前版本号以及兼容版本号LC_LOAD_WEAK_DYLIB:用于“弱引用”库。如果dyld找不到该库,链接不会报错,而是将相关符号设为NULL,由程序自行处理(否则会导致崩溃),使用此类库的二进制文件会在头部被标记为MH_BINDS_TO_WEAK。当库中的符号被标记为弱引用(如使用__attribute__((weak_import)))或整个库使用-weak_library链接时生成此命令LC_LAZY_LOAD_DYLIB:延迟加载命令,它在启动时不加载库,而是在程序第一次访问该库的符号时才触发加载LC_LOAD_UPWARD_DYLIB:向上加载命令,主要用于子库或框架,目的是避免循环依赖(Cyclical Dependencies)。当一个库依赖于它的调用者(直接或间接)时,使用此命令可以安全地建立依赖链LC_REEXPORT_DYLIB:用于实现“外观模式”(Façade pattern)。它不仅加载依赖库,还将其所有符号视为自身符号导出,简化了复杂的框架结构(如libSystem.B.dylib本身几乎是一个空库,但通过重导出命令集成了系统底层的众多关键库,从而简化了其他二进制文件的链接过程)LC_RPATH:定义运行时搜索路径。它用于扩展@rpath标记的搜索范围,允许使用占位符,如@executable_path(解析为可执行文件的当前目录)或@loader_path(解析为加载当前库的镜像所在目录)LC_ID_DYLIB:该命令仅存在于动态库(.dylib或Framework)自身中,它定义了该库的“安装名称”(Install Name)。当dyld处理主程序的LC_LOAD_DYLIB时,会通过这个标识符来确认加载的库是否是预期的目标
符号表与元数据 (Symbol Tables & Metadata)
这些命令描述了二进制文件中的符号位置及类型,是 dyld 进行绑定(Binding)的依据
LC_SYMTAB:它定义了主符号表(Symbol Table)和字符串表(String Table)在文件中的偏移量及条目数。符号表由一系列nlist(或64位的nlist_64)结构体组成,遵循传统的BSD 4.3格式。每个条目包含符号在字符串表中的偏移、符号类型(如外部、本地)以及其对应的内存地址。紧随符号表之后,是一个包含大量以NULL结尾的C字符串数组,符号名就存储在这里,供链接器匹配使用LC_DYSYMTAB:是对LC_SYMTAB的补充(为了支持动态加载),记录了符号的分类:本地符号(Local)、外部符号(Defined External)和未定义符号(Undefined)。其核心部分是间接符号表(Indirect Symbol Table),它是指向主符号表的索引数组。数据段中的一些特殊节,如__nl_symbol_ptr(非懒加载指针)和__la_symbol_ptr(懒加载指针),其内容正是通过该命令提供的索引,在间接符号表中找到真实的符号定义LC_DYLD_INFO/LC_DYLD_INFO_ONLY:这是dyld的核心指令集。它指向__LINKEDIT段中的五个信息流:重定位(Rebase)、绑定(Bind)、弱绑定(Weak Bind)、懒加载绑定(Lazy Bind)和导出(Export)。这些流由基于有限状态机的操作码(Opcodes)组成(导出除外,它使用Trie),用于驱动链接过程
dyld加载Mach-O的过程大致为:先通过LC_SYMTAB了解基本符号框架,利用LC_DYSYMTAB确定间接引用,最后运行LC_DYLD_INFO里的状态机,解释操作码完成具体的重定位和符号地址填充
现代与高效链接指令 (Modern & High-Efficiency Commands)
随系统演进(特别是 Darwin 19 和 dyld 4)引入的新型指令,旨在优化性能和安全性:
LC_DYLD_EXPORTS_TRIE:该命令使用一种名为 Trie(前缀树) 的结构来存储导出符号,Trie 是一种高度压缩的表示形式,它通过提取公共前缀来减少存储空间,用这种结构能显著压缩数据量并加快查找速度。如果传统的符号表和Trie之间存在冲突会优先使用TrieLC_DYLD_CHAINED_FIXUPS:支持 dyld 4 的链式修复机制。它指示内核在页入(Page-in)时延迟应用重定位和绑定工作,将修复信息直接编码在__DATA段中,下面会详细介绍LC_BUILD_VERSION:取代了旧的LC_VERSION_MIN_...系列命令,它提供了一个统一的描述格式,涵盖了目标平台、最低部署版本、SDK 版本以及用于构建该二进制文件的工具信息
环境控制与辅助命令 (Environment & Auxiliaries)
LC_DYLD_ENVIRONMENT:允许在二进制文件中直接嵌入DYLD_...._PATH类型的环境变量,在进程执行前传递给 dyld。需注意它只在部分版本中有效,在受限(Restricted)的二进制文件中,这些变量会被剔除LC_SUB_FRAMEWORK/LC_SUB_LIBRARY等:定义框架/动态库的从属关系,LC_SUB_FRAMEWORK指示某个库是特定的“伞形框架”(Umbrella Framework)的子框架,LC_SUB_CLIENT指定该动态库仅允许被特定的客户端(Client)链接,LC_SUB_LIBRARY定义该库为某个特定父库的子库LC_ROUTINES/LC_ROUTINES_64:用于标记构造函数(初始化器),在现代 Mach-O 中已基本被带有S_MOD_INIT_FUNC_POINTERS标志的节(通常是__DATA.__mod_init_func)来存储初始化函数指针数组,dyld 在加载所有依赖库后会遍历这些指针并逐一调用
此外,Mach-O格式还包含一类辅助性命令,以及一些已弃用的陈旧命令。它们通常用于提供元数据或支持逆向工具
辅助性加载命令
这类命令对现代系统的核心加载过程影响较小,主要起到描述作用:
1.部署版本信息 (LC_VERSION_MIN_...):
LC_VERSION_MIN_MACOSX/IPHONEOS/TVOS/WATCHOS:分别指定对应 OS 平台的最低部署目标版本LC_BUILD_VERSION:在 Darwin 17 (Xcode 10) 后引入,旨在取代上述分散的命令。它能更通用地描述目标平台、SDK 版本以及构建工具信息
2.分析与逆向辅助:
LC_FUNCTION_STARTS:提供一个使用 ULEB128 编码的表,列出了二进制文件中所有函数的起始地址。虽然这主要是为系统内部设计的(如用于堆栈回溯),但它极大地便利了自动化分析工具识别函数边界LC_DATA_IN_CODE:标识了嵌入在__text代码节中的非指令数据(如switch语句生成的跳转表数据岛)。这有助于工具区分代码和数据,防止反汇编错误
3.版本与构建元数据:
LC_SOURCE_VERSION:记录了用于构建该二进制文件的源代码版本号(格式为 a.b.c.d.e)LC_NOTE:包含嵌入在 Mach-O 中的任意附加数据
4.编译与链接优化:
- LC_LINKER_OPTION:仅存在于 MH_OBJECT 或 .ar 归档文件中,用于嵌入编译器(如 swiftc)传递给链接器的选项和提示
- LC_LINKER_OPTIMIZATION_HINT:为链接器提供代码优化方面的提示
现代性能优化加载命令 (Darwin 19+)
虽然这些命令属于现代 dyld 的核心特性,但在分类上常被放在辅助/新型载荷中:
LC_DYLD_EXPORTS_TRIE:指向以 Trie(前缀树)结构编码的导出符号数据块,用于优化符号查找速度LC_DYLD_CHAINED_FIXUPS:指示文件使用 dyld 4 的链式修复机制,将修复信息编码在数据段中,以便内核执行页入链接
已弃用(不再支持)的加载命令
这类命令源自 NeXTSTEP 时代,现代 Darwin 系统的内核、链接器 (ld) 或动态链接器 (dyld) 已不再支持或尊重这些指令
1.符号表相关:
LC_SYMSEG:旧式的 gdb 风格符号表,已被LC_SYMTAB取代
2.固定虚拟内存相关:
LC_LOADFVMLIB/LC_IDFVMLIB/LC_FVMFILE:涉及不可重定位的固定虚拟内存 (FVM) 库,这些技术在现代系统上早已绝迹
3.旧式性能优化:
LC_PREPAGE:用于指示段的预分页处理LC_PREBOUND_DYLIB/LC_PREBOUND_CKSUM:与早期的“预绑定”技术有关,该技术现已被共享库缓存 (Shared Library Cache) 彻底取代
4.安全性遗迹:
LC_DYLIB_CODE_SIGN_DRS:存储依赖库的代码签名信息,现已被更现代的签名需求(Requirement blobs)取代
这些命令的存在体现了Mach-O格式的演进历史,开发者通常无需手动干预它们,除非在进行深度的底层分析或适配特定的旧版系统
段与节
段(Segment)与节(Section)是组织文件内容并将其映射到内存的核心机制。段是虚拟内存中连续的页面组,在加载过程中通过mmap(2)映射到进程空间,每个段都具有特定的内存保护属性(如只读、可读写、可执行),这在加载命令中定义。段可以进一步划分为多个节,节实际上是父段地址空间内的子映射,尽管一个段内的所有节必须共享相同的内存保护权限,但它们通常包含不同性质的内容(如代码、字符串、符号存根等),因此需要进行人工划分。以下是对段与节的详细解析:
加载命令:LC_SEGMENT
段的布局由LC_SEGMENT(32位)或LC_SEGMENT_64(64位)命令定义。该命令包含以下关键信息:
- segname:段名(固定16字节)
- vmaddr / vmsize:该段在虚拟内存中的起始地址和占用大小
- fileoff / filesize:该段在磁盘文件中的偏移量和大小
- initprot / maxprot:初始和最大内存保护标志(如
r-x或rw-) - nsects:如果此值大于0,则命令后紧跟相应数量的
section结构体,用于描述该段内的节
常见的标准段及其包含的节
Mach-O对象通常包含以下四个预定义的“著名”段:
__PAGEZERO
它仅存在于可执行文件中,位于虚拟内存的第一页。它不被映射且没有任何权限(---/---),主要用于捕获NULL指针解引用,在64位二进制文将此段扩展到覆盖前4GB空间,以防止32位库被意外映射或拦截32位指针
__TEXT (代码段)
它包含程序代码和只读数据,在macOS上标记为r-x/rwx,在*OS上为r-x/r-x。常见子节有:
__text:实际的可执行机器码__stubs和__stub_helper:用于动态链接的符号存根和助手代码(作用与ELF的.plt类似)__cstring:以空字符结尾的ASCII字符串__const:只读常量数据__unwind_info:堆栈回溯(unwind)信息
__DATA (数据段)
它存储可变数据和供动态链接器(dyld)使用的符号指针,通常为rw-权限,常见的数据节有:
__got:全局偏移表,存储加载时绑定的外部全局变量指针__la_symbol_ptr:懒加载符号指针,在第一次调用时绑定__nl_symbol_ptr:非懒加载符号指针,在加载时立即绑定__cfstring:CoreFoundation字符串对象__data:通用的可变数据
__DATA_CONST
它是为了解决__DATA段内节(如GOT和符号指针)的可写性带来的安全漏洞,将原本位于__DATA的常量、初始化器和GOT放入此段。dyld在完成初始化后会使用mprotect(2)将该段设为只读,从而实现真正的常量保护
__LINKEDIT
作为动态链接器的通用容器,它存储不属于代码或数据的原始数据(如符号表、字符串表、绑定操作码)。注意此段不分节,其内容由其他加载命令(如LC_SYMTAB)描述
__RESTRICT
如果包含一个名为__restrict的空节,dyld将忽略所有环境变量,防止二进制文件加载被篡改(常见于amfid等安全敏感程序)
__DWARF
仅在.dSYM伴随文件中可见,包含大量DWARF格式的调试信息节
节的类型与属性
节的性质由其结构体中的flags字段定义,这是一个32位的字段,其中低8位用于定义节的类型(Section Type),高24位用于定义其属性(Section Attributes)。以下是具体解释:
节类型表格(Section Types)
| 类型值 (Type) | 常量名称 (Constant) | 含义/用途简述 |
|---|---|---|
| 0 | S_NORMAL |
普通节,如__text或__data |
| 1 | S_ZEROFILL |
初始值为0的节,不占磁盘空间 |
| 2 | S_CSTRING_LITERALS |
包含以NULL结尾的C字符串文字 |
| 3 | S_4BYTE_LITERALS |
4字节文字 |
| 4 | S_8BYTE_LITERALS |
8字节文字 |
| 5 | S_LITERAL_POINTERS |
文字指针 |
| 6 | S_NON_LAZY_SYMBOL_POINTERS |
非懒加载(加载时绑定)符号指针 |
| 7 | S_LAZY_SYMBOL_POINTERS |
懒加载(首次使用绑定)符号指针 |
| 8 | S_SYMBOL_STUBS |
符号存根代码 |
| 9 | S_MOD_INIT_FUNC_POINTERS |
模块初始化函数指针(构造函数) |
| 10 | S_MOD_TERM_FUNC_POINTERS |
模块终止函数指针(析构函数) |
| 11 | S_COALESCED |
联合(Coalesced)节 |
| 12 | S_GB_ZEROFILL |
大型的零填充节 |
| 13 | S_INTERPOSING |
函数拦截(Interposing)节,用于替换库函数实现 |
| 14 | S_16BYTE_LITERALS |
16字节文字 |
| 15 | S_DTRACE_DOF |
DTrace对象格式节 |
| 16 | S_LAZY_DYLIB_SYMBOL_POINTERS |
懒加载动态库符号指针 |
| 17 | S_THREAD_LOCAL_REGULAR |
线程本地存储(TLS)普通数据 |
| 18 | S_THREAD_LOCAL_ZEROFILL |
线程本地存储零填充数据 |
| 19 | S_THREAD_LOCAL_VARIABLES |
线程本地变量 |
| 20 | S_THREAD_LOCAL_VARIABLE_POINTERS |
线程本地变量指针 |
| 21 | S_THREAD_LOCAL_INIT_FUNCTION_POINTERS |
线程本地初始化函数指针 |
节属性表格(Section Attributes)
属性存储在flags的高24位中,关键属性包括:
| 属性掩码 (Flag) | 常量名称 (S_ATTR Constant) | 含义 |
|---|---|---|
| 0x80000000 | _PURE_INSTRUCTIONS |
节仅包含机器指令 |
| 0x10000000 | NO_DEAD_STRIP |
告诉链接器不要在“死代码剥离”过程中移除此节 |
| 0x00000400 | _SOME_INSTRUCTIONS |
节包含部分指令 |
| 0x00000200 | _EXT_RELOC |
包含外部重定位条目 |
| 0x00000100 | _LOC_RELOC |
包含本地重定位条目 |
dyld
动态链接器的作用
dyld (Dynamic Linker) 是Apple操作系统(Darwin家族)中的动态链接器,也被称为“链接编辑器”(Link Editor)。它的核心作用是在程序启动及运行期间,将Mach-O格式的可执行文件与它所依赖的动态库“缝合”在一起,并将其正确地布置在内存中。以下是详细的职能说明:
重定位(Rebasing)
由于地址空间布局随机化 (ASLR)的存在,二进制文件在内存中的实际加载地址(Load Address)通常会与其在磁盘中定义的“首选地址”(Preferred Address)之间存在一个偏移量,称为Slide。dyld需要根据这个Slide值,修正数据段(__DATA)中所有硬编码的指针地址。dyld使用存储在LC_DYLD_INFO加载命令中的重定位操作码 (Rebase Opcodes)流来驱动内部的状态机,从而高效地完成这些地址修正
符号绑定 (Binding)
绝大多数程序都不是自包含的,它们会调用外部库(如libc的printf)。绑定就是将程序中的符号引用(Stubs)指向库中函数实现的真实地址的过程。macho中分为如下几类:
- 非懒加载绑定 (Non-Lazy Binding):在程序启动时必须完成的绑定,通常是全局偏移表(GOT)中的符号指针
- 懒加载绑定 (Lazy Binding):在函数第一次被调用时才进行的绑定,由
dyld_stub_binder负责解析 - 弱绑定 (Weak Binding):针对弱符号的绑定,如果找不到目标库,符号将被设为NULL而不导致程序崩溃
它同样利用操作码(Binding Opcodes)来指导符号解析,将库的路径名、符号名与内存地址关联
注:后面会提到链式修复,那时主要修复工作将由内核完成,但在动态加载时依然会使用dyld做rebasing和binding
加载与启动流程
根据dyld的执行流,它在移交控制权给程序main函数之前,还执行了以下关键步骤:
- 环境配置与限制检查:检查
DYLD_INSERT_LIBRARIES等环境变量,并根据代码签名标志(如CS_RESTRICT)或段属性(如__RESTRICT)执行权限限制 - 加载依赖库:递归地查找并映射所有
LC_LOAD_DYLIB标记的依赖库 - 映射共享库缓存 (Shared Library Cache):优先从系统的预链接缓存中查找常用库,以提升加载速度
- 运行初始化器 (Initializers):执行所有动态库中由
__attribute__((constructor))定义的构造函数
发展历程
Apple 的动态链接器 dyld 经历了从 NeXTStep 时代到现代操作系统的多次重大演进,其发展历程可以划分为以下四个主要阶段:
dyld 1.0 (1996–2004)
dyld 1.0 最初随 NeXTStep 3.3 发布,由于其诞生早于 POSIX 标准化的 dlopen(),因此当时使用的是第三方封装函数。此外,当时的系统大多尚未广泛使用大型 C++ 动态库。在 macOS Cheetah (10.0) 中,Apple 为其引入了预绑定 (Prebinding) 技术
dyld 2.0 (2004–2007)
随 macOS Tiger发布,dyld 2.0 是一个完全重写的版本。它的核心改进包括:支持了正确的C++ 初始化语义,以及实现了完整的原生 dlopen() 和 dlsym() 语义。但该版本主要为速度而设计,因此完整性检查有限,存在一些安全隐患
dyld 2.x (2007–2017)
此阶段 dyld 扩展到了更多的架构(x86, x86_64, arm, arm64)和平台(iOS, tvOS, watchOS)。它引入了两个核心提升:安全强化与共享缓存,在前者中它引入了代码签名 (Codesigning)、地址空间布局随机化 (ASLR) 和边界检查,后者也是我们逆向分析绕不过的部分,它在 iOS 3.1 和 macOS Snow Leopard 中引入,完全取代了预绑定,简单来说共享缓存是一个包含大部分系统动态库(dylibs)的单文件,通过预链接库和预构建 dyld 及 ObjC 所需的数据结构,显著提高了加载速度并减少了内存占用,这个下面还会再聊
dyld 3 (2017-2022)
在 2017 年的 WWDC 上,Apple 宣布了 dyld 3,这是对动态链接机制的一次彻底重新构想,其核心目标是通过减少应用启动时的任务量来提升性能,通过更严格的检查提升安全性,并增强系统的可靠性。以下是 dyld 3 所做的详细改进和架构变化:
核心架构重构
dyld 3将解析操作移出应用进程,由三个组件协同工作:
1.进程外 (Out-of-process) Mach-O 解析器/编译器:这是一个常规的守护进程(daemon),负责解析 Mach-O 头部、查找依赖项并执行所有符号查找,它会解析搜索路径、@rpaths 和环境变量,由于它在进程外运行,可以使用正常的测试基础设施来保证稳定性,最终它会生成一个启动闭包 (Launch Closure),其中包含启动应用所需的所有信息
2.进程内 (In-process) 引擎:这是一个极简化的引擎,不再需要解析 Mach-O 头部或访问符号表,它的任务是验证启动闭包、映射所有的动态库 (dylibs)、应用修复(Fixups,如绑定和重定位)、运行初始化程序 (initializers),最后跳转到 main() 函数,这种设计极大地缩小了攻击面并加快了启动速度
3.启动闭包缓存服务 (Launch Closure Caching Service):系统应用的启动闭包被预先构建在共享缓存(Shared Cache)中,第三方应用的闭包则在应用安装或系统更新时构建,大多数情况下,应用启动会直接使用缓存,完全跳过复杂的解析步骤
及早符号解析 (Eager Symbol Resolution)
与dyld 2的延迟解析不同,dyld 3转向了及早解析。dyld 2只在第一次调用时查找符号以节省启动耗时,但可能导致运行中因符号缺失而崩溃,而dyld 3将查找结果缓存在启动闭包中,解析速度极快,且能预先检查符号完整性,如果符号缺失,应用在启动阶段就会报错,而不是运行到一半才崩溃
注:对于
dlopen()或dlsym()动态找到的符号,由于无法预先链接,在 dyld 3 中可能会变慢
安全性与规范强化
在执行过程中引入了更激进的安全检查和规范:包括对 Mach-O 文件的边界检查和防御 @rpath 混淆攻击,关注 __DATA 段中的非对齐指针,因为修复跨页面的非对齐指针非常复杂且存在原子性问题,它还计划移除浪费大量内存的 all_image_infos 结构,不再适合进行资源管理的dlclose()(变成空操作 no-op)
dyld 3的设计遵循了“最快的代码就是你从未写过的代码,其次是你几乎不执行的代码”这一原则。通过将复杂的Mach-O解析和依赖发现移至安装时或系统构建时处理,dyld 3使得应用每次启动时执行的指令量达到了最小化
dyld 4
dyld 4引入链式修复(Chained Fixups)和页入链接(Page-in Linking),进一步将链接压力推给内核,它由两部分组成:
1.页入链接 (Page-in Linking):这是dyld 4最显著的特性。在之前的版本(如dyld 2和dyld 3)中,dyld需要在启动时对所有动态库应用修复(Fixups)。在dyld 4中,内核可以在页入(Page-in)时延迟应用修复,当程序访问某个内存页触发缺页中断时,内核不仅会读取该页内容,还会同时应用该页所需的修复,这改变了以往dyld必须在启动阶段完成所有修复的模式,将工作分散到了运行过程中,显著减少了启动耗时
2.链式修复 (Chained Fixups):页入链接功能的实现依赖于一种全新的元数据编码格式——链式修复,这种格式让__LINKEDIT段变得更小。它不再存储所有的修复位置,而仅存储每页第一个修复的位置以及导入符号列表。具体的修复信息被编码在__DATA段本身,修复位置被“链接”在一起:每个64位指针位置不仅包含修复类型(绑定或重定位),还包含指向下一个修复位置的偏移量,正因为修复信息直接存在于磁盘上的__DATA段中,内核在页入该段时才能直接读取并应用修复
页入链接带来了多重好处,首先是减少脏内存(Dirty Memory),__DATA_CONST页面可以保持干净,在内存压力大时可被系统丢弃重刷;其次是由于修复指令减少,启动速度进一步提升;额外的它的工具支持更加完善,开发者可以使用dyld_usage追踪修复耗时
启动过程
首先是main的参数,相比与Linux它多了apple参数(Linux的env之上有辅助向量,但它不是main的参数):

apple[] 包含关键的安全信息,如执行路径、堆栈守卫值(stack_guard)以及代码签名哈希(executable_cdhash)等,类似于auxv可泄漏指针给攻击者,apple参数也有这个风险,所以后来在程序获得控制权前(main函数执行前)就把里面的敏感值改成了空字符串。
然后来看启动过程,即从开始执行到程序的 main() 函数被调用之间的过程:
1.它起始于内核(XNU),内核读取Mach-O头部,解析 LC_SEGMENT 命令,并将文件的各个段映射到进程的虚拟地址空间,除了静态链接的二进制文件外,它会识别 LC_LOAD_DYLINKER 命令,定位并加载动态链接器(通常是 /usr/lib/dyld)到内存中,除此之外新进程设置初始堆栈,即上面这张图里的参数信息。
2.内核返回到 dyld 的入口点 __dyld_start,dyld 首先通过计算实际加载地址与首选地址的差值来确定 Slide,然后根据计算出的 Slide 对自身的内部指针进行重定位,接着调用 mach_init() 设置Mach系统调用接口,并利用 apple[] 中的值初始化Stack Canary
3.引导完成后,控制权转交给 dyld::_main(),它先检查程序是否具有 setuid 权限、是否包含 __RESTRICT 段,或者是否拥有特定权利(Entitlements),如果受限则会清理危险的环境变量(如 DYLD_INSERT_LIBRARIES),并限制搜索路径以防攻击,然后dyld 检查并映射系统共享库缓存与解析主二进制文件的 LC_LOAD_DYLIB 命令,它会按照层级(Level 0, Level 1...)递归加载所有依赖的动态库
4.接着它开始执行修复 (Fixups),即重定位与绑定
5.在所有镜像加载并链接完成后,dyld 开始准备运行代码,如果使用了注入库,dyld 会在此阶段应用 S_INTERPOSING 段中的重定向逻辑,实现函数 Hook,然后它会自底向上地遍历所有加载的镜像,执行每个镜像的初始化函数组
6.最后dyld 解析Mach-O的入口点命令( LC_MAIN/ LC_UNIXTHREAD),清理堆栈并准备好 ABI 寄存器状态,最后跳转到 main()

重定位与绑定
回忆一下ELF,它使用重定位表(.rel)/全局偏移表(.got)/过程链接表(.plt)等实现该功能,它的优点是简单直观,缺点是这会占据更多空间且不灵活。而Mach-O追求可扩展性和架构独立性, 它内部(__LINKEDIT段)存储的是字节码,并通过dyld 内部的一个门的有限状态机来执行,这样既能极大压缩元数据体积,又能灵活的实现各种复杂功能(如两级命名空间绑定)。下面是对重定位和绑定操作码的详细说明:
重定位操作码 (Rebasing Opcodes)
dyld 需要执行重定位操作码去修正数据段中硬编码的内部指针值,操作码通常定义在字节的高 4 位(REBASE_OPCODE_MASK 为 0xF0),低 4 位用于存储“立即数”参数
| 常量名称 | 编码 (Hex) | 作用描述 |
|---|---|---|
DONE |
0x00 |
表示重定位列表结束。 |
SET_TYPE_IMM |
0x10 |
将重定位类型设置为立即数(如指针、绝对地址或 PC 相对地址)。 |
SET_SEGMENT_AND_OFFSET_ULEB |
0x20 |
设置目标段索引(立即数)和段内偏移量(后续 ULEB128 编码值)。 |
ADD_ADDR_ULEB |
0x30 |
将地址字段增加指定的 ULEB128 值。 |
ADD_ADDR_IMM_SCALED |
0x40 |
将地址字段增加立即数倍率后的值。 |
DO_REBASE_IMM_TIMES |
0x50 |
执行立即数次数的重定位操作。 |
DO_REBASE_ULEB_TIMES |
0x60 |
执行 ULEB128 指定次数的重定位操作。 |
DO_REBASE_ADD_ADDR_ULEB |
0x70 |
执行一次重定位并增加地址偏移。 |
DO_REBASE_ULEB_TIMES_SKIPPING_ULEB |
0x80 |
执行多次重定位,且每次操作间跳过指定的字节数。 |
这些操作码非常紧凑,例如通过 DO_REBASE_.. 变体可以批量处理连续或规律间隔的指针,显著减小了元数据体积
绑定操作码 (Binding Opcodes)
绑定操作码用于填充内存中的绑定表,包含非懒加载(Non-Lazy)、懒加载(Lazy)和弱绑定(Weak)三种类型
| 常量名称 | 编码 (Hex) | 作用描述 |
|---|---|---|
DONE |
0x00 | 结束当前操作码列表。 |
SET_DYLIB_ORDINAL_IMM |
0x10 | 设置动态库序号(0-15范围内)。 |
SET_DYLIB_ORDINAL_ULEB |
0x20 | 设置动态库序号(16及以上,使用ULEB128)。 |
SET_DYLIB_SPECIAL_IMM |
0x30 | 设置特殊的库序号,如自身可执行文件(-1)或平坦命名空间查找(-2)。 |
SET_SYMBOL_TRAILING_FLAGS_IMM |
0x40 | 设置符号名称(紧跟其后的以NULL结尾的字符串)和绑定标志(如弱导入)。 |
SET_TYPE_IMM |
0x50 | 设置绑定类型(如TYPE_POINTER)。 |
SET_ADDEND_SLEB |
0x60 | 为当前绑定行设置加数(Addend)。 |
SET_SEGMENT_AND_OFFSET_ULEB |
0x70 | 设置目标段索引及段内偏移量。 |
ADD_ADDR_ULEB |
0x80 | 增加目标地址偏移。 |
DO_BIND |
0x90 | 核心指令:根据当前状态机信息执行绑定,并移至下一行。 |
DO_BIND_ADD_ADDR_ULEB |
0xA0 | 执行绑定并增加后续ULEB128的地址偏移。 |
DO_BIND_ADD_ADDR_IMM_SCALED |
0xB0 | 执行绑定并增加立即数倍率的地址偏移。 |
DO_BIND_ADD_ADDR_ULEB_TIMES_SKIPPING_ULEB |
0xC0 | 为多个符号执行绑定,并在每次操作间跳过指定字节。 |
绑定状态机会根据这些指令重复利用已有的段名、库名等信息。例如,若连续多个符号来自同一个库,只需设置一次库序号即可
线程化绑定操作码 (Threaded Binding Opcodes)
随 ARM64e (A12+) 和 dyld-625 引入,为了支持指针认证(PAC)和加速启动过程,Apple 引入了新型操作码来处理“线程化”绑定
BIND_SUBOPCODE_THREADED_SET_BIND_ORDINAL_TABLE_SIZE_ULEB (0xD0):定义绑定符号表的大小。在 ARM64e 架构下,这几乎取代了传统的地址/段操作码,直接在加载时构建符号定义和库的对应表BIND_SUBOPCODE_THREADED_APPLY (0xD1):将上述表中的绑定规则应用到指定的段和偏移位置
链式修复
上面提到,链式修复(Chained Fixups) 是dyld 4引入的一种全新的修复信息编码方式,它彻底改变了传统的重定位(Rebase)和绑定(Bind)逻辑,将原本由 dyld 在启动时手动完成的大量工作交给了内核按需处理。在传统的 Mach-O 二进制文件中,所有的修复信息(即如何修正那些指向外部或内部地址的指针)都以操作码(Opcodes)流的形式存储在 __LINKEDIT 段中,而链式修复采用了一种截然不同的方案:
- 精简的
__LINKEDIT段:不再存储每一个修复的具体位置。相反,__LINKEDIT现在仅记录每个__DATA页面中第一个修复点(First Fixup)的位置,以及一份导入符号列表 - 在
__DATA段内成链:真正的修复详细信息被直接编码在__DATA段原本存放指针的 64 位内存位置中。这些位置被“链接”在一起:一个 64 位指针的某些位被借用来存储到下一个修复位置的偏移量,从而形成一个链表,这也是“链式修复”名称的由来 - 元数据编码:在这些 64 位位置的元数据中,还包含了一个关键位(Bit),用于指示该修复是重定位(Rebase)还是绑定(Bind)。如果是绑定,则剩余位存储符号索引;如果是重定位,则存储镜像内的目标偏移量
链式修复最大的优势是它启用了页入链接技术,这极大地优化了应用启动过程:
- 内核驱动的延迟修复:过去,
dyld必须在启动阶段遍历所有动态库并写入修复后的地址,这会导致大量内存页变“脏”且增加启动耗时。现在,由于修复信息直接存在于磁盘上的__DATA段中,内核可以在页入(Page-in)时应用修复 - 触发机制:当程序运行并第一次访问某个 DATA 页面触发缺页中断时,内核在将该页从磁盘读入 RAM 的同时,会自动根据链式信息完成修正
- 保持内存“干净”:这种方式使得
__DATA_CONST页面在内存中保持“干净”(Clean)状态。这意味着当系统面临内存压力时,这些页面可以像代码段(TEXT)一样被直接丢弃并随后从磁盘重新创建,而不需要驻留在交换区,从而显著缓解了内存压力
通过链式修复,它能继续缩小文件体积,由于大部分修复元数据从__LINKEDIT移到了__DATA段原本就要占用的空间里,使得二进制文件整体更小。通过将修复工作从启动阶段分散到了应用的整个生命周期(按需处理),显著减少了点击应用到看到界面的时间。在一个示例中,15ms的总启动耗时里,由于页入链接的功劳,修复过程仅占1ms。而且配合链式修复,开发者可以使用dyld_usage(仅限macOS)追踪启动过程,或使用dyld_info -fixups检查二进制文件中的所有修复点及其目标
不过链式修复的运行时支持最早出现在 iOS 13.4 及更高版本中,开发者需要将应用的部署目标(Deployment Target)设置为 iOS 13.4+ 才能启用此格式。而且页入链接机制仅在应用启动(Launch)时生效。如果是通过 dlopen() 在运行时动态加载的库,dyld 仍会退回到传统路径,在调用加载时手动应用修复
链式修复通过数据段编码和内核协同,实现了比传统操作码机制更高效、更节省内存的动态链接,虽然运行时支持始于iOS 13.4,但也让开发者有了更细粒度的性能追踪手段
共享库缓存
共享库缓存(Shared Library Cache, SLC)是Apple为提升系统性能和应用启动速度而设计的一项核心技术。它将系统中常用的动态库(dylibs)和框架预先处理并打包成一个巨大的单文件,从而优化加载流程并节省资源。它在iOS 3.1和macOS Snow Leopard中引入,旨在完全取代早期的预绑定(Prebinding)技术。它通过将数百个系统动态库合并,SLC允许系统通过单次mmap()操作加载所有库,显著减少了磁盘I/O,而最重要的优势在于减少RAM占用,多个进程可以共享物理内存中同一份SLC副本(通常标记为只读且可执行)
SLC对其中的二进制文件进行了深度优化:
- 预链接(Pre-linking):它预先解析库之间的依赖关系,并将函数地址硬编码。这意味着应用启动时不再需要通过符号存根(Stubs)进行昂贵的查找,而是可以直接跳转
- 数据结构预构建:预先构建dyld和Objective-C运行时所需的数据结构,减少了运行时的初始化开销
- 段合并:它将所有库的
__LINKEDIT段(存储符号、重定位等元数据的区域)合并成一个单一巨大段 - 重新排列:它会重新排列二进制文件的布局,以优化读取速度和页面对齐
在macOS中它位于/var/db/dyld,通常包含针对不同架构(如x86_64, x86_64h)的多个缓存文件,而在iOS/tvOS/watchOS中它位于/System/Library/Caches/com.apple.dyld。SLC通常定义三个主要的内存映射区域,对应dylib的段属性:
__TEXT(EX):可执行代码区(包含缓存头)__DATA(RW):可读写数据区__LINKEDIT(RO):只读元数据区
在 iOS 等移动平台上,为了节省磁盘空间,独立的系统 dylib 文件被移除,只保留 SLC。文件系统中的原始路径通常只是指向缓存的符号链接或干脆不存在
关键技术特性
- 分支池(Branch Pools):在64位iOS系统(iOS 9+)中引入。由于ARM64的直接跳转指令有距离限制,SLC每隔128MB会插入一个768K的“小岛”(包含符号存根),作为跨库调用的中转站
- 加速信息(Acceleration Info):Darwin 16后的SLC包含专门的优化节,存储了dylib前缀树(Trie)、初始化器列表和重新导出列表,进一步压缩加载耗时
- 启动闭包(Launch Closures):在dyld 3架构下,系统应用的启动闭包会被预先构建并内置在共享缓存中
- 页入链接(Page-in Linking):这是一项使用了十余年的技术。内核在发生缺页中断(Page-in)将SLC页面载入内存时,会自动应用该页面所需的修复(Fixups),从而减少“脏内存”产生
运行时的管理与安全
- 内核系统调用:
dyld通过专用系统调用shared_region_check_np检查缓存是否已映射,并通过shared_region_map_and_slide_np进行映射并应用ASLR偏移(Slide) - 环境变量控制:
DYLD_SHARED_REGION:可设为use(默认)、avoid(禁用缓存)或private(使用自定义路径的缓存)DYLD_SHARED_CACHE_DONT_VALIDATE:禁用macOS上的版本验证
注:由于所有进程通常共享同一个SLC的内存布局和ASLR偏移值,一旦攻击者确定了Slide,就有可能以此攻击其他进程,这在一定程度上削弱了ASLR的防护
常用分析工具
分析Mach-O通常涉及查看头部信息、解析链接逻辑以及运行时调试,下面是一些常见工具简介:
静态分析与查看
file:基础类型识别,确认是否为胖二进制,平台架构,有没有调试符号等lipo:处理通用二进制,常用-info查看架构,-thin提取特定架构切片arch:强制指定架构加载胖二进制程序otool:最常用的系统工具,-f查看胖头;-l查看加载命令(常用);-L查看库依赖;-tV结合符号表反汇编nm:列出符号表,区分本地与全局符号size -m:统计各Segment和Section的内存分布pagestuff:观察物理页面布局,辅助理解mmapdyld_info:dyld 4加入的检查工具,-fixups查看修复目标(支持链式修复);-exports直接从文件或共享缓存查看导出符号dyldinfo:专门解析__LINKEDIT中的Rebase/Bind操作码流及导出Trie
jtool / jtool2
由Jonathan Levin开发,它底层的 machlib 引擎支持在 Linux 和 Windows 上运行不依赖Xcode,输出格式更适合底层分析:
- -l -v:详细展示Load Commands信息
- --pages:提供段、节在虚拟内存与磁盘偏移间的映射视图
- -opcodes:解码Rebase和Bind的底层指令流
- -d:提取指定Section的原始数据(如__DATA.__interpose)
- -a/-S:支持缓存内地址查询(-a)和符号解析(-S),支持自动处理分支池
图形化工具
包括经典的MachO-Explorer, MachOView和XMachOViewer,需注意它们的更新时间,只有新版才支持Apple Silicon和新的加载命令。
运行时与性能调试
dyld_usage:监控dyld系统调用,分析启动耗时是花在符号修复还是静态初始化- Instruments:使用
Static Initializer Tracing模板定位+load或构造函数的耗时瓶颈 lldb:通过process launch -s停在__dyld_start,观察启动初期的栈帧和寄存器状态procexp/daii:用于转储内存镜像或解析dyld_all_image_infos结构,辅助脱壳分析
共享库缓存(SLC)处理
dyld_info: 它可以直接检查当前 SLC 中的二进制文件,即使磁盘上没有对应的实体文件,使用-exports选项可以列出缓存中特定库导出的所有符号及其偏移量Xcode:连接真机时会自动通过DTFetchSymbols服务将设备上的 SLC 拷贝到主机,自动执行Decache,在本地iOS DeviceSupport路径重建独立的库文件以供调试ida Pro:如果电脑性能强悍,直接加载整个SLC文件可以维持完整的地址上下文,从而正确解析函数调用关系
参考
- 《*OS Internals Volume I:User Space》chapter 6&7 -- Jonathan Levin
- App Startup Time: Past, Present, and Future -- zntfdr
- Link fast: Improve build and launch times -- Nick Kledzik
- DYLD Detailed -- Jonathan Levin