1. 程式人生 > >linux核心除錯方法

linux核心除錯方法

核心開發比使用者空間開發更難的一個因素就是核心除錯艱難。核心錯誤往往會導致系統宕機,很難保留出錯時的現場。除錯核心的關鍵在於你的對核心的深刻理解。

一  除錯前的準備

在除錯一個bug之前,我們所要做的準備工作有:

  • 有一個被確認的bug。

  • 包含這個bug的核心版本號,需要分析出這個bug在哪一個版本被引入,這個對於解決問題有極大的幫助。可以採用二分查詢法來逐步鎖定bug引入版本號。

  • 對核心程式碼理解越深刻越好,同時還需要一點點運氣。

  • 該bug可以復現。如果能夠找到復現規律,那麼離找到問題的原因就不遠了。

  • 最小化系統。把可能產生bug的因素逐一排除掉。

二  核心中的bug

核心中的bug也是多種多樣的。它們的產生有無數的原因,同時表象也變化多端。從隱藏在原始碼中的錯誤到展現在目擊者面前的bug,其發作往往是一系列連鎖反應的事件才可能出發的。雖然核心除錯有一定的困難,但是通過你的努力和理解,說不定你會喜歡上這樣的挑戰。

三  核心除錯配置選項

學習編寫驅動程式要構建安裝自己的核心(標準主線核心)。最重要的原因之一是:核心開發者已經建立了多項用於除錯的功能。但是由於這些功能會造成額外的輸出,並導致能下降,因此發行版廠商通常會禁止發行版核心中的除錯功能。

1  核心配置

為了實現核心除錯,在核心配置上增加了幾項:

 Kernel hacking  --->      
[*]
   Magic SysRq key  [*]   Kernel debugging  [*]   Debug slab memory allocations    [*]   Spinlock and rw-lock debuggingbasic checks  [*]   Spinlock debuggingsleep-inside-spinlock checking             [*]   Compile the kernel with debug info    Device Drivers  --->               Generic Driver
 Options  --->             [*]   Driver Core verbose debug messages  General setup  --->             [*]   Configure standard kernel features (for small systems)  --->             [*]   Load all symbols for debugging/ksymoops

啟用選項例如:

slab layer debugging(slab層除錯選項) 
high-memory debugging(高階記憶體除錯選項) 
I/O mapping debugging(I/O對映除錯選項) 
spin-lock debugging(自旋鎖除錯選項) 
stack-overflow checking(棧溢位檢查選項) 
sleep-inside-spinlock checking(自旋鎖內睡眠選項)

2  除錯原子操作

從核心2.5開發,為了檢查各類由原子操作引發的問題,核心提供了極佳的工具。
核心提供了一個原子操作計數器,它可以配置成,一旦在原子操作過程中,進城進入睡眠或者做了一些可能引起睡眠的操作,就列印警告資訊並提供追蹤線索。
所以,包括在使用鎖的時候呼叫schedule(),正使用鎖的時候以阻塞方式請求分配記憶體等,各種潛在的bug都能夠被探測到。
下面這些選項可以最大限度地利用該特性:

CONFIG_PREEMPT = y 
CONFIG_DEBUG_KERNEL = y 
CONFIG_KLLSYMS = y 
CONFIG_SPINLOCK_SLEEP = y

四  引發bug並列印資訊

1  BUG()和BUG_ON()

一些核心呼叫可以用來方便標記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(); 
   }

五  printk()

核心提供的格式化列印函式。

1  printk函式的健壯性

      健壯性是printk最容易被接受的一個特質,幾乎在任何地方,任何時候核心都可以呼叫它(中斷上下文、程序上下文、持有鎖時、多處理器處理時等)。

2  printk函式脆弱之處

      在系統啟動過程中,終端初始化之前,在某些地方是不能呼叫的。如果真的需要除錯系統啟動過程最開始的地方,有以下方法可以使用:

  • 使用串列埠除錯,將除錯資訊輸出到其他終端裝置。

  • 使用early_printk(),該函式在系統啟動初期就有列印能力。但它只支援部分硬體體系。

