計算機程式設計原理/維護/除錯
除錯是診斷程式中的錯誤並確定如何糾正它們的藝術。 “錯誤”有多種形式,包括:編碼錯誤、設計錯誤、複雜互動、使用者介面設計不良以及系統故障。 因此,學習如何有效地除錯程式需要您學習如何識別您所面臨的哪種問題,並應用適當的技術來消除問題。
錯誤在整個軟體生命週期中都會出現。 程式設計師可能會發現問題,軟體測試人員可能會識別出問題,或者終端使用者可能會報告意外結果。 有效除錯的一部分包括使用適當的技術從不同的問題報告來源獲取必要的資訊。
程式設計中最常見的錯誤型別是
- 不假思索地程式設計
- 以非結構化的方式編寫程式碼
那麼這些不同型別的錯誤是什麼呢?
對於編碼錯誤,問題來源在於程式設計師造成的錯誤或不正確的程式碼。 編碼錯誤的一些例子包括
- 無視採用的約定。
- 呼叫錯誤的函式(“moveUp”,而不是“moveDown”)
- 在錯誤的位置使用錯誤的變數名(“moveTo(y, x)”,而不是“moveTo(x, y)”)
- 未能初始化變數(“y = x + 1”,其中 x 未設定)時絕對需要。
- 跳過對錯誤返回的檢查。
軟體使用者很容易看到一些設計錯誤,而在其他情況下,設計缺陷會使程式更難改進或修復,並且這些缺陷對於使用者來說並不明顯。 明顯的設計缺陷通常由超出計算機限制執行的程式證明,例如可用記憶體、可用磁碟空間、可用處理器速度以及壓倒性的輸入/輸出裝置。 更難的設計錯誤分為幾個類別
- 未能隱藏複雜性
- 不完整或模稜兩可的“契約”
- 未記錄的副作用
複雜互動錯誤出現在單個程式的多個部分、多個程式或多臺計算機互動的場景中。
使用者介面設計不良通常會導致使用者以實現與其意圖不同的方式使用該程式。 例如,網站的“搜尋”頁面可能有一個“不區分大小寫”搜尋選項。 當用戶難以找到或看到該選項時,該使用者可能會報告一些資料“丟失”的錯誤,僅僅因為案例敏感搜尋未找到該資料。
有時,計算機硬體 simply fails,並且通常以完全出乎意料的方式出現故障。 確定問題不在於軟體本身,而在於其執行的計算機(s)通常很複雜,因為除錯軟體的人員可能無法訪問顯示問題的硬體。
關於軟體除錯的討論,如果沒有討論如何首先預防錯誤,那就不會完整。 無論你寫程式碼寫得多好,如果你寫錯了程式碼,它對任何人都沒有幫助。 如果你建立了正確的程式碼,但使用者無法使用使用者介面,那麼你可能沒有編寫程式碼。 簡而言之,一個好的偵錯程式應該對問題可能存在的位置保持開放的態度。
雖然討論各種避免錯誤的技術超出了本次討論的範圍,但這裡提到的許多技術在事後同樣有用,當你遇到錯誤需要發現並修復它時。 因此,下面將簡要介紹一下。
為了編寫有效的軟體,開發人員必須解決使用者需要解決的問題。 自然地,使用者不會以嚴格的演算法、視窗系統、網頁或命令列介面進行思考。 相反,使用者可能不會以與開發人員思考問題相同的方式思考問題。
為了解決這種差異,請與目標使用者坐在一起,詢問他們希望從軟體中獲得什麼。 使用者經常希望軟體能夠提供的比實際所能提供的更多,或者擁有相互矛盾的目標,例如功能更強大的軟體,但不需要他們學習任何新東西。 簡而言之,詢問使用者他們的目標是什麼。 如果沒有這些目標,使用者將繼續報告沒有形成連貫整體的錯誤。
單元測試是指檢查當前模組可以進入的所有可能狀態中會發生什麼。 因此,您應該準備一個“測試列表”,其中定義了當前模組的所有可能輸入。
例如:我們有一個程式,它從使用者那裡獲取正數並對其進行處理。 首先,我們需要檢查輸入是否為數字(它可以是字元),然後我們將檢查它是否為正數。 透過檢查,我的意思是輸入輸入並檢視會發生什麼。
提示:當您開始編寫此測試列表時,您會注意到預測所有可能性相當困難; 如果您有選擇詢問其他人(沒有幫助編寫模組)來提供幫助,這可能會卓有成效。
雖然每次除錯體驗都是獨一無二的,但在除錯時可以應用某些一般原則。 本節特別討論除錯軟體,儘管這些原則中的許多原則也可以應用於除錯硬體。
除錯的基本步驟是
- 識別錯誤的存在
- 隔離錯誤來源
- 確定錯誤原因
- 確定錯誤修復方法
- 應用修復並進行測試
檢測錯誤可以主動進行,也可以被動進行。
經驗豐富的程式設計師通常知道錯誤更有可能發生在哪裡,這取決於程式部分的複雜程度以及可能的資料損壞。例如,從使用者那裡獲得的任何資料都應該被視為可疑。應該非常小心地驗證資料的格式和內容是否正確。從傳輸中獲取的資料應該被檢查以確保接收到了整個訊息(資料)。必須解析和/或處理的複雜資料可能包含未預料到的值組合,並且沒有被正確處理。透過插入對可能錯誤症狀的檢查,程式可以檢測到資料何時被破壞或未被正確處理。
如果錯誤嚴重到導致程式異常終止,則錯誤的存在就會變得很明顯。如果程式檢測到不太嚴重的問題,則可以識別出錯誤,前提是監控錯誤和/或日誌訊息。但是,如果錯誤很小並且只會導致錯誤的結果,則很難檢測到錯誤的存在;如果難以或不可能驗證程式的結果,這尤其如此。
此步驟的目的是識別錯誤的症狀。觀察問題的症狀,在什麼條件下檢測到問題以及發現了哪些解決方法(如果有的話)將極大地幫助其餘步驟除錯問題。
此步驟通常是除錯中最困難(因此也是最有意義的)步驟。其目的是確定系統中哪個部分導致了錯誤。不幸的是,問題的根源並不總是與症狀的根源相同。例如,如果輸入記錄已損壞,則在程式處理不同的記錄或執行基於錯誤資訊的某些操作時,可能不會發生錯誤,這可能在讀取記錄很久之後才會發生。
此步驟通常涉及迭代測試。程式設計師可能會首先驗證輸入是否正確,然後驗證是否正確讀取了輸入,是否正確處理了輸入等等。對於模組化系統,透過檢查不同模組之間介面傳遞的資料的有效性,此步驟可能更容易。如果輸入正確,但輸出不正確,則錯誤的根源位於模組內。透過迭代測試輸入和輸出,除錯人員可以在幾行程式碼內識別出錯誤發生的位置。
熟練的除錯人員通常能夠假設問題可能出在哪裡(基於與以前類似情況的類比),並測試程式可疑區域的輸入和輸出。這種形式的除錯是科學方法的例項。不太熟練的除錯人員通常會按順序逐步執行程式,查詢程式的行為與預期不同的位置。請注意,這仍然是一種科學方法,因為程式設計師必須決定在尋找異常行為時檢查哪些變數。另一種方法是使用“二分查詢”型別的隔離過程。透過測試資料/處理流程中間部分附近的區域,程式設計師可以確定錯誤是發生在程式的較早部分還是較晚部分。如果未檢測到任何資料問題,則錯誤可能在流程的後面。
找到錯誤的位置後,下一步是確定錯誤的實際原因,這可能涉及程式的其他部分。例如,如果已經確定程式出現故障是因為某個欄位錯誤,則下一步是確定為什麼該欄位錯誤。這是錯誤的實際根源,儘管有些人會爭辯說,程式無法處理錯誤資料也可以被視為錯誤。
對系統的良好理解對於成功識別錯誤的根源至關重要。經過訓練的除錯人員可以隔離問題的根源,但只有熟悉系統的人才能準確地識別錯誤背後的實際原因。在某些情況下,它可能是外部的:輸入資料不正確。在其他情況下,它可能是由於邏輯錯誤,即正確的資料被錯誤處理。其他可能性包括意外值,即最初的假設是給定欄位只能具有“n”個值,而實際上它可以具有更多值,以及不同欄位中意外的值組合(欄位 x 只應該在欄位 y 是其他東西時才具有該值)。另一種可能性是錯誤的參考資料,例如包含與損壞記錄相關的錯誤值的查詢表。
確定錯誤原因後,最好檢查程式碼的類似部分,以檢視是否在其他地方重複了相同的錯誤。如果錯誤明顯是打字錯誤,則可能性較小,但如果原始程式設計師誤解了初始設計和/或需求,則可能在其他地方犯下了相同或類似的錯誤。
確定了問題根源後,下一步是確定如何修復問題。除了最簡單的問題外,對現有系統的深入瞭解至關重要。這是因為修復將修改系統的現有行為,這可能會產生意想不到的結果。此外,修復現有錯誤通常會建立額外的錯誤,或者暴露程式中已經存在的其他錯誤,但由於原始錯誤,這些錯誤從未暴露出來。這些問題通常是由程式執行以前未經測試的程式碼分支或在以前未經測試的條件下執行的程式碼分支引起的。
在某些情況下,修復簡單而明顯。對於邏輯錯誤,尤其如此,在這種情況下,原始設計被錯誤地實現。另一方面,如果問題揭示了貫穿系統很大一部分的主要設計缺陷,那麼修復的範圍可能從困難到不可能,需要對應用程式進行徹底重寫。
在某些情況下,可能希望實現“快速修復”,然後進行更永久的修復。此決定通常是透過考慮問題的嚴重程度、可見性、頻率和副作用以及修復的性質和產品時間表(例如,是否有更緊迫的問題?)來做出的。
應用修復後,重要的是測試系統並確定修復是否正確地處理了以前的問題。測試應完成兩個目的:(1) 修復是否現在可以正確地處理原始問題,以及 (2) 確保修復沒有建立任何不良副作用。
對於大型系統,最好有迴歸測試,一系列測試執行來練習系統。在進行重大更改和/或錯誤修復後,可以隨時重複這些測試以驗證系統是否仍按預期執行。隨著新功能的新增,可以將其他測試包含在測試套件中。
可以採取具體步驟來減少花費在除錯軟體上的時間。這些在下面的部分中列出。
當你開始除錯程式時,你能做的最重要的事情可能是意識到你並不理解發生了什麼。確信自己的程式應該正常工作的程式設計師不太可能找到錯誤,僅僅是因為他們拒絕承認自己的困惑。如果程式按照你認為的方式執行,你就不會除錯,程式會正常工作。即使程式看起來正常,如果你以至少存在一個錯誤並且你會找到它這樣的想法來檢查它,那麼你就更有可能發現程式的錯誤。
當你最清楚問題可能出現的地方通常是在最初設計和編寫程式碼的時候。透過在程式的不同位置插入完整性檢查,程式本身可以檢測和報告問題。除了檢測問題之外,還應考慮如何最好地處理每個錯誤。選項包括
- 報告錯誤,將無效欄位設定為預設值,並繼續
- 報告錯誤,丟棄與無效值關聯的記錄,並繼續
- 報告錯誤,將無效記錄轉移到單獨的檔案/表中,以便使用者可以檢查並可能糾正問題
- 報告錯誤並終止程式
來自使用者(包括外部系統)的任何資料都應受到懷疑。仔細驗證所有此類輸入資料,執行語法和語義完整性檢查。這種無效資料是程式設計錯誤的常見來源。不要只考慮錯誤輸入的資料,還要考慮惡意資料,例如 緩衝區溢位 利用。
如果資料是使用者互動式輸入的,你可以提供適當的 錯誤訊息 並允許使用者更正無效欄位。如果資料不是來自互動式源,則應按照上述方法處理錯誤記錄。
將資訊寫入日誌檔案的程式可以提供重要的資訊,這些資訊可用於分析在遇到問題之前、期間和之後發生的事情。透過建立各種日誌檔案可以減少要搜尋的條目數量,例如為系統的每個主要元件建立一個單獨的日誌,另外再建立一個專門用於錯誤的日誌檔案。每個條目都應進行日期/時間標記,以便可以將來自不同日誌的條目關聯起來。
一組標準測試,可以執行這些測試來執行 迴歸測試,可以幫助在錯誤進入生產環境之前找到它們。這些測試用例應儘可能自動化,以減少執行這些測試所需的精力。隨著向系統新增新功能,應建立其他測試來測試這些功能。
進行大量更改時,應增量應用它們。新增一項更改,然後徹底測試該更改,然後再進行下一項更改。這將減少新錯誤的可能來源。如果同時應用了幾個不同的更改,那麼就很難確定問題的來源。此外,不同區域的輕微錯誤會相互作用,產生可能在逐個應用更改時永遠不會發生的錯誤。
如果你做了一個更改來修復一個問題,但程式的行為仍然相同,在繼續之前撤銷這些更改。你的更改沒有產生任何效果,表明以下幾種情況之一
- 問題不在你認為的地方
- 你修改的區域要麼沒有被呼叫,要麼沒有按照你認為的方式被呼叫
- 假設你更改的部分沒有執行,你可能引入了新的錯誤,這些錯誤只有在修復當前錯誤後才會出現
在不同架構下可用的程式(例如,MS Windows、MacOSX、Linux 等作業系統或 Intel Pentium、PowerPC 或 DEC Alpha 等處理器)有時在其他系統上會有不同的反應(尤其是在後續錯誤方面)。有時在不同的架構上找到錯誤要容易得多。
當發現一個錯誤時,考慮可能出現相同錯誤的其他地方。檢查這些地方,看看是否存在相同的問題。
並非每種型別的程式都以相同的方式進行除錯,並非所有技術都可以用於所有型別的程式。
除錯的主要角色是偵錯程式。它是一個與新編寫的程式同時執行的軟體,允許你暫停程式並讀取記憶體地址、堆疊以及程式的其他通常不可見部分。
另一種除錯方法是日誌檔案。輸出某些變數的內容可以提供有關程式如何執行的有價值的資訊。輸出包含函式名稱的字串(在呼叫函式時)可以在定位錯誤引入的位置方面非常有用。為了找到程式崩潰的位置,使用偵錯程式更加實用。
大型程式難以除錯,小型程式(相對)容易除錯。因此,關鍵是將大型程式變成許多小型程式來進行除錯。這被稱為“單元測試”,它涉及將程式的一部分(一個例程、一組相關例程、一個模組甚至一個完整的子系統)與額外的程式碼一起編譯,以允許它在沒有其餘程式碼的情況下執行。
全屏應用程式(尤其是遊戲)難以除錯,因為你無法看到偵錯程式的輸出。解決方案在於使用空閒調變解調器電纜、第二臺計算機和一個終端程式(例如 Hyper-terminal)。透過空閒調變解調器電纜將偵錯程式的輸出管道到第二臺計算機。
e.g. In Dos with gdb using a serial null modem cable:
Configure the port with mode: mode COM2: 9600,n,8,1,none Pipe the output to COM2 by adding >COM2 when you invoke the debugger.
大多數語言都支援自己的特殊除錯技術
某些平臺有特殊的除錯技術
- 除錯 Linux 核心:Linux 核心#除錯
- 除錯 Palm OS 應用程式:為 Palm OS 程式設計/PrcTools#使用 GDB 除錯
- 可程式設計邏輯/除錯設計