野指針是指指向一個(gè)已刪除的對(duì)象或未申請(qǐng)?jiān)L問(wèn)受限內(nèi)存區(qū)域的指針。本文說(shuō)的Obj-C野指針,說(shuō)的是Obj-C對(duì)象釋放之后指針未置空,導(dǎo)致的野指針(Obj-C里面一般不會(huì)出現(xiàn)未初始化對(duì)象的常識(shí)性錯(cuò)誤)。
既然是訪問(wèn)已經(jīng)釋放的對(duì)象為什么不是必現(xiàn)Crash呢?
因?yàn)閐ealloc執(zhí)行后只是告訴系統(tǒng),這片內(nèi)存我不用了,而系統(tǒng)并沒(méi)有就讓這片內(nèi)存不能訪問(wèn)。
現(xiàn)實(shí)大概是下面幾種可能的情況:
1.對(duì)象釋放后內(nèi)存沒(méi)被改動(dòng)過(guò),原來(lái)的內(nèi)存保存完好,可能不Crash或者出現(xiàn)邏輯錯(cuò)誤(隨機(jī)Crash)。
2.對(duì)象釋放后內(nèi)存沒(méi)被改動(dòng)過(guò),但是它自己析構(gòu)的時(shí)候已經(jīng)刪掉某些必要的東西,可能不Crash、Crash在訪問(wèn)依賴的對(duì)象比如類成員上、出現(xiàn)邏輯錯(cuò)誤(隨機(jī)Crash)。
3.對(duì)象釋放后內(nèi)存被改動(dòng)過(guò),寫(xiě)上了不可訪問(wèn)的數(shù)據(jù),直接就出錯(cuò)了很可能Crash在objc_msgSend上面(必現(xiàn)Crash,常見(jiàn))。
4.對(duì)象釋放后內(nèi)存被改動(dòng)過(guò),寫(xiě)上了可以訪問(wèn)的數(shù)據(jù),可能不Crash、出現(xiàn)邏輯錯(cuò)誤、間接訪問(wèn)到不可訪問(wèn)的數(shù)據(jù)(隨機(jī)Crash)。
5.對(duì)象釋放后內(nèi)存被改動(dòng)過(guò),寫(xiě)上了可以訪問(wèn)的數(shù)據(jù),但是再次訪問(wèn)的時(shí)候執(zhí)行的代碼把別的數(shù)據(jù)寫(xiě)壞了,遇到這種Crash只能哭了(隨機(jī)Crash,難度大,概率低)!!
6.對(duì)象釋放后再次release(幾乎是必現(xiàn)Crash,但也有例外,很常見(jiàn))。

仔細(xì)看看上面的關(guān)鍵路徑只有出現(xiàn)被隨機(jī)填入的數(shù)據(jù)是不可訪問(wèn)的時(shí)候才會(huì)必現(xiàn)Crash。
所以把這一隨機(jī)的過(guò)程變成不隨機(jī)的過(guò)程。對(duì)象釋放后在內(nèi)存上填上不可訪問(wèn)的數(shù)據(jù),其實(shí)這種技術(shù)其實(shí)一直都有,xcode的Enable Scribble就是這個(gè)作用。

但是有個(gè)問(wèn)題:這個(gè)方法不能放在測(cè)試那邊用!因?yàn)榭偛荒茏寽y(cè)試裝了xcode來(lái)測(cè)試吧?
于是我們自己動(dòng)手實(shí)現(xiàn)一個(gè),這個(gè)過(guò)程中我們要解決幾個(gè)問(wèn)題:
1.怎么在內(nèi)存釋放后填上不可訪問(wèn)的數(shù)據(jù)?
內(nèi)存釋放很可能不在我們的代碼中。為此我們需要hook對(duì)象釋放的接口,內(nèi)存時(shí)候之后馬上執(zhí)行我們的破壞工作。
2.我們要重寫(xiě)對(duì)象釋放的接口,重寫(xiě)哪個(gè)呢?
NSObject的dealloc、runtime的 object_dispose,C的free應(yīng)該都是可以,但是各有優(yōu)點(diǎn),我選擇的是覆蓋面最廣的free,free是C的函數(shù),重寫(xiě)了它之后還可以順帶解決一部分C的野指針問(wèn)題。
3.怎么重寫(xiě)?
重寫(xiě)C的接口場(chǎng)景的有兩種:
a.替換系統(tǒng)動(dòng)態(tài)庫(kù)
b.hook
替換動(dòng)態(tài)庫(kù)太麻煩,還不知道行不行得通;hook我們就找現(xiàn)成的fishhook,github里面找的,但現(xiàn)成的代碼需要防止代碼沖突。
4.填充的不可訪問(wèn)的數(shù)據(jù)的長(zhǎng)度怎么確定?
獲取內(nèi)存長(zhǎng)度的接口不在標(biāo)準(zhǔn)庫(kù)中,好在在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;}
測(cè)試一下,出現(xiàn)了和Enable Scribble一樣的Crash!
以上就是一種在內(nèi)存釋放后填充0x55使野指針后數(shù)據(jù)不能訪問(wèn),從而使某些野指針從不必現(xiàn)Crash變成了必現(xiàn);

