天天看點

如何合理地制造“BUG”并且查找BUG

什麼是bug,簡單點說就是,程式沒有按照我們預想的方式運作。我比較喜歡把bug分成兩類:

1.crash掉的

2.沒有crash掉的

可能在平時的程式設計實踐中,往往簡單的把bug與crash基本等價了。而且我們很多精力也都放在解決crash的bug上面。而對于沒有crash掉的bug,似乎沒有過多的關注。但是,實際情況上那些讓人痛徹心扉的“天坑”往往是那些沒有crash掉的bug造成的,比如前一段時間openssl心髒大出血。為什麼這麼說呢?且聽我慢慢道來。

如何合理的制造bug

crash掉的bug,用程式的死證明了你的程式存在問題,你必須抓緊時間來解決程式的問題了。而沒有crash掉的bug,像是一個善于撒謊的人,僞裝成可以正常運轉的樣子,讓整個程式運作在一個不穩定的狀态下。雖然外表看起來好好地(沒有crash),但是裡子早就爛透了,一旦報露出問題往往是緻命的,比如openssl的心髒大出血。這就是前人總結的“死程式不說謊”。

crash不可怕,可怕的是程式沒有crash而是運作在一個不穩定的狀态下,如果程式還操作了資料,那帶來的危害将是災難性的。

是以放心的讓程式crash掉吧,因為當他crash掉的時候,你還有機會去修正自己的錯誤。如果沒有crash,那就有可能要給整個程式和産品收屍了。是以合理制造“bug”的原則之一,也是最大的原則就是:盡量制造crash的bug,減少沒有crash的bug,如果有可能将沒有crash掉的bug轉換成crash的bug以友善查找。

nsassert

這個應該都比較熟悉,他的名字叫做“斷言”。斷言(assertion)是指在開發期間使用的、讓程式在運作時進行自檢的代碼(通常是一個子程式或宏)。斷言為真,則表明程式運作正常,而斷言為假,則意味着它已經在代碼中發現了意料之外的錯誤。斷言對于大型的複雜程式或可靠性要求極高的程式來說尤其有用。而當斷言為假的時候,幾乎所有的系統的處理政策都是,讓程式死掉,即crash掉。友善你知道,程式出現了問題。

斷言其實是“防禦式程式設計”的常用的手段。防禦式程式設計的主要思想是:子程式應該不因傳入錯誤資料而被破壞,哪怕是由其他子程式産生的錯誤資料。這種思想是将可能出現的錯誤造成的影響控制在有限的範圍内。斷言能夠有效的保證資料的正确性,防止因為髒資料讓整個程式運作在不穩定的狀态下面。

關于如何使用斷言,還是參考《代碼大全2》中“防禦式程式設計”一章。這裡簡單的做了一點摘錄,概括其大意:

**1. 用錯誤處理代碼來處理預期會發生的狀況,用斷言來處理絕不應該發生的狀況。

避免把需要執行的代碼放到斷言中

用斷言來注解并驗證前條件和後條件

對于高健壯性的代碼,應該先使用斷言再處理錯誤

對來源于内部系統的可靠的資料使用斷言,而不要對外部不可靠的資料使用斷言,對于外部不可靠資料,應該使用錯誤處理代碼。**

而在ios程式設計中,我們可以使用nsassert來處理斷言。比如:

*- (void)printmyname:(nsstring )myname

{

}**

我們驗證myname的安全性,需要保證其不能為空。nsassert會檢查其内部的表達式的值,如果為假則繼續執行程式,如果不為假讓程式crash掉。

每一個線程都有它自己的斷言捕獲器(一個nsassertionhanlder的執行個體),當斷言發生時,捕獲器會列印斷言資訊和目前的類名、方法名等資訊。然後抛出一個nsinternalinconsistencyexception異常讓整個程式crash掉。并且在目前線程的斷言捕獲器中執行handlefailureinmethod:object:file:linenumber:description:以上述資訊為輸出。

當時,當程式釋出的時候,不能把斷言帶入安裝包,你不想讓程式在使用者機器上crash掉吧。打開和關閉斷言可以在項目設定中設定:

如何合理地制造“BUG”并且查找BUG

在release版本中設定了ns_block_assertions之後斷言失效。

盡可能不要用try-catch

并不是說try-catch這樣的異常處理機制不好。而是,很多人在程式設計中,錯誤了使用了try-catch,把異常處理機制用在了核心邏輯中。把其當成了一個變種的goto使用。把大量的邏輯寫在了catch中。弱弱的說一句,這種情況幹嘛不用ifelse呢。

而實際情況是,異常處理隻是使用者處理軟體中出現異常的情況。常用的情況是子程式抛出錯誤,讓上層調用者知道,子程式發生了錯誤,并讓調用者使用合适的政策來處理異常。一般情況下,對于異常的處理政策就是crash,讓程式死掉,并且列印出堆棧資訊。

而在ios程式設計中,抛出錯誤的方式,往往采用更直接的方式。如果上層需要知道錯誤資訊,一半會傳入一個nserror的指針的指針:

而能夠留給異常處理的場景就極少了,是以在ios程式設計中盡量不要使用try-catch。

(ps:見到過使用try-catch來防止程式crash的設計,如果不是迫不得已,盡量不要使用這種政策)

盡量将沒有crash掉的bug,讓它crash掉

上面主要講的是怎麼知道crash的“bug”。對于合理的制造“bug”還有一條就是盡量把沒有crash掉的“bug”,讓他crash掉。這個沒有比較靠譜的方法,靠暴力吧。比如寫一些數組越界在裡面之類的。比如那些難調的多線程bug,想辦法讓他crash掉吧,crash掉查找起來就比較友善了。

