文本由CocoaChina譯者小袋子(博客)翻譯
作者:Mike Ash(Blog GitHub)
原文:Friday Q&A 2014-01-10: Let's Break Cocoa
本文最初發布時間為2014年1月10日
Let's Build系列文章是這個博客中我最喜歡的部分。但是,有時候搞崩程序比編寫它們更有趣。現在,我將要開發一些好玩且不同尋常的方式去讓 Cocoa 崩潰。
帶有 NUL 的字符串
NUL(譯者:應該為 '\0') 字符在 ASCII 和 Unicode 中代表 0,是一個不尋常的麻煩鬼。當在 C 字符串中時,它不作為一個字符,而是一個代表字符串結束的標識符。在其他的上下文環境中,它就會跟其他字符一樣了。
當你混合 C 字符串和其它上下文環境,就會產生很有趣的結果。例如:`NSString` 對象,使用 NUL 字符毫無問題:
NSString *s = @"abc\0def";
如果我們仔細的話,我們可以使用 lldb 打印它:
(lldb) p (void)[[NSFileHandle fileHandleWithStandardOutput] writeData: [s dataUsingEncoding: 5]] abcdef
然而,展示這個字符串更為典型的方式是,字符串被當做 C 字符串在某個點結束。由於 '\0' 字符意味著 C 字符串的結尾,因此字符串會在轉換時縮短:
(lldb) po s abc (lldb) p (void)NSLog(s) LetsBreakCocoa[16689:303] abc
原始的字符依然包含預計的字符數量:
(lldb) p [s length] (unsigned long long) $1 = 7
對這個字符串進行操作會讓你真正感到困惑:
(lldb) po [s stringByAppendingPathExtension: @"txt"] abc
如果你不知道字符串的中間包含一個 NUL ,這類問題會讓你感到這個世界滿滿的惡意。
一般來說,你不會遇到 NUL 字符,但是它很有可能通過加載外部資源的數據進來。`-initWithData:encoding:` 會很輕易地讀入零比特並且在返回的 `NSString` 中產生 NUL 字符。
循環容器
這裡有一個數組:
NSMutableArray *a = [NSMutableArray array];
這裡有一個包含其他數組的數組
NSMutableArray *a = [NSMutableArray array]; NSMutableArray *b = [NSMutableArray array]; [a addObject: b];
目前為止,看起來還不錯。現在我們讓一個數組包含自身:
NSMutableArray *a = [NSMutableArray array]; [a addObject: a];
猜猜會打印出什麼?
NSLog(@"%@", a);
以下就是調用堆棧的信息(譯者:bt 命令為打印調用堆棧的信息):
(lldb) bt * thread #1: tid = 0x43eca, 0x00007fff8952815a CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 154, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8) frame #0: 0x00007fff8952815a CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 154 frame #1: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #2: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #3: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #4: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #5: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #6: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #7: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #8: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #9: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #10: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #11: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538
這裡還刪除了上千個棧幀。描述方法無法處理遞歸容器,所以它持續嘗試去追蹤到“樹”的結束,並最終發生異常。
我們可以用它跟自身比較對等性:
NSLog(@"%d", [a isEqual: a]);
這姑且看起來是 YES。讓我們創造另一個結構上相同的數組 b 然後用 a 和它比較:
NSMutableArray *b = [NSMutableArray array]; [b addObject: b]; NSLog(@"%d", [a isEqual: b]);
哎呦:
(lldb) bt * thread #1: tid = 0x4412a, 0x00007fff8946a8d7 CoreFoundation`-[NSArray isEqualToArray:] + 103, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3fff28) frame #0: 0x00007fff8946a8d7 CoreFoundation`-[NSArray isEqualToArray:] + 103 frame #1: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #2: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #3: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #4: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #5: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #6: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #7: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #8: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #9: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71
對等性檢查同樣也不知道如何處理遞歸容器。
循環視圖
你可以用`NSView`實例做同樣的實驗:
NSWindow *win = [self window]; NSView *a = [[NSView alloc] initWithFrame: NSMakeRect(0, 0, 1, 1)]; [a addSubview: a]; [[win contentView] addSubview: a];
為了讓這個程序崩潰,你只需要嘗試去顯示視窗。你甚至不需要打印一個描述或者做對等性比較。當試圖去顯示視窗時,應用就會因嘗試追蹤底部的視圖結構而崩潰。
(lldb) bt * thread #1: tid = 0x458bf, 0x00007fff8c972528 AppKit`NSViewGetVisibleRect + 130, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8) frame #0: 0x00007fff8c972528 AppKit`NSViewGetVisibleRect + 130 frame #1: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #2: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #3: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #4: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #5: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #6: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #7: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #8: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #9: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #10: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #11: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #12: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #13: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #14: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #15: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #16: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #17: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #18: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #19: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #20: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288
濫用 Hash
讓我們創建一個實例一直等於其他類的類 AlwaysEqual,但是 hash 值並不一樣:
@interface AlwaysEqual : NSObject @end @implementation AlwaysEqual - (BOOL)isEqual: (id)object { return YES; } - (NSUInteger)hash { return random(); } @end
這顯然違反了 Cocoa 的要求,當兩個對象被認為是相等時,他們的 hash 應該總是返回相等的值。當然,這不是非常嚴格的強制要求,所以上述代碼依然可以編譯和運行。
讓我們添加一個實例到 `NSMutableSet` 中:
NSMutableSet *set = [NSMutableSet set]; for(;;) { AlwaysEqual *obj = [[AlwaysEqual alloc] init]; [set addObject: obj]; NSLog(@"%@", set); }
這產生了一個有趣的日志:
每次運行都不能保證一樣,但是綜合看起來就是這樣。`addObject:`通常先添加一個新對象,然後在更多的對象添加進來的時候很少成功,最後頂部只有三個對象。現在這個集合包含三個看起來是獨一無二的對象,而且看起來應該不會包含更多的對象了。所以,在重寫 `isEqual:` 時總是應該重寫 `hash`方法。
濫用 Selector
Selector 是一個特殊的數據類型,在運行期用於表示方法名。在我們習慣中,它們必須是獨一無二的字符串,盡管它們並不是嚴格地要求是字符串。在現在的 Objective-C 運期間,它們是字符串,並且我們都知道利用 Selector 去搞崩程序是很好玩兒的事。
馬上行動,下面就是一個例子:
SEL sel = (SEL)""; [NSObject performSelector: sel];
編譯和運行後,在運行期產生了很令人費解的錯誤:
LetsBreakCocoa[17192:303] *** NSForwarding: warning: selector (0x100001f86) for message '' does not match selector known to Objective C runtime (0x6100000181f0)-- abort LetsBreakCocoa[17192:303] +[NSObject ]: unrecognized selector sent to class 0x7fff75570810
通過創建奇怪的 selector,會產生真正奇怪的錯誤:
SEL sel = (SEL)"]: unrecognized selector sent to class 0x7fff75570810"; [NSObject performSelector: sel]; LetsBreakCocoa[17262:303] +[NSObject ]: unrecognized selector sent to class 0x7fff75570810]: unrecognized selector sent to class 0x7fff75570810
你甚至讓錯誤看起來像是停止響應完整信息的 NSObject :
SEL sel = (SEL)"alloc"; [NSObject performSelector: sel]; LetsBreakCocoa[46958:303] *** NSForwarding: warning: selector (0x100001f77) for message 'alloc' does not match selector known to Objective C runtime (0x7fff8d38d879)-- abort LetsBreakCocoa[46958:303] +[NSObject alloc]: unrecognized selector sent to class 0x7fff75570810
顯然,這不是真正的 alloc selector,它是一個碰巧指向一個包含 "alloc" 字符串的偽裝 selector。但是,runtime 依然把它打印為 alloc 。
偽造對象
雖然現在越來越復雜,但是 Objective-C 對象依然是分配給所有對象類的大內存中的一小塊內存。在這樣的思維下,我們就可以創造一個偽造對象:
id obj = (__bridge id)(void *)&(Class){ [NSObject class] };
這些偽造對象也完全能工作:
NSMutableArray *array = [NSMutableArray array]; for(int i = 0; i < 10; i++) { id obj = (__bridge id)(void *)&(Class){ [NSObject class] }; [array addObject: obj]; } NSLog(@"%@", array);
上述代碼不僅可以運行,並且打印日志如下:
可惜的是,看起來所有偽造對象都是以同樣的地址結束的。但是還是可以繼續工作。好了,當你退出方法並且 autorelease pool 試圖去清理時:
(lldb) bt * thread #1: tid = 0x46790, 0x00007fff8b3d55c9 libobjc.A.dylib`realizeClass(objc_class*) + 156, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=1, address=0x7fff00006000) frame #0: 0x00007fff8b3d55c9 libobjc.A.dylib`realizeClass(objc_class*) + 156 frame #1: 0x00007fff8b3d820c libobjc.A.dylib`lookUpImpOrForward + 98 frame #2: 0x00007fff8b3cb169 libobjc.A.dylib`objc_msgSend + 233 frame #3: 0x00007fff8940186f CoreFoundation`CFRelease + 591 frame #4: 0x00007fff89414ad9 CoreFoundation`-[__NSArrayM dealloc] + 185 frame #5: 0x00007fff8b3cd65a libobjc.A.dylib`(anonymous namespace)::AutoreleasePoolPage::pop(void*) + 502 frame #6: 0x00007fff89420d72 CoreFoundation`_CFAutoreleasePoolPop + 50 frame #7: 0x00007fff8551ada7 Foundation`-[NSAutoreleasePool drain] + 147
因為這些偽造對象沒有合適分配內存,所以一旦autorelease pool 試圖在方法返回時去操作它們,就會出現嚴重的錯誤,並且內存會被重寫。
KVC
下面是一個類數組:
NSArray *classes = @[ [NSObject class], [NSString class], [NSView class] ]; NSLog(@"%@", classes); LetsBreakCocoa[17726:303] ( NSObject, NSString, NSView )
下面一個這些類實例的數組:
鍵值編碼並不意味著要這樣使用,但是看起來也可以正常運行。
調用者檢查
編譯器的 `builtin __builtin_return_address` 方法可以返回調用你的代碼的地址:
void *addr = __builtin_return_address(0);
因此,我們可以獲取調用者的信息,包括它的名字:
Dl_info info; dladdr(addr, &info); NSString *callerName = [NSString stringWithUTF8String: info.dli_sname];
通過這個,我們可以做一些窮凶極惡的事(譯者:並不認為是窮凶極惡的事,反而可作為調用動態方法的一種可選方法,雖然並不可靠),比如說完全可以根據不同的調用者調用合適的方法:
@interface CallerInspection : NSObject @end @implementation CallerInspection - (void)method { void *addr = __builtin_return_address(0); Dl_info info; dladdr(addr, &info); NSString *callerName = [NSString stringWithUTF8String: info.dli_sname]; if([callerName isEqualToString: @"__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__"]) NSLog(@"Do some notification stuff"); else NSLog(@"Do some regular stuff"); } @end
這裡是一些測試的代碼:
id obj = [[CallerInspection alloc] init]; [[NSNotificationCenter defaultCenter] addObserver: obj selector: @selector(method) name: @"notification" object: obj]; [[NSNotificationCenter defaultCenter] postNotificationName: @"notification" object: obj]; [obj method]; LetsBreakCocoa[47427:303] Do some notification stuff LetsBreakCocoa[47427:303] Do some regular stuff
當然,這種方式不是很可靠,因為 `__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__`是 Apple 的內部符號,並且很有可能在未來修改。
Dealloc Swizzle
讓我們使用 swizzle (方法調配技術)去調配`-[NSObject dealloc]`到一個不做任何事情的方法。在 ARC 下獲得 @selector(dealloc) 有點棘手,因為我們不能直接讀取它:
Method m = class_getInstanceMethod([NSObject class], sel_getUid("dealloc")); method_setImplementation(m, imp_implementationWithBlock(^{}));
現在我們來欣賞這個例子所產生的混亂(簡直就是代碼界的黑暗料理):
for(;;) @autoreleasepool { [[NSObject alloc] init]; }
調配 dealloc 方法導致這個代碼完美且合理地瘋狂洩露,因為對象不能被摧毀。
總結
用全新和有趣的方法搞崩 Cocoa 能夠提供無盡的娛樂性。這也在真實的代碼裡體現出來了。想起我第一次遇到字符串中嵌入了 NUL ,那是充滿痛苦的調試經歷。其他只是為了好玩和適當的教學目的。
就是這些了!如果你有任何想要討論的問題,可以給我發送郵件([email protected])。
(譯者注:作者此前已經將網站上Friday Q&A系列文章整理成了一本書,開發者可在iBooks和Kindle上查看,另外還有PDF和ePub格式供下載。點擊此處查看詳細信息。)