DYLD 及 Mach-O dynamic loader,动态加载器。在 MacOS/iOS 中,是使用 `/usr/lib/dyld` 程序来加载动态库的。 ## DYLD(dyld shared cache) 动态库共享缓存 UIKit、CoreGraphics 等。从 iOS13 开始,为了提高性能,绝大部分的系统动态库文件都被打包存放到了一个缓存文件中(dyld shared cache)中。 存放路径为:`/System/Libarey/Caches/com.apple.dyld/dyld_shared_cache_armX` 其中,`X` 代表 ARM 处理器的指令集架构。V6、V7、V7s、arm64、arm64e。不同架构,对应的动态库缓存不一致。 所有指令集原则是向下兼容的。动态库共享缓存一个非常明显的好处是节省内存。 具体底层源码可以查看,dyld 源码中 `dyld2.cpp` 文件,函数入口为 load 方法。 ```c ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex); ``` 某个 App 被第一次打开时候, dyld 根据动态库的依赖,循环加载动态库到动态库共享缓存中(内存),后续其他 App 第一次打开发现使用了动态库A已经在动态库共享缓存中存在,则不需要加载。这一步调用方法为 `findInSharedCacheImage` ## dyld 应用 窥探系统库底层实现的时候可能需要从动态库共享缓存中提取出某个 Framework,比如 UIKit。这时候要么用第三方工具,要么用 dyld 的能力。 查看 `dsc_extractor.cpp` 代码可以看到是具备抽取能力的。修改源码,将 if 宏定义去掉。如下 ```c #include #include #include typedef int (*extractor_proc)(const char* shared_cache_file_path, const char* extraction_root_path, void (^progress)(unsigned current, unsigned total)); int main(int argc, const char* argv[]) { if ( argc != 3 ) { fprintf(stderr, "usage: dsc_extractor \n"); return 1; } //void* handle = dlopen("/Volumes/my/src/dyld/build/Debug/dsc_extractor.bundle", RTLD_LAZY); void* handle = dlopen("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle", RTLD_LAZY); if ( handle == NULL ) { fprintf(stderr, "dsc_extractor.bundle could not be loaded\n"); return 1; } extractor_proc proc = (extractor_proc)dlsym(handle, "dyld_shared_cache_extract_dylibs_progress"); if ( proc == NULL ) { fprintf(stderr, "dsc_extractor.bundle did not have dyld_shared_cache_extract_dylibs_progress symbol\n"); return 1; } int result = (*proc)(argv[1], argv[2], ^(unsigned c, unsigned total) { printf("%d/%d\n", c, total); } ); fprintf(stderr, "dyld_shared_cache_extract_dylibs_progress() => %d\n", result); return 0; } ``` 然后用 clang++ 编译,命令为 `clang++ dsc_extractor.cpp` 将编译后的产物复制到动态库共享缓存目录下去。然后执行命令`./dsc_extractor dyld_shared_cache_armv7s armv7s`,代表将动态库提取到 armv7s 目录下。 dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图: 可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE ## Mach-O Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式 在 XNU 源码中可以查看 Mach-O 的定义。`loader.h` 属于 Mach-O 格式的文件类型有: ```c #define MH_OBJECT 0x1 /* relocatable object file */ #define MH_EXECUTE 0x2 /* demand paged executable file */ #define MH_FVMLIB 0x3 /* fixed VM shared library file */ #define MH_CORE 0x4 /* core file */ #define MH_PRELOAD 0x5 /* preloaded executable file */ #define MH_DYLIB 0x6 /* dynamically bound shared library */ #define MH_DYLINKER 0x7 /* dynamic link editor */ #define MH_BUNDLE 0x8 /* dynamically bound bundle file */ #define MH_DYLIB_STUB 0x9 /* shared library stub for static */ /* linking only, no section contents */ #define MH_DSYM 0xa /* companion file with only debug */ /* sections */ #define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */ ``` ### 常见的 Mach-O 文件类型 - MH_OBJECT - 目标文件(.o) - 静态库文件(.a),静态库其实就是 N 个 `.o` 合并在一起 - MH_EXECUTE:可执行文件 - MH_DYLIB:动态库文件 - dylib - .framework - MH_DYLINKER:动态链接器 (/usr/lib/dyld) - MH_DSYM:存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx` 常用于还原堆栈 Xcode 中也可以查看 Mach-O 文件类型 源代码(比如c文件),编译变为目标文件(比如.o文件),再经过链接变为可执行文件。 Tips:`file` 命令可以查看文件类型。 `find . -name "*.c"` 比如在当前路径查找 .c 文件 ### Universal Binary 通用二进制文件,可以同时适用于多种架构的二进制文件,包含多种不同架构的独立的二进制文件。 - 因为要存储多种架构的代码,所以通用二进制文件通常比单一平台的二进制程序更大 - 由于两种架构有共同的一些资源,所以并不会达到单一版本的2倍多。 - 执行的过程中,只调用一部分代码,运行起来不需要额外的内存。 因为通用二进制文件比原来的大,所以被成为“胖二进制文件”(Fat Binary),使用 Hopper 打开会显示 “FAT archive” 信息查看: - 查看某可执行文件(Test)支持的架构指令集 :`lipo -info Test` - 将某个指令集拆出来比如 arm64:`lipo Test -thin arm64 -o Test_arm64` - 也可以将多个指令集合并:`lipo -create Test_arm64 Test_armv7 -output Test_universal` Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: `Architectures->Architectures` `User-Defined->VALID_ARCHS`,取2者的交集。 其中 `Architectures->Architectures` 的值 `$(ARCHS_STANDARD)` 是 Xcode 内置的环境变量,不同版本的 Xcode 值不一样,是通用的一些架构值。比如 Xcode9下,`$(ARCHS_STANDARD)` 可能为 arm64、armv7。Xcode 4下,`$(ARCHS_STANDARD)` 可能为 armv7 ### Mach-O 结构 一个 Mach-O 文件包含3块 - Header:文件类型、目标架构类型信息 - Load commands:描述文件在虚拟内存中的逻辑结构、布局 - Raw segment data:在 Load Commands 中定义的 segment 的原始数据 可以用 [MachOView:](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息 比如我用 otool 查看我编写的一个 SwiftUIDemo 所依赖的共享缓存库 用 MachOView 查看 DDD Mach-O 文件 可以看到在 Mach-O 文件上,`__PAGEZERO` 的 `VM Size` 有值,但是 File Size 为0,也就是说 `__PAGEZERO` 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 `__PAGEZERO` 的 File size 有值,这里的 File Offset 就不为0)。 **在没有 ASLR 的时候:__TEXT 代码段地址 = __PAGEZEROR 地址** ## ASLR ### 未使用 ASLR 的问题 - 函数代码存放在 `__TEXT` 段 - 全局变量存放在 `__DATA` 段 - 可执行文件的内存地址为 `0x0` - 可执行文件 Header 的内存地址,就是 `LC_SEGMENT(__TEXT)` 中的 VM Address - arm64 :`0x100000000` - 非 arm64:`0x4000` 也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布 利用 MachOView 查看如下: - _PAGEZERO - VM Address:0x0 - VM Size:0x100000000 - _TEXT - VM Address:0x100000000 - VM Size:0x4000 - _DATA_CONST: - VM Address:0x10004000 - VM Size:0x4000 - _DATA - VM Address:0x10008000 - VM Size:0x4000 - _LINKEDIT - VM Address:0x1000C000 - VM Size:0x8000 File Offset:在 Mach-O 文件中的位置 File Size:在 Mach-O 文件中的占据的大小 从下面的图可以看出,`_PAGEZERO` 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。 我们会发现根据 Mach-O 文件中的信息,File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全 ### ASLR 诞生 Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。 - LC_SEGMENT (__TEXT) 的 VM Address `0x10005000` - ASLR 随机偏移 0x5000,也就是可执行文件的内存地址 在有 ASLR 的时候:__TEXT 代码段地址 = __`PAGEZEROR 地址` 在 Mach-O 文件中的地址是原始地址 ```shell 代码运行起来函数真实地址 = ASLR-Offset + __PAGEZERO(arm64:0x100000000,其他:0x4000)+ 函数基于 Mach-O 的地址 ```