Linux 的內核調試

※ 調試工作艱苦,是內核級開發區別於用戶級開發的一個顯著特點。
※ 駕馭內核調試的能力,很大程度上取決於經驗和對整個操作系統的把握。
 
一、調試前的準備
   內核級bug具有行爲不可靠定義不清晰或者說很難再現的諸多特定,爲內核級的bug跟蹤和調試帶來了很大的困難。
   ※ 對於一些定義不清楚地bug,問題的關鍵就是找到bug的源頭,很多時候,當你精確地重現一個bug的時候,你就離成功不遠了。
 
二、內核中的bug
   從隱藏在源代碼中的錯誤到展現在目擊者面前的bug,其發作往往是一系列連鎖反應的事件纔可能出發的。
   雖然內核調試有一定的困難,但是通過你的努力和理解,說不定你會喜歡上這樣的挑戰
 
三、printk( )
 內核提供的格式化打印函數。
 1、printk函數的健壯性
    健壯性是printk最容易被接受的一個特質,幾乎在任何地方,任何時候內核都可以調用它(中斷上下文、進程上下文、持有鎖時、多處理器處理時等)。
   ※ 在系統啓動過程中,終端初始化之前,在某些地方是不能調用的。
 
 2、記錄等級
    printk函數可以指定一個記錄級別,內核根據這個級別來判斷是否在終端上打印消息。
    記錄級別定義在<linux/kernel.h>中:
    
#define    KERN_EMERG    "<0>"   /* system is unusable */
#define    KERN_ALERT    "<1>"    /* action must be taken immediately    */
#define    KERN_CRIT        "<2>"    /* critical conditions */
#define    KERN_ERR        "<3>"    /* error conditions    */
#define    KERN_WARNING    "<4>"    /* warning conditions */
#define    KERN_NOTICE   "<5>"    /* normal but significant condition    */
#define    KERN_INFO        "<6>"    /* informational */
#define    KERN_DEBUG    "<7>"   /* debug-level messages    */
 
 調用方式:printk(KER_DEBUG “This is a debug notice!/n”);
 內核用這個指定的紀錄等級和當前終端的紀錄等級console_loglevel比較,來決定是不是向終端打印。
 
 關於< linux/kernel.h >的console_loglevel 定義:
 #define console_loglevel (console_printk[0])
 <printk.c>定義:
 int console_printk[4] = {
           DEFAULT_CONSOLE_LOGLEVEL,  /* console_loglevel */
           DEFAULT_MESSAGE_LOGLEVEL, /* default_message_loglevel */
           MINIMUM_CONSOLE_LOGLEVEL,  /* minimum_console_loglevel */
           DEFAULT_CONSOLE_LOGLEVEL, /* default_console_loglevel */
 };
 
 3、記錄緩衝區
   內核消息都被保存在一個LOG_BUF_LEN大小的環形隊列中。
   關於LOG_BUF_LEN定義:
   #define __LOG_BUF_LEN     (1 << CONFIG_LOG_BUF_SHIFT)
   ※ 變量CONFIG_LOG_BUF_SHIFT在內核編譯時由配置文件定義,對於i386平臺,其值定義如下(在linux26/arch/i386/defconfig中):
   CONFIG_LOG_BUF_SHIFT=18
 
   記錄緩衝區操作
   ①、消息被讀出到用戶空間時,此消息就會從環形隊列中刪除。
   ②、當消息緩衝區滿時,如果再有printk()調用時,新消息將覆蓋隊列中的老消息。
   ③、在讀寫環形隊列時,同步問題很容易得到解決。
   ※ 這個紀錄緩衝區之所以稱爲環形,是因爲它的讀寫都是按照環形隊列的方式進行操作的。
 
 4、syslogd和klogd
   在標準的Linux系統上,用戶空間的守護進程klogd從紀錄緩衝區中獲取內核消息,再通過syslogd守護進程把這些消息保存在系統日誌文件中。klogd進程既可以從/proc/kmsg文件中,也可以通過syslog()系統調用讀取這些消息。默認情況下,它選擇讀取/proc方式實現。klogd守護進程在消息緩衝區有新的消息之前,一直處於阻塞狀態。一旦有新的內核消息,klogd被喚醒,讀出內核消息並進行處理。默認情況下,處理例程就是把內核消息傳給syslogd守護進程。
   syslogd守護進程一般把接收到的消息寫入/var/log/messages文件中。不過,還是可以通過/etc/syslog.conf文件來進行配置,可以選擇其他的輸出文件。
  圖1 X光了此過程:
  
 