其實(shí)這就是上一篇文中留下了幾個(gè)問(wèn)題之一,如果我們填充0x55后內(nèi)存又被別的內(nèi)存覆蓋了,最終還是會(huì)出現(xiàn)隨機(jī)Crash。而在真實(shí)環(huán)境中,這種情況是非常常見(jiàn)的。
我們?cè)偈崂硪幌逻@個(gè)過(guò)程:
1.我們?cè)诩磳⒁尫诺奶盍?x55,之后調(diào)用了free真正釋放,內(nèi)存被系統(tǒng)回收。
2.這個(gè)時(shí)候系統(tǒng)隨時(shí)可能把這片內(nèi)存給別的代碼使用,也就是說(shuō)我們的0x55被再次寫(xiě)上隨機(jī)的數(shù)據(jù)(在這里再?gòu)?qiáng)調(diào)一下,訪問(wèn)野指針是不會(huì)Crash的,只有野指針指向的地址被寫(xiě)上了有問(wèn)題的數(shù)據(jù)才會(huì)引發(fā)Crash)。
3.假如釋放的內(nèi)存上又填上了另一個(gè)對(duì)象的指針,而那個(gè)對(duì)象也有同樣的一個(gè)方法,那很可能只是邏輯上有問(wèn)題,并不會(huì)直接Crash,甚至悄無(wú)聲息地像什么事情都沒(méi)發(fā)生一樣。(這個(gè)地方可能會(huì)發(fā)生多種情況,可以參考之上一篇文章中的圖)
沒(méi)有發(fā)生Crash可不是好事,因?yàn)檫@種情況如果后續(xù)再Crash,問(wèn)題就非常難查,因?yàn)槟憧吹降腃rash棧很可能和出錯(cuò)的代碼完全沒(méi)有關(guān)聯(lián)。既然這個(gè)問(wèn)題這么棘手,最好還是和之前一樣,讓這個(gè)Crash提前暴露。
首先,我們要解決的問(wèn)題就是怎么讓系統(tǒng)不再往這片釋放的內(nèi)存上亂放東西。
要控制底層內(nèi)存管理機(jī)制讓它不使用這些內(nèi)存可能很困難。但是,我們變通一下,簡(jiǎn)單粗暴地,我們干脆就不釋放這片內(nèi)存了。也就是當(dāng)free被調(diào)用的時(shí)候我們不真的調(diào)用free,而是自己保留著內(nèi)存,這樣系統(tǒng)不知道這片內(nèi)存已經(jīng)不需要用了,自然就不會(huì)被再次寫(xiě)上別的數(shù)據(jù)
為了防止系統(tǒng)內(nèi)存過(guò)快耗盡,還需要額外多做幾件事:
1.自己保留的內(nèi)存大于一定值的時(shí)候就釋放一部分,防止被系統(tǒng)殺死。
2.系統(tǒng)內(nèi)存警告的時(shí)候,也要釋放一部分內(nèi)存。

在safe_free以及它調(diào)用的函數(shù)里面盡量不要再用帶鎖的函數(shù),不然很容易導(dǎo)致死鎖。
加上這個(gè)代碼之后APP的內(nèi)存占用會(huì)增大不少,拿過(guò)來(lái)測(cè)試可以,但萬(wàn)萬(wàn)不能放在正式的發(fā)布版本中。
關(guān)于性能問(wèn)題,我的機(jī)器是iPhone5,跑在App里面運(yùn)行,還算流暢(不同App性能可能會(huì)有些不同)。
可能由于鎖的存在,會(huì)使cpu線程切換變得頻繁,這樣多線程的問(wèn)題Crash率也可能會(huì)提升(最近遇到一個(gè)多線程引起的Crash很難重現(xiàn),但我加了這個(gè)代碼后就變成了必現(xiàn)Crash)