總之,就是抱着讓程式“死掉”的心态去程式設計,向死而生。

如何查找bug

其實查找bug這個說法,有點不太靠譜。因為bug從來都不需要你去找,他就在那裡,隻增不減。都是bug來找你,你很少主動去找bug。程式死了,然後我們就得加班加點。其實我們找的是發生bug的原因。找到引發bug的罪魁禍首。說的比較理論化一點就是:在一堆可能的原因中,找到那些與bug有因果性的原因(注意,是因果性,不是相關性)。

于是解決bug一般可以分兩步進行:

合理性假設,找到可能性最高的一系列原因。

對上面找到的原因與bug之間的因果性進行分析。必須确定,這個bug是由某個原因引起的,而且隻由改原因引起。即确定特定原因是bug的充分必要條件。

找到原因之後,剩下的事情就比較簡單了,改代碼解決掉。

合理性假設

其實,bug發生的原因可以分成兩類:

1.我們自己程式的問題。

2.系統環境,包括os、庫、架構等的問題。

前者找到了,我們可以改。後者就比較無能為力了,要麼發發牢騷,要麼email開發商,最後能不能被改掉就不得而知了。比如ios制作framework的時候,category會報方法無法找的異常,到現在都沒有解決掉。

當然,一般情況下導緻程式出問題的原因的99.999999%都是我們自己造成的。是以合理性假設第一條:

首先懷疑自己和自己的程式,其次懷疑一切。

而程式的問題,其實就是開發者自己的問題。畢竟bug是程式員的親子親孫,我們一手創造了bug。而之是以能夠創造bug,開發者的原因大緻有三:

1.知識儲備不足,比如ios常見的空指針問題,發現很多時候就是因為對于ios的記憶體管理模型不熟悉導緻。

2.錯心大意,比較典型的就是數組越界錯誤。還有在類型轉化的時候沒注意。比如下面這個程式:

**//array.count = 9

for (int i = 100; array.count - (unsigned int)i > 10 ; )

按道理講,這應該是個可以正常執行的程式,但是你運作的話是個死循環。可能死循環的問題,你改了很多天也沒解決。直到同僚和你說array.count傳回的是nsuinterge,當與無符号整形相間的時候,如果出現負值是回越界的啊。你才恍然大悟:靠,類型的問題。

邏輯錯誤

這個就是思維方式的問題,但是也是問題最嚴重的。一旦發生,很難查找。人總是最難懷疑自己的思維方式。比如死循環的問題,最嚴重的是函數間的循環引用,還有多線程的問題。

但是慶幸的是絕大多數的bug都是由于知識儲備不足和粗心大意造成的。是以合理性假設的第二條:

首先懷疑基礎性的原因,比如自己知識儲備和粗心大意等人為因素,通過這些原因查找具體的問題。之後再去懷疑難處理的邏輯錯誤。

有了上面的合理性懷疑的一些基本政策,也不能缺少一些基本的素材啊。就是常見的crash原因,最後我們還是得落地到這些具體的原因或者代碼上,卻找與bug的因果性聯系。

1.通路了一個已經被釋放的對象,比如

2.通路數組類對象越界或插入了空對象

3.通路了不存在的方法

4.位元組對齊,(類型轉換錯誤)

5.堆棧溢出

6.多線程并發操作

7.repeating nstimer

合理性假設第三條:盡可能的查找就有可能性的具體原因。

因果性分析

首先必須先說明的是,我們要找的是“因果性”而不是“相關性“。這是兩個極度被混淆的概念。而且,很多時候我們錯誤的把相關性當成了因果性。比如,在解決一個多線程問題的時候,發現了一個資料混亂的問題,但是百思不得其解。終于,有一天你意外的給某個對象加了個鎖,資料就正常了。然後你就說這個問題是這個對象沒有枷鎖導緻的。

但是,根據上述你的分析,隻能夠得出該對象枷鎖與否與資料異常有關系,而不能得出就是資料異常的原因。因為你沒能證明對象加鎖是資料異常的充分必要條件,而隻是使用了一個單因變量實驗,變量是枷鎖狀态,取值x=[0,1],x為整形。然後實驗結果是枷鎖與否與資料異常呈現正相關性。

因果性分析的首要問題就是,别被自己的邏輯錯誤欺騙,正确的分辨出相關性和因果性之間的差別。不要把相關性等價于因果性。

之後便是因果性分析的内容了,之前一直反複說,因果性分析的目的就是确定特定原因是bug發生的充分必要條件。那麼确定這個事情,就需要兩步:

1.充分性證明

2.必要性證明

關于充分性證明,這個基本上就是正常的邏輯推理。基本思路就是,能夠還原出bug出現的路徑,從原因到bug發生處的代碼,走了怎樣的函數調用和控制邏輯。确定了這個基本上就能夠證明充分性。一般情況下根據crash的堆棧資訊能夠,非常直接的證明充分性。

關于必要性證明,這個就比較困難了。充分性和必要性的定義如下:當命題“若a則b”為真時,a稱為b的充分條件,b稱為a的必要條件。那麼必要性就是,bug能夠作為導緻bug的原因的原因。這個說法比較拗口。換種說法,就是你得确認這個bug能夠解釋原因,這個bug就是而且隻是這個原因造成的。

隻有證明了充分必要性,才能算是真正找到了bug的原因。

參考:

1.ios開發中斷言的使用—nsassert()

2.ios 常見 crash 及解決方案

3.相關性 ≠ 因果性

繼續閱讀