- 逆向工程
- 开发工具
【免费下载链接】LIEF
LIEF - Library to Instrument Executable Formats (C++, Python, Rust)
LIEF 的 Extended 扩展为 C++、Python 与 Rust 三种语言提供了完整的 Apple dyld shared cache(dyld 共享缓存)支持:你可以加载一个或多个缓存文件、枚举其中内嵌的数千个动态库、将其中的任意库提取为独立的 Mach-O 二进制对象,并借助反优化(deoptimization)与磁盘缓存机制在数十 GB 级的大文件上高效地完成分析。读完本文,你将掌握lief.dsc模块的完整调用链、Dylib::get()提取器的选项语义,以及“按需访问 + 随机访问迭代器 + 缓存”这套针对共享缓存专门设计的性能模型。
本文的主体内容源自仓库文档 doc/sphinx/extended/dsc/index.md,代码示例取自其配套源码 doc/code/python/dsc.py、doc/code/cpp/dsc.cpp 与 doc/code/rust/src/dsc.rs,并结合作业目录下 C++ 头文件、Python 类型桩与测试用例进行了源码级印证。
什么是 Dyld Shared Cache,为什么需要 LIEF 专门支持
macOS / iOS / visionOS 等 Apple 平台会将系统自带的绝大多数动态库(libSystem、libobjc、CoreFoundation等)预先链接、合并进一个名为 dyld shared cache 的巨型二进制文件,运行时由 dyld 直接按虚拟地址映射,以加快启动速度。一个缓存通常包含数千个库,体积动辄数 GB,且分为主缓存(dyld_shared_cache_arm64e)与多个子缓存(如dyld_shared_cache_arm64e.03、.62.dyldlinkedit)。
LIEF Extended 对这类文件提供了三类核心能力:
- 检视(inspect):读取缓存头部、映射信息、子缓存列表,枚举全部内嵌库;
- 提取(extract):将某个库还原为
LIEF::MachO::Binary/lief.MachO.Binary对象,供后续段、符号、重定位等常规 Mach-O 分析使用; - 反优化(deoptimize):缓存为了共享与性能会改写分支、合并 GOT、压缩重定位,提取时可以按需把这些结构恢复,尽量还原库在编译产物形态下的样子。
需要强调的是,这一整套功能只存在于 LIEF 的Extended版本中。仓库内的示例程序(如 examples/cpp/dyld_shared_cache_reader.cpp)在执行前都会先检查LIEF::is_extended(),非扩展版调用lief.dsc.load只会返回None/nullptr。使用前请确认你安装的是启用了扩展功能的 LIEF 构建。
加载缓存并枚举库
load函数的三种形态
缓存加载的入口是命名空间lief.dsc/LIEF::dsc/lief::dsc下的load系列函数。根据 include/LIEF/DyldSharedCache/DyldSharedCache.hpp 的定义,它有两个重载:
load(path, arch = ""):从单个文件或目录加载;load(files):从一个显式的文件列表加载。
Python 侧的类型签名在 api/python/lief/dsc.pyi 中同样给出两个@overload;Rust 侧则对应lief::dsc::load_from_path(path, arch)与lief::dsc::load_from_files(&[path])(见 api/rust/crates/lief/src/dsc.rs)。
原文档示例(doc/code/python/dsc.py、doc/code/cpp/dsc.cpp、doc/code/rust/src/dsc.rs):
```{tab} Python ```python import lief dyld_cache: lief.dsc.DyldSharedCache | None = lief.dsc.load("macos-15.0.1/") ``` ```{tab} C++ ```cpp #include <LIEF/DyldSharedCache.hpp> std::unique_ptr<LIEF::dsc::DyldSharedCache> dyld_cache = LIEF::dsc::load("macos-15.0.1/"); ``` ```{tab} Rust ```rust let dyld_cache = lief::dsc::load_from_path("macos-15.0.1/", ""); ```使用要点(来自原文档的note):
- 传目录即加载该目录下的整套缓存;传显式文件列表可只加载一个子集;
- 加载目录时若其中存在多个架构(例如同时有
arm64e与x86_64h),可通过arch参数指定优先架构; - 务必让主缓存与其子缓存文件保持在一起:一次提取可能需要横跨多个文件读取数据,缺了某个子缓存文件会导致提取失败或数据错乱。
从 DyldSharedCache 对象读取元数据
加载成功后返回的DyldSharedCache对象携带了缓存级别的元信息,全部可从 C++ 头文件与 Python 类型桩中确认:
| 属性 | 类型 | 说明 |
|---|---|---|
filename | str | 缓存文件名,如dyld_shared_cache_arm64e |
filepath | str | 缓存文件的完整路径 |
load_address | int | 缓存基址 |
version | DyldSharedCache.VERSION | 生成缓存的 dyld 版本标签 |
platform | DyldSharedCache.PLATFORM | 目标平台(macOS / iOS / visionOS 等) |
arch/arch_name | ARCH/str | 目标架构(ARM64E、X86_64H等) |
has_subcaches | bool | 是否存在子缓存 |
其中VERSION枚举(见 include/LIEF/DyldSharedCache/DyldSharedCache.hpp)按 dyld 的 git tag 刻画了缓存结构的演进:从DYLD_95_3(2007)一路到DYLD_1284_13(2025),未公开或尚未支持的版本归入UNRELEASED;PLATFORM枚举覆盖MACOS、IOS、TVOS、WATCHOS、BRIDGEOS、IOS_SIMULATOR、DRIVERKIT、VISIONOS、FIRMWARE、SEPOS等(见同文件 L65-L83)。
枚举库列表
DyldSharedCache的libraries()方法返回一个库迭代器,无需先做任何全量解析即可逐个打印每个库的加载地址与路径。原文档示例(doc/code/python/dsc.py、doc/code/cpp/dsc.cpp、doc/code/rust/src/dsc.rs):
```{tab} Python ```python for dylib in dyld_cache.libraries: print(f"{dylib.address:#016x}: {dylib.path}") ``` ```{tab} C++ ```cpp for (const LIEF::dsc::Dylib& dylib : dyld_cache->libraries()) { std::cout << dylib.address() << ' ' << dylib.path() << '\n'; } ``` ```{tab} Rust ```rust for dylib in dyld_cache.libraries() { println!("0x{:016x}: {}", dylib.address(), dylib.path()); } ```每个Dylib对象镜像了原始的dyld_cache_image_info结构(见 include/LIEF/DyldSharedCache/Dylib.hpp),除path()(如/usr/lib/libcryptex.dylib)与address()(库在缓存虚拟空间中的装载地址)外,还提供modtime()、inode()与padding()。需要注意的是,对于 iOS 缓存,inode()在modtime()为 0 时可能保存的是路径哈希。
仓库中 examples/python/dyld_shared_cache_reader.py 是一个可直接运行的完整版本——它接受缓存文件或目录参数,依次打印库列表、mapping_info的地址区间与文件偏移、子缓存的 UUID 与后缀:
$ python dyld_shared_cache_reader.py /System/Library/dyld/dyld_shared_cache_arm64e提取单个库为 Mach-O 对象
三种库查找方式
DyldSharedCache提供了三个查找库的入口(见 DyldSharedCache.hpp):
find_lib_from_va(va):按虚拟地址定位包含该地址的库;find_lib_from_path(path):按完整路径匹配(如/usr/lib/liblockdown.dylib);find_lib_from_name(name):按文件名匹配,同名时返回第一个命中者。
原文档示例使用find_lib_from_name("liblockdown.dylib"),随后调用Dylib::get()得到LIEF::MachO::Binary,之后就能像分析普通 Mach-O 一样遍历其段(doc/code/python/dsc.py、doc/code/cpp/dsc.cpp、doc/code/rust/src/dsc.rs):
```{tab} Python ```python liblockdown = dyld_cache.find_lib_from_name("liblockdown.dylib") macho = liblockdown.get() for segment in macho.segments: print(segment.name) ``` ```{tab} C++ ```cpp std::unique_ptr<Dylib> liblockdown = dyld_cache->find_lib_from_name("liblockdown.dylib"); std::unique_ptr<LIEF::MachO::Binary> macho = liblockdown->get(); for (const LIEF::MachO::SegmentCommand& segment : macho->segments()) { std::cout << segment.name() << '\n'; } ``` ```{tab} Rust ```rust let liblockdown = dyld_cache.find_lib_from_name("liblockdown.dylib").unwrap(); let macho = liblockdown.get().unwrap(); for segment in macho.segments() { println!("{}", segment.name()); } ```原文档特别强调:库查找与提取的结果都要先判空再使用。Python 中find_lib_from_name可能返回None(名字不匹配任何库),Rust 中对应Option/unwrap语义;get()也可能因数据缺失而失败。务必对两个返回值都做校验,不要直接解引用。
写回磁盘
得到的 Mach-O 对象可以用标准的write()保存为独立文件(doc/code/python/dsc.py、doc/code/cpp/dsc.cpp、doc/code/rust/src/dsc.rs):
```{tab} Python ```python liblockdown = dyld_cache.find_lib_from_name("liblockdown.dylib") macho = liblockdown.get() macho.write("on-disk-liblockdown.dylib") ``` ```{tab} C++ ```cpp std::unique_ptr<Dylib> liblockdown = dyld_cache->find_lib_from_name("liblockdown.dylib"); std::unique_ptr<LIEF::MachO::Binary> macho = liblockdown->get(); macho->write("on-disk-liblockdown.dylib"); ``` ```{tab} Rust ```rust let liblockdown = dyld_cache.find_lib_from_name("liblockdown.dylib").unwrap(); let mut macho = liblockdown.get().unwrap(); macho.write("on-disk-liblockdown.dylib"); ```重要的前提警示
原文档在“Extract a library”一节末尾给出了一个warning,必须牢记:
默认情况下,LIEF 会保留 dyld shared cache 的优化痕迹。当提取出的库需要恢复对其它缓存结构的引用(如指向其它库的符号引用、缓存特有的 stub 岛调用等)时,必须检查并启用
Dylib::extract_opt_t中对应的反优化选项。仅仅写出一个 Mach-O 文件,并不保证它能脱离缓存被独立加载执行。
也就是说,get()默认产出的是一个“贴着缓存视角”的表示:缓存为了消除跨库重定位而做的各种改写仍保留在提取结果中。要让提取物更接近原始编译产物,需要显式开启反优化选项,详见下一节。
反优化选项extract_opt_t详解
Dylib::get()接受一个可选的extract_opt_t参数,用于微调提取过程。其完整字段定义在 include/LIEF/DyldSharedCache/Dylib.hpp:
| 字段 | 默认值 | 作用 | 性能影响 |
|---|---|---|---|
pack | true | 写回时压缩段偏移,避免内存态体积膨胀 | 无(注释明确说明不影响性能) |
fix_branches | false | 修复跳转到当前库虚拟空间之外的 call 指令 | 显著,可能需要多次遍历 stub 岛 |
fix_memory | false | 修复对库虚拟空间之外的内存访问 | 显著 |
fix_relocations | false | 恢复并修复重定位信息 | 显著 |
fix_objc | false | 修复 Objective-C 相关信息 | 视库大小而定 |
create_dyld_chained_fixup_cmd | 未设置 | 是否(重新)创建LC_DYLD_CHAINED_FIXUPS命令;未设置时 LIEF 会依据其它选项自行判断是否值得添加 | 一般 |
Python 侧同名类Dylib.extract_opt_t在 api/python/lief/dsc.pyi 中暴露了完全一致的字段,使用方法为:
opt = lief.dsc.Dylib.extract_opt_t() opt.fix_branches = True opt.fix_relocations = True macho = liblockdown.get(opt)Rust 侧对应ExtractOpt结构(api/rust/crates/lief/src/dsc/dylib.rs),通过get_with_opt(&opt)传入;值得留意的是,从该文件的Default实现看,Rust 的默认值在fix_memory、fix_relocations、fix_objc三项上为true,与 C++/Python 侧的默认false并不一致——如果你的 Rust 程序发现提取结果与预期不符或耗时异常,可以检查是否受默认选项差异影响。
头文件中反复出现的@warning提醒:凡是会显著影响性能的选项,务必同时开启内部缓存机制(LIEF::dsc::enable_cache或DyldSharedCache::enable_caching),否则反复提取多个库时会付出极高的重复计算代价。
性能考量:为“数十 GB”设计的内存模型
与普通二进制解析的原则差异
原文档明确说明:dyld shared cache 文件太大,不能按普通MachO::Binary/ELF::Binary的方式来处理。普通格式的lief.parse/LIEF::Parser之所以一次性解析全部结构,是因为:
- 绝大多数二进制体积小于 1 GB;
- 修改二进制需要完整的结构表示。
而共享缓存动辄数十 GB,全量解析既不现实也无必要。因此 LIEF Extended 对缓存的处理奉行一条相反的原则:
don't pay overhead for what you don't access(不为你不访问的东西付出开销)。
FileStream:按需访问而不是全量载入
从技术实现上看,LIEF 用LIEF::FileStream访问缓存的各个结构,数据按需从文件读取,因此内存占用只与“实际访问到的结构大小”成正比。DyldSharedCache::stream()在 DyldSharedCache.hpp 中暴露了该底层流对象。
代价也是明确的:文件型访问(FileStream)比一次性载入内存的VectorStream更耗时——这是用时间换内存空间的刻意取舍。如果你只是要把某个小范围内的数据读进内存,get_content_from_va(va, size)可以精确地按虚拟地址取一段原始字节(DyldSharedCache.hpp)。
迭代器模式与随机访问
除了底层流,LIEF 还重度依赖迭代器模式来贯彻“按需”原则。例如libraries()返回的是一个对Dylib的迭代器(dylib_iterator,见 DyldSharedCache.hpp):如果你不去遍历它,就不会付出访问与解析Dylib对象的开销。
更进一步的优化是:迭代器实现了 C++ 的random access iteratortrait(std::random_access_iterator_tag,见 include/LIEF/DyldSharedCache/Dylib.hpp 以及MappingInfo、SubCache的迭代器),因此可以程序化地做到:
```{tab} Python ```python # No cost:只是取得迭代器/序列视图 libraries = dyld_cache.libraries # O(1) cost:随机访问第一个库,不会物化其前面的对象 first_lib = libraries[0] # O(len(libraries)) cost:只有真正遍历才逐项解析 for lib in libraries: print(lib.path) ``` ```{tab} C++ ```cpp // No cost auto libraries = dyld_cache->libraries(); // O(1) cost:迭代器支持随机访问,可按索引直达任意库 std::cout << "First library: " << libraries[0]->path() << '\n'; // O(libraries.size()) cost for (const Dylib& dylib : libraries) { std::cout << dylib.path() << '\n'; } ```也就是说:取列表不花钱,按下标取第 N 个库是 O(1),完整遍历才付出 O(N) 的解析开销。mapping_info()与subcaches()返回的迭代器同样实现了随机访问 trait,可以libraries.size()、libraries.at(i)、libraries[i]任意取用。
提取为什么慢,以及缓存机制
即便有了上面的设计,Dylib::get()提取单个库仍可能耗时显著——尤其是在开启某些反优化选项的情况下。原文档以fix_branches为例:该选项可能需要多次遍历缓存的 stub 岛(stub islands,缓存为跨库调用集中生成的跳板代码段),才能把所有跳转到库外地址的调用全部改写。
为缓解这种重复开销,LIEF 提供了一套基于磁盘的缓存/记忆化机制,可用两种粒度开启:
lief.dsc.enable_cache()/LIEF::dsc::enable_cache():全局开启(Python 签名见 api/python/lief/dsc.pyi);DyldSharedCache.enable_caching(target_cache_dir):针对单个缓存对象开启并指定缓存目录(C++ 签名见 DyldSharedCache.hpp),配套flush_cache()可将内部信息刷写到磁盘缓存。
开启后,GOT 符号、rebase 信息、stub 符号等“访问成本高昂”的数据会被记录并复用,而不是每次提取都重新计算。环境变量同样受支持(见 include/LIEF/DyldSharedCache/caching.hpp):
DYLDSC_ENABLE_CACHE=1 DYLDSC_CACHE_DIR=/tmp/my_dir ./my-program未设置DYLDSC_CACHE_DIR时,缓存根目录按以下优先级选择:
- 系统或用户缓存目录
- macOS:
DARWIN_USER_TEMP_DIR/DARWIN_USER_CACHE_DIR+/dyld_shared_cache - Linux:
${XDG_CACHE_HOME}/dyld_shared_cache - Windows:
%LOCALAPPDATA%\dyld_shared_cache
- macOS:
- 主目录
- macOS/Linux:
$HOME/.dyld_shared_cache - Windows:
%USERPROFILE%\.dyld_shared_cache
- macOS/Linux:
什么时候应该开启缓存
原文档给出了一份明确的决策清单。可以跳过缓存的情形:
- 你不打算从缓存中提取任何库;
- 你只打算提取一个库且只提取一次;
- 你不希望LIEF 在你的系统上落盘缓存产物。
除此之外的其它场景(例如多次提取、提取多个库、开启反优化选项),都应开启enable_cache。并且要注意:默认情况下缓存机制是关闭的。
进阶能力:映射信息、子缓存与缓存内反汇编
除了原文档主线提到的加载、枚举与提取,DyldSharedCache对象还暴露了一组与缓存内部布局相关的 API,这里结合 tests/dyld-shared-cache/test_dsc_misc.py 中的真实用法一并说明。
MappingInfo:磁盘布局与虚拟布局的桥梁
mapping_info()返回一组MappingInfo,每个条目对应原始dyld_cache_mapping_info,描述一段“磁盘文件偏移 → 虚拟地址区间”的映射(include/LIEF/DyldSharedCache/MappingInfo.hpp):address()为映射起始虚拟地址,size()为区间大小,end_address()返回address() + size(),file_offset()为对应文件偏移,max_prot()/init_prot()给出最大与初始内存保护位。可配合va_to_offset(va)将虚拟地址换算为文件偏移(注意:多子缓存时需先通过cache_for_address()定位到正确的子缓存对象再换算,见 DyldSharedCache.hpp)。
SubCache:处理分裂缓存
subcaches()返回主缓存的子缓存迭代器,SubCache镜像dyld_subcache_entry/dyld_subcache_entry_v1(include/LIEF/DyldSharedCache/SubCache.hpp):uuid()给出子缓存文件的 UUID,vm_offset()是该子缓存相对主缓存基址的偏移,suffix()是文件名后缀(如.25.data、.03.development),cache()返回该子缓存对应的独立DyldSharedCache对象。
测试用例展示了相关对象导航的典型套路(tests/dyld-shared-cache/test_dsc_misc.py):
_main = dsc.main_cache # 主缓存对象 _subcache03 = dsc.find_subcache("dyld_shared_cache_arm64e.03") # 按文件名找子缓存 _cache1 = dsc.cache_for_address(0x1886F4A44) # 定位包含某虚拟地址的子缓存 assert _cache1.va_to_offset(0x1886F4A44) == 0x320CA44缓存内直接反汇编
disassemble(va)可在缓存虚拟地址处直接反汇编并返回指令迭代器。测试里用它读取0x1886F4A44处的 20 条指令,并断言第 11 条是RET;对 stub 岛0x25CD2C0E0反汇编则得到典型的跳板序列adrp x16, .../add x16, x16, #0xbd4/br x16/brk #0x1(tests/dyld-shared-cache/test_dsc_misc.py)。这为分析缓存内部共享代码(如 stub 岛、GOT 填充区)提供了直接入口,其指令类型定义位于 include/LIEF/asm 之下。
快速判别工具
include/LIEF/DyldSharedCache/utils.hpp 提供is_shared_cache()的重载集合:可接收BinaryStream、文件路径字符串、uint8_t*缓冲区或std::vector<uint8_t>;Python 侧对应lief.is_shared_cache(见 api/python/lief/dsc.pyi 的 Utilities 一节)。在处理未知文件时先用它做前置判别,可以避免把普通二进制误送进昂贵的缓存解析流程。
参考与延伸
- 原文档正文见 doc/sphinx/extended/dsc/index.md,另有分语言 API 文档 python.md 与 cpp.md;
- 可运行的完整示例:examples/python/dyld_shared_cache_reader.py、examples/cpp/dyld_shared_cache_reader.cpp、examples/rust/dyld_shared_cache_reader.rs;
- 测试用例:tests/dyld-shared-cache/test_dsc_misc.py(对象导航、反汇编、
get_content_from_va断言)与 tests/dyld-shared-cache/test_ios.py; - 缓存目录解析逻辑参考 include/LIEF/DyldSharedCache/caching.hpp,提取选项参考 include/LIEF/DyldSharedCache/Dylib.hpp;
- 社区中同类思路的实现(如 DyldExtractor、blacktop/ipsw 以及 apple-oss-distributions/dyld)可帮助你对照理解缓存格式本身,但本文所述 API 行为均以本仓库为准。
小结:面对数十 GB 的 Apple dyld shared cache,LIEF Extended 给出的解题思路是——用FileStream按需读盘、用随机访问迭代器延迟物化、用可选的磁盘缓存摊销重复计算、用extract_opt_t按需反优化。记住三件事即可放心上手:加载时保持主/子缓存文件完整、提取前先判空、要提取多个库或开启反优化时务必打开缓存。
- 逆向工程
- 开发工具
【免费下载链接】LIEF
LIEF - Library to Instrument Executable Formats (C++, Python, Rust)
相关推荐
LIEF Extended C++ API 实战:解析 Apple dyld shared cache 并提取 Mach-O 库
LIEF Extended C++ API 实战:解析 Apple dyld shared cache 并提取 Mach O 库 本指南基于 LIEF 仓库中的
逆向工程开发工具dyld-shared-cache-extractor 项目使用教程
dyld shared cache extractor 项目使用教程 1. 项目的目录结构及介绍 dyld shared cache extractor/ ├─
探索macOS内核的秘密:dyld-shared-cache-extractor
探索macOS内核的秘密:dyld shared cache extractor 在macOS Big Sur及其后续版本中,苹果引入了一种新的系统库处理方式—
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考