野指針是指指向一個已刪除的對象或未申請訪問受限內存區域的指針。本文說的Obj-C野指針,說的是Obj-C對象釋放之后指針未置空,導致的野指針(Obj-C里面一般不會出現未初始化對象的常識性錯誤)。
既然是訪問已經釋放的對象為什么不是必現Crash呢?
因為dealloc執行后只是告訴系統,這片內存我不用了,而系統并沒有就讓這片內存不能訪問。
現實大概是下面幾種可能的情況:
1.對象釋放后內存沒被改動過,原來的內存保存完好,可能不Crash或者出現邏輯錯誤(隨機Crash)。
2.對象釋放后內存沒被改動過,但是它自己析構的時候已經刪掉某些必要的東西,可能不Crash、Crash在訪問依賴的對象比如類成員上、出現邏輯錯誤(隨機Crash)。
3.對象釋放后內存被改動過,寫上了不可訪問的數據,直接就出錯了很可能Crash在objc_msgSend上面(必現Crash,常見)。
4.對象釋放后內存被改動過,寫上了可以訪問的數據,可能不Crash、出現邏輯錯誤、間接訪問到不可訪問的數據(隨機Crash)。
5.對象釋放后內存被改動過,寫上了可以訪問的數據,但是再次訪問的時候執行的代碼把別的數據寫壞了,遇到這種Crash只能哭了(隨機Crash,難度大,概率低)!!
6.對象釋放后再次release(幾乎是必現Crash,但也有例外,很常見)。
仔細看看上面的關鍵路徑只有出現被隨機填入的數據是不可訪問的時候才會必現Crash。
所以把這一隨機的過程變成不隨機的過程。對象釋放后在內存上填上不可訪問的數據,其實這種技術其實一直都有,xcode的Enable Scribble就是這個作用。
但是有個問題:這個方法不能放在測試那邊用!因為總不能讓測試裝了xcode來測試吧?
于是我們自己動手實現一個,這個過程中我們要解決幾個問題:
1.怎么在內存釋放后填上不可訪問的數據?
內存釋放很可能不在我們的代碼中。為此我們需要hook對象釋放的接口,內存時候之后馬上執行我們的破壞工作。
2.我們要重寫對象釋放的接口,重寫哪個呢?
NSObject的dealloc、runtime的 object_dispose,C的free應該都是可以,但是各有優點,我選擇的是覆蓋面最廣的free,free是C的函數,重寫了它之后還可以順帶解決一部分C的野指針問題。
3.怎么重寫?
重寫C的接口場景的有兩種:
a.替換系統動態庫
b.hook
替換動態庫太麻煩,還不知道行不行得通;hook我們就找現成的fishhook,github里面找的,但現成的代碼需要防止代碼沖突。
4.填充的不可訪問的數據的長度怎么確定?
獲取內存長度的接口不在標準庫中,好在在Mac和iOS中可以用malloc_size就可以。
5.填什么? ? ? ? ? ? ??和xcode一樣,填0x55。
上hook后的free代碼:
[size=0.85em]void safe_free(void* p){? ? size_t memSiziee=malloc_size(p);? ? memset(p, 0x55, memSiziee);? ? orig_free(p);? ? return;}
測試一下,出現了和Enable Scribble一樣的Crash!
以上就是一種在內存釋放后填充0x55使野指針后數據不能訪問,從而使某些野指針從不必現Crash變成了必現;
其實這就是上一篇文中留下了幾個問題之一,如果我們填充0x55后內存又被別的內存覆蓋了,最終還是會出現隨機Crash。而在真實環境中,這種情況是非常常見的。
我們再梳理一下這個過程:
1.我們在即將要釋放的填了0x55,之后調用了free真正釋放,內存被系統回收。
2.這個時候系統隨時可能把這片內存給別的代碼使用,也就是說我們的0x55被再次寫上隨機的數據(在這里再強調一下,訪問野指針是不會Crash的,只有野指針指向的地址被寫上了有問題的數據才會引發Crash)。
3.假如釋放的內存上又填上了另一個對象的指針,而那個對象也有同樣的一個方法,那很可能只是邏輯上有問題,并不會直接Crash,甚至悄無聲息地像什么事情都沒發生一樣。(這個地方可能會發生多種情況,可以參考之上一篇文章中的圖)
沒有發生Crash可不是好事,因為這種情況如果后續再Crash,問題就非常難查,因為你看到的Crash棧很可能和出錯的代碼完全沒有關聯。既然這個問題這么棘手,最好還是和之前一樣,讓這個Crash提前暴露。
首先,我們要解決的問題就是怎么讓系統不再往這片釋放的內存上亂放東西。
要控制底層內存管理機制讓它不使用這些內存可能很困難。但是,我們變通一下,簡單粗暴地,我們干脆就不釋放這片內存了。也就是當free被調用的時候我們不真的調用free,而是自己保留著內存,這樣系統不知道這片內存已經不需要用了,自然就不會被再次寫上別的數據
為了防止系統內存過快耗盡,還需要額外多做幾件事:
1.自己保留的內存大于一定值的時候就釋放一部分,防止被系統殺死。
2.系統內存警告的時候,也要釋放一部分內存。
在safe_free以及它調用的函數里面盡量不要再用帶鎖的函數,不然很容易導致死鎖。
加上這個代碼之后APP的內存占用會增大不少,拿過來測試可以,但萬萬不能放在正式的發布版本中。
關于性能問題,我的機器是iPhone5,跑在App里面運行,還算流暢(不同App性能可能會有些不同)。
可能由于鎖的存在,會使cpu線程切換變得頻繁,這樣多線程的問題Crash率也可能會提升(最近遇到一個多線程引起的Crash很難重現,但我加了這個代碼后就變成了必現Crash)