你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 讓我們來搞崩 Cocoa 吧 (黑暗代碼)

讓我們來搞崩 Cocoa 吧 (黑暗代碼)

編輯:IOS開發基礎

QQ截圖20150929150906.png

文本由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);
}

這產生了一個有趣的日志:

QQ截圖20151113150646.png

每次運行都不能保證一樣,但是綜合看起來就是這樣。`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);

上述代碼不僅可以運行,並且打印日志如下:

QQ截圖20151113150601.png

可惜的是,看起來所有偽造對象都是以同樣的地址結束的。但是還是可以繼續工作。好了,當你退出方法並且 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
)

下面一個這些類實例的數組:

16.png

鍵值編碼並不意味著要這樣使用,但是看起來也可以正常運行。

調用者檢查

編譯器的 `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格式供下載。點擊此處查看詳細信息。)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved