日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

iOS逆向之深入解析如何计算+load方法的耗时

發布時間:2024/5/28 51 豆豆
生活随笔 收集整理的這篇文章主要介紹了 iOS逆向之深入解析如何计算+load方法的耗时 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、類方法 +load

  • 在 pre-main 時期,objc 會向 dyld 注冊一個 init 回調:
void _objc_init(void) {static bool initialized = false;if (initialized) return;initialized = true;// fixme defer initialization until an objc-using image is found?environ_init();tls_init();static_init();lock_init();exception_init();_dyld_objc_notify_register(&map_images, load_images, unmap_image); }
  • 當 dyld 將要執行載入 image 的 initializers 流程時(依賴的所有 image 已走完 initializers 流程時),init 回調被觸發,在這個回調中,objc 會按照父類-子類-分類順序調用 +load 方法:
void prepare_load_methods(const headerType *mhdr) {size_t count, i;runtimeLock.assertLocked();classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);for (i = 0; i < count; i++) {schedule_class_load(remapClass(classlist[i]));}category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);for (i = 0; i < count; i++) {category_t *cat = categorylist[i];Class cls = remapClass(cat->cls);if (!cls) continue; // category for ignored weak-linked classrealizeClass(cls);assert(cls->ISA()->isRealized());add_category_to_loadable_list(cat);} }
  • 因為 +load 方法執行地足夠早,并且只執行一次,所以通常會在這個方法中進行 method swizzling 或者自注冊操作。也正是因為 +load 方法調用時間點的特殊性,導致此方法的耗時監測較為困難,而如何使監測代碼先于 +load 方法執行成為解決此問題的關鍵點。
  • 關于初始化流程的執行順序,NSObject 文檔中有以下說明:
1. All initializers in any framework you link to. 2. All +load methods in your image. 3. All C++ static initializers and C/C++ __attribute__(constructor) functions in your image. 4. All initializers in frameworks that link to you.
  • 為了方便描述,這里統稱 2、3 步驟為 initializers 流程。可以看到,只要把監測代碼塞進依賴動態庫的 initializers 流程里(監測耗時庫),就可以解決執行時間問題。考慮到工程內可能添加了其他動態庫,還需要讓監測耗時庫的初始化函數早于這些庫執行。解決了監測代碼的執行問題,接下來就可以實現這些代碼,本文采用在 attribute(constructor) 初始化函數中 hook 所有 +load 方法來計算原 +load 執行的時間。

二、獲取需要監測的 image

  • 由于 dyld 加載的鏡像中包含系統鏡像,需要對這些鏡像做次過濾,獲取需要監測的鏡像,也就是主 App 可執行文件和添加的自定義動態庫對應的鏡像:
static bool isSelfDefinedImage(const char *imageName) {return !strstr(imageName, "/Xcode.app/") &&!strstr(imageName, "/Library/PrivateFrameworks/") &&!strstr(imageName, "/System/Library/") &&!strstr(imageName, "/usr/lib/"); }static const struct mach_header **copyAllSelfDefinedImageHeader(unsigned int *outCount) {unsigned int imageCount = _dyld_image_count();unsigned int count = 0;const struct mach_header **mhdrList = NULL;if (imageCount > 0) {mhdrList = (const struct mach_header **)malloc(sizeof(struct mach_header *) * imageCount);for (unsigned int i = 0; i < imageCount; i++) {const char *imageName = _dyld_get_image_name(i);if (isSelfDefinedImage(imageName)) {const struct mach_header *mhdr = _dyld_get_image_header(i);mhdrList[count++] = mhdr;}}mhdrList[count] = NULL;}if (outCount) *outCount = count;return mhdrList; }
  • 上面代碼邏輯很簡單,遍歷 dyld 加載的鏡像,過濾掉名稱中包含 /Xcode.app/、/Library/PrivateFrameworks/、/System/Library/ 、/usr/lib/ 的常見系統庫,剩下的就是需要添加的自定義鏡像和主鏡像。

三、獲取定義 +load 方法的類和分類

  • 獲取擁有 +load 類和分類的方法有兩種:
    • 一種是通過 Runtime Api,去讀取對應鏡像下所有類及其元類,并逐個遍歷元類的實例方法,如果方法名稱為 load ,則執行 hook 操作;
    • 一種是和 Runtime 一樣,直接通過 getsectiondata 函數,讀取編譯時期寫入 MachO 文件 DATA 段的 __objc_nlclslist 和 __objc_nlcatlist 節,這兩節分別用來保存 no lazy class 列表和 no lazy category 列表,所謂的 no lazy 結構,就是定義了 +load 方法的類或分類。
  • 上文說過 objc 會向 dyld 注冊一個 init 回調,其實這個注冊函數還會接收一個 mapped 回調 _read_images,dyld 會把當前已經載入或新添加的鏡像信息通過回調函數傳給 objc 設置程序,一般來說,除了手動 dlopen 的鏡像外,在 objc 調用注冊函數時,工程運行所需的鏡像已經被 dyld 加載進內存,所以 _read_images 回調會立即被調用,并讀取這些鏡像 DATA 段中保存的類、分類、協議等信息。
  • 對于 no lazy 的類和分類,_read_images 函數會提前對關聯的類做 realize 操作,這個操作包含給類開辟可讀寫的信息存儲空間、調整成員變量布局、插入分類方法屬性等操作,簡單來說就是讓類可用 (realized)。值得注意的是,使用 objc_getClass 等查找接口,會觸發對應類的 realize 操作,而正常情況下,只有使用某個類時,這個類才會執行上述操作,即類的懶加載。
  • 反觀 +initialize ,只有首次向類發送消息時才會調用,不過兩者目的不同,+initialize 更多的是提供一個入口,讓開發者能在首次向類發送消息時,處理一些額外業務。
  • 回到上面的兩種方法,第一種方法需要借助 objc_copyClassNamesForImage 和 objc_getClass 函數,而后者會觸發類的 realize 操作,也就說需要把讀取鏡像中訪問的所有類都變成 realized 狀態,當類較多時,這樣做會比較明顯地影響到 pre-main 的整體時間,并且 objc_copyClassNamesForImage 無法獲取自定義 image 中分類的信息,特別是系統分類,比如定義 +load 方法的 NSObject+Custom 分類,對自定義 image 調用 objc_copyClassNamesForImage 函數,其返回值將不會包含 NSObject 類,這導致后續操作將不會包含 NSObject 類,也就無法測量它的 +load 耗時(可以使用 objc_copyClassList 獲取所有類,并判斷類方法列表是否有 +load 方法來規避這個問題,但是和 objc_copyClassNamesForImage 一樣,此方法將更加耗時,也無法確認 +load 方法屬于那個分類),所以本文采用了第二種方法:
static NSArray <LMLoadInfo *> *getNoLazyArray(const struct mach_header *mhdr) {NSMutableArray *noLazyArray = [NSMutableArray new];unsigned long bytes = 0;Class *clses = (Class *)getDataSection(mhdr, "__objc_nlclslist", &bytes);for (unsigned int i = 0; i < bytes / sizeof(Class); i++) {LMLoadInfo *info = [[LMLoadInfo alloc] initWithClass:clses[i]];if (!shouldRejectClass(info.clsname)) [noLazyArray addObject:info];}bytes = 0;Category *cats = getDataSection(mhdr, "__objc_nlcatlist", &bytes);for (unsigned int i = 0; i < bytes / sizeof(Category); i++) {LMLoadInfo *info = [[LMLoadInfo alloc] initWithCategory:cats[i]];if (!shouldRejectClass(info.clsname)) [noLazyArray addObject:info];}return noLazyArray; }

四、hook 類和分類的 +load 方法

  • 獲得了擁有 +load 方法的類和分類,就可以 hook 對應的 +load 方法。no lazy 分類的方法在 _read_images 階段就已經插入到對應類的方法列表中,因此可以在元類的方法列表中拿到在類和分類中的定義的 +load 方法:
static void hookAllLoadMethods(LMLoadInfoWrapper *infoWrapper) {unsigned int count = 0;Class metaCls = object_getClass(infoWrapper.cls);Method *methodList = class_copyMethodList(metaCls, &count);for (unsigned int i = 0, j = 0; i < count; i++) {Method method = methodList[i];SEL sel = method_getName(method);const char *name = sel_getName(sel);if (!strcmp(name, "load")) {LMLoadInfo *info = nil;if (j > infoWrapper.infos.count - 1) {info = [[LMLoadInfo alloc] initWithClass:infoWrapper.cls];[infoWrapper insertLoadInfo:info];LMAllLoadNumber++;} else {info = infoWrapper.infos[j];}++j;swizzleLoadMethod(infoWrapper.cls, method, info);}}free(methodList); }
  • 處理多個動態庫時,無法利用讀取的 image 順序對方法進行匹配,因為讀取的 image 順序并未考慮依賴關系,和 objc 初始化時遍歷的 image 順序并不一致,所以這里的處理方式是錯誤的,為了保證準確性,依舊需要使用 +load 方法的 imp 地址做對比。
  • 為了讓 infos 列表能和類方法列表中的 +load 方法順序一致,在構造 infoWrapper 時,按照后編譯分類-先編譯分類-類次序,將類信息追加入 infos 列表中,然后在遍歷元類的方法列表時,將對應的 LMLoadInfo 對象取出以設置 +load 方法執行耗時變量:
static void swizzleLoadMethod(Class cls, Method method, LMLoadInfo *info) { retry:do {SEL hookSel = getRandomLoadSelector();Class metaCls = object_getClass(cls);IMP hookImp = imp_implementationWithBlock(^ {info->_start = CFAbsoluteTimeGetCurrent();((void (*)(Class, SEL))objc_msgSend)(cls, hookSel);info->_end = CFAbsoluteTimeGetCurrent();if (!--LMAllLoadNumber) printLoadInfoWappers();});BOOL didAddMethod = class_addMethod(metaCls, hookSel, hookImp, method_getTypeEncoding(method));if (!didAddMethod) goto retry;info->_sel = hookSel;Method hookMethod = class_getInstanceMethod(metaCls, hookSel);method_exchangeImplementations(method, hookMethod);} while(0); }
  • 在所有的 +load 方法執行完畢后,輸出工程的 +load 耗時信息。

五、 打印所有 +load 耗時信息

  • 基本上統計 +load 的耗時主要想看到兩個信息:總耗時和最大耗時,因此除了輸出總耗時,還按照 +load 執行時間降序打印出類和分類:
static void printLoadInfoWappers(void) {NSMutableArray *infos = [NSMutableArray array];for (LMLoadInfoWrapper *infoWrapper in LMLoadInfoWappers) {[infos addObjectsFromArray:infoWrapper.infos];}NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:@"duration" ascending:NO];[infos sortUsingDescriptors:@[descriptor]];CFAbsoluteTime totalDuration = 0;for (LMLoadInfo *info in infos) {totalDuration += info.duration;}printf("\n\t\t\t\t\t\t\tTotal load time: %f milliseconds", totalDuration * 1000);for (LMLoadInfo *info in infos) {NSString *clsname = [NSString stringWithFormat:@"%@", info.clsname];if (info.catname) clsname = [NSString stringWithFormat:@"%@(%@)", clsname, info.catname];printf("\n%40s load time: %f milliseconds", [clsname cStringUsingEncoding:NSUTF8StringEncoding], info.duration * 1000);}printf("\n"); }
  • 輸出如下:
Total load time: 2228.866100 millisecondsB(sleep_1_s) load time: 1001.139998 milliseconds DynamicFramework(sleep_1_s) load time: 1001.088023 millisecondsA(sleep_100_ms) load time: 101.074934 millisecondsA(copy_class_list) load time: 68.153024 milliseconds ViewController(sleep_50_ms) load time: 51.078916 millisecondsDynamicFramework load time: 4.286051 millisecondsViewController(sleep_1_ms) load time: 1.210093 millisecondsViewController load time: 0.580072 millisecondsA load time: 0.254989 milliseconds

六、制作動態庫集成至主工程

  • 編寫完監測代碼,需要將其打包成動態庫加入工程中,也就是 Embedded Binaries 和 Linked Frameworks And Libraries:
    • Embedded Binaries 一欄表示把列表中的二進制文件,集成到最終生成的 .app 文件中;
    • Linked Frameworks And Libraries 一欄表示鏈接時,按順序依次鏈接列表中的庫文件。
  • 如果是我們自己添加的庫文件,需要將庫文件添加進上面的兩個列表中,否則要么 dyld 加載庫鏡像時出現 Library not loaded 錯誤,要么直接不鏈接這個庫文件。而系統庫則不需要設置 Embedded 欄 ,只需要設置 Linked 欄,因為實際設備中會預置這些庫。

  • Linked 欄中庫的排列順序,最終會體現在鏈接階段命令的入參順序上:
