通过Method运行时内存布局hook方法探索
在iOS開發中, Method Swizzling想必大家都不陌生, 可以以此來對方法進行hook, 做一些我們希望做的事情, 比如頁面進入退出, 可以對viewWillAppear及viewWillDisappear進行hook, 從而進行一些埋點日志相關的事情。
那么, Method Swizzling的原理到底是怎樣的呢? 這個問題, 即使沒自己研究過, 大多數人也有所耳聞, 簡單來說, 無非就是修改方法的imp指向, 讓其指向我們hook的方法。如果是這樣的話, 我們是否可以不用Runtime提供的API如method_setImplementation、method_exchangeImplementation等函數而通過對象及方法的內存布局來實現呢? 答案是肯定的, 下面便是我在此過程中的一些探索和理解。
本文描述大部分內容對開發沒有太大幫助, 但是對于更加了解運行時方法調用有一定幫助。
直接賦值Method的IMP進行hook
要想通過方法的內存布局來修改, 一定要對方法的內存布局有所了解, 查看源碼可以知道Method的內存布局如下所示:
struct method_t {SEL name;const char *types;IMP imp;struct SortBySELAddress :public std::binary_function<const method_t&,const method_t&, bool>{bool operator() (const method_t& lhs,const method_t& rhs){ return lhs.name < rhs.name; }}; }; 復制代碼上面結構中, 很容易就找到我們想要的東西IMP, 話不多少, 趕緊進行hook。
@implementation Person+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class aClass = [self class];SEL originalSelector = @selector(sayHello);Method originalMethod = class_getInstanceMethod(aClass, originalSelector);struct method_t *method = (struct method_t *)originalMethod;method->imp = (IMP)hookedSayHello;}); }- (void)sayHello {NSLog(@"Hello, everybody!"); }void hookedSayHello (id self, SEL _cmd, ...) {NSLog(@"This is hooked sayHello"); }@end 復制代碼然后再main.m中調用:
Person *person = [[Person alloc] init];[person sayHello]; 復制代碼遇到的問題, 還是調用原來的方法實現
此時卻發現, 打印出來的卻和我想象不太一樣, 仍然是調用了原來的sayHello方法, 而且打個斷點發現method的imp指針也確實指向了 void hookedSayHello (id self, SEL _cmd, ...) 這個函數, 這確實有些讓人捉摸不透。
淺嘗輒止--method _setImplementation
于是懷疑人生的我, 又使用Runtime提供的API method_setImplementation進行相同操作, 發現和以往一樣, 毫無問題, 那么一定是做了一些處理, 查其源碼, 發現了一個很可疑的函數 flushCaches, 見名知意, 清除緩存。
static IMP _method_setImplementation(Class cls, method_t *m, IMP imp) {runtimeLock.assertWriting();if (!m) return nil;if (!imp) return nil;IMP old = m->imp;m->imp = imp;// Cache updates are slow if cls is nil (i.e. unknown)// RR/AWZ updates are slow if cls is nil (i.e. unknown)// fixme build list of classes whose Methods are known externally?flushCaches(cls); updateCustomRR_AWZ(cls, m);return old; }/*********************************************************************** * _objc_flush_caches * Flushes all caches. * (Historical behavior: flush caches for cls, its metaclass, * and subclasses thereof. Nil flushes all classes.) * Locking: acquires runtimeLock **********************************************************************/ static void flushCaches(Class cls) {runtimeLock.assertWriting();mutex_locker_t lock(cacheUpdateLock);if (cls) {foreach_realized_class_and_subclass(cls, ^(Class c){ // 遍歷子類cache_erase_nolock(c);});}else {foreach_realized_class_and_metaclass(^(Class c){cache_erase_nolock(c);});} }// Reset this entire cache to the uncached lookup by reallocating it. // This must not shrink the cache - that breaks the lock-free scheme. void cache_erase_nolock(Class cls) {cacheUpdateLock.assertLocked();cache_t *cache = getCache(cls);mask_t capacity = cache->capacity();if (capacity > 0 && cache->occupied() > 0) {auto oldBuckets = cache->buckets();auto buckets = emptyBucketsForCapacity(capacity);cache->setBucketsAndMask(buckets, capacity - 1); // also clears occupiedcache_collect_free(oldBuckets, capacity);cache_collect(false);} }復制代碼如上述源碼可知, 在flushCaches函數中, 這個函數會把當前類本身, 當前類的元類以及當前類的子類的方法緩存全部清空, 這里我們也可以自己驗證一下,
+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class aClass = [self class];SEL originalSelector = @selector(sayHello);Method originalMethod = class_getInstanceMethod(aClass, originalSelector);// method_setImplementation(originalMethod, (IMP)hookedSayHello); //Runtime API, 可以發現cache被清除了, 可以打開注釋, 驗證結果struct method_t *method = (struct method_t *)originalMethod; // method->imp = (IMP)hookedSayHello; // 直接復制imp指針struct my_objc_class *clz = (__bridge struct my_objc_class *)aClass;uint32_t cacheCount = clz->cache.capacity();NSLog(@"cacheCount : %d", cacheCount);for (NSInteger i = 0; i < cacheCount; i++) {char *key = (char *)((clz->cache._buckets + i)->_key);// 這里設置一下printf("%ld - %s\n", i, key); // 測試}); }復制代碼當調用Runtime API method_setImplementation, 打印如下圖所示:
當直接給imp指針賦值, 打印如下圖所示:可以看出, 當直接給imp指針復制, 不清除方法緩存, 其中打印的sayHello正是我們hook的方法, 之前的疑惑也一掃而空, 雖然方法的imp指向發生了改變, 但是方法緩存中的sayHello對應的imp并沒有發生改變。
我們知道, Objective-C通過方法緩存來提升方法調用速度, 緩存中找不到, 再去類對象的方法列表中去查找, 調用后便加入到方法緩存中, 這點也可以通過objc_msgSend的源碼來確認, objc_msgSend的源碼是匯編實現的, 即使看不懂匯編也沒事, 通過旁邊的注釋, 大概來看出來調用流程: 在方法緩存中尋找, 找到直接返回方法IMP, 否則調用__objc_msgSend_uncached, 去方法列表中查找。
/// objc_msgSend, 除去一些nil驗證檢測后, 調用 CacheLookup LOOKUP LLookup_GetIsaDone:CacheLookup LOOKUP // returns imp/// CacheLookup.macro CacheHit .if $0 == NORMALMESSENGER_END_FASTbr x17 // call imp .elseif $0 == GETIMPmov x0, x17 // return impret .elseif $0 == LOOKUPret // return imp via x17 .else .abort oops .endif .endmacro.macro CheckMiss// miss if bucket->sel == 0 .if $0 == GETIMPcbz x9, LGetImpMiss .elseif $0 == NORMALcbz x9, __objc_msgSend_uncached .elseif $0 == LOOKUPcbz x9, __objc_msgLookup_uncached復制代碼作怪到底--自己修改方法緩存對應的imp
既然都到這里, 不妨嘗試自己去修改方法緩存中對應imp。其實從Objective-C Runtime層面來說, 對象、方法、block等都是以結構體的形式存在內存中, 想去改對象的屬性, 方法的實現會是block的實現, 都是要對它們的內存布局有所了解。
前面的分析把疑惑基本解決了, 現在要做的就比較簡單是了, 只需要將方法緩存以及其他需要用到的結構體如對象、方法等的結構抽出來, 自己聲明一個結構體, 把需要用上的成員變量和方法帶上即可, 不需要用上可以直接刪除。
struct bucket_t {cache_key_t _key;IMP _imp; };struct cache_t {bucket_t *_buckets;mask_t _mask;mask_t _occupied; public:struct bucket_t *buckets();mask_t mask();mask_t occupied();void incrementOccupied();void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);void initializeToEmpty();mask_t capacity();bool isConstantEmptyCache();bool canBeFreed();static size_t bytesForCapacity(uint32_t cap);static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);void expand();void reallocate(mask_t oldCapacity, mask_t newCapacity);struct bucket_t * find(cache_key_t key, id receiver);static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn)); };復制代碼接下來, 只需要將load方法中添加一點代碼進行驗證即可:
+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{Class aClass = [self class]; // Class aClass = self; // 不給self發消息, cache不會生成, 結果就和我們的預想一樣SEL originalSelector = @selector(sayHello);Method originalMethod = class_getInstanceMethod(aClass, originalSelector);// method_setImplementation(originalMethod, (IMP)hookedSayHello); //Runtime API, 可以發現cache被清除了, 可以打開注釋, 驗證結果struct method_t *method = (struct method_t *)originalMethod;method->imp = (IMP)hookedSayHello;// cache問題, 因為 已經和 imp緩存了, 直接會調用原來方法// method_setImplementation 中有個函數 flushCache -> cache_erase_nolock, 會重新設置 cache// 修改cachestruct my_objc_class *clz = (__bridge struct my_objc_class *)aClass;uint32_t cacheCount = clz->cache.capacity();NSLog(@"cacheCount : %d", cacheCount);for (NSInteger i = 0; i < cacheCount; i++) {char *key = (char *)((clz->cache._buckets + i)->_key);// 這里設置一下printf("%ld - %s\n", i, key); // 測試if (key) {NSString *selectorName = [NSString stringWithUTF8String:key];if ([selectorName isEqualToString:@"sayHello"]) {(clz->cache._buckets + i)->_imp = (IMP)hookedSayHello;}}}}); }復制代碼發現打印的確實是我們希望的實現, 當然這里只是一個簡單的類, 對于有子類的情況沒做驗證, 如果有子類的情況下, 還是比較復雜的, 對于子類是否實現了該方法也是有區別的, 這也許也是 method_setImplementation 直接暴力地將當前類和子類的緩存都清空的原因吧!
總結
通過本次探索, 對方法調用以及底層的一些流程有了一定的了解, 雖然對于開發確實沒太大幫助, 但對于理解底層機制有一定幫助。在日常學習中, 可以配合源碼, 通過自己的嘗試, 一定可以對相關知識有更深刻地理解。
代碼地址: github.com/LoyalToOrig…
轉載于:https://juejin.im/post/5b37177d51882574825a5224
總結
以上是生活随笔為你收集整理的通过Method运行时内存布局hook方法探索的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Powershell】【性能计数器】基
- 下一篇: XMPP键盘订制实现图文混排