BUAA OS實驗調試指南:從看懂到看開

通常的調試流程其實很簡單:發現問題,穩定復現,肯定臨界條件,定位問題,修復問題,覈查結果。迭代這個過程,造成一個閉環python

老實說,OS的實驗代碼,開箱體驗極差,程序跳來跳去,進了Lab4後還要考慮內核態切換,很難靠肉眼完成上述閉環。debug愉悅指數爲負。編程

因此在幾周的探索後,我大概總結整理了一些調試經驗,主要是如何在當前體系下利用或構建調試工具,改善調試體驗。編程語言

咱們的口號是:沒有蛀牙函數

拋磚引玉。工具

從0開始:如今咱們有什麼

如今咱們手裏有:編碼

  • 一坨新鮮的系統代碼。
  • 上學期積累的老練的MIPS 32彙編開發與理論基礎,雖然這個系統用的是MIPS 2
  • 對高級語言特別是面嚮對象語言的美好眷戀
  • 全宇宙最牛逼的編程語言:C,以及全宇宙最牛逼的C tool chain:GCC

除此以外咱們還有:debug

  • 湊活能用的printf
  • 助教爲咱們封裝好的panicassert,她們是海灘上最搶眼的寶貝兒
  • 用戶態的一些函數,但說真的它們不太好用
  • 一顆勇敢的心

Level 0:靜態調試

也就是所謂的「瞪眼法」。能瞪出來的Bug請直接瞪出來。調試

瞪眼法的升級版是小黃鴨調試法,進一步升級版是面向室友調試,終極形態是面向男友/女友/老媽調試code

別漏了這一步,有時候瞪是最快的,但這也要求你對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語言程序的,彙編則不太好辦。大致上有兩個思路:

  1. 爲某一部分特定信息的輸出單獨封裝一個函數,並在彙編中JAL跳轉連接調用,好比上機時咱們寫過的output_ov_info(我怎麼可能記得它叫什麼,反正差很少就這個東西)。這個東西使用場景太雜亂,不展開了
  2. 加斷點,斷點是好文明。

MIPS爲咱們提供了BREAK和一系列Trap指令,這裏先只討論Break的使用,Trap是相似的。

BREAK的做用是,拋出一個bp斷點異常並使CPU切入內核態處理異常。bp的cause編碼是9,爲了讓咱們的小系統能夠處理這個異常咱們須要仿照課上的ov爲其添加一個handler

  1. 修改lib/trap.c,聲明外連接extern handle_bp();,而後將其綁定在9號異常上
  2. 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(目前我還沒遇到),沉着冷靜處理,好比更換函數聲明的位置
  • 少造輪子,多看代碼
  • 舒服的纔是好的,按本身的使用習慣改造本身的調試工具與調試流程
  • 不要嘗試學個人代碼風格,我已經沒有救了,你還有
相關文章
相關標籤/搜索