在做二進制重排之前,首先需要了解到幾個知識點.例如:物理內(nèi)存,虛擬內(nèi)存,內(nèi)存分頁管理 等
物理內(nèi)存
早期的操作系統(tǒng),只有物理內(nèi)存
當一個應(yīng)用啟動后,會全部加載到內(nèi)存中,并按照內(nèi)存真實地址排列
這樣就會面臨一些問題,比如:
- 內(nèi)存會不夠用
- 不安全,因為在內(nèi)存中App是使用真實地址訪問,所以App可以訪問其以外的內(nèi)存
虛擬內(nèi)存(官方文檔)
iOS中,一個虛擬內(nèi)存與一個進程 一一對應(yīng),大小為4G, 虛擬內(nèi)存里會分為很多頁(Page),每頁的大小16KB
當有了虛擬內(nèi)存之后.CPU訪問進程數(shù)據(jù)相對上面的有了變化:
- 一個進程啟動后,系統(tǒng)為該進程建立一個對應(yīng)的虛擬內(nèi)存,里面記錄了進程每項數(shù)據(jù)的虛擬內(nèi)存地址(比如圖中"進程1虛擬頁表")
- 當進程的某部分活躍后,MMU(
Memory Management Unit-內(nèi)存管理單元)會把這部分數(shù)據(jù)的虛擬內(nèi)存地址翻譯成其對應(yīng)的物理內(nèi)存地址,然后CPU通過物理地址訪問到物理內(nèi)存上的數(shù)據(jù)。 - 如果在page上沒有找到對應(yīng)的物理地址時(
圖中"進程1虛擬頁表的"P2),說明此page上所關(guān)聯(lián)的進程數(shù)據(jù)沒被加載到物理內(nèi)存中,此時會觸發(fā)缺頁異常(Page Fault),中斷當前進程,先將當前頁所對應(yīng)的進程數(shù)據(jù)加載到物理內(nèi)存中,然后page會記錄該項數(shù)據(jù)的物理地址,CPU再通過物理地址來訪問內(nèi)存上的數(shù)據(jù)(此過程耗時是毫秒級)
相比早期的純物理內(nèi)存,虛擬內(nèi)存的優(yōu)勢
- 內(nèi)存使用更高效:進程的數(shù)據(jù)經(jīng)過分頁管理后,只將活躍的page所關(guān)聯(lián)的數(shù)據(jù)加載在物理內(nèi)存中,當物理內(nèi)存都被占用的時候,此時會覆蓋掉不活躍的內(nèi)存,加載當前活躍的page數(shù)據(jù),這樣就能提高對內(nèi)存的使用效率
- 內(nèi)存數(shù)據(jù)更安全:每次啟動進程,系統(tǒng)都會重新建立對應(yīng)的虛擬內(nèi)存,并為虛擬內(nèi)存分配一個
ASLR隨機值(Address Space Layout Randomization),數(shù)據(jù)的虛擬地址即為:ASLR隨機值+偏移值,這樣數(shù)據(jù)的虛擬地址每次都會變,并且CPU是通過虛擬內(nèi)存來間接訪問物理內(nèi)存的,在這個過程中物理內(nèi)存地址沒有暴露出來,所以就能保證內(nèi)存數(shù)據(jù)的安全性
ASLR (Address Space Layout Randomization)
首先如果沒有ASLR,虛擬內(nèi)存是有安全隱患的
- 每個虛擬頁表開頭都是0 (0~4G).如果做靜態(tài)分析,定位到一個函數(shù),找到函數(shù)偏移地址,每次都可拿到該函數(shù)
ASLR可以彌補上述的安全缺陷
百度百科上ASLR的解釋:(Address Space Layout Randomization ) 地址空間配置隨機化;ASLR通過隨機放置進程關(guān)鍵數(shù)據(jù)區(qū)域的地址空間來防止攻擊者能可靠地跳轉(zhuǎn)到內(nèi)存的特定位置來利用函數(shù)
更直白的解釋就如上面提到的:每次啟動進程,系統(tǒng)都會重新建立對應(yīng)的虛擬內(nèi)存,并為虛擬內(nèi)存分配一個ASLR隨機值(Address Space Layout Randomization),數(shù)據(jù)的虛擬地址即為:ASLR隨機值+偏移值,這樣數(shù)據(jù)的虛擬地址每次都會變
了解上面的知識點后,下面會介紹二進制重排
二進制重排
上面有提到,當加載一個未加載到物理內(nèi)存的數(shù)據(jù)時,會觸發(fā)一個系統(tǒng)中斷 (PageFault), 雖然單次耗時是毫秒級,但是有一種情況會出現(xiàn)大量的PageFault,那就是App啟動, 通過二進制重排來減少App啟動速度的核心就是減少App啟動時PageFault的次數(shù)
在重排之前,我們可以先通過Link Map文件,來查看我們的項目加入到內(nèi)存時的默認順序是什么 (LinkMap記錄了二進制文件的布局)
- xcode - build settings - Write Link Map File - 設(shè)為YES
- run一下項目,然后到項目工程目錄的Products找到 xxx.app,右鍵show in finder
- 往回退兩層目錄,然后按照目錄
Intermediates.noindex/項目名字.build/Debug-iphonesimulator/項目名字.build/項目名字-LinkMap-normal-arm64.txt找到linkMap文件
內(nèi)容如下:
可以發(fā)現(xiàn)這個順序默認是按照Compile Source的順序,單個文件內(nèi)的不同方法是按照代碼書寫的順序
另外,我們還可以通過xcode - Instruments - System Trace來查看App啟動時的pageFault次數(shù)
如截圖:(用的是真實項目,項目相關(guān)信息打了馬賽克)
這里有個問題:app首次打開的時候Page Fault的次數(shù)很多,打開之后再打開的話就比較少,當打開多個其他app的時候,在打開檢測的app發(fā)現(xiàn)也會有不少Page Faults
這是由于操作系統(tǒng)的機制,當應(yīng)用殺掉了,他所訪問的物理內(nèi)存不是立馬就清空;它所訪問的物理內(nèi)存,需要通過其他app申請開辟覆蓋釋放掉,
我們要做的就是把啟動所需要的代碼,放在一起,放在最靠前的位置,減少啟動時非必要的pageFault次數(shù).
總結(jié)來講就是以下兩點
- 找到App啟動時所需要調(diào)用的所有函數(shù)
- 更改App數(shù)據(jù)加入到內(nèi)存的順序
獲取項目中啟動時刻所調(diào)用的方法順序
- HOOK objc_msgSend(); 能夠覆蓋所有OC的方法,此篇文章不考慮. (可以看我的另一篇文章有一些Hook的介紹)
- fishhook: 可hook到c方法
- clang插樁(官方文檔)
本文采用clang插樁方式:
原理: 在編譯時刻,在每個函數(shù)內(nèi)部,都會靜態(tài)插入方法__sanitizer_cov_trace_pc_guard,然后我們在項目中注冊其回調(diào)函數(shù),App每次調(diào)用方法(包括OC方法,C語言方法,block等所有方法),都會通過__sanitizer_cov_trace_pc_guard來回調(diào),由此我們可以記錄App啟動時所需的所有方法
- 配置 other c Flags
build Settings - other c Flags
添加內(nèi)容為 -fsanitize-coverage=func,trace-pc-guard
注: 官方文檔上的-fsanitize-coverage=trace-pc-guard這種方式,會在while循環(huán)中同樣插入hook代碼,多次靜態(tài)加入__sanitizer_cov_trace_pc_guard調(diào)用,導(dǎo)致死循環(huán)
所以我們要加func參數(shù),代表只有hook函數(shù)時調(diào)用
- 導(dǎo)入頭文件
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
- 注冊回調(diào)函數(shù)
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("%s\n",info.dli_sname); //打印方法名字
}
可以通過LLDB來簡單調(diào)試一下,為了效果明顯,可以按照以下操作
- 新建一個項目,隨便寫幾個方法,并把上述代碼加進去(加哪里都可以,可以加在ViewController.m)
- 然了后再來個點擊相應(yīng)的方法,運行項目后,斷電加在__sanitizer_cov_trace_pc_guard(如圖), 然后點擊屏幕觸發(fā)點擊方法,走入斷點
可以通過log看到star和stop分別是0x102591898和0x1025918f8.
此時進行memory read,讀取一下最后的內(nèi)存地址里面的內(nèi)容
(lldb) x 0x1025918f4
這里為什么是0x1025918f8 - 0x4 ?
start和stop都是uint32_t類型,占4個字節(jié),end指向最后(如圖),所以要獲取最后一塊內(nèi)存地址中的內(nèi)容,需要減0x4
如果在這基礎(chǔ)上,再添加一個方法之后,同樣的操作獲取上述圖中紅框內(nèi)的數(shù)字,我們會發(fā)現(xiàn)圖中空框內(nèi)的數(shù)字正是方法的數(shù)量 (注:紅框內(nèi)的18是16進制,代表有個24個方法)
我們可以添加匯編代碼來看一下發(fā)生了什么
xcode - Debug - Debug Workflow - Always show Disassembly
給當前viewcontroller添加touchesBegan:withEvent:方法,并在方法內(nèi)部添加斷點,點擊屏幕后:
可以看出已經(jīng)給touchesBegan:withEvent:注入了方法__sanitizer_cov_trace_pc_guard.
此時如果在touchesBegan:withEvent:方法內(nèi)部再調(diào)用一個方法testMethod(), 通過斷點可以看到testMethod()方法內(nèi)部也會被注入__sanitizer_cov_trace_pc_guard方法.
上述記錄的方法是通過NSLog方式來打印的,如果在大型實戰(zhàn)項目中,我們可以考慮把方法名字寫入到本地文件, 我是參考了iOS啟動優(yōu)化:二進制重排這篇文章的方法,以下是全部代碼,可拿來直接用,BinarySortTool.h公開一個類方法+ (void)writeSortedFileMethod;可以在App啟動之后調(diào)用此方法,來寫入文件
// BinarySortTool.m
// Created by qwer on 2021/8/10.
#import "BinarySortTool.h"
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
#import <libkern/OSAtomic.h>
@implementation BinarySortTool
+ (void)writeSortedFileMethod {
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
//offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
// 添加 _
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//去重
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//取反
NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
//將結(jié)果寫入到文件
NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];
NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件寫入出錯");
}
}
//原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct{
void * pc;
void * next;
}SymbolNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入隊
// offsetof 用在這里是為了入隊添加下一個節(jié)點找到 前一個節(jié)點next指針的位置
OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}
@end
至此,我們會拿到.order的文件,由于項目隱私問題就不提供.order內(nèi)容的截圖了
拿到了.order文件后,就剩下最后一步了
更改App數(shù)據(jù)加入到內(nèi)存的順序
這一步相對上面的操作,就輕松很多了,直接去build settings設(shè)置一下order file的路徑即可
到此,啟動優(yōu)化之二進制重排就結(jié)束了.我們可以通過上述介紹過的Instruments - System trace來驗證一下page fault次數(shù),不過要注意上述提到過當殺死App后,其所對應(yīng)的物理內(nèi)存的內(nèi)容不會立刻被清除的問題,可以嘗試多打開幾個App后再打開自己的項目,或者清除所有后臺然后關(guān)機開機.
參考: