docs: refine

This commit is contained in:
LiuBinPeng
2022-04-19 17:15:49 +08:00
parent e2871d54e4
commit 7241220c8e
92 changed files with 10837 additions and 1963 deletions

View File

@@ -1,99 +1,204 @@
# 二进制重排
# DYLD
dynamic loader动态加载器。在 MacOS/iOS 中,使用 `/usr/lib/dyld` 程序来加载动态库的。
## DYLDdyld 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 <stdio.h>
#include <stddef.h>
#include <dlfcn.h>
## 启动检测
typedef int (*extractor_proc)(const char* shared_cache_file_path, const char* extraction_root_path,
void (^progress)(unsigned current, unsigned total));
- App 动态库不要超过6个。
int main(int argc, const char* argv[])
{
if ( argc != 3 ) {
fprintf(stderr, "usage: dsc_extractor <path-to-cache-file> <path-to-device-dir>\n");
return 1;
}
- 静态库:影响 Mach-O 文件。
//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;
}
- 动态库:影响 App 启动时间。
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 目录下。
早期计算机内部都是物理内存。物理内存一个问题就是安全问题,通过地址访问到别的应用程序的数据。进程如果能直接访问物理内存无疑是很不安全的,(诞生了解决方案:虚拟内存)所以操作系统在物理内存的上又建立了一层虚拟内存。
## 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 文件类型
内存分页iOS 一页就是16KB。
MH_OBJECT
物理内存如果满了,则将最活跃的内存数据,覆盖掉,最不活跃的内存数据;
- 目标文件(.o
ASLR为了安全问题诞生。
- 静态库文件(.a静态库其实就是 N 个 `.o` 合并在一起
自己的代码中有 NSLog 代码,可是 NSLog 函数的代码实现在 Foundation 动态库中。
MH_EXECUTE可执行文件
App 启动则用 dyld 去加载库,共享缓存库。
MH_DYLIB动态库文件
- dylib
- .framework
虚拟地址:偏移是编译后就能确定的。
MY_DYLINKER动态链接编辑器 (/usr/lib/dyld)
MH_DSYM存储着二进制文件符号。`.dSYM/Contents/Resource/DWARF/xx` 常用于还原堆栈
Xcode 中也可以查看 Mach-O 文件类型
内存缺页异常:在使用中,访问虚拟内存的一个 page 而对应的物理内存缺不存在(没有被加载到物理内存中),则发生缺页异常。影响耗时,在几毫秒之内。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOFileType.png)
什么时候发生大量的缺页异常?一个应用程序刚启动的时候。
### Universal Binary
启动时所需要的代码分布在第一页、第二页、第三页...第200页。这样的情况下启动时间会影响较大所以解决思路就是将应用程序启动刻所需要的代码二进制优化一下统一放到某几页这样就可以避免内存缺页异常则优化了 App 启动时间
通用二进制文件,可以同时适用于多种架构的二进制文件,包含多种不同架构的独立的二进制文件
因为要存储多种架构的代码,所以通用二进制文件通常比单一平台的二进制程序更大
由于两种架构有共同的一些资源所以并不会达到单一版本的2倍多。
执行的过程中,只调用一部分代码,运行起来不需要额外的内存。
因为通用二进制文件比原来的大所以被成为“胖二进制文件”Fat Binary
dylib loading time
查看某可执行文件(Test)支持的架构指令集
rebase/binding time 修正符号是由于 ASLR 导致。binding time 是动态库去 bind lazy table、non-lazy table 所占用的时间。rebase 地址偏移ASLR。 rebase 的时间如何缩小Mach-O 文件大小变小。 binding time 变小则需要动态库变小。2者优化手段冲突
`lipo -info Test`
Objc setup timeSwift 这部分占优势
将某个指令集拆出来比如 arm64
initializer timeload 方法耗时。
`lipo Test -thin arm64 -o Test_arm64`
slowest intializers
也可以将多个指令集合并
libS
`lipo -create Test_arm64 Test_armv7 -output Test_universal`
libMain
## Mach-O 结构
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/Mach-OStructure.png)
一个 Mach-O 文件包含3块
查看 LinkMap。发现方法展示顺序是按照写代码的顺序展示的。
- Header文件类型、目标架构类型信息
![image-20200814211215976](/Users/lbp/Library/Application Support/typora-user-images/image-20200814211215976.png)
- Load commands描述文件在虚拟内存中的逻辑结构、布局
- Raw segment data在 Load Commands 中定义的 segment 的原始数据
可以用 [GitHub - fangshufeng/MachOView: 分析Macho必备工具](https://github.com/fangshufeng/MachOView) 和系统自带的 atool 查看 Mach-O 信息
## 有没有办法将 App 启动需要的方法集中收拢?
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/otoolhelp.png)
1. 在 Xcode 的 Build Settings 中设置 **Order File**Write Link Map Files 设置为 YES进行观察
用 MachOView 查看 DDD Mach-O 文件
2. 如果你给 Xcode 工程根目录下指定一个 order 文件,比如 `refine.order`,则 Xcode 会按照指定的文件顺序进行二进制数据重排。分析 App 启动阶段,将优先需要加载的函数、方法,集中合并,利用 Order File减小缺页异常从而减小启动时间。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOPageZero.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOText.png)
可以看到在 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 地址**
clang 插桩,才可以 hook OC、C、block、Swift 全部。LLVM 官方文档。
## ASLR
### 未使用 ASLR 的问题
- 函数代码存放在 `__TEXT`
- 全局变量存放在 `__DATA`
二进制重排提升 App 启动速度是通过「解决内存缺页异常」(内存缺页会有几毫秒的耗时)来提速的。
- 可执行文件的内存地址为 `0x0`
一个 App 发生大量「内存缺页」的时机就是 App 刚启动的时候。所以优化手段就是「将影响 App 启动的方法集中处理放到某一页或者某几页」虚拟内存中的页。Xcode 工程允许开发者指定 「Order File」可以「按照文件中的方法顺序去加载」可以查看 linkMap 文件(需要在 Xcode 中的 「Buiild Settings」中设置 Order File、Write Link Map Files 参数)。
- 可执行文件 Header 的内存地址,就是 `LC_SEGMENT(__TEXT)` 中的 VM Address
- arm64 0x100000000
- 非 arm640x4000
其实难点是如何拿到启动时刻所调用的所用方法?代码可能是 Swift、block、c、OC所以hook 肯定不行、fishhook 也不行,用 clang 插桩可行
也可以使用 `size -l -m -x DDD` 指令来查看 Mach-O 的内存分布
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/MachOInsepect.png)
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLRDemo.png)
我们会发现根据 Mach-O 文件中的信息File Size、File Offset、VM Address、VM Size 可以判断出 `__TEXT` 段内函数信息,这样子不够安全
### ASLR 诞生
- https://mp.weixin.qq.com/s/SUHaGD1T2Vce4Ag-qgxtgg
Address Space Layout Randomization地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术通过堆、栈、共享库映射等线性区布局的随机化通过增加攻击者预测目的地址的难度防止攻击者直接定位攻击代码的位置达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。
![](/Users/lbp/Desktop/GitHub/knowledge-kit/assets/ASLROffset.png)
- LC_SEGMENT (__TEXT) 的 VM Address 0x10005000
- ASLR 随机偏移 0x5000也就是可执行文件的内存地址
在有 ASLR 的时候__TEXT 代码段地址 = __PAGEZEROR 地址
在 Mach-O 文件中的地址是原始地址
```shell
代码运行起来函数真实地址 = ASLR-Offset + __PAGEZEROarm640x100000000其他0x4000+ 函数基于 Mach-O 的地址
```