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

發表於2015-11-16

Let’s Build系列文章是這個部落格中我最喜歡的部分。但是,有時候搞崩程式比編寫它們更有趣。現在,我將要開發一些好玩且不同尋常的方式去讓 Cocoa 崩潰。

帶有 NUL 的字串

NUL(譯者:應該為 ”) 字元在 ASCII 和 Unicode 中代表 0,是一個不尋常的麻煩鬼。當在 C 字串中時,它不作為一個字元,而是一個代表字串結束的識別符號。在其他的上下文環境中,它就會跟其他字元一樣了。

當你混合 C 字串和其它上下文環境,就會產生很有趣的結果。例如:NSString 物件,使用 NUL 字元毫無問題:

如果我們仔細的話,我們可以使用 lldb 列印它:

然而,展示這個字串更為典型的方式是,字串被當做 C 字串在某個點結束。由於 ” 字元意味著 C 字串的結尾,因此字串會在轉換時縮短:

原始的字元依然包含預計的字元數量:

對這個字串進行操作會讓你真正感到困惑:

如果你不知道字串的中間包含一個 NUL ,這類問題會讓你感到這個世界滿滿的惡意。

一般來說,你不會遇到 NUL 字元,但是它很有可能通過載入外部資源的資料進來。-initWithData:encoding: 會很輕易地讀入零位元並且在返回的 NSString 中產生 NUL 字元。

迴圈容器

這裡有一個陣列:

這裡有一個包含其他陣列的陣列

目前為止,看起來還不錯。現在我們讓一個陣列包含自身:

猜猜會列印出什麼?

以下就是呼叫堆疊的資訊(譯者:bt 命令為列印呼叫堆疊的資訊):

 

這裡還刪除了上千個棧幀。描述方法無法處理遞迴容器,所以它持續嘗試去追蹤到“樹”的結束,並最終發生異常。

我們可以用它跟自身比較對等性:

這姑且看起來是 YES。讓我們創造另一個結構上相同的陣列 b 然後用 a 和它比較:

哎呦:

對等性檢查同樣也不知道如何處理遞迴容器。

迴圈檢視

你可以用NSView例項做同樣的實驗:

為了讓這個程式崩潰,你只需要嘗試去顯示視窗。你甚至不需要列印一個描述或者做對等性比較。當試圖去顯示視窗時,應用就會因嘗試追蹤底部的檢視結構而崩潰。

濫用 Hash

讓我們建立一個例項一直等於其他類的類 AlwaysEqual,但是 hash 值並不一樣:

這顯然違反了 Cocoa 的要求,當兩個物件被認為是相等時,他們的 hash 應該總是返回相等的值。當然,這不是非常嚴格的強制要求,所以上述程式碼依然可以編譯和執行。

讓我們新增一個例項到 NSMutableSet 中:

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

QQ截圖20151113150646.png

每次執行都不能保證一樣,但是綜合看起來就是這樣。addObject:通常先新增一個新物件,然後在更多的物件新增進來的時候很少成功,最後頂部只有三個物件。現在這個集合包含三個看起來是獨一無二的物件,而且看起來應該不會包含更多的物件了。所以,在重寫 isEqual: 時總是應該重寫 hash方法。

濫用 Selector

Selector 是一個特殊的資料型別,在執行期用於表示方法名。在我們習慣中,它們必須是獨一無二的字串,儘管它們並不是嚴格地要求是字串。在現在的 Objective-C 運期間,它們是字串,並且我們都知道利用 Selector 去搞崩程式是很好玩兒的事。

馬上行動,下面就是一個例子:

編譯和執行後,在執行期產生了很令人費解的錯誤:

通過建立奇怪的 selector,會產生真正奇怪的錯誤:

你甚至讓錯誤看起來像是停止響應完整資訊的 NSObject :

顯然,這不是真正的 alloc selector,它是一個碰巧指向一個包含 “alloc” 字串的偽裝 selector。但是,runtime 依然把它列印為 alloc 。

偽造物件

雖然現在越來越複雜,但是 Objective-C 物件依然是分配給所有物件類的大記憶體中的一小塊記憶體。在這樣的思維下,我們就可以創造一個偽造物件:

這些偽造物件也完全能工作:

上述程式碼不僅可以執行,並且列印日誌如下:

QQ截圖20151113150601.png

可惜的是,看起來所有偽造物件都是以同樣的地址結束的。但是還是可以繼續工作。好了,當你退出方法並且 autorelease pool 試圖去清理時:

因為這些偽造物件沒有合適分配記憶體,所以一旦autorelease pool 試圖在方法返回時去操作它們,就會出現嚴重的錯誤,並且記憶體會被重寫。

KVC

下面是一個類陣列:

下面一個這些類例項的陣列:

16.png

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

呼叫者檢查

編譯器的 builtin __builtin_return_address 方法可以返回撥用你的程式碼的地址:

因此,我們可以獲取呼叫者的資訊,包括它的名字:

通過這個,我們可以做一些窮凶極惡的事(譯者:並不認為是窮凶極惡的事,反而可作為呼叫動態方法的一種可選方法,雖然並不可靠),比如說完全可以根據不同的呼叫者呼叫合適的方法:

這裡是一些測試的程式碼:

當然,這種方式不是很可靠,因為 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__是 Apple 的內部符號,並且很有可能在未來修改。

Dealloc Swizzle

讓我們使用 swizzle (方法調配技術)去調配-[NSObject dealloc]到一個不做任何事情的方法。在 ARC 下獲得 @selector(dealloc) 有點棘手,因為我們不能直接讀取它:

現在我們來欣賞這個例子所產生的混亂(簡直就是程式碼界的黑暗料理):

調配 dealloc 方法導致這個程式碼完美且合理地瘋狂洩露,因為物件不能被摧毀。

總結

用全新和有趣的方法搞崩 Cocoa 能夠提供無盡的娛樂性。這也在真實的程式碼裡體現出來了。想起我第一次遇到字串中嵌入了 NUL ,那是充滿痛苦的除錯經歷。其他只是為了好玩和適當的教學目的。

就是這些了!如果你有任何想要討論的問題,可以給我傳送郵件(mike@mikeash.com)。

相關文章