生活随笔
收集整理的這篇文章主要介紹了
iOS逆向之深入解析如何计算+load方法的耗时
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、類方法 +load
在 pre-main 時期,objc 會向 dyld 注冊一個 init 回調:
void _objc_init ( void ) { static bool initialized
= false
; if ( initialized
) return ; initialized
= true
; 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 ; realizeClass ( 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 milliseconds
B ( sleep_1_s
) load time
: 1001.139998 milliseconds
DynamicFramework ( sleep_1_s
) load time
: 1001.088023 milliseconds
A ( sleep_100_ms
) load time
: 101.074934 milliseconds
A ( copy_class_list
) load time
: 68.153024 milliseconds
ViewController ( sleep_50_ms
) load time
: 51.078916 millisecondsDynamicFramework load time
: 4.286051 milliseconds
ViewController ( 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 欄中庫的排列順序,最終會體現在鏈接階段命令的入參順序上:
Ld
. . . . . . / 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
. sort
if key
== : force_loadlist
+ = sorted
. map
{ | l
| % ( #
{ modifier
} #
{ l
} ) } else list
+ = 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方法的耗时 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。