軟件故障的剋星:斷言調試

斷言調試(Assertive debugging)是利用自帶代碼對程序進行監控並能確保嵌入式系統性能的新型調試方法。

調試是一門有待進一步深入研究的“藝術”……最有效的調試技術是那些在程序本身基礎上設計並構建的技術。現在,許多最優秀程序員都利用近一半的程序對另一半程序進行調試;而用於調試的這一半程序最終將完全被摒棄。出人意料的是,這最終竟也能提高生產效率。 —節選自Donald Knuth的《計算機編程藝術(The Art of Computer Programming)》。

正如Don Knuth所述,調試經常被我們嚴重忽視,而我們也因此付出了慘重的代價。近半個世紀以來,我們在調試領域取得的成就微乎其微,結果因軟件程序故障而陷入困境的項目比比皆是。因調試而浪費的時間和資源,在商業項目中,成本可能高達數十億美元;在軍事項目中,損失的不僅是金錢,甚至包括生命。這種現狀簡直讓人無法忍受:我們必須探尋新的方法和途徑。本文就提出了這樣一種新方法。

本文提出的新型軟件調試系統“斷言調試系統(ADS)”可以將調試由次要的“藝術形式”提升爲現代工業流程。ADS雖然利用了既有的思想,即John von Neumann於1947年首先提出的“斷言”理論,但據我所知,ADS處理斷言的方式卻是Neumann或任何其他人從未提出的新方式:ADS更系統也更徹底地利用斷言,而不像其他工具只在程序員想到的時候才使用。爲此,ADS將斷言由半個世紀以來一直漂浮不定且鮮有建樹的理論轉化爲足以引領程序開發革命的技術。與Knuth的論述不同,ADS並不摒棄那部分用於調試的程序,而是將其作爲程序主體補充的文檔進行保存,這樣,當程序需要修改時完全可以加以複用。

程序故障是主要的瓶頸

現在幾乎不可能找到完全不需要編程運算的科學或工程項目,同樣地,也很難找到不因程序故障原因而無法預期交付的軟件。調試問題對幾乎所有的項目都至關重要,而因軟件程序故障帶來教訓也足夠深刻:當客戶對產品不滿意時,我們會喪失業務;當產品遲遲無法推向市場時,我們的銷售額會下降。隨着我們在關鍵應用中越來越頻繁地使用計算機,我們的教訓也越來越慘重,這不僅關乎任務完成,甚至性命攸關。

在這些關鍵應用中,慎重地選擇調試方法並加以證明變得越來越重要,甚至成爲法律需要。對於那些高度依賴調試的應用而言,一半程序完全用於調試已日益無法忍受。ADS方法在編程的同時就能直接觸及這些問題:這有助於開發人員縮短調試進程並支持軟件對象的系統級和存檔級調試。本文極力主張採用該方法,這或許有助於防止陷入困境。

調試的現狀

調試發展歷程中最值得關注的是,現代調試技術居然與半個世紀前剛剛進入現代計算時代時沒有太大區別。我們仍然讓故障程序運行至預測的關鍵點,然後停止運行並查看關鍵變量的狀態。只要其中一個關鍵變量的值與預期的不同,我們就會努力分析爲什麼會造成這種結果。如果不知道什麼地方出錯,我們會重複執行這個過程,在程序更早的地方停下來。經過若干次反覆,我們就能在充分接近程序故障的地方停下來,結果發現,故障的原因是:我們忘了重置某個計數器或清空某段內存、從而使某些數據結構產生溢出或犯了其他6種經典編程錯誤中的任意一種。

這就是上個世紀50年代中葉的軟件調試方法,至今仍在沿用。如果時間允許且客戶足夠耐心,那麼該方法仍將繼續沿用下去,直至最終找到困擾的程序故障。但這種方法的缺點也很明顯:通常只適用於一些特定情形,調試需要的時間也無法預知,而且並不能幫助程序員更好地理解程序調試或找到類似的程序故障。

什麼是程序故障?

爲了說明哪些程序故障纔是真正麻煩的程序故障,即ADS專注解決的問題,這裏簡要地對軟件故障進行了分類並說明了各類故障的嚴重性。這種分類本身並無任何新意,只是收集並組織了一些通用常識,然後以便於理解ADS的形式組織起來。這裏,我們考慮的只是程序員產生的錯誤,而由硬件故障、操作員錯誤操作或其他不受程序員控制的條件引發的故障本身並不複雜,所以不在ADS處理的故障之列。程序員錯誤包括:

1. 算法設計錯誤。程序員或其客戶錯誤理解了問題,因此解決問題的方法(即算法)即便完美無缺,也無法發揮效用。例如,程序員假定地球是個完美的球體,然後基於此計算人造地球衛星的軌道。他的錯誤本身與算法無關,但與他或客戶對問題的理解有關。

