Let’s Build系列文章是這個部落格中我最喜歡的部分。但是,有時候搞崩程式比編寫它們更有趣。現在,我將要開發一些好玩且不同尋常的方式去讓 Cocoa 崩潰。
帶有 NUL 的字串
NUL(譯者:應該為 ”) 字元在 ASCII 和 Unicode 中代表 0,是一個不尋常的麻煩鬼。當在 C 字串中時,它不作為一個字元,而是一個代表字串結束的識別符號。在其他的上下文環境中,它就會跟其他字元一樣了。
當你混合 C 字串和其它上下文環境,就會產生很有趣的結果。例如:NSString
物件,使用 NUL 字元毫無問題:
1 |
NSString *s = @"abcdef"; |
如果我們仔細的話,我們可以使用 lldb 列印它:
1 2 |
(lldb) p (void)[[NSFileHandle fileHandleWithStandardOutput] writeData: [s dataUsingEncoding: 5]] abcdef |
然而,展示這個字串更為典型的方式是,字串被當做 C 字串在某個點結束。由於 ” 字元意味著 C 字串的結尾,因此字串會在轉換時縮短:
1 2 3 4 |
(lldb) po s abc (lldb) p (void)NSLog(s) LetsBreakCocoa[16689:303] abc |
原始的字元依然包含預計的字元數量:
1 2 |
(lldb) p [s length] (unsigned long long) $1 = 7 |
對這個字串進行操作會讓你真正感到困惑:
1 2 |
(lldb) po [s stringByAppendingPathExtension: @"txt"] abc |
如果你不知道字串的中間包含一個 NUL ,這類問題會讓你感到這個世界滿滿的惡意。
一般來說,你不會遇到 NUL 字元,但是它很有可能通過載入外部資源的資料進來。-initWithData:encoding:
會很輕易地讀入零位元並且在返回的 NSString
中產生 NUL 字元。
迴圈容器
這裡有一個陣列:
1 |
NSMutableArray *a = [NSMutableArray array]; |
這裡有一個包含其他陣列的陣列
1 2 3 |
NSMutableArray *a = [NSMutableArray array]; NSMutableArray *b = [NSMutableArray array]; [a addObject: b]; |
目前為止,看起來還不錯。現在我們讓一個陣列包含自身:
1 2 |
NSMutableArray *a = [NSMutableArray array]; [a addObject: a]; |
猜猜會列印出什麼?
1 |
NSLog(@"%@", a); |
以下就是呼叫堆疊的資訊(譯者:bt 命令為列印呼叫堆疊的資訊):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(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 |
這裡還刪除了上千個棧幀。描述方法無法處理遞迴容器,所以它持續嘗試去追蹤到“樹”的結束,並最終發生異常。
我們可以用它跟自身比較對等性:
1 |
NSLog(@"%d", [a isEqual: a]); |
這姑且看起來是 YES。讓我們創造另一個結構上相同的陣列 b 然後用 a 和它比較:
1 2 3 |
NSMutableArray *b = [NSMutableArray array]; [b addObject: b]; NSLog(@"%d", [a isEqual: b]); |
哎呦:
1 2 3 4 5 6 7 8 9 10 11 12 |
(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
例項做同樣的實驗:
1 2 3 4 |
NSWindow *win = [self window]; NSView *a = [[NSView alloc] initWithFrame: NSMakeRect(0, 0, 1, 1)]; [a addSubview: a]; [[win contentView] addSubview: a]; |
為了讓這個程式崩潰,你只需要嘗試去顯示視窗。你甚至不需要列印一個描述或者做對等性比較。當試圖去顯示視窗時,應用就會因嘗試追蹤底部的檢視結構而崩潰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
(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 值並不一樣:
1 2 3 4 5 6 7 |
@interface AlwaysEqual : NSObject @end @implementation AlwaysEqual - (BOOL)isEqual: (id)object { return YES; } - (NSUInteger)hash { return random(); } @end |
這顯然違反了 Cocoa 的要求,當兩個物件被認為是相等時,他們的 hash 應該總是返回相等的值。當然,這不是非常嚴格的強制要求,所以上述程式碼依然可以編譯和執行。
讓我們新增一個例項到 NSMutableSet
中:
1 2 3 4 5 6 7 |
NSMutableSet *set = [NSMutableSet set]; for(;;) { AlwaysEqual *obj = [[AlwaysEqual alloc] init]; [set addObject: obj]; NSLog(@"%@", set); } |
這產生了一個有趣的日誌:
每次執行都不能保證一樣,但是綜合看起來就是這樣。addObject:
通常先新增一個新物件,然後在更多的物件新增進來的時候很少成功,最後頂部只有三個物件。現在這個集合包含三個看起來是獨一無二的物件,而且看起來應該不會包含更多的物件了。所以,在重寫 isEqual:
時總是應該重寫 hash
方法。
濫用 Selector
Selector 是一個特殊的資料型別,在執行期用於表示方法名。在我們習慣中,它們必須是獨一無二的字串,儘管它們並不是嚴格地要求是字串。在現在的 Objective-C 運期間,它們是字串,並且我們都知道利用 Selector 去搞崩程式是很好玩兒的事。
馬上行動,下面就是一個例子:
1 2 |
SEL sel = (SEL)""; [NSObject performSelector: sel]; |
編譯和執行後,在執行期產生了很令人費解的錯誤:
1 2 |
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,會產生真正奇怪的錯誤:
1 2 3 |
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 :
1 2 3 4 |
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 物件依然是分配給所有物件類的大記憶體中的一小塊記憶體。在這樣的思維下,我們就可以創造一個偽造物件:
1 |
id obj = (__bridge id)(void *)&(Class){ [NSObject class] }; |
這些偽造物件也完全能工作:
1 2 3 4 5 6 7 |
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 試圖去清理時:
1 2 3 4 5 6 7 8 9 10 |
(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
下面是一個類陣列:
1 2 3 4 5 6 7 8 9 10 11 |
NSArray *classes = @[ [NSObject class], [NSString class], [NSView class] ]; NSLog(@"%@", classes); LetsBreakCocoa[17726:303] ( NSObject, NSString, NSView ) |
下面一個這些類例項的陣列:
鍵值編碼並不意味著要這樣使用,但是看起來也可以正常執行。
呼叫者檢查
編譯器的 builtin __builtin_return_address
方法可以返回撥用你的程式碼的地址:
1 |
void *addr = __builtin_return_address(0); |
因此,我們可以獲取呼叫者的資訊,包括它的名字:
1 2 3 |
Dl_info info; dladdr(addr, &info); NSString *callerName = [NSString stringWithUTF8String: info.dli_sname]; |
通過這個,我們可以做一些窮凶極惡的事(譯者:並不認為是窮凶極惡的事,反而可作為呼叫動態方法的一種可選方法,雖然並不可靠),比如說完全可以根據不同的呼叫者呼叫合適的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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 |
這裡是一些測試的程式碼:
1 2 3 4 5 6 7 |
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) 有點棘手,因為我們不能直接讀取它:
1 2 |
Method m = class_getInstanceMethod([NSObject class], sel_getUid("dealloc")); method_setImplementation(m, imp_implementationWithBlock(^{})); |
現在我們來欣賞這個例子所產生的混亂(簡直就是程式碼界的黑暗料理):
1 2 3 4 |
for(;;) @autoreleasepool { [[NSObject alloc] init]; } |
調配 dealloc 方法導致這個程式碼完美且合理地瘋狂洩露,因為物件不能被摧毀。
總結
用全新和有趣的方法搞崩 Cocoa 能夠提供無盡的娛樂性。這也在真實的程式碼裡體現出來了。想起我第一次遇到字串中嵌入了 NUL ,那是充滿痛苦的除錯經歷。其他只是為了好玩和適當的教學目的。
就是這些了!如果你有任何想要討論的問題,可以給我傳送郵件(mike@mikeash.com)。