一般的調試流程其實很簡單:發現問題,穩定復現,確定臨界條件,定位問題,修復問題,核查結果。迭代這個過程,形成一個閉環
老實說,OS的實驗代碼,開箱體驗極差,程序跳來跳去,進了Lab4后還要考慮內核態切換,很難靠肉眼完成上述閉環。debug愉悅指數為負。
所以在幾周的探索后,我大概總結整理了一些調試經驗,主要是如何在當前體系下利用或構建調試工具,改善調試體驗。
我們的口號是:沒有蛀牙。
拋磚引玉。
從0開始:現在我們有什么
現在我們手里有:
- 一坨新鮮的系統代碼。
 - 上學期積累的老練的MIPS 32匯編開發與理論基礎,雖然這個系統用的是MIPS 2
 - 對高級語言特別是面向對象語言的美好眷戀
 - 全宇宙最牛逼的編程語言:C,以及全宇宙最牛逼的C tool chain:GCC
 
除此之外我們還有:
- 湊活能用的
printf - 助教為我們封裝好的
panic和assert,她們是海灘上最搶眼的寶貝兒 - 用戶態的一些函數,但說真的它們不太好用
 - 一顆勇敢的心
 
Level 0:靜態調試
也就是所謂的“瞪眼法”。能瞪出來的Bug請直接瞪出來。
瞪眼法的升級版是小黃鴨調試法,進一步升級版是面向室友調試,終極形態是面向男朋友/女朋友/老媽調試
別漏了這一步,有時候瞪是最快的,但這也要求你對OS實驗到底要干什么有完全清晰的把控。
記得調VIM配色,記得買珍視明。
Level 1: 斷言與防御性編程
assert這個東西可能我們已經在各種check里面見過了。它的實現在include/mm.h中(很奇怪的位置),在我們的代碼中是以宏的形式實現的:
#define assert(x) do {if(!(x)) panic("...%s", #x); } while(0)
 
        作者十分老練,這里的trick是用拼接運算將參數x作為一個字符串傳入panic,這樣輸出時我們就可以看到斷言內的具體內容。
現在assert的功能就是這樣的:如果傳入的語句x布爾值為假,就陷入panic(順帶一提,panic其實就是在輸出信息后陷入死循環)。熟悉其他語言的斷言機制,比如python,的同學應該會感到十分親切
這個東西有什么用呢?我們可以在代碼中合適的位置加入斷言,來顯示地檢查某段運行邏輯是否如我們的預期。
舉個例子,page_insert后,插入的頁面所對應的物理地址應該等於虛擬地址va所映射的物理地址(這正是這個函數的功能),因此在函數的末尾我們可以添加一句斷言:
assert(va2pa(pgdir, va) == page2pa(pp));
 
        當程序運行到這里時,如果沒有出現錯誤,那么這句assert不會產生任何作用,如果出現了各種情況導致這兩個物理地址不一致,那么程序就會自陷終止並輸出信息。當我們調試時,如果看到了它自陷,我們就能知道這里的代碼一定存在邏輯漏洞(或者,大災難),在它引起更隱晦的錯誤前將其修復。
當然assert也不是完全沒有副作用:它依舊會占用指令運行周期數,這個問題我們之后再討論。
Level 2:把printf撒的滿屋子都是
 
        然后因為忘了刪調試語句,喜提評測0/100
Level 3:更好用的輸出調試信息:封裝一個debug()
 
         
         
        printf的問題在於,開關並不方便。很難做到只做少量的修改將所有調試信息全部關閉。因此這里開始我們需要自己造輪子
稍微優雅一些的做法是,聲明一個調試宏或一個全局變量,來控制全局的調試開關
宏的寫法類似於:
# ifndef __DEBUG__
# define __DEBUG__
# endif
...
# ifdef __DEBUG__
printf("SOME DEBUG INFO\n");
# endif
 
        全局變量的寫法則是
extern int is_debug_mode = 1;
...
if (is_debug_mode) {
    printf("SOME DEBUG INFO\n");
}
 
        宏的一個優勢在於,如果我們關掉了調試,整個調試代碼塊不會被編譯,執行的語句更少
直接用變量則當然更可讀,更美觀,並且可以更輕松地實現調試信息分級
無論哪一種,每次輸出的時候敲三行代碼太復雜了,因此終極形式是包裝一個debug函數
void _debug(char * file, int line, char *fmt, ...){
    if (is_debug_mode) printf("SAY SOMETHING I'M GIVING UPON YOU.\n");
}
# define debug(...) _debug(__FILE__, __LINE__, __VA__ARGS__)
 
        這樣我們就能像使用printf一樣使用debug(支持可變參數),同時還能輸出更多調試信息,比如這一句在哪個文件的哪一行
然后在init中控制debug的全局開閉就好
Level 3.5 匯編調試:相信聰明的MIPS一定幫我們搞定了
上面的各種方法都是針對C語言程序的,匯編則不太好辦。大體上有兩個思路:
- 為某一部分特定信息的輸出單獨封裝一個函數,並在匯編中JAL跳轉鏈接調用,比如上機時我們寫過的
output_ov_info(我怎么可能記得它叫什么,反正差不多就這個東西)。這個東西使用場景太雜亂,不展開了 - 加斷點,斷點是好文明。
 
MIPS為我們提供了BREAK和一系列Trap指令,這里先只討論Break的使用,Trap是類似的。
BREAK的作用是,拋出一個bp斷點異常並使CPU切入內核態處理異常。bp的cause編碼是9,為了讓我們的小系統可以處理這個異常我們需要仿照課上的ov為其添加一個handler
- 修改
lib/trap.c,聲明外鏈接extern handle_bp();,然后將其綁定在9號異常上 - 在
lib/genex.S中添加handle_bp的處理程序。推薦使用BUILD_HANDLER bp break_handler cli將異常處理移交給一個C語言函數break_handler(struct Trapframe *)完成 
然后我們在異常處理中輸出各種需要的信息,末尾死循環即可(也就是,panic)。最重要的信息或許是pc。
如果想要更好的體驗,添加一個讀入字符syscall(參考lab1某次課上),讓我們可以向系統輸入字符,然后把break的死循環換成等待讀入。這么做的時候記得處理中斷屏蔽
回過頭看C程序的調試,用類似的技巧也可以實現斷點調試。如果需要可以再封裝一個bp()配合debug()使用。
再回過頭看Trap(TEQ、TNE等一系列條件內陷語句),我們可以用類似的方法為其添加異常處理程序,這些指令拋出的異常都是TRAP(13),因此我們只需要把處理程序掛在13號異常上就可以令其運作。
Trap很像MIPS版的assert,當滿足某個條件時讓內核自陷。因此在匯編代碼中善用這些指令也能起到盡早發現並規避錯誤的作用。
或許我們可以進一步挖掘MIPS的片上調試,但目前我還沒有遇到這種粒度的需求。
Level 4
更多奇技淫巧,我自己還在探索與試用,如果體驗不錯的話之后再更新。
 stay tuned
注意事項
- 自己實現的函數,聲明盡量放在單獨的頭文件中,定義盡量放在單獨的源文件中,所有東西盡可能封裝,做到可插拔。萬一課上把自己實現的某個關鍵文件替換掉導致CE(目前我還沒遇到),沉着冷靜處理,比如更換函數聲明的位置
 - 少造輪子,多看代碼
 - 舒服的才是好的,按自己的使用習慣改造自己的調試工具與調試流程
 - 不要嘗試學我的代碼風格,我已經沒有救了,你還有
 