2. 程序設計錯誤。儘管程序員對問題的理解及解決問題的方法都正確,但是在爲解決方案設計程序時犯了錯誤。例如,他沒有意識到計算機執行程序的時間比根據常規預測的時間更長。該問題與計算機相關:反映了程序員的理解能力,不與任何特定的計算機或編程語言相關,而與對通用計算的理解有關。

3. 程序實現錯誤。程序員在生成計算機執行的指令時出現錯誤。這類錯誤中,有以下兩種變形:

a. 體系或語法錯誤。程序違背了程序開發工具規定的準則,但這種錯誤僅被這些工具捕獲。
b. 獨立或邏輯錯誤。程序本身沒有錯誤,但無法運行結束或產生錯誤輸出。程序員要麼犯了機械錯誤(如拼寫錯誤),要麼使用了不能被開發系統捕獲的類格式錯誤,要麼更嚴重地在詳細程序設計中犯下了邏輯錯誤,如疏忽了緩存刷新或在數據區外進行寫操作(這時,顯然程序開發工具出現了故障。這雖然不是這位程序員的錯,卻是另一位程序員的失誤)。

類型1與算法無關:這些故障只是因爲疏忽大意或愚鈍而產生,因此也無須採取任何特殊補救措施。類型2與計算機相關,但也不是太棘手:這些故障顯而易見,一般程序設計早期階段就能發現,因此問題也不是太突出。類型3a現在已經得到了很好的解決,大多數現代程序開發系統能檢測出所有的通用語法錯誤並精確地定位。有時,這些軟件甚至還能糾錯,例如一些專用於文字處理的程序就能自動地將“hte”糾正爲“the”。

真正危險的程序故障

類型3b纔是真正棘手的問題:它的特點是容易引入、難以發現,通常直到出現最壞結果才顯露出來。這類問題之所以難以對付,完全是因爲故障非常瑣碎、不引人注目,因此很難定位。類型3b程序故障(以後簡稱爲“程序故障”)確實非常危險,因爲這類故障很少立即表現出來。感染了這類故障的程序通常情況下不會表現出任何症狀,直到程序災難性崩潰或產生明顯錯誤的輸出才表現出來。這類故障一般能使程序無故障地長時間運行,直到實際影響結果。顯然,這時不僅程序出現了故障,而且大多數情況下,程序還將試圖刪除或破壞定位故障所需的信息;於是,我們又不得不重新開始漫長而艱辛的回退調試流程。

因此,我們需要的調試方法是希望通過某種途徑讓程序故障迅速自動暴露出來,這樣我們就能在第一時間意識到問題存在並在程序破壞定位信息之前採取保護措施。理想情況下,我們希望程序故障甚至能在出現之前就自動“跳出來”,也就是說,我們希望能在程序故障“幹壞事”之前就一把抓住。這就是設計ADS的初衷。

ADS的工作原理

程序故障一出現即能立即捕獲的調試方法是通過在程序運行時監控衆多變量以找到那些違反程序員定義的斷言約束的故障。這裏的“變量”並不單單指那些數學意義上的變量,還包括那些屬性以可預知方式改變的程序結構,而改變的方式可以是絕對改變,也可以是基於其他程序結構的相對改變。在這之中,還包括那些描述循環遍歷次數、緩存寫入前能容納的字符個數、分支開關能處理的狀態數目等的數字變量。這些變量共同定義了程序執行過程。ADS一個重要的前提是,如果變量不違反某項約束,程序故障將不會生效。如果系統地檢測出這些約束違反,那麼每個程序故障都將在其一出現就發出警告,從而便於發現和理解。

在整個程序執行中對斷言進行嚴格且系統的測試相當於在程序執行通道兩側建立起“防護牆”,這樣任何偏離通道的程序執行都將使運行的程序與某些斷言發生直接碰撞。因此,我們就能在每次執行失敗中找到一些有價值的東西:找到程序故障(至少顯著縮小故障搜索範圍)或程序員的錯誤理解。

使用ADS

對於每個程序結構,程序員可以在變量定義的時候,就指定變量的約束條件。可能的約束條件如下所示,其他的約束條件可以在使用ADS過程中不斷擴充:

* 最大值和最小值;
* 變化的步長;
* 對於變量的取值,可以循環取值,還是隻能使用一次,或是隨機取值;
* 該變量與其他一個或多個變量間的關係;
* 變量可取值或不可取值的顯式列表;
* 指針或鏈接變量所能指向的結構類型;

這些斷言的表示方式是程序員使用的編程語言的自然延伸並可以幾種方式進行歸類,因此,程序員可以通過一條命令使一組相關斷言生效或失效。

在主程序的每次編譯中,主程序代碼中激活的斷言可被用來檢驗其監控的變量,如變量值的每次改變,是否違反了任何約束條件等(“可被用來”的含義表示,並不是每次都需要執行每項測試)。當監控代碼檢測到任何變量已違反(或在某些情形下,即將違反)某個斷言,斷言將停止程序執行並運行程序員指定的異常處理程序。

