版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
1、第 4 章 對程序進行逐條跟蹤前面我們講過, 發(fā)現程序中錯誤的最好方法是執(zhí)行程序。 在程序執(zhí)行過程中, 通過我們 的眼睛或者利用斷言和子系統一致性檢查這些自動的測試工具來發(fā)現錯誤。 然而, 雖然斷言 和子系統檢查都很有用, 但是如果程序員事先沒有想到應該對某些問題進行檢查也不能保證 程序不會遇到這些問題。這就好比家庭安全檢查系統一樣。如果只在門和窗戶上安裝了警報線, 那么當竊賊從天窗或地下室的入口進入家中時, 就 不會引起警報。 如果在錄像機、 立體聲音響或者其它一些竊賊可能盜取的物品上安裝了干擾 傳感器,而竊賊卻偷取了你的 Barry Manilow 組合音響,那么他很可能會不被發(fā)現地逃走。
2、 這就是許多安全檢查系統的通病。 因此, 唯一保證家中物品不被偷走的辦法是在竊賊有可能 光顧的期間內呆在家里。防止錯誤進入程序的辦法也是這樣,在最有可能出現錯誤的時候, 必須密切注視。那么什么時候錯誤最有時能出現呢?是在編寫或修改程序的時候嗎?確實是這樣。 雖然 現在程序員都知道這一點, 但他們卻并不總能認識到這一點的重要性, 并不總能認識到編寫 無錯代碼的最好辦法是在編譯時對其進行詳盡的測試。在這一章中, 我們不談為什么在編寫程序時對程序進行測試非常重要, 只講在編寫程序 時對程序進行有效測試的方法。增加對程序的置信最近, 我一直為 Microsoft 的內部 Macintosh 開發(fā)系統編
3、寫某個功能。 但當我對所編代 碼進行測試時, 發(fā)現了一個錯誤。 經過跟蹤, 確定這個錯誤是出在另一個程序員新編的代碼 中。使我迷惑不解的是, 這部分代碼對其他程序員的所編代碼非常重要, 我想不出他這部分 代碼怎么還能工作。我來到他的辦公室,以問究竟“我想,在你最近完成的代碼中我發(fā)現了一個錯誤” 。我說道。“你能抽空看一下嗎?” 他把相應的代碼裝入編輯程序, 我指給他看我認為的問題所在。 當他看到那部分代碼時不禁 大吃一驚?!澳闶菍Φ?,這部分代碼確實有錯??墒俏业臏y試程序為什么沒有查出這個錯誤呢?” 我也對此感到奇怪。 “你到底用什么方法測試的這部分代碼?” ,我問道。 他向我解釋了他的測試方法
4、, 聽起來似乎它應該能夠查出這個錯誤。 我們都感到很費解。 “讓我們在該函數上設置一個斷點對其進行逐條跟蹤,看看實際的情況到底怎樣” ,我提議 道。我們給該函數設置了一個斷點。 但當找們按下運行鍵之后, 相應的測試程序卻運行結束 了,它根本就沒有碰上我們所設置的斷點。 沒過多久, 我們就發(fā)現了測試程序沒有執(zhí)行該函 數的原因在該函數所在調用鏈上幾層, 一個函數的優(yōu)化功能使這個函數在某種情況下面跳過了不必要的工作。讀者還記得我在第 1 章中所說的黑箱測試問題嗎?測試者給程序提供大量的輸入, 然后 通過檢查其對應的輸出來判斷該程序是否有問題。如果測試者認為相應的輸出結果沒有問 題,那么相應的程序就被
5、認為沒有問題。 但這種方法的問題是除了提供輸入和接受輸出之外,測試者再沒有別的辦法可以發(fā)現程序中的問題。上述程序員漏掉錯誤的原因是他采用了黑箱方法對其代碼進行測試,他給了一些輸入,得到了正確的輸出,就認為該代碼是正確的。他沒有利用程序員可用的其他工具對其代碼進行測試。同大多數的測試者不同,程序員可以在代碼中設置斷點,一步一步地跟蹤代碼的運行, 觀察輸入變?yōu)檩敵龅倪^程。盡管如此,但奇怪的是很少有程序員在進行代碼測試時習慣于對 其代碼進行逐條的跟蹤。許多程序員甚至不耐煩在代碼中設置一個斷點,以確定相應代碼是否被執(zhí)行到了。還是讓我們回到這一章開始所談論的問題上:捕捉錯誤的最好辦法是在編寫或修改程序時
6、進行相應的檢查。那么,程序員測試其程序的最好辦法是什么呢?是對其進行逐條的跟蹤, 對中間的結果進行認真的查看。對于能夠始終如一地編寫出沒有錯誤程序的程序員,我并不認識許多。但我所認識的幾個全都有對其程序進行逐條跟蹤的習慣。這就好比你在家時夜賊光臨了 除非此時你睡著了,否則就不會不知道麻煩來了。作為一個項目負責人,我總是教導許多程序員在進行代碼測試時,要對其代碼進行遍查,而他們總是會吃驚地看著我。這倒不是他們不同意我的看法,而是因為進行代碼遍查聽起來太費時間了。他們好容易才能趕得上進度,又哪有時間對其代碼進行逐條的跟蹤呢?幸好這 一直觀的感受是錯誤的。是的,對代碼進行逐條的跟蹤確實需要時間,但它
7、同編寫代碼相比,只是其一小部分。要知道,當實現一個新函數時,你必須為其設計出函數的外部界面,勾畫出相應的算法并把源程序全部輸入到計算機中。與此相比,在你第一次運行相應的的程序時,為其設置一個斷點, 按下“步進”鍵檢查每行的代碼又能多花多少時間呢?并不太多,尤其是在習慣成自然之后。 這就好比學習駕駛一輛手扳變速器的轎車,一開始好象不可能, 但練習了幾天以后,當需要變速時你甚至可以無意識地將其完成。同樣,一旦逐條地跟蹤代碼成為習慣之后,我們也會不加思索地設置斷點并對整個過程進行跟蹤。可以很自然地完成這一過程,并最后檢查出錯誤。代碼中的分支不要等到出了錯誤再對程序進行逐條的跟蹤當然有些技術可以使我們
8、更加有效地對代碼進行逐條的跟蹤。但是如果我們只對部分而不是全部的代碼進行逐條跟蹤,那么也不會取得特別好的效果。例如,所有的程序員都知道錯誤處理代碼常常有錯,其原因是這部分代碼極少被測試到,而且除非你專門對這部分代碼進行測試,否則這些錯誤就不會被發(fā)現。為了發(fā)現錯誤處理程序中的錯誤,我們可以建立使錯誤情況發(fā)生的測試用例, 或者在對代碼進行逐條跟蹤時可以對錯誤的情況進行模擬。后-種方法通常費時較少。例如,考慮下面的代碼中斷:pbBlock = (byte*)malloc(32);if( pbBlock = NULL )處理相應的錯誤情況通常在逐條跟蹤這段代碼時,malloc會分配一個32字節(jié)的內存塊
9、,并返回一個非NULL的指針值使其中的錯誤處理代碼被繞過。跟蹤這段代碼并在執(zhí)行完下行語句之后,但為了對該錯誤處理代碼進行測試, 可以再次逐條立即用跟蹤程序命令將 pbBlock置為NULL指針值:pbBlock =(byte*) malloc(32);雖然malloc可能分配成功,但將pbBlock置為NULL指針就相當于 malloc產生了分配失敗,從而使我們可以步進到相應的錯誤處理部分。(注意:在改變了 pbBlock的值之后,malloc剛分配的的內存塊即被丟失,但不要忘了這只是在做測試!)除了要對錯誤情況進行逐條的跟蹤之外,對程序中每一條可能的路徑都應該進行逐條的跟蹤。程序中具有多條代
10、碼路徑的明顯情況是if和switch語句,但還有一些其它的情況: && |和?:運算符,它們 每個都有兩條路徑。為了驗證程序的正確性,至少要對程序中的每條指令逐條跟蹤一遍。在做完了這件事之后,我們對程序中不含錯誤就有了更高的置信。至少我們知道對于某些輸入,相應的程序肯 定沒錯。如果測試用例選擇得好,代碼的逐條跟蹤會使我們受益非淺。對每一條代碼路徑進行逐條的跟蹤 大的變動過去程序員問過這樣的問題:“如果我增加的功能與許多地方的代碼都有關系怎么辦?那對所有增加的新代碼進行逐條的跟蹤不是太費時間了嗎?”假如你是這么想的,那么我不妨問你另一個問題:“如果你做了這么大的變動,在進行這些改
11、動時可能不引進任何的問題 嗎?“習慣于對代碼進行逐條跟蹤會產生一個有趣的負反饋回路。例如,對代碼進行逐條跟蹤的程序員很快就會學會編寫較小的容易測試的函數,因為對于大函數進行逐條的跟蹤非常痛苦。(測試一個10頁長的的函數比測試 10個一頁長的函數要難得多)程序員還會花更多的 時間去考慮如何使必需做的大變動局部化,以便能夠更容易地進行相應的測試。這些不正是我們所期望的嗎?沒有一個項目的負責人喜歡程序員做大的變動,它們會使整個項目太不穩(wěn)定。也沒有一個項目負責人喜歡大的、不好管理的函數,因為它們常常不好維護。如果發(fā)現必須做大的變動, 那么要檢查相應的改變并進行判斷。同時要記住,在大多數情況下,對代碼進
12、行逐條跟蹤所花的時間要比實現相應代碼所花的時間少得多。數據流程序的命脈在我編寫的第 2 章中介紹的快速 memset 函數之前,該函數的形式如下(不含斷言)void* memset( void *pv, byte b, size _tsize )byte pb=(byte*)pv;if( size >= sizeThreshold )unsigned long l;/* 用 4 個字節(jié)拼成一個長字 */l = (b<<24) | (b<<16) | (b<<8) | b;pb = (byte*)longfill( (long*)pb, 1, size/
13、4 );size = size 4;while( size- > 0 )*pb+ = b;return(pv);這段代碼看起來好象正確, 其實有個小錯誤。 在我編完了上述代碼之后, 我把它用到了一個現成的應用程序中, 結果沒有問題, 該函數工作得很好。 但為了確信該函數確實起作用了,我在該函數上設置了一個斷點并重新運行該應用程序。在進入代碼跟蹤程序得到了控制之后我檢查了該函數的參數: 其指針參數值看起來沒問題,大小參數亦如此, 字節(jié)參數值為零。這時我感到使用字節(jié)值 0 來測試這個函數真是太不應該,因為它使我很難觀察到許多類型的錯誤,所以我立即把字節(jié)參數的值改成了比較奇怪的0x4E。我首先
14、測試了 size 小于 sizeThreshold 的情況,那條路徑沒有問題。 隨后我測試了 size大于或等于 sizeThreshold 的情況, 本來我想也不會有什么問題。 但當我執(zhí)行了下條語句之后:l = (b<<24) | (b<<16) | (b<<8) | b;我發(fā)現I被置成了 0X00004E4E,而不是我所期望的值 Ox4E4E4E4E在對該函數進行匯 編語言的快速轉儲之后, 我發(fā)現了這一錯誤, 并且知道了為什么在有這個錯誤的情況下該應 用程序仍能工作。我用來編譯該函數的編譯程序將整數處理為16位。在整數為16位的情況下,b<<
15、24會產生什么樣的結果呢?結果會是0。同樣b<<16所產生的結果也會是 0。雖然這個程序在邏輯上并沒有什么錯誤, 但其具體的實現卻是錯的。 之所以該函數在相應應用程序中能夠工作, 是因為該應用程序使用 memset來把內存塊填寫為 0,而0<<24則仍是0,所以結果正確。我?guī)缀趿⒓淳桶l(fā)現了這個錯誤, 因為在把它擱置在一邊繼續(xù)往下走查之前, 我又多花了一點時間逐條跟蹤了這部分代碼。確實,這個錯誤很嚴重,最終一定會被發(fā)現。但要記住, 我們的目標是盡可能早地查出錯誤。對代碼進行逐條跟蹤可以幫助我們達到這個目標。對代碼進行逐條跟蹤的真正作用是它可以使我們觀察到數據在函數中的流動
16、。如果在對代碼進行逐條跟蹤時密切地注視數據流,就會幫助你查出下面這么多的錯誤:上溢和下溢錯誤;數據轉換錯誤;差1錯誤;NULL指針錯誤;使用廢料內存單元錯誤(0xA3類錯誤);用=代替=的賦值錯誤;運算優(yōu)先級錯誤;邏輯錯誤。如果不注重數據流,我們能發(fā)現所有這些錯誤嗎?注重數據流的價值在于它可以使你以另一種非常不同的觀點看待你的代碼。你也許沒能夠注意到下面程序中的賦值錯誤:if( ch =''Expa ndTab();但當你對其進行逐條跟蹤,密切注視其數據流時, 很容易就會發(fā)現ch的內容被破壞了。為什么編譯當序代碼進行逐條跟發(fā)出警要密切注視數據流在我用來測試本書中程序的五個編譯程
17、序中盡管每個編譯程序的警告級別都被設置到最大,但仍沒有一個編譯程序對于 b<<24這個錯誤發(fā)生警告。這一代碼雖然是合法的 ANSI C, 但我想象不出在什么情況下這一代碼實際能夠完成程序員的意圖。既然如此,為什么不給出警告呢?當你遇到這種錯誤,要告訴相應編譯程序的制造商,以使該編譯程序的新版本可以對這 種錯誤送出警告。不要低估作為一個花了錢的顧客你手中的權利。你遺漏了什么東西嗎?使用源級調試程序的一個問題是在執(zhí)行一行代碼時可能會漏掉某些重要的細節(jié)。例如,假定在下面的代碼中錯誤地將 &&輸入了 & :/*如果該符號存在并且它有對應的正文名字,*那么就釋放這個名
18、字*/if( psym != NULL & psym->strName != NULL )FreeMemory( psym->strName );psym->strName = NULL;這段程序雖然合法但卻是錯誤的。 if語句的使用目的是避免使用 NULL指針psym去引 用結構symbol的成員strName,但上面的代碼做的卻并不是這件事情。相反,不管psym的值是否為NULL這段程序總會引用 strName域。如果使用源級調試程序對代碼進行逐條跟蹤,并在到達該if語句時,按了“步進” 鍵,那么調試程序將把整個 if語句當做一個操作來執(zhí)行。如果發(fā)現了這個錯誤,你
19、就會注意到 即使在其表達式的左邊是FALSE的情況下,表達式的右邊仍會被執(zhí)行。(或者,如果你很幸運,當程序間接引用了 NULL指針時系統會出現錯誤。 但并沒有許多的臺式計算機會這樣做, 至少在目前它們不這樣做。)記得我們以前說過:& 和?:運算符都有兩條路徑,因此要查出錯誤就必須對每條路徑進行逐條的跟蹤。源級調試程序的問題是用一個單步就越過了&&、|和?:的兩條路徑。有兩個實用的方法可以解決這一問題。第一個方法,只要步進到使用&&和|運算符的復合條件語句,就掃描相應的一些條 件,驗證這些條件拼寫無誤然后使用調試程序命令顯示條件中每個比較的結果。這樣做可以
20、幫助我們查出在某些情況下雖然整個表達式的計算結果正確,但該表達式中確實有錯誤這一情況。例如,如果你認為在這種情況下|表達式的第一部分應該是TRUE第二部分應該是FALSE但其結果恰恰相反。此時雖然整個表達式的計算結果雖然正確,但表達式中卻有 錯誤。觀察表達式的各個部分可以發(fā)現這類問題。第二個,也是更徹底的方法是在匯編語言級步進到復合條件語句和?:運算符的內部。是的,這要花費更多的工夫,但對于關鍵的代碼, 為了觀看到中間的計算結果而對其內部的代碼實際地走上一遍是很重要的。這同對C語句進行逐條的跟蹤一樣,一旦你習慣之后。對匯編語言指令進行逐條地跟蹤也很快,只不過需要經過練習而已。關掉優(yōu)化?源級調試
21、程序可能會隱瞞執(zhí)行的細節(jié)對關鍵部分的代碼要進行匯編指令級的逐條跟蹤如果一有趣的練習。因為編譯程序在生成優(yōu)化的代碼時,可能會把相鄰源語句對應的機器代碼混在一塊。對于這種編譯程序,一條“單步”命令跳過三行代碼并非不常見;同樣,利用“單步”指令 執(zhí)行完一行將數據從一處送到另一處的源語句之后卻發(fā)現相應的數據尚未傳送過去的情況 也很常見。為了對代碼進行逐條跟蹤容易一些, 在編譯調試版本時可以考慮關掉不必要的編譯程序 優(yōu)化。這些優(yōu)化除了擾亂所生成的機器代碼之外, 毫無用處。 我聽到過某些程序員反對關掉 編譯程序的優(yōu)化功能他們認為這會在程序的調試版本和交付版本之問產生不必要的差別從 而帶來風險。 如果擔心編譯程序會產生代碼生成錯誤的話, 這種觀點還有點道理。 但同時我 們還應該想到, 我們建立調試版本的目的是要查出程序中的錯誤, 既然如此, 如果關掉編譯 的優(yōu)化功能可以幫助我們做到這點,那么就值得考慮。最好的辦法是對優(yōu)化過的代碼進行逐條的跟蹤, 先看看這樣做的困難有多大, 然后為了 有效地對代碼進行逐條跟蹤,只關閉那些你認為必須關閉的編譯程序優(yōu)化功能。小結我希望我知道一種能夠說服程序員對其代碼進行逐條跟蹤的方法, 或者至少能夠使他們 嘗試一個月。但是我發(fā)現,程序員一般說來都克服不了“那太費時間”這一想法。作為項目 負責人的一個好處是對
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024跨國廣告代理協議
- 2025年度產學研合作項目技術研發(fā)與市場應用協議4篇
- 2024年04月浙江臺州銀行寧波分行社會招考(422)筆試歷年參考題庫附帶答案詳解
- 2025年度分手后子女撫養(yǎng)協議書范本下載3篇
- 2025年度城市綜合體場地服務合作合同4篇
- 2025年度國際商務大廈廠房租賃合同英文版3篇
- 2024版智能穿戴設備技術轉讓合同
- 2025年度廠房設備融資租賃與市場拓展合同4篇
- 2024年03月重慶重慶銀行貿易金融部招考筆試歷年參考題庫附帶答案詳解
- 2025年度產學研合作人才培養(yǎng)及項目支持協議4篇
- 《線控底盤技術》2024年課程標準(含課程思政設計)
- 學校對口幫扶計劃
- 倉庫倉儲安全管理培訓課件模板
- 風力發(fā)電場運行維護手冊
- 《3-6歲兒童學習與發(fā)展指南》專題培訓
- 河道旅游開發(fā)合同
- 情人合同范例
- 建筑公司勞務合作協議書范本
- 安徽省合肥市2023-2024學年高一上學期物理期末試卷(含答案)
- 《基于杜邦分析法的公司盈利能力研究的國內外文獻綜述》2700字
- 儒家思想講解課程設計
評論
0/150
提交評論