*OS可执行文件结构与动态链接器

Published: 2024年10月18日

In Kernel.

Mach-O

胖文件

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

fat bin
全局头部结构(struct fat_header)

这是文件的最开始部分,包含以下两个关键字段:

  • magic(魔数):占用4个字节,其标准值为0xcafebabe,需要注意的是这个值在磁盘上通常以大端(Big-Endian)格式存储,如果以小端格式读取则显示为0xbebafeca。该魔数与Java的.class文件魔数恰好冲突)
  • nfat_arch(架构数量):占用4个字节,它定义了该通用二进制文件中包含的架构切片的总数,常见情况如:
nfat_arch 的值 包含的架构切片示例 适用场景与背景
1 x86_64arm64 单架构(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 -fjtool -flipo)

Mach-O文件头

先来个文件总览,它的格式如下:

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_EXECUTEMH_DYLINKER是可以直接执行的。其中MH_DYLINKER特指动态链接器,虽然系统默认使用/usr/lib/dyld,但Mach-O格式允许指定自定义链接器

  • 加载要求(Loading Requirements):MH_DYLIBMH_BUNDLE类型不能独立运行,必须被加载到可执行文件中。动态库(Dylib)可以通过加载命令隐式加载,也可以通过dlopen()显式加载;而插件(Bundle)通常必须通过dlopen()NSBundle API显式加载

  • 调试与符号关联(DSYM):MH_DSYM是Apple处理调试信息的独特方式。与ELF格式将调试信息嵌入节区导致文件体积剧增不同,Mach-O将调试信息分离到独立的.dSYM文件中。这种伴随文件通过128位的UUID(由LC_UUID命令定义)与主可执行文件保持耦合,确保调试器(如LLDB)能准确匹配符号

  • 内核扩展(KEXT):文件类型的选择也随架构演进:旧的32位内核扩展使用MH_OBJECTMH_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),当内核遇到此命令时,会标记指定的内存页范围(从cryptoffcryptoff + 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之间存在冲突会优先使用Trie
  • LC_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-xrw-)
  • 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)

绝大多数程序都不是自包含的,它们会调用外部库(如libcprintf)。绑定就是将程序中的符号引用(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的参数):

stack

apple[] 包含关键的安全信息,如执行路径、堆栈守卫值(stack_guard)以及代码签名哈希(executable_cdhash)等,类似于auxv可泄漏指针给攻击者,apple参数也有这个风险,所以后来在程序获得控制权前(main函数执行前)就把里面的敏感值改成了空字符串。

然后来看启动过程,即从开始执行到程序的 main() 函数被调用之间的过程:

1.它起始于内核(XNU),内核读取Mach-O头部,解析 LC_SEGMENT 命令,并将文件的各个段映射到进程的虚拟地址空间,除了静态链接的二进制文件外,它会识别 LC_LOAD_DYLINKER 命令,定位并加载动态链接器(通常是 /usr/lib/dyld)到内存中,除此之外新进程设置初始堆栈,即上面这张图里的参数信息。

2.内核返回到 dyld 的入口点 __dyld_startdyld 首先通过计算实际加载地址与首选地址的差值来确定 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()

start

重定位与绑定

回忆一下ELF,它使用重定位表(.rel)/全局偏移表(.got)/过程链接表(.plt)等实现该功能,它的优点是简单直观,缺点是这会占据更多空间且不灵活。而Mach-O追求可扩展性和架构独立性, 它内部(__LINKEDIT段)存储的是字节码,并通过dyld 内部的一个门的有限状态机来执行,这样既能极大压缩元数据体积,又能灵活的实现各种复杂功能(如两级命名空间绑定)。下面是对重定位和绑定操作码的详细说明:

重定位操作码 (Rebasing Opcodes)

dyld 需要执行重定位操作码去修正数据段中硬编码的内部指针值,操作码通常定义在字节的高 4 位(REBASE_OPCODE_MASK0xF0),低 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的段属性:

  1. __TEXT (EX):可执行代码区(包含缓存头)
  2. __DATA (RW):可读写数据区
  3. __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:观察物理页面布局,辅助理解mmap
  • dyld_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-ExplorerMachOViewXMachOViewer,需注意它们的更新时间,只有新版才支持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文件可以维持完整的地址上下文,从而正确解析函数调用关系

参考

  1. 《*OS Internals Volume I:User Space》chapter 6&7 -- Jonathan Levin
  2. App Startup Time: Past, Present, and Future -- zntfdr
  3. Link fast: Improve build and launch times -- Nick Kledzik
  4. DYLD Detailed -- Jonathan Levin

social