3  LOG等級

       printk和printf一個主要的區別就是前者可以指定一個LOG等級。核心根據這個等級來判斷是否在終端上列印訊息。核心把比指定等級高的所有訊息顯示在終端。
       可以使用下面的方式指定一個LOG級別:
printk(KERN_CRIT  “Hello, world!\n”);
注意,第一個引數並不一個真正的引數,因為其中沒有用於分隔級別(KERN_CRIT)和格式字元的逗號(,)。KERN_CRIT本身只是一個普通的字串(事實上,它表示的是字串 "<2>";表 1 列出了完整的日誌級別清單)。作為預處理程式的一部分,C 會自動地使用一個名為 字串串聯 的功能將這兩個字串組合在一起。組合的結果是將日誌級別和使用者指定的格式字串包含在一個字串中。

核心使用這個指定LOG級別與當前終端LOG等級console_loglevel來決定是不是向終端列印。
下面是可使用的LOG等級:

#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                       */
#define KERN_DEFAULT     "<d>"/* Use the default kernel loglevel           */

注意,如果呼叫者未將日誌級別提供給 printk,那麼系統就會使用預設值 KERN_WARNING "<4>"(表示只有KERN_WARNING 級別以上的日誌訊息會被記錄)。由於預設值存在變化,所以在使用時最好指定LOG級別。有LOG級別的一個好處就是我們可以選擇性的輸出LOG。比如平時我們只需要列印KERN_WARNING級別以上的關鍵性LOG,但是除錯的時候,我們可以選擇列印KERN_DEBUG等以上的詳細LOG。而這些都不需要我們修改程式碼,只需要通過命令修改預設日誌輸出級別:

mtj@ubuntu :~$ cat /proc/sys/kernel/printk
4 4 1 7
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay
0
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit
5
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst
10

第一項定義了 printk API 當前使用的日誌級別。這些日誌級別表示了控制檯的日誌級別、預設訊息日誌級別、最小控制檯日誌級別和預設控制檯日誌級別。printk_delay 值表示的是 printk 訊息之間的延遲毫秒數(用於提高某些場景的可讀性)。注意,這裡它的值為 0,而它是不可以通過 /proc 設定的。printk_ratelimit 定義了訊息之間允許的最小時間間隔(當前定義為每 5 秒內的某個核心訊息數)。訊息數量是由 printk_ratelimit_burst 定義的(當前定義為 10)。如果您擁有一個非正式核心而又使用有頻寬限制的控制檯裝置(如通過串列埠), 那麼這非常有用。注意,在核心中,速度限制是由呼叫者控制的,而不是在printk 中實現的。如果一個 printk 使用者要求進行速度限制,那麼該使用者就需要呼叫printk_ratelimit 函式。

4  記錄緩衝區

  核心訊息都被儲存在一個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()呼叫時,新訊息將覆蓋佇列中的老訊息。
  ③ 在讀寫環形佇列時,同步問題很容易得到解決。

  ※ 這個紀錄緩衝區之所以稱為環形,是因為它的讀寫都是按照環形佇列的方式進行操作的。

5  syslogd/klogd

6  dmesg

dmesg 命令也可用於列印和控制核心環緩衝區。這個命令使用 klogctl 系統呼叫來讀取核心環緩衝區,並將它轉發到標準輸出(stdout)。這個命令也可以用來清除核心環緩衝區(使用 -c 選項),設定控制檯日誌級別(-n 選項),以及定義用於讀取核心日誌訊息的緩衝區大小(-s 選項)。注意,如果沒有指定緩衝區大小,那麼 dmesg 會使用 klogctl 的SYSLOG_ACTION_SIZE_BUFFER 操作確定緩衝區大小。

7 注意 

a) 雖然printk很健壯,但是看了原始碼你就知道,這個函式的效率很低:做字元拷貝時一次只拷貝一個位元組,且去呼叫console輸出可能還產生中斷。所以如果你的驅動在功能除錯完成以後做效能測試或者釋出的時候千萬記得儘量減少printk輸出,做到僅在出錯時輸出少量資訊。否則往console輸出無用資訊影響效能。
b) printk的臨時快取printk_buf只有1K,所有一次printk函式只能記錄<1K的資訊到log buffer,並且printk使用的“ringbuffer”.

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 
    …
https://my.oschina.net/fgq611/blog/113249