四、OOPS
 OOPS(也稱 Panic)消息包含系統錯誤的細節,如 CPU 寄存器的內容等。是內核告知用戶有不幸發生的最常用的方式。
 內核只能發佈OOPS,這個過程包括向終端上輸出錯誤消息,輸出寄存器保存的信息,並輸出可供跟蹤的回溯線索。通常,發送完OOPS之後,內核會處於一種不穩定的狀態。
 OOPS的產生有很多可能原因,其中包括內存訪問越界或非法的指令等。
 ※ 作爲內核的開發者,必定將會經常處理OOPS。
 ※ OOPS中包含的重要信息,對所有體系結構的機器都是完全相同的:寄存器上下文和回溯線索(回溯線索顯示了導致錯誤發生的函數調用鏈
 
 1、ksymoops
    在 Linux 中,調試系統崩潰的傳統方法是分析在發生崩潰時發送到系統控制檯的 Oops 消息。一旦您掌握了細節,就可以將消息發送到 ksymoops 實用程序,它將試圖將代碼轉換爲指令並將堆棧值映射到內核符號。
    ※ 如:回溯線索中的地址,會通過ksymoops轉化成名稱可見的函數名。
    圖2X光了格式化 Oops 消息過程:
   
 
    ksymoops需要幾項內容:Oops 消息輸出、來自正在運行的內核的 System.map 文件,還有 /proc/ksyms、vmlinux 和 /proc/modules。
關於如何使用 ksymoops,內核源代碼 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊頁上有完整的說明可以參考。Ksymoops 反彙編代碼部分,指出發生錯誤的指令,並顯示一個跟蹤部分表明代碼如何被調用。
 
 2、kallsyms
    開發版2.5內核引入了kallsyms特性,它可以通過定義CONFIG_KALLSYMS編譯選項啓用。該選項可以載入內核鏡像所對應的內存地址的符號名稱(即函數名),所以內核可以打印解碼之後的跟蹤線索。相應,解碼OOPS也不再需要System.map和ksymoops工具了。另外,
這樣做,會使內核變大些,因爲地址對應符號名稱必須始終駐留在內核所在內存上。
    #cat /proc/kallsyms
     c0100240   T       _stext
     c0100240   t       run_init_process
     c0100240   T      stext
     c0100269   t       init
        …
 
五、內核調試配置選項
 在編譯內核的時候,爲了方便調試和測試代碼,內核提供了許多配置選項。
 ※  啓用選項例如:
   slab layer debugging(slab層調試選項)、high-memory debugging(高端內存調試選項)、I/O mapping debugging(I/O映射調試選項)、spin-lock debugging(自旋鎖調試選項)、stack-overflow checking(棧溢出檢查選項)和sleep-inside-spinlock checking(自旋鎖內睡眠選項)等。
 
1、調試原子操作
 從內核2.5開發,爲了檢查各類由原子操作引發的問題,內核提供了極佳的工具。
 內核提供了一個原子操作計數器,它可以配置成,一旦在原子操作過程中,進城進入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息並提供追蹤線索。
 所以,包括在使用鎖的時候調用schedule(),正使用鎖的時候以阻塞方式請求分配內存等,各種潛在的bug都能夠被探測到。
 下面這些選項可以最大限度地利用該特性:
 CONFIG_PREEMPT = y
 CONFIG_DEBUG_KERNEL = y
 CONFIG_KLLSYMS = y
 CONFIG_SPINLOCK_SLEEP = y
 
六、引發bug並打印信息
 1、一些內核調用可以用來方便標記bug,提供斷言並輸出信息。最常用的兩個是BUG()BUG_ON()
 定義在<include/asm-generic>中:

#ifndef HAVE_ARCH_BUG
#define BUG() do { 
    printk(
"BUG: failure at %s:%d/%s()! ", __FILE__, __LINE__, __FUNCTION__); 
    panic(
"BUG!");   /* 引發更嚴重的錯誤,不但打印錯誤消息,而且整個系統業會掛起 */
while (0)
#endif

#ifndef HAVE_ARCH_BUG_ON
    
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0)
#endif
  
當調用這兩個宏的時候,它們會引發OOPS,導致棧的回溯和錯誤消息的打印
※ 可以把這兩個調用當作斷言使用,如:BUG_ON(bad_thing);
 
 2、dump_stack()
    有些時候,只需要在終端上打印一下棧的回溯信息來幫助你調試。這時可以使用dump_stack()。這個函數只在終端上打印寄存器上下文和函數的跟蹤線索。
    if (!debug_check) {
        printk(KERN_DEBUG “provide some information…/n”);
        dump_stack();
    }
 備注:大部分內容引自《Linux內核設計實現 - 2版》

發佈了32 篇原創文章 · 獲贊 23 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章