上一篇文章《iOS-底層原理28-啟動優化》介紹了二進制重排能減少缺頁中斷PageFault的頁數,優化應用程序的啟動時間,那啟動時刻調用了哪些方法呢?此篇文章將分析啟動時刻調用的方法。進而將啟動時刻調用的方法盡量都放在集中的頁中,從而減少啟動時間。
clang插樁:參考官方文檔Tracing PCs
- 1.添加標記,Build Settings -> 搜索Other C Flags,添加-fsanitize-coverage=trace-pc-guard
- 2.編譯,會報兩個符號找不到的錯___sanitizer_cov_trace_pc_guard_init和___sanitizer_cov_trace_pc_guard,添加這兩個方法,說明配置了上面的標記位后,調用了這兩個函數
根據官方文檔添加這兩個函數。
報錯的方法找不到的情況,刪掉方法__sanitizer_symbolize_pc
,注釋掉不用的代碼,能正常運行,此時運行,打印出結果,start和end的值分別為0x104ca1100和0x104ca1118
開始地址start為0x104919100,讀取該開始地址下的內存值,一直讀取16個字節,一排有16個字節,4個字節4個字節的讀取,一直讀到結尾,stop指向的地址為0x104919118,指向數據最后的端,要獲取數據結尾處的數據應該往前再讀4個字節,因為uint32_t是無符號整形占用4個字節,06表示的十進制數為6
(lldb) x 0x104919100
0x104919100: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x104919110: 05 00 00 00 06 00 00 00 78 2f a7 04 01 00 00 00 ........x/......
(lldb) x 0x104919118-0x4
0x104919114: 06 00 00 00 78 2f a7 04 01 00 00 00 00 00 00 00 ....x/..........
0x104919124: 00 00 00 00 43 75 91 04 01 00 00 00 00 00 00 00 ....Cu..........
在ViewController增加一個方法touchBegin,此時發現結尾處的數據為07,這說明了啥?結尾處的數據存的就是方法的個數嗎?繼續在ViewController.m文件里面增加函數,block。無論是函數,方法,block都能獲取到。
增加一個屬性呢?增加了三個,setter,getter方法,還有一個cxx的析構函數。
- 綜上,上述哨兵函數能監聽方法,函數,block,屬性。
1.獲取符號地址:監聽方法的調用
點擊屏幕,調用了touchBegan方法,點一下監聽一次
若touchBegan中調用了其他方法,則繼續監聽到了其他方法,__sanitizer_cov_trace_pc_guard
全免捕捉到了函數,方法,block的調用
那么__sanitizer_cov_trace_pc_guard
為什么能捕捉到所有函數的調用呢,原理是什么?
進入匯編代碼查看,sp操作棧空間,bl是跳轉函數,斷點進入test方法,查看匯編,發現只要是在Build Settings設置了讓clang開辟這個功能-fsanitize-coverage=trace-pc-guard
,clang在讀取所有代碼的時候生成ir(中間代碼)之前,都將__sanitizer_cov_trace_pc_guard
這個函數插入到每個函數、方法、block的開頭或邊緣,從而能進行監聽,從而每一個方法,函數,block的調用都會來到此函數void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
避開load方法if (!*guard) return;
,驗證下,注釋掉此行代碼,在ViewController類中重寫load方法,發現會多走一次方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
,從而多打印一次guard: 0x100e99388 0 PC
我們可以利用上面的void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
來進行clang插樁,目標是拿到所有調用的方法名
獲取方法名:引入void *PC = __builtin_return_address(0);
,打印PC指針變量的地址,PC指向的地址0x0000000104b646f8和main函數的地址0x0000000104b646f8一致
INIT: 0x104b69388 0x104b693bc
(lldb) p PC
(void *) $0 = 0x0000000104b646f8
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
* frame #0: 0x0000000104b64264 TraceDemo`__sanitizer_cov_trace_pc_guard(guard=0x0000000104b693b8) at ViewController.m:38:34
frame #1: 0x0000000104b646f8 TraceDemo`main(argc=0, argv=0x0000000000000000) at main.m:12
frame #2: 0x000000018212256c libdyld.dylib`start + 4
(lldb)
- 點擊屏幕touchBegan再次進行驗證,touchBegan方法的內存地址和PC內存地址是否一致,驗證結果一致
- 查看匯編代碼,touchBegan怎么調用到函數
void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
中去,void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
調用完畢,返回ret,還要返回到touchBegan方法中
void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
調用完畢,返回ret,還要返回到touchBegan方法中
函數調用棧:調用棧里面的內存地址并不是函數的開始位置,查看匯編代碼可以得知函數棧中的touchBegan的地址為0x0000000102fa82f0,匯編代碼中touchBegan的地址為0x102fa82c4,兩者并不相等,所以函數棧中的地址并不是touchBegan的開始地址,而是上一個函數void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
調用后的返回地址
這是有原因的,因為lldb的bt也是用的這個函數__builtin_return_address(0)
,驗證如下:刪除Other C Flags里面的哨兵函數標記-fsanitize-coverage=trace-pc-guard
- 在touchBegan函數里面調用兩個方法test()和block(),查看函數棧和匯編代碼,如果函數棧中的touchBegan的地址為函數的首地址,則在同一個函數中,函數的地址不變,則block的函數棧中touchBegin的地址也為函數的首地址,兩個值應該相等,若函數棧中的touchBegan的地址不為函數的首地址,為touchBegan上一個函數(test)或block()的返回地址,則查看test()和block()的匯編,就會發現不一致,兩個值并不相等,所以得出test()和block()返回后地址并不是touchBegan函數的首地址,而是test()和block()調用的返回地址
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
test();
block();
}
void test(){
}
void (^block)(void)=^(void){
};
- 斷點在test函數中查看bt,發現函數調用棧中touchBegan的地址為0x00000001001ec560
- 斷點在block中查看bt,發現函數調用棧中的touchBegan的地址為0x000000010270c578,和test函數中的地址不一致,因為0x000000010270c578和0x00000001001ec560根本就不是touchBegan的起始地址,而是test和block的返回地址,返回到了touchBegan中,這就lldb的原理
blr
2.獲取符號
綜上,通過void *PC = __builtin_return_address(0);
,PC為當前函數返回到上一個調用的地址,0代表我當前函數回到哪里去,1代表我上一個函數回到哪里去
- 由上得知PC指向了當前函數返回到上一個函數調用的地址,怎么能拿到函數的符號呢?
引入#import <dlfcn.h>
,通過dladdr(PC, &info);
函數獲取到函數所在的MachO文件名和地址,以及函數符號名和地址
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("fname:%s \nfbase:%p\nsname:%s \nsaddr:%p\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
char PcDescr[1024];
printf("\nguard: %p %x PC %s\n", guard, *guard, PcDescr);
}
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo
fbase:0x1003c4000
sname:-[ViewController touchesBegan:withEvent:]
saddr:0x1003cc268
guard: 0x1003d13a0 5 PC X\260m\261?
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo
fbase:0x1003c4000
sname:test
saddr:0x1003cc00c
guard: 0x1003d1394 2 PC \224\302<
fname:/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo
fbase:0x1003c4000
sname:block_block_invoke
saddr:0x1003cc028
guard: 0x1003d1398 3 PC ?m=
- 此時拿到的地址是函數的起始地址,0x1003cc00c,為test函數的起始地址,通過dis -s 0x1003cc00c能查看
- TraceDemo的MachO的文件名為/var/containers/Bundle/Application/D1D94998-5B0B-49FB-B5AF-87783F492AC7/TraceDemo.app/TraceDemo,文件地址為0x1003c4000,同時可以通過image list鏡像拿到,拿到文件和動態庫的地址,都為虛擬地址。隨機值怎么獲取呢?ASLR的值,TraceDemo的MachO文件的首地址為0x00000001003c4000,則隨機值為0x3c4000,本身為000,加上隨機值3c4,從1開始為4個G的內存地址了,逆向班會講
3.符號拿到之后,生成相應的order文件
注意:在Build Settings ->Other C Flags,添加-fsanitize-coverage=trace-pc-guard后,在任何地方寫這兩個方法void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop)
和void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
都能監聽到,在main.m文件中實現都行,且只需要實現一次,在哪里結束寫在哪里
- 1.在監聽方法里面存PC,啟動結束的方法里面取PC,此處有個坑點,在touchBegan方法的while循環中取符號方法,一直跳不出循環,什么導致循環無法停止呢?
因為while循環也被方法void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
hook了,循環一次hook一次,點擊屏幕,在循環中進入匯編查看,多的一次__sanitizer_cov_trace_pc_guard
屬于循環,bl是條件跳轉,b是無條件跳轉,一次循環也會被hook一次,只要是跳轉(bl和b的匯編指令),就會被hook
解決辦法:在Other C Flags中添加參數func,-fsanitize-coverage=func,trace-pc-guard,再次點擊屏幕,不會產生循環,打印的方法如下
sname:-[ViewController touchesBegan:withEvent:]
sname:-[AppDelegate window]
sname:-[ViewController viewDidLoad]
sname:-[AppDelegate window]
sname:-[AppDelegate window]
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
sname:-[AppDelegate setWindow:]
sname:-[AppDelegate window]
sname:main
- 2.load沒加載,被
if (!*guard) return;
直接return了,在void __sanitizer_cov_trace_pc_guard(uint32_t *guard)方法中刪除這一句
sname:-[ViewController touchesBegan:withEvent:]
sname:-[AppDelegate window]
sname:-[ViewController viewDidLoad]
sname:-[AppDelegate window]
sname:-[AppDelegate window]
sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
sname:-[AppDelegate setWindow:]
sname:-[AppDelegate window]
sname:main
sname:+[ViewController load]
- 3.去重,取反,不是OC方法添加_
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name :[@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//創建一個新的數組
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//數組中不包含name
[funcs addObject:name];
}
}
//去除touchBegan
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
- 4.寫入cloud.order文件到沙盒中
//數組轉成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串寫入文件
//文件路徑
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cloud.order"];
//文件內容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
- 5.將生成的cloud.order文件拖入工程中,按照cloud.order文件進行編譯,重新運行,查看運行順序是否和配置的OrderFile文件一致
將Build Settings中Write Link Map File選項改為Yes,運行程序,在Product同級目錄Intermediates.noindex/Demo.build/Debug-iphoneos/Demo.build/Demo-LinkMap-normal-arm64.txt中查看文件中每一個方法的排列順序
- 6.若調用了swift的函數呢?應該怎么處理,在Build Settings中設置
-sanitize-coverage=func -sanitize=undefined