這一點充分顯示了ADS調試方法與目前衆多程序員使用的斷點終止法的主要區別。ADS停止程序執行並不是因爲程序執行到某個程序員希望在此通過檢查某些變量以獲取信息的點上;這個點或許遠比程序員預想插入的斷點提前或滯後,只要在這點上檢測到異常。此外,中止點與異常實際發生點非常接近。中止點的檢測也不受程序員的控制,當ADS報告該事件時,與目前使用的斷點中止法所採用的隨機搜索機制不同,如果程序故障不馬上出現的話,ADS用戶下一步可以返回程序並採取更大的監控力度以使所有代碼動態地指示異常檢測,這樣就能儘早地捕獲程序故障。

遺憾的是,由於ADS並未得到充分構建和使用,因此缺乏有效展示ADS有效性的方法,但我們可以藉助一個並不具有結論性的假想試驗進行說明。根據最近解決的問題或以往經驗人爲地構造一個實際的程序故障。在錯誤指令導致程序正常行爲出現首次異常之前,要記錄下程序中的變量值,同時啓動斷言檢測。請記住,如果採用了ADS,那麼ADS將監控程序中的每個變量(即每個變化可預測的結構)的每次取值變化是否違反了當初設定的範圍。通過比較程序故障首次顯現與ADS停下來指示程序故障之間的間隔,就能深刻體會ADS在調試中帶來的便捷。在幾乎所有情形下,我們都能找到傳統調試方法與ADS調試方法的巨大區別,兩者完全不可同日而語。

斷言調試的成本

雖然大多數程序員都表示ADS能幫助他們更快地找出程序故障,但許多人仍堅持ADS的成本實在過於昂貴:實時檢測使ADS程序執行佔用的系統資源是普通程序執行的數百倍。很多程序員一想到提供全部斷言檢測將使ADS嚴格地監控整個程序就覺得難以承受。這些擔心無疑是杞人憂天,因爲他們只看到了表面而沒有深入分析。

爲了真正對ADS方法的成本進行評估,首先需要明確的是,該方法比較的對象是實際應用的現有調試方法。現有調試方法在查詢程序故障中給出的提示信息往往很少或者甚至沒有,因此,對於整個調試的成本,這部分成本也必須加以考慮。另一方面,採用ADS方法,每次程序執行都將得到有用的存檔信息:要麼找到違反斷言約束之處,要麼在運行到結束時報告程序完全無故障。即便ADS報告了斷言違反約束,而結果表明程序代碼完全正確,那麼說明程序員設定的斷言有誤,從而可以獲得一些有效信息。實際上,其中最有價值的並不是找到單個程序故障,而是找到程序員對程序的錯誤理解,這無疑更爲重要。此外,需要注意的是,ADS的成本貫穿於整個產品週期,ADS節省的是項目預算時間、軟件工程師調試時間和產品的上市時間。總而言之,ADS是通過犧牲那些成本幾乎可以忽略不計的低成本資源,實現節省高成本的資源的目標。

斷言在程序員聲明變量和數據結構就已明確表示,也就是說,當程序員在構造斷言時就已充分理解了該斷言。現有的實現系統需要程序員對變量和結構進行完整的靜態定義;而ADS只要求對變量或結構在程序實時運行中的允許取值或禁止取值添加顯式聲明。實際上,用戶在最理想的時間完成了大量的調試工作:他並不會迫於壓力在規定的時間內定位特定的程序故障,而且這時候他的頭腦最清晰,思維最敏銳。

基本原理比較

採用ADS工具與其他傳統工具的最大區別在於,只要預先加以定義,那麼從定義開始並在整個用戶設定的限制約束內,ADS將進行完整的系統工作。在傳統調試方法中,系統只向用戶反饋這樣的信息:“我也不知道爲什麼程序會在這個點上停止下來,或許你在這裏設定了一個斷點,因此這時候你可以在既有認知能力條件下,通過一個窗口觀察任何一個可能與程序故障檢測相關的變量。如果程序當前狀態下存在異常,你將能識別該異常,但如果跳過某個變量,我將無法識別某些異常。”

相反,ADS會這樣表示:“早在程序設計和開發時期,你就告訴我,對於程序中衆多變量和結構中的任意一個,那些情況屬於異常;接着,你又告訴我將跟蹤那些異常,我就會按照你的指示尋找程序異常。現在,我找到一個異常並記錄下詳細信息,如下所示……”有了ADS,軟件工程師將完全從事設計規劃,而調試系統則完成大量的煩瑣工作。

上述兩種方法可以通過比較每個項目因軟件故障而導致的項目延遲以及能否使編程更高效可靠來區分。

參考文獻
1.The epigraph is from Knuth's The Art of Computer Programming (ed. 1), Vol 1, Addison-Wesley, 1998, p.189.
2.Goldstine, Herman H. The Computer from Pascal to von Neumann. Princeton University Press, 1972, p. 268.

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章