精華區beta mud_sanc 關於我們 聯絡資訊
中階 LPC Descartes of Borg November 1993 第七章: 除錯 7.1 錯誤的種類 至今, 你大概已經到處碰過各式各樣的錯誤. 一般上, 你可能看到的錯誤有三種: 編譯時段錯誤 (compile time error) 、執行時段錯誤 (run time error) 、故 障的程式碼 (malfunctioning code). 在大多數的 mud 中, 你會找到一個私人 的檔案, 裡頭記錄著你的編譯時段錯誤. 對大多數人來說, 你可以在你的家 (home) 目錄找到名叫 "log" 或 ".log" 的檔案, 或在 "/log" 目錄找到以你 的名字命名的檔案. 另外, mud 執行時, 會維持一份執行時段錯誤的紀錄. 而此 檔案也在 "/log" 目錄中. 對 MudOS mud 來說, 它叫做 "debug.log". 其他的 mud 中, 稱為不同的名字, 像是 "lpmud.log". 如果你還不知道編譯時段和執行 時段錯誤紀錄在哪裡, 請詢問你的系統管理者. 編譯時段錯誤是 driver 試著載入一個物件到記憶體的時候發生的錯誤. 如果此 時它看不懂你寫的東西, 它會無法把物件載入記憶體, 並在你私人的錯誤紀錄檔 中記錄為什麼它無法載入該物件. 最普遍的編譯時段錯誤是打字錯誤、遺漏或多 加 () , {}, [], ""、沒有正確宣告物件所使用的函式和變數. 執行時段錯誤是一個在記憶體中的物件, 當它執行某段敘述時所發生的錯誤. 舉 例來說, driver 不可能知道任何情況下, "x/y" 是否有效. 實際上, 它是一個 有效的 LPC 運算式. 但是, 如果 y 的值為 0, 則會發生執行時段錯誤, 因為 你不能除以 0. 當 driver 執行一個函式時碰上錯誤, 它放棄執行函式並紀錄在 遊戲執行時段錯誤紀錄檔中. 如果有定義、如果玩家是創作者, driver 也會對 this_player() 顯示錯誤訊息, 不然就只對玩家顯示 "什麼 ?". 大多數導致執 行時段錯誤的原因, 是不正確的值和試著執行沒有定義運算資料型態的運算式. 不過, 最狡猾的錯誤種類, 就是故障的程式碼. 這些錯誤不會紀錄下來, 因為 driver 永遠不可能知道有地方出錯. 簡單地說, 這種錯誤就是你認為程式碼做 的是一件事, 但是實際上它做的是另一件事. 常遇到這種錯誤的人, 一定會認定 是 mudlib 或 driver 的錯誤. 每個人都製造過各式各樣的錯誤, 而更常見的不 是程式碼不按照它該運作的的方式工作, 而是你錯讀它. 7.2 修正編譯時段錯誤 編譯時段錯誤是最常見以及最容易修正的錯誤. 新手程式撰寫人常常因為一些怪 異的錯誤訊息, 而感到挫折. 雖然如此, 只要一個人變得習慣於他們 driver 產 生的錯誤訊息, 修正編譯時段錯誤就成了例行公事. 在你的錯誤紀錄中, driver 會告訴你錯誤的種類, 還有它最後在第幾行注意到 該錯誤. 注意, 這不表示此行一定是錯誤實際發生的地方. 除了打字錯誤, 最常 見的編譯時段錯誤是遺漏或多加各式括號和引號: (), [], {}, "". 這種是最常 困擾新手程式撰寫人的錯誤, 因為 driver 不會注意到遺漏或多加的部分, 直到 稍後出問題為止. 以下是範例: 1 int test(string str) { 2 int x; 3 for(x =0; x<10; x++) 4 write(x+"\n"); 5 } 6 write("Done.\n"); 7 } 看你想做的是什麼, 此處實際上的錯誤在第三行 (表示你遺漏了一個 {) 或第五 行 (表示你多加一個 }) . 但是, driver 會回報它在第六行找到一個錯誤. 實 際的 driver 訊息每種 driver 可能都不一樣, 但是不管是哪一種 driver, 你 會看到第六行產生一個錯誤. 因為第五行的 } 會解釋為 test() 函式結束. 在 第六行, driver 看見你有一個 write() 出現在函式定義之外, 所以回報為錯 誤. 一般來說, driver 也會繼續回報它在第七行找到一個多加 } 的錯誤. 修正這種錯誤的秘訣在於程式撰寫風格. 將結束的 } 與該子句開頭的 { 垂直 對齊, 在你除錯時, 會讓你看到你哪裡遺漏它們. 同樣, 當你使用多組括號時, 像這樣用空白將各組分開: if( (x=sizeof(who=users()) > ( (y+z)/(a-b) + (-(random(7))) ) ) 你可以看到, for() 敘述的括號與其餘的敘述以空白隔開. 另外, 個別的子群也 用空白隔開, 讓它們在產生錯誤時易於找出. 一旦你擁有幫助你找出錯誤的程式撰寫風格, 你就會學到哪一種錯誤訊息傾向於 指出哪一種錯誤. 修正此種錯誤時, 你會檢查出問題的那一行之前與之後的程式 碼. 大多數的情況下, 你會直接找到錯誤. 另一種普遍的編譯時段錯誤是 driver 回報一個不明的 identifier. 一般來說, 打字錯誤和錯誤宣告變數導致此種錯誤. 幸運的是, 錯誤紀錄檔中幾乎都能告訴 你錯誤所發生的實際位置. 所以修正此種錯誤時, 進入編輯程式並找到出問題的 該行. 如果該問題出在變數上而不是打字錯誤, 請確定你正確地宣告該變數. 另 一方面, 如果是打字錯誤, 就改正它 ! 但是, 小心一件事, 這種錯誤有時候會與遺漏括號的錯誤結合在一起. 在這種情 形下, 不明 identifier 的問題常常是誤報. driver 誤讀 {} 或其他東西, 而 導致變數宣告混淆. 因此在煩惱此種錯誤困擾之前, 請確定已修正所有其他的編 譯時段錯誤. 與前述的錯誤同一級的是普通的語法錯誤. 當 driver 無法了解你寫的東西時, 它就產生此種錯誤. 這又常是打字錯誤引起的, 卻也是因為不了解某些特徵正確 的語法所致, 像是把 for() 敘述寫成這樣: for(x=0, x<10, x++) 如果你像這樣的錯誤, 卻不是語法錯誤, 試著重新檢查錯誤發生的敘述中, 語法 是否正確. 7.3 修正執行時段錯誤 執行時段錯誤比起編譯時段錯誤要複雜得多. 幸運的是, 這些錯誤都有紀錄, 但 是許多創作人並不了解, 或是他們不知道紀錄在哪裡. 執行時段錯誤的紀錄一般 也紀錄得比編譯時段錯誤詳細, 也就是你可以從它開始到它出錯之處, 追蹤執行 程序的過程. 所以你可以利用紀錄檔, 使用前編譯器敘述 (precompiler statement) 設置除錯陷阱 (debugging trap). 但是, 執行時段錯誤常肇因於 複雜的程式撰寫技巧, 而初學者並不使用這些技巧. 這表示你一般會碰上比簡單 的編譯時段錯誤還要複雜的錯誤. 執行時段錯誤幾乎都是肇因於使用錯誤的 LPC 資料型態. 最常見的是, 試著用 NULL 值的物件變數做外界呼叫, 索引指向 NULL 值的映射、陣列、字串變數, 或函式傳入錯誤的參數. 我們看一個 Nightmare 真實的執行時段錯誤: Bad argument 1 to explode() 程式: bin/system/_grep.c, 物件: bin/system/_grep 第 32 行 ' cmd_hook' in ' std/living.c' (' std/user#4002') 第 83 行 ' cmd_grep' in ' bin/system/_grep.c' (' bin/system/_grep') 第 32 行 Bad argument 2 to message() 程式: adm/obj/simul_efun.c, 物件: adm/obj/simul_efun 第 34 行 ' cmd_hook' in ' std/living.c' (' std/user#4957') 第 83 行 ' cmd_look' in ' bin/mortal/_look.c' (' bin/mortal/_look') 第 23 行 ' examine_object' in ' bin/mortal/_look.c' (' bin/mortal/_look') 第 78 行 ' write' in 'adm/obj/simul_efun.c' (' adm/obj/simul_efun') 第 34 行 Bad argument 1 to call_other() 程式: bin/system/_clone.c, 物件: bin/system/_clone 第 25 行 ' cmd_hook' in ' std/living.c' (' std/user#3734') 第 83 行 ' cmd_clone' in ' bin/system/_clone.c' (' bin/system/_clone') 第 25 行 Illegal index 程式: std/monster.c, 物件: wizards/zaknaifen/spy#7205 第 76 行 ' heart_beat' in ' std/monster.c' ('wizards/zaknaifen/spy#7205') 第 76 行 除了最後一個以外, 所有的錯誤, 都對一個函式傳入一個錯誤的參數. 第一個錯 誤, 是對 explode() 傳入錯誤的第一個參數. explode() 外部函式要一個字串 當作第一個參數. 修正這類型的錯誤時, 我們會到 /bin/system/_grep.c 的第 32 行檢查第一個傳入參數到底其資料型態為何. 在此情況下, 傳入的值應是字 串. 如果因為某些原因, 我實際上傳入其他的東西, 我在此只要確定傳入字串就能修 正錯誤. 但是在此情況要複雜得多. 我需要追蹤傳入 explode() 的變數值為何, 我才能知道傳入 explode() 外部函式的值到底是什麼. 出問題的那行是: borg[files[i]] = regexp(explode(read_file(files[i]), "\n"), exp); files 是一個字串陣列, i 是整數, borg 是映射. 所以很明顯, 我們需要找出 read_file(file[i]) 的值到底是什麼. 好, read_file() 這個外部函式傳回一 個字串, 除非該檔案根本不存在, 或是該物件沒有權限讀取該檔案, 或是該檔案 是個空的檔案, 這些情形都會導致此函式傳回 NULL. 很明顯, 我們的問題是這 些情形的其中一種. 要找出是哪一種, 我們要看 file[i]. 檢查程式碼, 這個檔案陣列透過 get_dir() 外部函式取得它的值. 如果該物件 有權限讀取此目錄, get_dir() 就傳回目錄中所有的檔案. 所以問題不在於權限 不足或檔案不存在. 導致這個錯誤的檔案一定是空的. 而且事實上, 這就是導致 錯誤的原因. 要修正此錯誤, 我們要透過 filter_array() 外部函式傳入檔案, 確定只有檔案大小大於 0 的檔案可以讀入陣列. 修正執行時段錯誤的關鍵在於, 了解有問題的所有變數值在產生錯誤之時, 它們 確實的值為何. 你閱讀你的執行時段錯誤紀錄時, 小心地經由錯誤發生的檔案分 辨物件. 舉個例子, 上面的索引錯誤是物件 /wizard/zaknaifen/spy 產生, 但 是錯誤發生在執行它所繼承的 /std/monster.c 函式. 7.4 故障的程式碼 你所遇到最陰險的問題, 就是你程式碼的行為不是預期中的行為. 物件順利載入, 沒有產生任何執行時段錯誤, 但是事情就是不對勁. 既然 driver 不可能認出這 種錯誤, 就沒有任何紀錄. 所以你需要一行接一行瀏覽整個程式碼, 並搞清楚到 底發生了什麼事. 第一步: 找出你已知能順利執行的最後一行程式碼. 第二步: 找出你已知開始出錯的第一行程式碼. 第三步: 從已知順利執行的地方到第一個出錯的地方, 檢查程式碼的流程. 常常, 這些問題出現於你使用 if() 敘述沒有料到所有的可能情形. 舉個例: int cmd(string tmp) { if(stringp(tmp)) return do_a() else if(intp(tmp)) return do_b() return 1; } 在此段程式碼中, 我們發現它編譯和執行起來沒有問題. 問題是它執行起來完全 沒作用. 我們確定 cmd() 函式已經執行, 所以我們可以從此著手. 我們也知道 實際上 cmd() 傳回 1, 因為我們輸入此命令時, 沒看到 "什麼 ?" . 馬上, 我 們可以看到因為某些原因, tmp 變數有字串或整數以外的值. 就此得到的答案是, 我們輸入的命令沒有參數, 所以 tmp 是 NULL , 並讓所有的測試條件失敗. 上面的例子相當簡單, 幾近於愚蠢. 但是, 它讓你知道在修正故障的程式碼時, 如何檢查程式碼的流程. 其他的工具能協助你除錯. 最重要的工具就是使用前編 譯器來除錯. 以前面的例子來說, 我們有一個子句檢查傳入 cmd() 的整數. 我 們輸入 "cmd 10" 時, 我們希望執行 do_b() . 進入迴圈之前, 我們需要看 tmp 的值為何: #define DEBUG int cmd(string tmp) { #ifdef DEBUG write(tmp); #endif if(stringp(tmp)) return do_a(); else if(intp(tmp)) return do_b(); else return 1; } 在我們輸入命令之後, 立刻就知道 tmp 的值是 "10" . 回頭看程式碼, 我們會 怪自己愚蠢, 忘了我們把 tmp 當整數使用之前, 必須要用 sscanf() 把命令參 數轉換成整數. 7.5 總結 修正任何 LPC 問題的關鍵是, 永遠要知道你程式碼中任何一步的變數值為何. LPC 的執行降到變數值改變這種最簡單的層級上, 所以程式碼載入記憶體時, 不 正確的值導致錯誤發生. 如果你遇到函式有不正確的參數時, 常常是你對一個函 式傳入 NULL 值的參數. 這情形常發生於物件, 因為大家常常會做以下的事: 1) 使用設定在一個物件中的值, 而該物件已經被摧毀. 2) 使用 this_player() 的傳回值, 而根本沒有 this_player(). 3) 使用 this_object() 的傳回值, 而 this_object() 剛好已被摧毀. 另外, 大家會常常遇上不合法的索引 (illegal indexing) 或索引指向不合法的 型態 (indexing on illegal types). 最常見的是因為有問題的映射或陣列沒有 初始化, 所以無法索引之. 關鍵在於了解出問題的地方, 其陣列和映射的完整值. 另外, 注意索引編號是否比陣列的長度還大. 最後, 使用前編譯器暫時扔出或扔進顯示變數值的程式碼. 前編譯器讓你很容易 地刪掉除錯程式碼. 你只需要在除錯完畢之後, 刪除 DEBUG 定義. Copyright (c) George Reese 1993 譯者: Spock of the Final Frontier 98.Jul.29.