萬盛學電腦網

 萬盛學電腦網 >> 服務器教程 >> Linux系統驅動開發調試技術指南

Linux系統驅動開發調試技術指南

   一、使用printk

  這是驅動開發中最樸實無華,同時也是最常用和有效的手段。scull驅動的main.c第338行如下,就是使用printk進行調試的例子,這樣的例子相信大家在閱讀驅動源碼時隨處可見。

  printk(KERN_ALERT "wakeup by signal in process %dn", current->;pid);

  printk的功能與我們經常在應用程序中使用的printf是一樣的,不同之處在於printk可以在打印字符串前面加上內核定義的宏,例如上面例子中的KERN_ALERT(注意:宏與字符串之間沒有逗號):

  #define KERN_EMERG ""

  #define KERN_ALERT ""

  #define KERN_CRIT ""

  #define KERN_ERR ""

  #define KERN_WARNING ""

  #define KERN_NOTICE ""

  #define KERN_INFO ""

  #define KERN_DEBUG ""

  #define DEFAULT_CONSOLE_LOGLEVEL 7

  這個宏是用來定義需要打印的字符串的級別,值越小,級別越高。內核中有個參數用來控制是否將printk打印的字符串輸出到控制台(屏幕或者/sys/log/syslog日志文件)

  # cat /proc/sys/kernel/printk

  6 4 1 7

  第一個6表示級別高於(小於)6的消息才會被輸出到控制台,第二個4表示如果調用printk時沒有指定消息級別(宏)則消息的級別為4,第三個1表示接受的最高(最小)級別是1,第四個7表示系統啟動時第一個6原來的初值是7。

  因此,如果你發現在控制台上看不到你程序中某些printk的輸出,請使用 echo 8 > /proc/sys/kernel/printk 來解決。

  我們在復雜驅動的開發過程中,為了調試會在源碼中加入成百上千的printk語句。而當調試完畢形成最終產品的時候必然會將這些printk語句刪除(為什麼?想想你自己是驅動的使用者而不是開發者吧。記住:己所不欲,勿施於人),這個工作量是不小的。最要命的是,如果我們將調試用的printk語句刪除後,用戶又報告我們的驅動有bug,所以我們又不得不手工將這些上千條的printk語句再重新加上。OMG,殺了我吧。所以,我們需要一種能方便地打開和關閉調試信息的手段。哪裡能找到這種手段呢?哈哈,遠在天邊,近在眼前。看看scull驅動或者leds驅動的源代碼吧!

  #define LEDS_DEBUG

  #undef PDEBUG

  #ifdef LEDS_DEBUG

  #ifdef __KERNEL__

  #define PDEBUG(fmt, args…) printk( KERN_EMERG "leds: " fmt, ## args)

  #else

  #define PDEBUG(fmt, args…) fprintf(stderr, fmt, ## args)

  #endif

  #else

  #define PDEBUG(fmt, args…)

  #endif

  #undef PDEBUGG

  #define PDEBUGG(fmt, args…)

  這樣一來,在開發驅動的過程中,如果想打印調試消息,我們就可以用 PDEBUG("address of i_cdev is %pn", inode->i_cdev); ,如果不想看到該調試消息,就只需要簡單的將 PDEBUG 改為 PDEBUGG 即可。而當我們調試完畢形成最終產品時,只需要簡單地將第1行注釋掉即可。

  上邊那一段代碼中的__KERNEL__是內核中定義的宏,當我們編譯內核(包括模塊)時,它會被定義。當然如果你不明白代碼中的…和##是什麼意思的話,就請認真查閱一下gcc關於預處理部分的資料吧!如果你實在太懶不願意去查閱的話,那就充當VC工程師把上面的代碼copy到你的代碼中去吧。

  二、查看OOP消息

  OOP意為驚訝。當你的驅動有問題,內核不驚訝才怪:嘿!小子,你干嗎亂來!好吧,讓我們來看看內核是如何驚訝的。

  根據 faulty.c 編譯出faulty.ko,並 insmod faulty.ko。執行 echo yang > /dev/faulty ,結果內核就驚訝了。內核為什麼會驚訝呢?因為faulty驅動的write函數執行了*(int *)0 = 0,向內存0地址寫入,這是內核絕對不會容許的。

  ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)

  {

  *(int *)0 = 0;

  return 0;

  }

  Unable to handle kernel NULL pointer dereference at virtual address 00000000

  pgd = c3894000

  [00000000] *pgd=33830031, *pte=00000000, *ppte=00000000

  Internal error: Oops: 817 [#1] PREEMPT

  Modules linked in: faulty scull

  CPU: 0 Not tainted (2.6.22.6 #4)

  PC is at faulty_write+0×10/0×18 [faulty]

  LR is at vfs_write+0xc4/0×148

  pc : [] lr : [] psr: a0000013

  sp : c3871f44 ip : c3871f54 fp : c3871f50

  r10: 4021765c r9 : c3870000 r8 : 00000000

  r7 : 00000004 r6 : c3871f78 r5 : 40016000 r4 : c38e5160

  r3 : c3871f78 r2 : 00000004 r1 : 40016000 r0 : 00000000

  Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user

  Control: c000717f Table: 33894000 DAC: 00000015

  Process sh (pid: 745, stack limit = 0xc3870258)

  Stack: (0xc3871f44 to 0xc3872000)

  1f40: c3871f74 c3871f54 c0088eb8 bf00608c 00000004 c38e5180 c38e5160

  1f60: c3871f78 00000000 c3871fa4 c3871f78 c0088ffc c0088e04 00000000 00000000

  1f80: 00000000 00000004 40016000 40215730 00000004 c002c0e4 00000000 c3871fa8

  1fa0: c002bf40 c0088fc0 00000004 40016000 00000001 40016000 00000004 00000000

  1fc0: 00000004 40016000 40215730 00000004 00000001 00000000 4021765c 00000000

  1fe0: 00000000 bea60964 0000266c 401adb40 60000010 00000001 00000000 00000000

  Backtrace:

  [] (faulty_write+0×0/0×18 [faulty]) from [] (vfs_write+0xc4/0×148)

  [] (vfs_write+0×0/0×148) from [] (sys_write+0x4c/0×74)

  r7:00000000 r6:c3871f78 r5:c38e5160 r4:c38e5180

  [] (sys_write+0×0/0×74) from [] (ret_fast_syscall+0×0/0x2c)

  r8:c002c0e4 r7:00000004 r6:40215730 r5:40016000 r4:00000004

  Code: e1a0c00d e92dd800 e24cb004 e3a00000 (e5800000)

  1行驚訝的原因,也就是報告出錯的原因;

  2-4行是OOP信息序號;

  5行是出錯時內核已加載模塊;

  6行是發生錯誤的CPU序號;

  7-15行是發生錯誤的位置,以及當時CPU各個寄存器的值,這最有利於我們找出問題所在地;

  16行是當前進程的名字及進程ID

  17-23行是出錯時,棧內的內容

  24-29行是棧回溯信息,可看出直到出錯時的函數遞進調用關系(確保CONFIG_FRAME_POINTER被定義)

  30行是出錯指令及其附近指令的機器碼,出錯指令本身在小括號中

  反匯編 faulty.ko:

  arm-linux-objdump -D faulty.ko > faulty.dis ;cat faulty.dis

  可以看到如下的語句如下:

  0000007c :

  7c: e1a0c00d mov ip, sp

  80: e92dd800 stmdb sp!, {fp, ip, lr, pc}

  84: e24cb004 sub fp, ip, #4 ; 0×4

  88: e3a00000 mov r0, #0 ; 0×0

  8c: e5800000 str r0, [r0]

  90: e89da800 ldmia sp, {fp, sp, pc}

  定位出錯位置以及獲取相關信息的過程:

  9 pc : [] lr : [] psr: a0000013

  25 [] (faulty_write+0×0/0×18 [faulty]) from [] (vfs_write+0xc4/0×148)

  26 [] (vfs_write+0×0/0×148) from [] (sys_write+0x4c/0×74)

  出錯代碼是 faulty_write 函數中的第5條指令((0xbf00608c-0xbf00607c)/4+1=5),該函數的首地址是0xbf00607c,該函數總共6條指令(0×18),該函數是被0xc0088eb8的前一條指令調用的(即:函數返回地址是0xc0088eb8。這一點可以從出錯時lr的值正好等於0xc0088eb8得到印證)。調用該函數的指令是vfs_write的第49條(0xc4/4=49)指令。

  達到出錯處的函數調用流程是:write(用戶空間的系統調用) –> sys_write –> vfs_write -> faulty_write .

  OOP消息不僅讓我定位了出錯的地方,更讓我驚喜的是,它讓我知道了一些秘密:1、gcc中fp到底有何用處?2、為什麼gcc編譯任何函數的時候,總是要把3條看上去傻傻的指令放在整個函數的最開始?3、內核和gdb是如何知道函數調用棧順序,並使用函數的名字而不是地址? 4、我如何才能知道各個函數入棧的內容?哈哈,我漸漸喜歡上了讓內核驚訝,那就再看一次內核驚訝吧。

  執行 cat /dev/faulty,內核又再一次驚訝!

  1 Unable to handle kernel NULL pointer dereference at virtual address 0000000b

  2 pgd = c3a88000

  3 [0000000b] *pgd=33a79031, *pte=00000000, *ppte=00000000

  4 Internal error: Oops: 13 [#2] PREEMPT

  5 Modules linked in: faulty

  6 CPU: 0 Not tainted (2.6.22.6 #4)

  7 PC is at vfs_read+0xe0/0×140

  8 LR is at 0xffffffff

copyright © 萬盛學電腦網 all rights reserved