// Build MessageLd ....../clang ... -framework One -framework Two ... -o .../Demo.app/Demo
  • 當參與鏈接的是動態庫時,在生成主 App 可執行文件的 Load Commands 中,這些動態庫對應的 LC_LOAD_DYLIB 排列順序將和入參順序一致。

  • 當這些動態庫間不存在依賴關系時,其初始化函數的調用順序將和 LC_LOAD_DYLIB 的排列順序一致,否則會優先調用依賴庫的初始化函數:

  • 因為監測耗時庫不依賴其他自定義動態庫,所以直接將監測耗時庫拖入工程,并調整其至 Linked 欄首位即可。

七、制作 pod 集成至主工程

  • 如果工程依賴由 CocoaPods 管理,我可能想要通過以下語句引入 +load 監測庫:
pod 'A4LoadMeasure', configuration: ['Debug']
  • 只有在 Debug 狀態下才會引入監測庫。需要注意的是 CocoaPods 引入的動態庫是由 xcconfig 文件的 OTHER_LDFLAGS 設置的,我們無法通過調整其在 Linked 欄的順序來決定鏈接順序,不過 Other Linker Flags 中 -framework 指定的庫優先級比 Linked 欄中的要高,所以只需要關心 CocoaPods 如何生成 xcconfig 的 OTHER_LDFLAGS 字段即可。
  • CocoaPods 在生成 Pods 工程時,會創建一個名稱為 Pods-主target名的 target (AggregateTarget),這個 target 的 xcconfig 匯集了所有 pods target 的 xcconfig ,來看下 CocoaPods 是如何創建這個文件的:
# Pod::Generator::XCConfig::AggregateXCConfig def generate...@xcconfig = Xcodeproj::Config.new(config)...XCConfigHelper.generate_other_ld_flags(target, pod_targets, @xcconfig)...@xcconfig end def save_as(path)generate.save_as(path) end# Xcodeproj::Config def save_as(path)# 間接執行了 to_hash 并保存至 xcconfig 文件中 end def to_hash(prefix = nil)...[:libraries, :frameworks, :weak_frameworks, :force_load].each do |key|modifier = modifiers[key]sorted = other_linker_flags[key].to_a.sortif key == :force_loadlist += sorted.map { |l| %(#{modifier} #{l}) }elselist += sorted.map { |l| %(#{modifier}"#{l}") }endend... end
  • 可以看到,xcconfig 在保存時才對鏈接庫進行排序,如 frameworks 會根據名稱生序排序后再 map 成“-framework 庫名”的形式保存在文件的 OTHER_LDFLAGS 字段中,因此只要保證監測庫名比 Pods 工程引入的其它自定義動態庫小就可以了,由于 0LoadMeasure、A+LoadMeasure 等非主流名稱無法生成正確的 modulemap ,所以采用 A4LoadMeasure 作為監測庫名,A4 的值比 AA 等英文字母組成的名稱小,針對這種情況已經基本夠用了,畢竟很少會有用 A0 作為名稱前綴的組件或動態庫。
  • 經過以上命名處理,開發者就可以直接通過 CocoaPods 引入監測庫,而不需要進行額外的調整操作。

八、完整示例

  • Objective C之計算+load方法的耗時。

總結

以上是生活随笔為你收集整理的iOS逆向之深入解析如何计算+load方法的耗时的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。