目錄[-]
一 ?調(diào)試前的準(zhǔn)備
二 ?內(nèi)核中的bug
三 ?內(nèi)核調(diào)試配置選項(xiàng)
1 ?內(nèi)核配置
2 ?調(diào)試原子操作
四 ?引發(fā)bug并打印信息
1 ?BUG()和BUG_ON()
2 ?dump_stack()
五 ?printk()
1 ?printk函數(shù)的健壯性
2 ?printk函數(shù)脆弱之處
3 ?LOG等級(jí)
4 ?記錄緩沖區(qū)
5 ?syslogd/klogd
6 ?dmesg
7 注意?
8 內(nèi)核printk和日志系統(tǒng)的總體結(jié)構(gòu)
9 ?動(dòng)態(tài)調(diào)試
六 ?內(nèi)存調(diào)試工具
1 ?MEMWATCH
2 ?YAMD
3 ?Electric Fence
七 ?strace
八 ?OOPS
1 ?ksymoops
2 ?kallsyms
3 ?Kdump
九 ?KGDB
1 ?kgdb的調(diào)試原理
2 ?Kgdb的安裝與設(shè)置
3 ?在VMware中搭建調(diào)試環(huán)境
4 ?kgdb的一些特點(diǎn)和不足
十 ?使用SkyEye構(gòu)建Linux內(nèi)核調(diào)試環(huán)境
1 ?SkyEye的安裝和μcLinux內(nèi)核編譯
2 ?使用SkyEye調(diào)試
3 ?使用SkyEye調(diào)試內(nèi)核的特點(diǎn)和不足
十一 ?KDB
1 ?入門
2 ?初始化并設(shè)置環(huán)境變量
3 ?激活 KDB
4 ?KDB 命令
5 ?技巧和訣竅
6 ?結(jié)束語
十二 ?Kprobes
1 ?安裝
2 ?編寫 Kprobes 模塊
3 ?使用 Kprobes 更好地進(jìn)行調(diào)試
內(nèi)核開發(fā)比用戶空間開發(fā)更難的一個(gè)因素就是內(nèi)核調(diào)試艱難。內(nèi)核錯(cuò)誤往往會(huì)導(dǎo)致系統(tǒng)宕機(jī),很難保留出錯(cuò)時(shí)的現(xiàn)場。調(diào)試內(nèi)核的關(guān)鍵在于你的對(duì)內(nèi)核的深刻理解。?
一 ?調(diào)試前的準(zhǔn)備
在調(diào)試一個(gè)bug之前,我們所要做的準(zhǔn)備工作有:?
有一個(gè)被確認(rèn)的bug。
包含這個(gè)bug的內(nèi)核版本號(hào),需要分析出這個(gè)bug在哪一個(gè)版本被引入,這個(gè)對(duì)于解決問題有極大的幫助??梢圆捎枚植檎曳▉碇鸩芥i定bug引入版本號(hào)。
對(duì)內(nèi)核代碼理解越深刻越好,同時(shí)還需要一點(diǎn)點(diǎn)運(yùn)氣。
該bug可以復(fù)現(xiàn)。如果能夠找到復(fù)現(xiàn)規(guī)律,那么離找到問題的原因就不遠(yuǎn)了。
最小化系統(tǒng)。把可能產(chǎn)生bug的因素逐一排除掉。
二 ?內(nèi)核中的bug
內(nèi)核中的bug也是多種多樣的。它們的產(chǎn)生有無數(shù)的原因,同時(shí)表象也變化多端。從隱藏在源代碼中的錯(cuò)誤到展現(xiàn)在目擊者面前的bug,其發(fā)作往往是一系列連鎖反應(yīng)的事件才可能出發(fā)的。雖然內(nèi)核調(diào)試有一定的困難,但是通過你的努力和理解,說不定你會(huì)喜歡上這樣的挑戰(zhàn)。?
三 ?內(nèi)核調(diào)試配置選項(xiàng)
學(xué)習(xí)編寫驅(qū)動(dòng)程序要構(gòu)建安裝自己的內(nèi)核(標(biāo)準(zhǔn)主線內(nèi)核)。最重要的原因之一是:內(nèi)核開發(fā)者已經(jīng)建立了多項(xiàng)用于調(diào)試的功能。但是由于這些功能會(huì)造成額外的輸出,并導(dǎo)致能下降,因此發(fā)行版廠商通常會(huì)禁止發(fā)行版內(nèi)核中的調(diào)試功能。?
1 ?內(nèi)核配置
為了實(shí)現(xiàn)內(nèi)核調(diào)試,在內(nèi)核配置上增加了幾項(xiàng):
Kernel hacking ---> [*] Magic SysRq key [*] Kernel debugging [*] Debug slab memory allocations [*] Spinlock and rw-lock debugging: basic checks [*] Spinlock debugging: sleep-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
啟用選項(xiàng)例如:?
slab layer debugging(slab層調(diào)試選項(xiàng)) high-memory debugging(高端內(nèi)存調(diào)試選項(xiàng)) I/O mapping debugging(I/O映射調(diào)試選項(xiàng)) spin-lock debugging(自旋鎖調(diào)試選項(xiàng)) stack-overflow checking(棧溢出檢查選項(xiàng)) sleep-inside-spinlock checking(自旋鎖內(nèi)睡眠選項(xiàng))
2 ?調(diào)試原子操作
從內(nèi)核2.5開發(fā),為了檢查各類由原子操作引發(fā)的問題,內(nèi)核提供了極佳的工具。?
內(nèi)核提供了一個(gè)原子操作計(jì)數(shù)器,它可以配置成,一旦在原子操作過程中,進(jìn)城進(jìn)入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追蹤線索。?
所以,包括在使用鎖的時(shí)候調(diào)用schedule(),正使用鎖的時(shí)候以阻塞方式請(qǐng)求分配內(nèi)存等,各種潛在的bug都能夠被探測到。?
下面這些選項(xiàng)可以最大限度地利用該特性:?
CONFIG_PREEMPT = y CONFIG_DEBUG_KERNEL = y CONFIG_KLLSYMS = y CONFIG_SPINLOCK_SLEEP = y
四 ?引發(fā)bug并打印信息
1 ?BUG()和BUG_ON()
一些內(nèi)核調(diào)用可以用來方便標(biāo)記bug,提供斷言并輸出信息。最常用的兩個(gè)是BUG()和BUG_ON()。
定義在中:
#ifndef HAVE_ARCH_BUG #define BUG() do { printk("BUG: failure at %s:%d/%s()! ", __FILE__, __LINE__, __FUNCTION__); panic("BUG!"); /* 引發(fā)更嚴(yán)重的錯(cuò)誤,不但打印錯(cuò)誤消息,而且整個(gè)系統(tǒng)業(yè)會(huì)掛起 */ } while (0) #endif #ifndef HAVE_ARCH_BUG_ON #define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0) #endif
當(dāng)調(diào)用這兩個(gè)宏的時(shí)候,它們會(huì)引發(fā)OOPS,導(dǎo)致棧的回溯和錯(cuò)誤消息的打印。?
※ 可以把這兩個(gè)調(diào)用當(dāng)作斷言使用,如:BUG_ON(bad_thing);?
2.WARN(x) 和 WARN_ON(x)
而WARN_ON則是調(diào)用dump_stack,打印堆棧信息,不會(huì)OOPS
定義在中:
#ifndef __WARN_TAINT#ifndef __ASSEMBLY__extern void warn_slowpath_fmt(const char *file, const int line, const char *fmt, ...) __attribute__((format(printf, 3, 4)));extern void warn_slowpath_fmt_taint(const char *file, const int line, unsigned taint, const char *fmt, ...) __attribute__((format(printf, 4, 5)));extern void warn_slowpath_null(const char *file, const int line);#define WANT_WARN_ON_SLOWPATH#endif#define __WARN() warn_slowpath_null(__FILE__, __LINE__)#define __WARN_printf(arg...) warn_slowpath_fmt(__FILE__, __LINE__, arg)#define __WARN_printf_taint(taint, arg...) warn_slowpath_fmt_taint(__FILE__, __LINE__, taint, arg)#else#define __WARN() __WARN_TAINT(TAINT_WARN)#define __WARN_printf(arg...) do { printk(arg); __WARN(); } while (0)#define __WARN_printf_taint(taint, arg...) do { printk(arg); __WARN_TAINT(taint); } while (0)#endif#ifndef WARN_ON#define WARN_ON(condition) ({ int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on)) __WARN(); unlikely(__ret_warn_on); })#endif#ifndef WARN#define WARN(condition, format...) ({ int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on)) __WARN_printf(format); unlikely(__ret_warn_on); })#endif
3 dump_stack()
有些時(shí)候,只需要在終端上打印一下棧的回溯信息來幫助你調(diào)試。這時(shí)可以使用dump_stack()。這個(gè)函數(shù)只在終端上打印寄存器上下文和函數(shù)的跟蹤線索。?
if (!debug_check) { printk(KERN_DEBUG “provide some information…/n”); dump_stack(); }
五 ?printk()
內(nèi)核提供的格式化打印函數(shù)。?
1 ?printk函數(shù)的健壯性
健壯性是printk最容易被接受的一個(gè)特質(zhì),幾乎在任何地方,任何時(shí)候內(nèi)核都可以調(diào)用它(中斷上下文、進(jìn)程上下文、持有鎖時(shí)、多處理器處理時(shí)等)。?
2 ?printk函數(shù)脆弱之處
在系統(tǒng)啟動(dòng)過程中,終端初始化之前,在某些地方是不能調(diào)用的。如果真的需要調(diào)試系統(tǒng)啟動(dòng)過程最開始的地方,有以下方法可以使用:?
使用串口調(diào)試,將調(diào)試信息輸出到其他終端設(shè)備。
使用early_printk(),該函數(shù)在系統(tǒng)啟動(dòng)初期就有打印能力。但它只支持部分硬件體系。
3 ?LOG等級(jí)
printk和printf一個(gè)主要的區(qū)別就是前者可以指定一個(gè)LOG等級(jí)。內(nèi)核根據(jù)這個(gè)等級(jí)來判斷是否在終端上打印消息。內(nèi)核把比指定等級(jí)高的所有消息顯示在終端。?
???????可以使用下面的方式指定一個(gè)LOG級(jí)別:?
printk(KERN_CRIT ?“Hello, world!
”);?
注意,第一個(gè)參數(shù)并不一個(gè)真正的參數(shù),因?yàn)槠渲袥]有用于分隔級(jí)別(KERN_CRIT)和格式字符的逗號(hào)(,)。KERN_CRIT本身只是一個(gè)普通的字符串(事實(shí)上,它表示的是字符串 "<2>";表 1 列出了完整的日志級(jí)別清單)。作為預(yù)處理程序的一部分,C 會(huì)自動(dòng)地使用一個(gè)名為 字符串串聯(lián) 的功能將這兩個(gè)字符串組合在一起。組合的結(jié)果是將日志級(jí)別和用戶指定的格式字符串包含在一個(gè)字符串中。?
內(nèi)核使用這個(gè)指定LOG級(jí)別與當(dāng)前終端LOG等級(jí)console_loglevel來決定是不是向終端打印。?
下面是可使用的LOG等級(jí):?
#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 "" /* Use the default kernel loglevel */
注意,如果調(diào)用者未將日志級(jí)別提供給 printk,那么系統(tǒng)就會(huì)使用默認(rèn)值 KERN_WARNING "<4>"(表示只有KERN_WARNING 級(jí)別以上的日志消息會(huì)被記錄)。由于默認(rèn)值存在變化,所以在使用時(shí)最好指定LOG級(jí)別。有LOG級(jí)別的一個(gè)好處就是我們可以選擇性的輸出LOG。比如平時(shí)我們只需要打印KERN_WARNING級(jí)別以上的關(guān)鍵性LOG,但是調(diào)試的時(shí)候,我們可以選擇打印KERN_DEBUG等以上的詳細(xì)LOG。而這些都不需要我們修改代碼,只需要通過命令修改默認(rèn)日志輸出級(jí)別:?
mtj@ubuntu :~$ cat /proc/sys/kernel/printk4 4 1 7mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay0mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit5mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst10
第一項(xiàng)定義了 printk API 當(dāng)前使用的日志級(jí)別。這些日志級(jí)別表示了控制臺(tái)的日志級(jí)別、默認(rèn)消息日志級(jí)別、最小控制臺(tái)日志級(jí)別和默認(rèn)控制臺(tái)日志級(jí)別。printk_delay 值表示的是 printk 消息之間的延遲毫秒數(shù)(用于提高某些場景的可讀性)。注意,這里它的值為 0,而它是不可以通過 /proc 設(shè)置的。printk_ratelimit 定義了消息之間允許的最小時(shí)間間隔(當(dāng)前定義為每 5 秒內(nèi)的某個(gè)內(nèi)核消息數(shù))。消息數(shù)量是由 printk_ratelimit_burst 定義的(當(dāng)前定義為 10)。如果您擁有一個(gè)非正式內(nèi)核而又使用有帶寬限制的控制臺(tái)設(shè)備(如通過串口), 那么這非常有用。注意,在內(nèi)核中,速度限制是由調(diào)用者控制的,而不是在printk 中實(shí)現(xiàn)的。如果一個(gè) printk 用戶要求進(jìn)行速度限制,那么該用戶就需要調(diào)用printk_ratelimit 函數(shù)。?
4 ?記錄緩沖區(qū)
內(nèi)核消息都被保存在一個(gè)LOG_BUF_LEN大小的環(huán)形隊(duì)列中。?
??關(guān)于LOG_BUF_LEN定義:?
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
※ 變量CONFIG_LOG_BUF_SHIFT在內(nèi)核編譯時(shí)由配置文件定義,對(duì)于i386平臺(tái),其值定義如下(在linux26/arch/i386/defconfig中):?
CONFIG_LOG_BUF_SHIFT=18
記錄緩沖區(qū)操作:?
??① 消息被讀出到用戶空間時(shí),此消息就會(huì)從環(huán)形隊(duì)列中刪除。?
??② 當(dāng)消息緩沖區(qū)滿時(shí),如果再有printk()調(diào)用時(shí),新消息將覆蓋隊(duì)列中的老消息。?
??③ 在讀寫環(huán)形隊(duì)列時(shí),同步問題很容易得到解決。?
※ 這個(gè)紀(jì)錄緩沖區(qū)之所以稱為環(huán)形,是因?yàn)樗淖x寫都是按照環(huán)形隊(duì)列的方式進(jìn)行操作的。
5 ?syslogd/klogd
在標(biāo)準(zhǔn)的Linux系統(tǒng)上,用戶空間的守護(hù)進(jìn)程klogd從紀(jì)錄緩沖區(qū)中獲取內(nèi)核消息,再通過syslogd守護(hù)進(jìn)程把這些消息保存在系統(tǒng)日志文件中。klogd進(jìn)程既可以從/proc/kmsg文件中,也可以通過syslog()系統(tǒng)調(diào)用讀取這些消息。默認(rèn)情況下,它選擇讀取/proc方式實(shí)現(xiàn)。klogd守護(hù)進(jìn)程在消息緩沖區(qū)有新的消息之前,一直處于阻塞狀態(tài)。一旦有新的內(nèi)核消息,klogd被喚醒,讀出內(nèi)核消息并進(jìn)行處理。默認(rèn)情況下,處理例程就是把內(nèi)核消息傳給syslogd守護(hù)進(jìn)程。syslogd守護(hù)進(jìn)程一般把接收到的消息寫入/var/log/messages文件中。不過,還是可以通過/etc/syslog.conf文件來進(jìn)行配置,可以選擇其他的輸出文件。
6 ?dmesg
dmesg 命令也可用于打印和控制內(nèi)核環(huán)緩沖區(qū)。這個(gè)命令使用 klogctl 系統(tǒng)調(diào)用來讀取內(nèi)核環(huán)緩沖區(qū),并將它轉(zhuǎn)發(fā)到標(biāo)準(zhǔn)輸出(stdout)。這個(gè)命令也可以用來清除內(nèi)核環(huán)緩沖區(qū)(使用 -c 選項(xiàng)),設(shè)置控制臺(tái)日志級(jí)別(-n 選項(xiàng)),以及定義用于讀取內(nèi)核日志消息的緩沖區(qū)大小(-s 選項(xiàng))。注意,如果沒有指定緩沖區(qū)大小,那么 dmesg 會(huì)使用 klogctl 的SYSLOG_ACTION_SIZE_BUFFER 操作確定緩沖區(qū)大小。?
7 注意?
a) 雖然printk很健壯,但是看了源碼你就知道,這個(gè)函數(shù)的效率很低:做字符拷貝時(shí)一次只拷貝一個(gè)字節(jié),且去調(diào)用console輸出可能還產(chǎn)生中斷。所以如果你的驅(qū)動(dòng)在功能調(diào)試完成以后做性能測試或者發(fā)布的時(shí)候千萬記得盡量減少printk輸出,做到僅在出錯(cuò)時(shí)輸出少量信息。否則往console輸出無用信息影響性能。?
b) printk的臨時(shí)緩存printk_buf只有1K,所有一次printk函數(shù)只能記錄<1K的信息到log buffer,并且printk使用的“ringbuffer”.?
?
8 內(nèi)核printk和日志系統(tǒng)的總體結(jié)構(gòu)
9 ?動(dòng)態(tài)調(diào)試
動(dòng)態(tài)調(diào)試是通過動(dòng)態(tài)的開啟和禁止某些內(nèi)核代碼來獲取額外的內(nèi)核信息。?
首先內(nèi)核選項(xiàng)CONFIG_DYNAMIC_DEBUG應(yīng)該被設(shè)置。所有通過pr_debug()/dev_debug()打印的信息都可以動(dòng)態(tài)的顯示或不顯示。?
可以通過簡單的查詢語句來篩選需要顯示的信息。?
-源文件名
-函數(shù)名
-行號(hào)(包括指定范圍的行號(hào))
-模塊名
-格式化字符串
將要打印信息的格式寫入/dynamic_debug/control中。?
nullarbor:~ # echo 'file svcsock.c line 1603 +p' > /dynamic_debug/control
參考:?
1 ??內(nèi)核日志及printk結(jié)構(gòu)淺析?--?Tekkaman Ninja?
2 ??內(nèi)核日志:API 及實(shí)現(xiàn)?
3 ??printk實(shí)現(xiàn)分析?
4 ??dynamic-debug-howto.txt?
?
六 ?內(nèi)存調(diào)試工具
1 ?MEMWATCH
MEMWATCH 由 Johan Lindh 編寫,是一個(gè)開放源代碼 C 語言內(nèi)存錯(cuò)誤檢測工具,您可以自己下載它。只要在代碼中添加一個(gè)頭文件并在 gcc 語句中定義了 MEMWATCH 之后,您就可以跟蹤程序中的內(nèi)存泄漏和錯(cuò)誤了。MEMWATCH 支持ANSIC,它提供結(jié)果日志紀(jì)錄,能檢測雙重釋放(double-free)、錯(cuò)誤釋放(erroneous free)、沒有釋放的內(nèi)存(unfreedmemory)、溢出和下溢等等。?
清單 1. 內(nèi)存樣本(test1.c)
#include #include #include "memwatch.h"int main(void){ char *ptr1; char *ptr2; ptr1 = malloc(512); ptr2 = malloc(512); ptr2 = ptr1; free(ptr2); free(ptr1);}
清單 1 中的代碼將分配兩個(gè) 512 字節(jié)的內(nèi)存塊,然后指向第一個(gè)內(nèi)存塊的指針被設(shè)定為指向第二個(gè)內(nèi)存塊。結(jié)果,第二個(gè)內(nèi)存塊的地址丟失,從而產(chǎn)生了內(nèi)存泄漏。?
現(xiàn)在我們編譯清單 1 的 memwatch.c。下面是一個(gè) makefile 示例:?
test1
gcc -DMEMWATCH -DMW_STDIO test1.c memwatchc -o test1
當(dāng)您運(yùn)行 test1 程序后,它會(huì)生成一個(gè)關(guān)于泄漏的內(nèi)存的報(bào)告。清單 2 展示了示例 memwatch.log 輸出文件。?
清單 2. test1 memwatch.log 文件
MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh...double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)...unfreed: <2> test1.c(11), 512 bytes at 0x80519e4{FE FE FE FE FE FE FE FE FE FE FE FE ..............}Memory usage statistics (global): N)umber of allocations made: 2 L)argest memory usage : 1024 T)otal of all alloc() calls: 1024 U)nfreed bytes totals : 512
MEMWATCH 為您顯示真正導(dǎo)致問題的行。如果您釋放一個(gè)已經(jīng)釋放過的指針,它會(huì)告訴您。對(duì)于沒有釋放的內(nèi)存也一樣。日志結(jié)尾部分顯示統(tǒng)計(jì)信息,包括泄漏了多少內(nèi)存,使用了多少內(nèi)存,以及總共分配了多少內(nèi)存。
2 ?YAMD
YAMD 軟件包由 Nate Eldredge 編寫,可以查找 C 和 C++ 中動(dòng)態(tài)的、與內(nèi)存分配有關(guān)的問題。在撰寫本文時(shí),YAMD 的最新版本為 0.32。請(qǐng)下載 yamd-0.32.tar.gz。執(zhí)行 make 命令來構(gòu)建程序;然后執(zhí)行 make install 命令安裝程序并設(shè)置工具。?
一旦您下載了 YAMD 之后,請(qǐng)?jiān)?test1.c 上使用它。請(qǐng)刪除 #include memwatch.h 并對(duì) makefile 進(jìn)行如下小小的修改:
使用 YAMD 的 test1
gcc -g test1.c -o test1
清單 3 展示了來自 test1 上的 YAMD 的輸出。?
清單 3. 使用 YAMD 的 test1 輸出
YAMD version 0.32Executable: /usr/src/test/yamd-0.32/test1...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal deallocation of this blockAddress 0x40025e00, size 512...ERROR: Multiple freeing Atfree of pointer already freedAddress 0x40025e00, size 512...WARNING: Memory leakAddress 0x40028e00, size 512WARNING: Total memory leaks:1 unfreed allocations totaling 512 bytes*** Finished at Tue ... 10:07:15 2002Allocated a grand total of 1024 bytes 2 allocationsAverage of 512 bytes per allocationMax bytes allocated at one time: 102424 K alloced internally / 12 K mapped now / 8 K maxVirtual program size is 1416 KEnd.
YAMD 顯示我們已經(jīng)釋放了內(nèi)存,而且存在內(nèi)存泄漏。讓我們?cè)谇鍐?4 中另一個(gè)樣本程序上試試 YAMD。?
清單 4. 內(nèi)存代碼(test2.c)
#include #include int main(void){ char *ptr1; char *ptr2; char *chptr; int i = 1; ptr1 = malloc(512); ptr2 = malloc(512); chptr = (char *)malloc(512); for (i; i <= 512; i++) { chptr[i] = 'S'; } ptr2 = ptr1; free(ptr2); free(ptr1); free(chptr);}
您可以使用下面的命令來啟動(dòng) YAMD:?
./run-yamd /usr/src/test/test2/test2
清單 5 顯示了在樣本程序 test2 上使用 YAMD 得到的輸出。YAMD 告訴我們?cè)?for 循環(huán)中有“越界(out-of-bounds)”的情況。?
清單 5. 使用 YAMD 的 test2 輸出
Running /usr/src/test/test2/test2Temp output to /tmp/yamd-out.1243*********./run-yamd: line 101: 1248 Segmentation fault (core dumped)YAMD version 0.32Starting run: /usr/src/test/test2/test2Executable: /usr/src/test/test2/test2Virtual program size is 1380 K...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal allocation of this blockAddress 0x4002be00, size 512ERROR: Crash...Tried to write address 0x4002c000Seems to be part of this block:Address 0x4002be00, size 512...Address in question is at offset 512 (out of bounds)Will dump core after checking heap.Done.
MEMWATCH 和 YAMD 都是很有用的調(diào)試工具,它們的使用方法有所不同。對(duì)于 MEMWATCH,您需要添加包含文件memwatch.h 并打開兩個(gè)編譯時(shí)間標(biāo)記。對(duì)于鏈接(link)語句,YAMD 只需要 -g 選項(xiàng)。?
3 ?Electric Fence
多數(shù) Linux 分發(fā)版包含一個(gè) Electric Fence 包,不過您也可以選擇下載它。Electric Fence 是一個(gè)由 Bruce Perens 編寫的malloc()調(diào)試庫。它就在您分配內(nèi)存后分配受保護(hù)的內(nèi)存。如果存在 fencepost 錯(cuò)誤(超過數(shù)組末尾運(yùn)行),程序就會(huì)產(chǎn)生保護(hù)錯(cuò)誤,并立即結(jié)束。通過結(jié)合 Electric Fence 和 gdb,您可以精確地跟蹤到哪一行試圖訪問受保護(hù)內(nèi)存。ElectricFence 的另一個(gè)功能就是能夠檢測內(nèi)存泄漏。?
七 ?strace
strace 命令是一種強(qiáng)大的工具,它能夠顯示所有由用戶空間程序發(fā)出的系統(tǒng)調(diào)用。strace 顯示這些調(diào)用的參數(shù)并返回符號(hào)形式的值。strace 從內(nèi)核接收信息,而且不需要以任何特殊的方式來構(gòu)建內(nèi)核。將跟蹤信息發(fā)送到應(yīng)用程序及內(nèi)核開發(fā)者都很有用。在清單 6 中,分區(qū)的一種格式有錯(cuò)誤,清單顯示了 strace 的開頭部分,內(nèi)容是關(guān)于調(diào)出創(chuàng)建文件系統(tǒng)操作(mkfs )的。strace 確定哪個(gè)調(diào)用導(dǎo)致問題出現(xiàn)。?
清單 6. mkfs 上 strace 的開頭部分
execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &...open("/dev/test1", O_RDWR|O_LARGEFILE) = 4stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -cannot set blocksize on block device /dev/test1: Invalid argument ) = 98stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)write(2, "mkfs.jfs: can't determine device"..., ..._exit(1) = ?
清單 6 顯示 ioctl 調(diào)用導(dǎo)致用來格式化分區(qū)的 mkfs 程序失敗。 ioctl BLKGETSIZE64 失敗。( BLKGET-SIZE64 在調(diào)用 ioctl的源代碼中定義。) BLKGETSIZE64 ioctl 將被添加到 Linux 中所有的設(shè)備,而在這里,邏輯卷管理器還不支持它。因此,如果BLKGETSIZE64 ioctl 調(diào)用失敗,mkfs 代碼將改為調(diào)用較早的 ioctl 調(diào)用;這使得 mkfs 適用于邏輯卷管理器。?
參考:?
http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html#resources?
八 ?OOPS
OOPS(也稱 Panic)消息包含系統(tǒng)錯(cuò)誤的細(xì)節(jié),如 CPU 寄存器的內(nèi)容等。是內(nèi)核告知用戶有不幸發(fā)生的最常用的方式。?
內(nèi)核只能發(fā)布OOPS,這個(gè)過程包括向終端上輸出錯(cuò)誤消息,輸出寄存器保存的信息,并輸出可供跟蹤的回溯線索。通常,發(fā)送完OOPS之后,內(nèi)核會(huì)處于一種不穩(wěn)定的狀態(tài)。?
OOPS的產(chǎn)生有很多可能原因,其中包括內(nèi)存訪問越界或非法的指令等。?
※ 作為內(nèi)核的開發(fā)者,必定將會(huì)經(jīng)常處理OOPS。
※ OOPS中包含的重要信息,對(duì)所有體系結(jié)構(gòu)的機(jī)器都是完全相同的:寄存器上下文和回溯線索(回溯線索顯示了導(dǎo)致錯(cuò)誤發(fā)生的函數(shù)調(diào)用鏈)。
1 ?ksymoops
在 Linux 中,調(diào)試系統(tǒng)崩潰的傳統(tǒng)方法是分析在發(fā)生崩潰時(shí)發(fā)送到系統(tǒng)控制臺(tái)的 Oops 消息。一旦您掌握了細(xì)節(jié),就可以將消息發(fā)送到 ksymoops 實(shí)用程序,它將試圖將代碼轉(zhuǎn)換為指令并將堆棧值映射到內(nèi)核符號(hào)。?
※ 如:回溯線索中的地址,會(huì)通過ksymoops轉(zhuǎn)化成名稱可見的函數(shù)名。
ksymoops需要幾項(xiàng)內(nèi)容:Oops 消息輸出、來自正在運(yùn)行的內(nèi)核的 System.map 文件,還有 /proc/ksyms、vmlinux和/proc/modules。?
關(guān)于如何使用 ksymoops,內(nèi)核源代碼 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊(cè)頁上有完整的說明可以參考。Ksymoops 反匯編代碼部分,指出發(fā)生錯(cuò)誤的指令,并顯示一個(gè)跟蹤部分表明代碼如何被調(diào)用。?
首先,將 Oops 消息保存在一個(gè)文件中以便通過 ksymoops 實(shí)用程序運(yùn)行它。清單 7 顯示了由安裝 JFS 文件系統(tǒng)的 mount命令創(chuàng)建的 Oops 消息。?
清單 7. ksymoops 處理后的 Oops 消息
ksymoops 2.4.0 on i686 2.4.17. Options used... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference atvirtual address 0000000... 15:59:37 sfb1 kernel: c01588fc... 15:59:37 sfb1 kernel: *pde = 0000000... 15:59:37 sfb1 kernel: Oops: 0000... 15:59:37 sfb1 kernel: CPU: 0... 15:59:37 sfb1 kernel: EIP: 0010:[jfs_mount+60/704]... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688] [get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208][do_page_fault+0/1264]... 15:59:37 sfb1 kernel: Call Trace: []...... 15:59:37 sfb1 kernel: [
接下來,您要確定 jfs_mount 中的哪一行代碼引起了這個(gè)問題。Oops 消息告訴我們問題是由位于偏移地址 3c 的指令引起的。做這件事的辦法之一是對(duì) jfs_mount.o 文件使用 objdump 實(shí)用程序,然后查看偏移地址 3c。Objdump 用來反匯編模塊函數(shù),看看您的 C 源代碼會(huì)產(chǎn)生什么匯編指令。清單 8 顯示了使用 objdump 后您將看到的內(nèi)容,接著,我們查看jfs_mount 的 C 代碼,可以看到空值是第 109 行引起的。偏移地址 3c 之所以很重要,是因?yàn)?Oops 消息將該處標(biāo)識(shí)為引起問題的位置。?
清單 8. jfs_mount 的匯編程序清單
109 printk("%d ",*ptr);objdump jfs_mount.ojfs_mount.o: file format elf32-i386Disassembly of section .text:00000000 : 0:55 push %ebp ... 2c: e8 cf 03 00 00 call 400 31: 89 c3 mov %eax,%ebx 33: 58 pop %eax 34: 85 db test %ebx,%ebx 36: 0f 85 55 02 00 00 jne 291 0x291> 3c: 8b 2d 00 00 00 00 mov 0x0,%ebp << problem line above 42: 55 push %ebp
2 ?kallsyms
開發(fā)版2.5內(nèi)核引入了kallsyms特性,它可以通過定義CONFIG_KALLSYMS編譯選項(xiàng)啟用。該選項(xiàng)可以載入內(nèi)核鏡像所對(duì)應(yīng)的內(nèi)存地址的符號(hào)名稱(即函數(shù)名),所以內(nèi)核可以打印解碼之后的跟蹤線索。相應(yīng),解碼OOPS也不再需要System.map和ksymoops工具了。另外,?
這樣做,會(huì)使內(nèi)核變大些,因?yàn)榈刂穼?duì)應(yīng)符號(hào)名稱必須始終駐留在內(nèi)核所在內(nèi)存上。?
#cat /proc/kallsyms c0100240 T _stext c0100240 t run_init_process c0100240 T stext c0100269 t init …
3 ?Kdump
3.1 ?Kdump 的基本概念
3.1.1 ?什么是 kexec ?
Kexec 是實(shí)現(xiàn) kdump 機(jī)制的關(guān)鍵,它包括 2 個(gè)組成部分:一是內(nèi)核空間的系統(tǒng)調(diào)用 kexec_load,負(fù)責(zé)在生產(chǎn)內(nèi)核(production kernel 或 first kernel)啟動(dòng)時(shí)將捕獲內(nèi)核(capture kernel 或 sencond kernel)加載到指定地址。二是用戶空間的工具 kexec-tools,他將捕獲內(nèi)核的地址傳遞給生產(chǎn)內(nèi)核,從而在系統(tǒng)崩潰的時(shí)候能夠找到捕獲內(nèi)核的地址并運(yùn)行。沒有 kexec 就沒有 kdump。先有 kexec 實(shí)現(xiàn)了在一個(gè)內(nèi)核中可以啟動(dòng)另一個(gè)內(nèi)核,才讓 kdump 有了用武之地。kexec 原來的目的是為了節(jié)省 kernel 開發(fā)人員重啟系統(tǒng)的時(shí)間,誰能想到這個(gè)“偷懶”的技術(shù)卻孕育了最成功的內(nèi)存轉(zhuǎn)存機(jī)制呢?
3.1.2 ?什么是 kdump ?
Kdump 的概念出現(xiàn)在 2005 左右,是迄今為止最可靠的內(nèi)核轉(zhuǎn)存機(jī)制,已經(jīng)被主要的 linux? 廠商選用。kdump是一種先進(jìn)的基于 kexec 的內(nèi)核崩潰轉(zhuǎn)儲(chǔ)機(jī)制。當(dāng)系統(tǒng)崩潰時(shí),kdump 使用 kexec 啟動(dòng)到第二個(gè)內(nèi)核。第二個(gè)內(nèi)核通常叫做捕獲內(nèi)核,以很小內(nèi)存啟動(dòng)以捕獲轉(zhuǎn)儲(chǔ)鏡像。第一個(gè)內(nèi)核保留了內(nèi)存的一部分給第二內(nèi)核啟動(dòng)用。由于 kdump 利用 kexec 啟動(dòng)捕獲內(nèi)核,繞過了 BIOS,所以第一個(gè)內(nèi)核的內(nèi)存得以保留。這是內(nèi)核崩潰轉(zhuǎn)儲(chǔ)的本質(zhì)。
kdump 需要兩個(gè)不同目的的內(nèi)核,生產(chǎn)內(nèi)核和捕獲內(nèi)核。生產(chǎn)內(nèi)核是捕獲內(nèi)核服務(wù)的對(duì)像。捕獲內(nèi)核會(huì)在生產(chǎn)內(nèi)核崩潰時(shí)啟動(dòng)起來,與相應(yīng)的 ramdisk 一起組建一個(gè)微環(huán)境,用以對(duì)生產(chǎn)內(nèi)核下的內(nèi)存進(jìn)行收集和轉(zhuǎn)存。
3.1.3 ?如何使用 kdump
構(gòu)建系統(tǒng)和 dump-capture 內(nèi)核,此操作有 2 種方式可選:
1)構(gòu)建一個(gè)單獨(dú)的自定義轉(zhuǎn)儲(chǔ)捕獲內(nèi)核以捕獲內(nèi)核轉(zhuǎn)儲(chǔ);
2) 或者將系統(tǒng)內(nèi)核本身作為轉(zhuǎn)儲(chǔ)捕獲內(nèi)核,這就不需要構(gòu)建一個(gè)單獨(dú)的轉(zhuǎn)儲(chǔ)捕獲內(nèi)核。
方法(2)只能用于可支持可重定位內(nèi)核的體系結(jié)構(gòu)上;目前 i386,x86_64,ppc64 和 ia64 體系結(jié)構(gòu)支持可重定位內(nèi)核。構(gòu)建一個(gè)可重定位內(nèi)核使得不需要構(gòu)建第二個(gè)內(nèi)核就可以捕獲轉(zhuǎn)儲(chǔ)。但是可能有時(shí)想構(gòu)建一個(gè)自定義轉(zhuǎn)儲(chǔ)捕獲內(nèi)核以滿足特定要求。
3.1.4 ?如何訪問捕獲內(nèi)存
在內(nèi)核崩潰之前所有關(guān)于核心映像的必要信息都用 ELF 格式編碼并存儲(chǔ)在保留的內(nèi)存區(qū)域中。ELF 頭所在的物理地址被作為命令行參數(shù)(fcorehdr=)傳遞給新啟動(dòng)的轉(zhuǎn)儲(chǔ)內(nèi)核。
在 i386 體系結(jié)構(gòu)上,啟動(dòng)的時(shí)候需要使用物理內(nèi)存開始的 640K,而不管操作系統(tǒng)內(nèi)核轉(zhuǎn)載在何處。因此,這個(gè)640K 的區(qū)域在重新啟動(dòng)第二個(gè)內(nèi)核的時(shí)候由 kexec 備份。
在第二個(gè)內(nèi)核中,“前一個(gè)系統(tǒng)的內(nèi)存”可以通過兩種方式訪問:
1) 通過 /dev/oldmem 這個(gè)設(shè)備接口。
一個(gè)“捕捉”設(shè)備可以使用“raw”(裸的)方式 “讀”這個(gè)設(shè)備文件并寫出到文件。這是關(guān)于內(nèi)存的 “裸”的數(shù)據(jù)轉(zhuǎn)儲(chǔ),同時(shí)這些分析 / 捕捉工具應(yīng)該足夠“智能”從而可以知道從哪里可以得到正確的信息。ELF 文件頭(通過命令行參數(shù)傳遞過來的 elfcorehdr)可能會(huì)有幫助。
2) 通過 /proc/vmcore。
這個(gè)方式是將轉(zhuǎn)儲(chǔ)輸出為一個(gè) ELF 格式的文件,并且可以使用一些文件拷貝命令(比如 cp,scp 等)將信息讀出來。同時(shí),gdb 可以在得到的轉(zhuǎn)儲(chǔ)文件上做一些調(diào)試(有限的)。這種方式保證了內(nèi)存中的頁面都以正確的途徑被保存 ( 注意內(nèi)存開始的 640K 被重新映射了 )。
3.1.5 ?kdump 的優(yōu)勢
1)?高可靠性
崩潰轉(zhuǎn)儲(chǔ)數(shù)據(jù)可從一個(gè)新啟動(dòng)內(nèi)核的上下文中獲取,而不是從已經(jīng)崩潰內(nèi)核的上下文。
2) 多版本支持
LKCD(Linux Kernel Crash Dump),netdump,diskdump 已被納入 LDPs(Linux Documen-tation Project) 內(nèi)核。SUSE 和 RedHat 都對(duì) kdump 有技術(shù)支持。
3.2 ?Kdump 實(shí)現(xiàn)流程
圖 1. RHEL6.2 執(zhí)行流程
圖 2. sles11 執(zhí)行流程
3.3 ?配置 kdump
3.3.1 ?安裝軟件包和實(shí)用程序
Kdump 用到的各種工具都在 kexec-tools 中。kernel-debuginfo 則是用來分析 vmcore 文件。從 rhel5 開始,kexec-tools 已被默認(rèn)安裝在發(fā)行版。而 novell 也在 sles10 發(fā)行版中把 kdump 集成進(jìn)來。所以如果使用的是rhel5 和 sles10 之后的發(fā)行版,那就省去了安裝 kexec-tools 的步驟。而如果需要調(diào)試 kdump 生成的 vmcore文件,則需要手動(dòng)安裝 kernel-debuginfo 包。檢查安裝包操作:
3.3.2 參數(shù)相關(guān)設(shè)置 uli13lp1:/ # rpm -qa|grep kexec kexec-tools-2.0.0-53.43.10 uli13lp1:/ # rpm -qa 'kernel*debuginfo*' kernel-default-debuginfo-3.0.13-0.27.1 kernel-ppc64-debuginfo-3.0.13-0.27.1
系統(tǒng)內(nèi)核設(shè)置選項(xiàng)和轉(zhuǎn)儲(chǔ)捕獲內(nèi)核配置選擇在《使用 Crash 工具分析 Linux dump 文件》一文中已有說明,在此不再贅述。僅列出內(nèi)核引導(dǎo)參數(shù)設(shè)置以及配置文件設(shè)置。
1) 修改內(nèi)核引導(dǎo)參數(shù),為啟動(dòng)捕獲內(nèi)核預(yù)留內(nèi)存
通過下面的方法來配置 kdump 使用的內(nèi)存大小。添加啟動(dòng)參數(shù)"crashkernel=Y@X",這里,Y 是為 kdump 捕捉內(nèi)核保留的內(nèi)存,X 是保留部分內(nèi)存的開始位置。
對(duì)于 i386 和 x86_64, 編輯 /etc/grub.conf, 在內(nèi)核行的最后添加"crashkernel=128M" 。
對(duì)于 ppc64,在 /etc/yaboot.conf 最后添加"crashkernel=128M"。
在 ia64, 編輯 /etc/elilo.conf,添加"crashkernel=256M"到內(nèi)核行。
2) kdump 配置文件
kdump 的配置文件是 /etc/kdump.conf(RHEL6.2);/etc/sysconfig/kdump(SLES11 sp2)。每個(gè)文件頭部都有選項(xiàng)說明,可以根據(jù)使用需求設(shè)置相應(yīng)的選項(xiàng)。
3.3.3 ?啟動(dòng) kdump 服務(wù)
在設(shè)置了預(yù)留內(nèi)存后,需要重啟機(jī)器,否則 kdump 是不可使用的。啟動(dòng) kdump 服務(wù):
Rhel6.2:
# chkconfig kdump on # service kdump status Kdump is operational # service kdump start
SLES11SP2:
# chkconfig boot.kdump on # service boot.kdump start
3.3.4 ?測試配置是否有效
可以通過 kexec 加載內(nèi)核鏡像,讓系統(tǒng)準(zhǔn)備好去捕獲一個(gè)崩潰時(shí)產(chǎn)生的 vmcore??梢酝ㄟ^ sysrq 強(qiáng)制系統(tǒng)崩潰。
# echo c > /proc/sysrq-trigger
這造成內(nèi)核崩潰,如配置有效,系統(tǒng)將重啟進(jìn)入 kdump 內(nèi)核,當(dāng)系統(tǒng)進(jìn)程進(jìn)入到啟動(dòng) kdump 服務(wù)的點(diǎn)時(shí),vmcore 將會(huì)拷貝到你在 kdump 配置文件中設(shè)置的位置。RHEL 的缺省目錄是 : /var/crash;SLES 的缺省目錄是 : /var/log/dump。然后系統(tǒng)重啟進(jìn)入到正常的內(nèi)核。一旦回復(fù)到正常的內(nèi)核,就可以在上述的目錄下發(fā)現(xiàn) vmcore 文件,即內(nèi)存轉(zhuǎn)儲(chǔ)文件。可以使用之前安裝的 kernel-debuginfo 中的 crash 工具來進(jìn)行分析(crash 的更多詳細(xì)用法將在本系列后面的文章中有介紹)。
# crash /usr/lib/debug/lib/modules/2.6.17-1.2621.el5/vmlinux /var/crash/2006-08-23-15:34/vmcore crash> bt
3.4 ?載入“轉(zhuǎn)儲(chǔ)捕獲”內(nèi)核
需要引導(dǎo)系統(tǒng)內(nèi)核時(shí),可使用如下步驟和命令載入“轉(zhuǎn)儲(chǔ)捕獲”內(nèi)核:
kexec -p --initrd=for-dump-capture-kernel> --args-linux --append="root= init 1 irqpoll"
裝載轉(zhuǎn)儲(chǔ)捕捉內(nèi)核的注意事項(xiàng):
轉(zhuǎn)儲(chǔ)捕捉內(nèi)核應(yīng)當(dāng)是一個(gè) vmlinux 格式的映像(即是一個(gè)未壓縮的 ELF 映像文件),而不能是 bzImage 格式;
默認(rèn)情況下,ELF 文件頭采用 ELF64 格式存儲(chǔ)以支持那些擁有超過 4GB 內(nèi)存的系統(tǒng)。但是可以指定“--elf32-core-headers”標(biāo)志以強(qiáng)制使用 ELF32 格式的 ELF 文件頭。這個(gè)標(biāo)志是有必要注意的,一個(gè)重要的原因就是:當(dāng)前版本的 GDB 不能在一個(gè) 32 位系統(tǒng)上打開一個(gè)使用 ELF64 格式的 vmcore 文件。ELF32 格式的文件頭不能使用在一個(gè)“沒有物理地址擴(kuò)展”(non-PAE)的系統(tǒng)上(即:少于 4GB 內(nèi)存的系統(tǒng));
一個(gè)“irqpoll”的啟動(dòng)參數(shù)可以減低由于在“轉(zhuǎn)儲(chǔ)捕獲內(nèi)核”中使用了“共享中斷”技術(shù)而導(dǎo)致出現(xiàn)驅(qū)動(dòng)初始化失敗這種情況發(fā)生的概率 ;
必須指定 ,指定的格式是和要使用根設(shè)備的名字。具體可以查看 mount 命令的輸出;“init 1”這個(gè)命令將啟動(dòng)“轉(zhuǎn)儲(chǔ)捕捉內(nèi)核”到一個(gè)沒有網(wǎng)絡(luò)支持的單用戶模式。如果你希望有網(wǎng)絡(luò)支持,那么使用“init 3”。
3.5 ?后記
Kdump 是一個(gè)強(qiáng)大的、靈活的內(nèi)核轉(zhuǎn)儲(chǔ)機(jī)制,能夠在生產(chǎn)內(nèi)核上下文中執(zhí)行捕獲內(nèi)核是非常有價(jià)值的。本文僅介紹在 RHEL6.2 和 SLES11 中如何配置 kdump。望拋磚引玉,對(duì)閱讀本文的讀者有益。
參考:
1??kallsyms的分析?
2 ??深入探索 Kdump
九 ?KGDB
kgdb提供了一種使用 gdb調(diào)試 Linux 內(nèi)核的機(jī)制。使用KGDB可以象調(diào)試普通的應(yīng)用程序那樣,在內(nèi)核中進(jìn)行設(shè)置斷點(diǎn)、檢查變量值、單步跟蹤程序運(yùn)行等操作。使用KGDB調(diào)試時(shí)需要兩臺(tái)機(jī)器,一臺(tái)作為開發(fā)機(jī)(Development Machine),另一臺(tái)作為目標(biāo)機(jī)(Target Machine),兩臺(tái)機(jī)器之間通過串口或者以太網(wǎng)口相連。串口連接線是一根RS-232接口的電纜,在其內(nèi)部兩端的第2腳(TXD)與第3腳(RXD)交叉相連,第7腳(接地腳)直接相連。調(diào)試過程中,被調(diào)試的內(nèi)核運(yùn)行在目標(biāo)機(jī)上,gdb調(diào)試器運(yùn)行在開發(fā)機(jī)上。?
目前,kgdb發(fā)布支持i386、x86_64、32-bit PPC、SPARC等幾種體系結(jié)構(gòu)的調(diào)試器。?
1 ?kgdb的調(diào)試原理
安裝kgdb調(diào)試環(huán)境需要為Linux內(nèi)核應(yīng)用kgdb補(bǔ)丁,補(bǔ)丁實(shí)現(xiàn)的gdb遠(yuǎn)程調(diào)試所需要的功能包括命令處理、陷阱處理及串口通訊3個(gè)主要的部分。kgdb補(bǔ)丁的主要作用是在Linux內(nèi)核中添加了一個(gè)調(diào)試Stub。調(diào)試Stub是Linux內(nèi)核中的一小段代碼,提供了運(yùn)行g(shù)db的開發(fā)機(jī)和所調(diào)試內(nèi)核之間的一個(gè)媒介。gdb和調(diào)試stub之間通過gdb串行協(xié)議進(jìn)行通訊。gdb串行協(xié)議是一種基于消息的ASCII碼協(xié)議,包含了各種調(diào)試命令。當(dāng)設(shè)置斷點(diǎn)時(shí),kgdb負(fù)責(zé)在設(shè)置斷點(diǎn)的指令前增加一條trap指令,當(dāng)執(zhí)行到斷點(diǎn)時(shí)控制權(quán)就轉(zhuǎn)移到調(diào)試stub中去。此時(shí),調(diào)試stub的任務(wù)就是使用遠(yuǎn)程串行通信協(xié)議將當(dāng)前環(huán)境傳送給gdb,然后從gdb處接受命令。gdb命令告訴stub下一步該做什么,當(dāng)stub收到繼續(xù)執(zhí)行的命令時(shí),將恢復(fù)程序的運(yùn)行環(huán)境,把對(duì)CPU的控制權(quán)重新交還給內(nèi)核?
2 ?Kgdb的安裝與設(shè)置
下面我們將以Linux 2.6.7內(nèi)核為例詳細(xì)介紹kgdb調(diào)試環(huán)境的建立過程。
2.1 ?軟硬件準(zhǔn)備
以下軟硬件配置取自筆者進(jìn)行試驗(yàn)的系統(tǒng)配置情況:?
kgdb補(bǔ)丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的內(nèi)核版本號(hào),B為kgdb的版本號(hào)。以試驗(yàn)使用的kgdb補(bǔ)丁為例,linux內(nèi)核的版本為linux-2.6.7,補(bǔ)丁版本為kgdb-2.2。?
物理連接好串口線后,使用以下命令來測試兩臺(tái)機(jī)器之間串口連接情況,stty命令可以對(duì)串口參數(shù)進(jìn)行設(shè)置:?
在development機(jī)上執(zhí)行:?
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
在target機(jī)上執(zhí)行:?
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
在developement機(jī)上執(zhí)行:?
echo hello > /dev/ttyS0
在target機(jī)上執(zhí)行:?
cat /dev/ttyS0
如果串口連接沒問題的話在將在target機(jī)的屏幕上顯示"hello"。?
2.2 ?安裝與配置
下面我們需要應(yīng)用kgdb補(bǔ)丁到Linux內(nèi)核,設(shè)置內(nèi)核選項(xiàng)并編譯內(nèi)核。這方面的資料相對(duì)較少,筆者這里給出詳細(xì)的介紹。下面的工作在開發(fā)機(jī)(developement)上進(jìn)行,以上面介紹的試驗(yàn)環(huán)境為例,某些具體步驟在實(shí)際的環(huán)境中可能要做適當(dāng)?shù)母膭?dòng):?
I、內(nèi)核的配置與編譯
[root@lisl tmp]# tar -jxvf linux-2.6.7.tar.bz2[root@lisl tmp]#tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar[root@lisl tmp]#cd inux-2.6.7
請(qǐng)參照目錄補(bǔ)丁包中文件README給出的說明,執(zhí)行對(duì)應(yīng)體系結(jié)構(gòu)的補(bǔ)丁程序。由于試驗(yàn)在i386體系結(jié)構(gòu)上完成,所以只需要安裝一下補(bǔ)?。篶ore-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。應(yīng)用補(bǔ)丁文件時(shí),請(qǐng)遵循kgdb軟件包內(nèi)series文件所指定的順序,否則可能會(huì)帶來預(yù)想不到的問題。eth.patch文件是選擇以太網(wǎng)口作為調(diào)試的連接端口時(shí)需要運(yùn)用的補(bǔ)丁。?
應(yīng)用補(bǔ)丁的命令如下所示:?
[root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch
如果內(nèi)核正確,那么應(yīng)用補(bǔ)丁時(shí)應(yīng)該不會(huì)出現(xiàn)任何問題(不會(huì)產(chǎn)生*.rej文件)。為Linux內(nèi)核添加了補(bǔ)丁之后,需要進(jìn)行內(nèi)核的配置。內(nèi)核的配置可以按照你的習(xí)慣選擇配置Linux內(nèi)核的任意一種方式。?
[root@lisl tmp]#make menuconfig
在內(nèi)核配置菜單的Kernel hacking選項(xiàng)中選擇kgdb調(diào)試項(xiàng),例如:?
[*] KGDB: kernel debugging with remote gdb Method for KGDB communication (KGDB: On generic serial port (8250)) ---> [*] KGDB: Thread analysis [*] KGDB: Console messages through gdb[root@lisl tmp]#make
編譯內(nèi)核之前請(qǐng)注意Linux目錄下Makefile中的優(yōu)化選項(xiàng),默認(rèn)的Linux內(nèi)核的編譯都以-O2的優(yōu)化級(jí)別進(jìn)行。在這個(gè)優(yōu)化級(jí)別之下,編譯器要對(duì)內(nèi)核中的某些代碼的執(zhí)行順序進(jìn)行改動(dòng),所以在調(diào)試時(shí)會(huì)出現(xiàn)程序運(yùn)行與代碼順序不一致的情況??梢园袽akefile中的-O2選項(xiàng)改為-O,但不可去掉-O,否則編譯會(huì)出問題。為了使編譯后的內(nèi)核帶有調(diào)試信息,注意在編譯內(nèi)核的時(shí)候需要加上-g選項(xiàng)。?
不過,當(dāng)選擇"Kernel debugging->Compile the kernel with debug info"選項(xiàng)后配置系統(tǒng)將自動(dòng)打開調(diào)試選項(xiàng)。另外,選擇"kernel debugging with remote gdb"后,配置系統(tǒng)將自動(dòng)打開"Compile the kernel with debug info"選項(xiàng)。?
內(nèi)核編譯完成后,使用scp命令進(jìn)行將相關(guān)文件拷貝到target機(jī)上(當(dāng)然也可以使用其它的網(wǎng)絡(luò)工具,如rcp)。?
[root@lisl tmp]#scp arch/i386/boot/bzImage root@192.168.6.13:/boot/vmlinuz-2.6.7-kgdb[root@lisl tmp]#scp System.map root@192.168.6.13:/boot/System.map-2.6.7-kgdb
如果系統(tǒng)啟動(dòng)使所需要的某些設(shè)備驅(qū)動(dòng)沒有編譯進(jìn)內(nèi)核的情況下,那么還需要執(zhí)行如下操作:?
[root@lisl tmp]#mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7[root@lisl tmp]#scp initrd-2.6.7-kgdb root@192.168.6.13:/boot/ initrd-2.6.7-kgdb
II、kgdb的啟動(dòng)?
在將編譯出的內(nèi)核拷貝的到target機(jī)器之后,需要配置系統(tǒng)引導(dǎo)程序,加入內(nèi)核的啟動(dòng)選項(xiàng)。以下是kgdb內(nèi)核引導(dǎo)參數(shù)的說明:?
?
如表中所述,在kgdb 2.0版本之后內(nèi)核的引導(dǎo)參數(shù)已經(jīng)與以前的版本有所不同。使用grub引導(dǎo)程序時(shí),直接將kgdb參數(shù)作為內(nèi)核vmlinuz的引導(dǎo)參數(shù)。下面給出引導(dǎo)器的配置示例。?
title 2.6.7 kgdbroot (hd0,0)kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
在使用lilo作為引導(dǎo)程序時(shí),需要把kgdb參放在由append修飾的語句中。下面給出使用lilo作為引導(dǎo)器時(shí)的配置示例。?
image=/boot/vmlinuz-2.6.7-kgdblabel=kgdb read-only root=/dev/hda3append="gdb gdbttyS=1 gdbbaud=115200"
保存好以上配置后重新啟動(dòng)計(jì)算機(jī),選擇啟動(dòng)帶調(diào)試信息的內(nèi)核,內(nèi)核將在短暫的運(yùn)行后在創(chuàng)建init內(nèi)核線程之前停下來,打印出以下信息,并等待開發(fā)機(jī)的連接。?
Waiting for connection from remote gdb...?
在開發(fā)機(jī)上執(zhí)行:?
gdbfile vmlinuxset remotebaud 115200target remote /dev/ttyS0
其中vmlinux是指向源代碼目錄下編譯出來的Linux內(nèi)核文件的鏈接,它是沒有經(jīng)過壓縮的內(nèi)核文件,gdb程序從該文件中得到各種符號(hào)地址信息。?
這樣,就與目標(biāo)機(jī)上的kgdb調(diào)試接口建立了聯(lián)系。一旦建立聯(lián)接之后,對(duì)Linux內(nèi)的調(diào)試工作與對(duì)普通的運(yùn)用程序的調(diào)試就沒有什么區(qū)別了。任何時(shí)候都可以通過鍵入ctrl+c打斷目標(biāo)機(jī)的執(zhí)行,進(jìn)行具體的調(diào)試工作。?
在kgdb 2.0之前的版本中,編譯內(nèi)核后在arch/i386/kernel目錄下還會(huì)生成可執(zhí)行文件gdbstart。將該文件拷貝到target機(jī)器的/boot目錄下,此時(shí)無需更改內(nèi)核的啟動(dòng)配置文件,直接使用命令:?
[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
可以在KGDB內(nèi)核引導(dǎo)啟動(dòng)完成后建立開發(fā)機(jī)與目標(biāo)機(jī)之間的調(diào)試聯(lián)系。?
2.3 ?通過網(wǎng)絡(luò)接口進(jìn)行調(diào)試?
kgdb也支持使用以太網(wǎng)接口作為調(diào)試器的連接端口。在對(duì)Linux內(nèi)核應(yīng)用補(bǔ)丁包時(shí),需應(yīng)用eth.patch補(bǔ)丁文件。配置內(nèi)核時(shí)在Kernel hacking中選擇kgdb調(diào)試項(xiàng),配置kgdb調(diào)試端口為以太網(wǎng)接口,例如:?
[*]KGDB: kernel debugging with remote gdbMethod for KGDB communication (KGDB: On ethernet) ---> ( ) KGDB: On generic serial port (8250)(X) KGDB: On ethernet
另外使用eth0網(wǎng)口作為調(diào)試端口時(shí),grub.list的配置如下:?
title 2.6.7 kgdbroot (hd0,0)kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.5.13/,@192.168. 6.13/
其他的過程與使用串口作為連接端口時(shí)的設(shè)置過程相同。?
注意:盡管可以使用以太網(wǎng)口作為kgdb的調(diào)試端口,使用串口作為連接端口更加簡單易行,kgdb項(xiàng)目組推薦使用串口作為調(diào)試端口。?
2.4 ?模塊的調(diào)試方法?
內(nèi)核可加載模塊的調(diào)試具有其特殊性。由于內(nèi)核模塊中各段的地址是在模塊加載進(jìn)內(nèi)核的時(shí)候才最終確定的,所以develop機(jī)的gdb無法得到各種符號(hào)地址信息。所以,使用kgdb調(diào)試模塊所需要解決的一個(gè)問題是,需要通過某種方法獲得可加載模塊的最終加載地址信息,并把這些信息加入到gdb環(huán)境中。?
I、在Linux 2.4內(nèi)核中的內(nèi)核模塊調(diào)試方法?
在Linux2.4.x內(nèi)核中,可以使用insmod -m命令輸出模塊的加載信息,例如:?
[root@lisl tmp]# insmod -m hello.ko >modaddr
查看模塊加載信息文件modaddr如下:?
.this 00000060 c88d8000 2**2.text 00000035 c88d8060 2**2.rodata 00000069 c88d80a0 2**5…….data 00000000 c88d833c 2**2.bss 00000000 c88d833c 2**2……
在這些信息中,我們關(guān)心的只有4個(gè)段的地址:.text、.rodata、.data、.bss。在development機(jī)上將以上地址信息加入到gdb中,這樣就可以進(jìn)行模塊功能的測試了。?
(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s .rodata 0xc88d80a0 -s .bss 0x c88d833c
這種方法也存在一定的不足,它不能調(diào)試模塊初始化的代碼,因?yàn)榇藭r(shí)模塊初始化代碼已經(jīng)執(zhí)行過了。而如果不執(zhí)行模塊的加載又無法獲得模塊插入地址,更不可能在模塊初始化之前設(shè)置斷點(diǎn)了。對(duì)于這種調(diào)試要求可以采用以下替代方法。?
在target機(jī)上用上述方法得到模塊加載的地址信息,然后再用rmmod卸載模塊。在development機(jī)上將得到的模塊地址信息導(dǎo)入到gdb環(huán)境中,在內(nèi)核代碼的調(diào)用初始化代碼之前設(shè)置斷點(diǎn)。這樣,在target機(jī)上再次插入模塊時(shí),代碼將在執(zhí)行模塊初始化之前停下來,這樣就可以使用gdb命令調(diào)試模塊初始化代碼了。?
另外一種調(diào)試模塊初始化函數(shù)的方法是:當(dāng)插入內(nèi)核模塊時(shí),內(nèi)核模塊機(jī)制將調(diào)用函數(shù)sys_init_module(kernel/modle.c)執(zhí)行對(duì)內(nèi)核模塊的初始化,該函數(shù)將調(diào)用所插入模塊的初始化函數(shù)。程序代碼片斷如下:?
…… ……if (mod->init != NULL)ret = mod->init();…… ……
在該語句上設(shè)置斷點(diǎn),也能在執(zhí)行模塊初始化之前停下來。?
II、在Linux 2.6.x內(nèi)核中的內(nèi)核模塊調(diào)試方法
Linux 2.6之后的內(nèi)核中,由于module-init-tools工具的更改,insmod命令不再支持-m參數(shù),只有采取其他的方法來獲取模塊加載到內(nèi)核的地址。通過分析ELF文件格式,我們知道程序中各段的意義如下:?
.text(代碼段):用來存放可執(zhí)行文件的操作指令,也就是說是它是可執(zhí)行程序在內(nèi)存種的鏡像。?
.data(數(shù)據(jù)段):數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量,也就是存放程序靜態(tài)分配的變量和全局變量。?
.bss(BSS段):BSS段包含了程序中未初始化全局變量,在內(nèi)存中 bss段全部置零。?
.rodata(只讀段):該段保存著只讀數(shù)據(jù),在進(jìn)程映象中構(gòu)造不可寫的段。?
通過在模塊初始化函數(shù)中放置一下代碼,我們可以很容易地獲得模塊加載到內(nèi)存中的地址。?
……int bss_var;static int hello_init(void){ printk(KERN_ALERT "Text location .text(Code Segment):%p ",hello_init); static int data_var=0; printk(KERN_ALERT "Data Location .data(Data Segment):%p ",&data_var); printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p ",&bss_var); ……}Module_init(hello_init);
這里,通過在模塊的初始化函數(shù)中添加一段簡單的程序,使模塊在加載時(shí)打印出在內(nèi)核中的加載地址。.rodata段的地址可以通過執(zhí)行命令readelf -e hello.ko,取得.rodata在文件中的偏移量并加上段的align值得出。?
為了使讀者能夠更好地進(jìn)行模塊的調(diào)試,kgdb項(xiàng)目還發(fā)布了一些腳本程序能夠自動(dòng)探測模塊的插入并自動(dòng)更新gdb中模塊的符號(hào)信息。這些腳本程序的工作原理與前面解釋的工作過程相似,更多的信息請(qǐng)閱讀參考資料[4]。?
2.5 ?硬件斷點(diǎn)?
kgdb提供對(duì)硬件調(diào)試寄存器的支持。在kgdb中可以設(shè)置三種硬件斷點(diǎn):執(zhí)行斷點(diǎn)(Execution Breakpoint)、寫斷點(diǎn)(Write Breakpoint)、訪問斷點(diǎn)(Access Breakpoint)但不支持I/O訪問的斷點(diǎn)。 目前,kgdb對(duì)硬件斷點(diǎn)的支持是通過宏來實(shí)現(xiàn)的,最多可以設(shè)置4個(gè)硬件斷點(diǎn),這些宏的用法如下:?
?
在有些情況下,硬件斷點(diǎn)的使用對(duì)于內(nèi)核的調(diào)試是非常方便的。?
3 ?在VMware中搭建調(diào)試環(huán)境
kgdb調(diào)試環(huán)境需要使用兩臺(tái)微機(jī)分別充當(dāng)development機(jī)和target機(jī),使用VMware后我們只使用一臺(tái)計(jì)算機(jī)就可以順利完成kgdb調(diào)試環(huán)境的搭建。以windows下的環(huán)境為例,創(chuàng)建兩臺(tái)虛擬機(jī),一臺(tái)作為開發(fā)機(jī),一臺(tái)作為目標(biāo)機(jī)。?
3.1 ?虛擬機(jī)之間的串口連接?
虛擬機(jī)中的串口連接可以采用兩種方法。一種是指定虛擬機(jī)的串口連接到實(shí)際的COM上,例如開發(fā)機(jī)連接到COM1,目標(biāo)機(jī)連接到COM2,然后把兩個(gè)串口通過串口線相連接。另一種更為簡便的方法是:在較高一些版本的VMware中都支持把串口映射到命名管道,把兩個(gè)虛擬機(jī)的串口映射到同一個(gè)命名管道。例如,在兩個(gè)虛擬機(jī)中都選定同一個(gè)命名管道 \.pipecom_1,指定target機(jī)的COM口為server端,并選擇"The other end is a virtual machine"屬性;指定development機(jī)的COM口端為client端,同樣指定COM口的"The other end is a virtual machine"屬性。對(duì)于IO mode屬性,在target上選中"Yield CPU on poll"復(fù)選擇框,development機(jī)不選。這樣,可以無需附加任何硬件,利用虛擬機(jī)就可以搭建kgdb調(diào)試環(huán)境。 即降低了使用kgdb進(jìn)行調(diào)試的硬件要求,也簡化了建立調(diào)試環(huán)境的過程。?
?
3.2 ?VMware的使用技巧
VMware虛擬機(jī)是比較占用資源的,尤其是象上面那樣在Windows中使用兩臺(tái)虛擬機(jī)。因此,最好為系統(tǒng)配備512M以上的內(nèi)存,每臺(tái)虛擬機(jī)至少分配128M的內(nèi)存。這樣的硬件要求,對(duì)目前主流配置的PC而言并不是過高的要求。出于系統(tǒng)性能的考慮,在VMware中盡量使用字符界面進(jìn)行調(diào)試工作。同時(shí),Linux系統(tǒng)默認(rèn)情況下開啟了sshd服務(wù),建議使用SecureCRT登陸到Linux進(jìn)行操作,這樣可以有較好的用戶使用界面。?
3.3 ?在Linux下的虛擬機(jī)中使用kgdb?
對(duì)于在Linux下面使用VMware虛擬機(jī)的情況,筆者沒有做過實(shí)際的探索。從原理上而言,只需要在Linux下只要?jiǎng)?chuàng)建一臺(tái)虛擬機(jī)作為target機(jī),開發(fā)機(jī)的工作可以在實(shí)際的Linux環(huán)境中進(jìn)行,搭建調(diào)試環(huán)境的過程與上面所述的過程類似。由于只需要?jiǎng)?chuàng)建一臺(tái)虛擬機(jī),所以使用Linux下的虛擬機(jī)搭建kgdb調(diào)試環(huán)境對(duì)系統(tǒng)性能的要求較低。(vmware已經(jīng)推出了Linux下的版本)還可以在development機(jī)上配合使用一些其他的調(diào)試工具,例如功能更強(qiáng)大的cgdb、圖形界面的DDD調(diào)試器等,以方便內(nèi)核的調(diào)試工作。?
?
4 ?kgdb的一些特點(diǎn)和不足
使用kgdb作為內(nèi)核調(diào)試環(huán)境最大的不足在于對(duì)kgdb硬件環(huán)境的要求較高,必須使用兩臺(tái)計(jì)算機(jī)分別作為target和development機(jī)。盡管使用虛擬機(jī)的方法可以只用一臺(tái)PC即能搭建調(diào)試環(huán)境,但是對(duì)系統(tǒng)其他方面的性能也提出了一定的要求,同時(shí)也增加了搭建調(diào)試環(huán)境時(shí)復(fù)雜程度。另外,kgdb內(nèi)核的編譯、配置也比較復(fù)雜,需要一定的技巧,筆者當(dāng)時(shí)做的時(shí)候也是費(fèi)了很多周折。當(dāng)調(diào)試過程結(jié)束后時(shí),還需要重新制作所要發(fā)布的內(nèi)核。使用kgdb并不能進(jìn)行全程調(diào)試,也就是說kgdb并不能用于調(diào)試系統(tǒng)一開始的初始化引導(dǎo)過程。?
不過,kgdb是一個(gè)不錯(cuò)的內(nèi)核調(diào)試工具,使用它可以進(jìn)行對(duì)內(nèi)核的全面調(diào)試,甚至可以調(diào)試內(nèi)核的中斷處理程序。如果在一些圖形化的開發(fā)工具的幫助下,對(duì)內(nèi)核的調(diào)試將更方便。?
參考:?
透過虛擬化技術(shù)體驗(yàn)kgdb?
Linux 系統(tǒng)內(nèi)核的調(diào)試?
Debugging The Linux Kernel Using Gdb?
十 ?使用SkyEye構(gòu)建Linux內(nèi)核調(diào)試環(huán)境
SkyEye是一個(gè)開源軟件項(xiàng)目(OPenSource Software),SkyEye項(xiàng)目的目標(biāo)是在通用的Linux和Windows平臺(tái)上模擬常見的嵌入式計(jì)算機(jī)系統(tǒng)。SkyEye實(shí)現(xiàn)了一個(gè)指令級(jí)的硬件模擬平臺(tái),可以模擬多種嵌入式開發(fā)板,支持多種CPU指令集。SkyEye 的核心是 GNU 的 gdb 項(xiàng)目,它把gdb和 ARM Simulator很好地結(jié)合在了一起。加入ARMulator 的功能之后,它就可以來仿真嵌入式開發(fā)板,在它上面不僅可以調(diào)試硬件驅(qū)動(dòng),還可以調(diào)試操作系統(tǒng)。Skyeye項(xiàng)目目前已經(jīng)在嵌入式系統(tǒng)開發(fā)領(lǐng)域得到了很大的推廣。?
1 ?SkyEye的安裝和μcLinux內(nèi)核編譯
1.1 ?SkyEye的安裝?
SkyEye的安裝不是本文要介紹的重點(diǎn),目前已經(jīng)有大量的資料對(duì)此進(jìn)行了介紹。有關(guān)SkyEye的安裝與使用的內(nèi)容請(qǐng)查閱參考資料[11]。由于skyeye面目主要用于嵌入式系統(tǒng)領(lǐng)域,所以在skyeye上經(jīng)常使用的是μcLinux系統(tǒng),當(dāng)然使用Linux作為skyeye上運(yùn)行的系統(tǒng)也是可以的。由于介紹μcLinux 2.6在skyeye上編譯的相關(guān)資料并不多,所以下面進(jìn)行詳細(xì)介紹。?
1.2 ?μcLinux 2.6.x的編譯?
要在SkyEye中調(diào)試操作系統(tǒng)內(nèi)核,首先必須使被調(diào)試內(nèi)核能在SkyEye所模擬的開發(fā)板上正確運(yùn)行。因此,正確編譯待調(diào)試操作系統(tǒng)內(nèi)核并配置SkyEye是進(jìn)行內(nèi)核調(diào)試的第一步。下面我們以SkyEye模擬基于Atmel AT91X40的開發(fā)板,并運(yùn)行μcLinux 2.6為例介紹SkyEye的具體調(diào)試方法。?
I、安裝交叉編譯環(huán)境?
先安裝交叉編譯器。盡管在一些資料中說明使用工具鏈arm-elf-tools-20040427.sh ,但是由于arm-elf-xxx與arm-linux-xxx對(duì)宏及鏈接處理的不同,經(jīng)驗(yàn)證明使用arm-elf-xxx工具鏈在鏈接vmlinux的最后階段將會(huì)出錯(cuò)。所以這里我們使用的交叉編譯工具鏈?zhǔn)牵篴rm-uclinux-tools-base-gcc3.4.0-20040713.sh,關(guān)于該交叉編譯工具鏈的下載地址請(qǐng)參見[6]。注意以下步驟最好用root用戶來執(zhí)行。?
[root@lisl tmp]#chmod +x arm-uclinux-tools-base-gcc3.4.0-20040713.sh[root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh
安裝交叉編譯工具鏈之后,請(qǐng)確保工具鏈安裝路徑存在于系統(tǒng)PATH變量中。?
II、制作μcLinux內(nèi)核?
得到μcLinux發(fā)布包的一個(gè)最容易的方法是直接訪問uClinux.org站點(diǎn)[7]。該站點(diǎn)發(fā)布的內(nèi)核版本可能不是最新的,但你能找到一個(gè)最新的μcLinux補(bǔ)丁以及找一個(gè)對(duì)應(yīng)的Linux內(nèi)核版本來制作一個(gè)最新的μcLinux內(nèi)核。這里,將使用這種方法來制作最新的μcLinux內(nèi)核。目前(筆者記錄編寫此文章時(shí)),所能得到的發(fā)布包的最新版本是uClinux-dist.20041215.tar.gz。?
下載uClinux-dist.20041215.tar.gz,文件的下載地址請(qǐng)參見[7]。?
下載linux-2.6.9-hsc0.patch.gz,文件的下載地址請(qǐng)參見[8]。?
下載linux-2.6.9.tar.bz2,文件的下載地址請(qǐng)參見[9]。?
現(xiàn)在我們得到了整個(gè)的linux-2.6.9源代碼,以及所需的內(nèi)核補(bǔ)丁。請(qǐng)準(zhǔn)備一個(gè)有2GB空間的目錄里來完成以下制作μcLinux內(nèi)核的過程。?
[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2[root@lisl uClinux-dist]# tar -jxvf linux-2.6.9.tar.bz2[root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0
或者使用:
[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz [root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch
執(zhí)行以上過程后,將在linux-2.6.9/arch目錄下生成一個(gè)補(bǔ)丁目錄-armnommu。刪除原來μcLinux目錄里的linux-2.6.x(即那個(gè)linux-2.6.9-uc0),并將我們打好補(bǔ)丁的Linux內(nèi)核目錄更名為linux-2.6.x。?
[root@lisl uClinux-dist]# rm -rf linux-2.6.x/[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x
III、配置和編譯μcLinux內(nèi)核?
因?yàn)橹皇浅鲇谡{(diào)試μcLinux內(nèi)核的目的,這里沒有生成uClibc庫文件及romfs.img文件。在發(fā)布μcLinux時(shí),已經(jīng)預(yù)置了某些常用嵌入式開發(fā)板的配置文件,因此這里直接使用這些配置文件,過程如下:?
[root@lisl uClinux-dist]# cd linux-2.6.x[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_deconfig
atmel_deconfig文件是μcLinux發(fā)布時(shí)提供的一個(gè)配置文件,存放于目錄linux-2.6.x /arch/armnommu/configs/中。?
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- oldconfig
下面編譯配置好的內(nèi)核:?
[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1
一般情況下,編譯將順利結(jié)束并在Linux-2.6.x/目錄下生成未經(jīng)壓縮的μcLinux內(nèi)核文件vmlinux。需要注意的是為了調(diào)試μcLinux內(nèi)核,需要打開內(nèi)核編譯的調(diào)試選項(xiàng)-g,使編譯后的內(nèi)核帶有調(diào)試信息。打開編譯選項(xiàng)的方法可以選擇:?
"Kernel debugging->Compile the kernel with debug info"后將自動(dòng)打開調(diào)試選項(xiàng)。也可以直接修改linux-2.6.x目錄下的Makefile文件,為其打開調(diào)試開關(guān)。方法如下:。?
CFLAGS += -g
最容易出現(xiàn)的問題是找不到arm-uclinux-gcc命令的錯(cuò)誤,主要原因是PATH變量中沒有 包含arm-uclinux-gcc命令所在目錄。在arm-linux-gcc的缺省安裝情況下,它的安裝目錄是/root/bin/arm-linux-tool/,使用以下命令將路徑加到PATH環(huán)境變量中。?
Export PATH=$PATH:/root/bin/arm-linux-tool/bin
IV、根文件系統(tǒng)的制作?
Linux內(nèi)核在啟動(dòng)的時(shí)的最后操作之一是加載根文件系統(tǒng)。根文件系統(tǒng)中存放了嵌入式 系統(tǒng)使用的所有應(yīng)用程序、庫文件及其他一些需要用到的服務(wù)。出于文章篇幅的考慮,這里不打算介紹根文件系統(tǒng)的制作方法,讀者可以查閱一些其他的相關(guān)資料。值得注意的是,由配置文件skyeye.conf指定了裝載到內(nèi)核中的根文件系統(tǒng)。?
2 ?使用SkyEye調(diào)試
編譯完μcLinux內(nèi)核后,就可以在SkyEye中調(diào)試該ELF執(zhí)行文件格式的內(nèi)核了。前面已經(jīng)說過利用SkyEye調(diào)試內(nèi)核與使用gdb調(diào)試運(yùn)用程序的方法相同。?
需要提醒讀者的是,SkyEye的配置文件-skyeye.conf記錄了模擬的硬件配置和模擬執(zhí)行行為。該配置文件是SkyEye系統(tǒng)中一個(gè)及其重要的文件,很多錯(cuò)誤和異常情況的發(fā)生都和該文件有關(guān)。在安裝配置SkyEye出錯(cuò)時(shí),請(qǐng)首先檢查該配置文件然后再進(jìn)行其他的工作。此時(shí),所有的準(zhǔn)備工作已經(jīng)完成,就可以進(jìn)行內(nèi)核的調(diào)試工作了。?
3 ?使用SkyEye調(diào)試內(nèi)核的特點(diǎn)和不足
在SkyEye中可以進(jìn)行對(duì)Linux系統(tǒng)內(nèi)核的全程調(diào)試。由于SkyEye目前主要支持基于ARM內(nèi)核的CPU,因此一般而言需要使用交叉編譯工具編譯待調(diào)試的Linux系統(tǒng)內(nèi)核。另外,制作SkyEye中使用的內(nèi)核編譯、配置過程比較復(fù)雜、繁瑣。不過,當(dāng)調(diào)試過程結(jié)束后無需重新制作所要發(fā)布的內(nèi)核。?
SkyEye只是對(duì)系統(tǒng)硬件進(jìn)行了一定程度上的模擬,所以在SkyEye與真實(shí)硬件環(huán)境相比較而言還是有一定的差距,這對(duì)一些與硬件緊密相關(guān)的調(diào)試可能會(huì)有一定的影響,例如驅(qū)動(dòng)程序的調(diào)試。不過對(duì)于大部分軟件的調(diào)試,SkyEye已經(jīng)提供了精度足夠的模擬了。?
SkyEye的下一個(gè)目標(biāo)是和eclipse結(jié)合,有了圖形界面,能為調(diào)試和查看源碼提供一些方便。?
參考:?
Linux 系統(tǒng)內(nèi)核的調(diào)試?
?
十一 ?KDB
Linux 內(nèi)核調(diào)試器(KDB)允許您調(diào)試 Linux 內(nèi)核。這個(gè)恰如其名的工具實(shí)質(zhì)上是內(nèi)核代碼的補(bǔ)丁,它允許高手訪問內(nèi)核內(nèi)存和數(shù)據(jù)結(jié)構(gòu)。KDB 的主要優(yōu)點(diǎn)之一就是它不需要用另一臺(tái)機(jī)器進(jìn)行調(diào)試:您可以調(diào)試正在運(yùn)行的內(nèi)核。?
設(shè)置一臺(tái)用于 KDB 的機(jī)器需要花費(fèi)一些工作,因?yàn)樾枰o內(nèi)核打補(bǔ)丁并進(jìn)行重新編譯。KDB 的用戶應(yīng)當(dāng)熟悉 Linux 內(nèi)核的編譯(在一定程度上還要熟悉內(nèi)核內(nèi)部機(jī)理)。?
在本文中,我們將從有關(guān)下載 KDB 補(bǔ)丁、打補(bǔ)丁、(重新)編譯內(nèi)核以及啟動(dòng) KDB 方面的信息著手。然后我們將了解 KDB 命令并研究一些較常用的命令。最后,我們將研究一下有關(guān)設(shè)置和顯示選項(xiàng)方面的一些詳細(xì)信息。?
1 ?入門
KDB 項(xiàng)目是由 Silicon Graphics 維護(hù)的,您需要從它的 FTP 站點(diǎn)下載與內(nèi)核版本有關(guān)的補(bǔ)丁。(在編寫本文時(shí))可用的最新 KDB 版本是 4.2。您將需要下載并應(yīng)用兩個(gè)補(bǔ)丁。一個(gè)是“公共的”補(bǔ)丁,包含了對(duì)通用內(nèi)核代碼的更改,另一個(gè)是特定于體系結(jié)構(gòu)的補(bǔ)丁。補(bǔ)丁可作為 bz2 文件獲取。例如,在運(yùn)行 2.4.20 內(nèi)核的 x86 機(jī)器上,您會(huì)需要 kdb-v4.2-2.4.20-common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。?
這里所提供的所有示例都是針對(duì) i386 體系結(jié)構(gòu)和 2.4.20 內(nèi)核的。您將需要根據(jù)您的機(jī)器和內(nèi)核版本進(jìn)行適當(dāng)?shù)母摹D€需要擁有 root 許可權(quán)以執(zhí)行這些操作。?
將文件復(fù)制到 /usr/src/linux 目錄中并從用 bzip2 壓縮的文件解壓縮補(bǔ)丁文件:
#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2
您將獲得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。?
現(xiàn)在,應(yīng)用這些補(bǔ)?。?/p>
#patch -p1 2-2.4.20-common-1#patch -p1 2-2.4.20-i386-1
這些補(bǔ)丁應(yīng)該干凈利落地加以應(yīng)用。查找任何以 .rej 結(jié)尾的文件。這個(gè)擴(kuò)展名表明這些是失敗的補(bǔ)丁。如果內(nèi)核樹沒問題,那么補(bǔ)丁的應(yīng)用就不會(huì)有任何問題。?
接下來,需要構(gòu)建內(nèi)核以支持 KDB。第一步是設(shè)置 CONFIG_KDB 選項(xiàng)。使用您喜歡的配置機(jī)制(xconfig 和 menuconfig 等)來完成這一步。轉(zhuǎn)到結(jié)尾處的“Kernel hacking”部分并選擇“Built-in Kernel Debugger support”選項(xiàng)。?
您還可以根據(jù)自己的偏好選擇其它兩個(gè)選項(xiàng)。選擇“Compile the kernel with frame pointers”選項(xiàng)(如果有的話)則設(shè)置CONFIG_FRAME_POINTER 標(biāo)志。這將產(chǎn)生更好的堆?;厮?,因?yàn)閹羔樇拇嫫鞅挥米鲙羔樁皇峭ㄓ眉拇嫫鳌D€可以選擇“KDB off by default”選項(xiàng)。這將設(shè)置 CONFIG_KDB_OFF 標(biāo)志,并且在缺省情況下將關(guān)閉 KDB。我們將在后面一節(jié)中對(duì)此進(jìn)行詳細(xì)介紹。?
保存配置,然后退出。重新編譯內(nèi)核。建議在構(gòu)建內(nèi)核之前執(zhí)行“make clean”。用常用方式安裝內(nèi)核并引導(dǎo)它。?
2 ?初始化并設(shè)置環(huán)境變量
您可以定義將在 KDB 初始化期間執(zhí)行的 KDB 命令。需要在純文本文件 kdb_cmds 中定義這些命令,該文件位于 Linux 源代碼樹(當(dāng)然是在打了補(bǔ)丁之后)的 KDB 目錄中。該文件還可以用來定義設(shè)置顯示和打印選項(xiàng)的環(huán)境變量。文件開頭的注釋提供了編輯文件方面的幫助。使用這個(gè)文件的缺點(diǎn)是,在您更改了文件之后需要重新構(gòu)建并重新安裝內(nèi)核。?
3 ?激活 KDB
如果編譯期間沒有選中 CONFIG_KDB_OFF ,那么在缺省情況下 KDB 是活動(dòng)的。否則,您需要顯式地激活它 - 通過在引導(dǎo)期間將kdb=on 標(biāo)志傳遞給內(nèi)核或者通過在掛裝了 /proc 之后執(zhí)行該工作:
#echo "1" >/proc/sys/kernel/kdb
倒過來執(zhí)行上述步驟則會(huì)取消激活 KDB。也就是說,如果缺省情況下 KDB 是打開的,那么將 kdb=off 標(biāo)志傳遞給內(nèi)核或者執(zhí)行下面這個(gè)操作將會(huì)取消激活 KDB:
#echo "0" >/proc/sys/kernel/kdb
在引導(dǎo)期間還可以將另一個(gè)標(biāo)志傳遞給內(nèi)核。 kdb=early 標(biāo)志將導(dǎo)致在引導(dǎo)過程的初始階段就把控制權(quán)傳遞給 KDB。如果您需要在引導(dǎo)過程初始階段進(jìn)行調(diào)試,那么這將有所幫助。?
調(diào)用 KDB 的方式有很多。如果 KDB 處于打開狀態(tài),那么只要內(nèi)核中有緊急情況就自動(dòng)調(diào)用它。按下鍵盤上的 PAUSE 鍵將手工調(diào)用 KDB。調(diào)用 KDB 的另一種方式是通過串行控制臺(tái)。當(dāng)然,要做到這一點(diǎn),需要設(shè)置串行控制臺(tái)并且需要一個(gè)從串行控制臺(tái)進(jìn)行讀取的程序。按鍵序列 Ctrl-A 將從串行控制臺(tái)調(diào)用 KDB。?
4 ?KDB 命令
KDB 是一個(gè)功能非常強(qiáng)大的工具,它允許進(jìn)行幾個(gè)操作,比如內(nèi)存和寄存器修改、應(yīng)用斷點(diǎn)和堆棧跟蹤。根據(jù)這些,可以將 KDB 命令分成幾個(gè)類別。下面是有關(guān)每一類中最常用命令的詳細(xì)信息。?
4.1 ?內(nèi)存顯示和修改?
這一類別中最常用的命令是 md 、 mdr 、 mm 和 mmW 。?
md 命令以一個(gè)地址/符號(hào)和行計(jì)數(shù)為參數(shù),顯示從該地址開始的 line-count 行的內(nèi)存。如果沒有指定 line-count ,那么就使用環(huán)境變量所指定的缺省值。如果沒有指定地址,那么 md 就從上一次打印的地址繼續(xù)。地址打印在開頭,字符轉(zhuǎn)換打印在結(jié)尾。?
mdr 命令帶有地址/符號(hào)以及字節(jié)計(jì)數(shù),顯示從指定的地址開始的 byte-count 字節(jié)數(shù)的初始內(nèi)存內(nèi)容。它本質(zhì)上和 md 一樣,但是它不顯示起始地址并且不在結(jié)尾顯示字符轉(zhuǎn)換。 mdr 命令較少使用。?
mm 命令修改內(nèi)存內(nèi)容。它以地址/符號(hào)和新內(nèi)容作為參數(shù),用 new-contents 替換地址處的內(nèi)容。?
mmW 命令更改從地址開始的 W 個(gè)字節(jié)。請(qǐng)注意, mm 更改一個(gè)機(jī)器字。?
示例?
顯示從 0xc000000 開始的 15 行內(nèi)存:
[0]kdb> md 0xc000000 15
將內(nèi)存位置為 0xc000000 上的內(nèi)容更改為 0x10:
[0]kdb> mm 0xc000000 0x10
4.2 ?寄存器顯示和修改?
這一類別中的命令有 rd 、 rm 和 ef 。?
rd 命令(不帶任何參數(shù))顯示處理器寄存器的內(nèi)容。它可以有選擇地帶三個(gè)參數(shù)。如果傳遞了 c 參數(shù),則 rd 顯示處理器的控制寄存器;如果帶有 d 參數(shù),那么它就顯示調(diào)試寄存器;如果帶有 u 參數(shù),則顯示上一次進(jìn)入內(nèi)核的當(dāng)前任務(wù)的寄存器組。?
rm 命令修改寄存器的內(nèi)容。它以寄存器名稱和 new-contents 作為參數(shù),用 new-contents 修改寄存器。寄存器名稱與特定的體系結(jié)構(gòu)有關(guān)。目前,不能修改控制寄存器。?
ef 命令以一個(gè)地址作為參數(shù),它顯示指定地址處的異常幀。?
示例?
顯示通用寄存器組:
[0]kdb> rd[0]kdb> rm %ebx 0x25
4.3 ?斷點(diǎn)?
常用的斷點(diǎn)命令有 bp 、 bc 、 bd 、 be 和 bl 。?
bp 命令以一個(gè)地址/符號(hào)作為參數(shù),它在地址處應(yīng)用斷點(diǎn)。當(dāng)遇到該斷點(diǎn)時(shí)則停止執(zhí)行并將控制權(quán)交予 KDB。該命令有幾個(gè)有用的變體。 bpa 命令對(duì) SMP 系統(tǒng)中的所有處理器應(yīng)用斷點(diǎn)。 bph 命令強(qiáng)制在支持硬件寄存器的系統(tǒng)上使用它。 bpha 命令類似于 bpa 命令,差別在于它強(qiáng)制使用硬件寄存器。?
bd 命令禁用特殊斷點(diǎn)。它接收斷點(diǎn)號(hào)作為參數(shù)。該命令不是從斷點(diǎn)表中除去斷點(diǎn),而只是禁用它。斷點(diǎn)號(hào)從 0 開始,根據(jù)可用性順序分配給斷點(diǎn)。?
be 命令啟用斷點(diǎn)。該命令的參數(shù)也是斷點(diǎn)號(hào)。?
bl 命令列出當(dāng)前的斷點(diǎn)集。它包含了啟用的和禁用的斷點(diǎn)。?
bc 命令從斷點(diǎn)表中除去斷點(diǎn)。它以具體的斷點(diǎn)號(hào)或 * 作為參數(shù),在后一種情況下它將除去所有斷點(diǎn)。?
示例?
對(duì)函數(shù) sys_write() 設(shè)置斷點(diǎn):
[0]kdb> bp sys_write
列出斷點(diǎn)表中的所有斷點(diǎn):
[0]kdb> bl
清除斷點(diǎn)號(hào) 1:
[0]kdb> bc 1
4.4 ?堆棧跟蹤?
主要的堆棧跟蹤命令有 bt 、 btp 、 btc 和 bta 。?
bt 命令設(shè)法提供有關(guān)當(dāng)前線程的堆棧的信息。它可以有選擇地將堆棧幀地址作為參數(shù)。如果沒有提供地址,那么它采用當(dāng)前寄存器來回溯堆棧。否則,它假定所提供的地址是有效的堆棧幀起始地址并設(shè)法進(jìn)行回溯。如果內(nèi)核編譯期間設(shè)置了CONFIG_FRAME_POINTER 選項(xiàng),那么就用幀指針寄存器來維護(hù)堆棧,從而就可以正確地執(zhí)行堆?;厮荨H绻麤]有設(shè)置CONFIG_FRAME_POINTER ,那么 bt 命令可能會(huì)產(chǎn)生錯(cuò)誤的結(jié)果。?
btp 命令將進(jìn)程標(biāo)識(shí)作為參數(shù),并對(duì)這個(gè)特定進(jìn)程進(jìn)行堆?;厮荨?
btc 命令對(duì)每個(gè)活動(dòng) CPU 上正在運(yùn)行的進(jìn)程執(zhí)行堆?;厮?。它從第一個(gè)活動(dòng) CPU 開始執(zhí)行 bt ,然后切換到下一個(gè)活動(dòng) CPU,以此類推。?
bta 命令對(duì)處于某種特定狀態(tài)的所有進(jìn)程執(zhí)行回溯。若不帶任何參數(shù),它就對(duì)所有進(jìn)程執(zhí)行回溯??梢杂羞x擇地將各種參數(shù)傳遞給該命令。將根據(jù)參數(shù)處理處于特定狀態(tài)的進(jìn)程。選項(xiàng)以及相應(yīng)的狀態(tài)如下:?
D:不可中斷狀態(tài)
R:正運(yùn)行
S:可中斷休眠
T:已跟蹤或已停止
Z:僵死
U:不可運(yùn)行
這類命令中的每一個(gè)都會(huì)打印出一大堆信息。
示例?
跟蹤當(dāng)前活動(dòng)線程的堆棧:
[0]kdb> bt
跟蹤標(biāo)識(shí)為 575 的進(jìn)程的堆棧:
[0]kdb> btp 575
4.5 ?其它命令?
下面是在內(nèi)核調(diào)試過程中非常有用的其它幾個(gè) KDB 命令。?
id 命令以一個(gè)地址/符號(hào)作為參數(shù),它對(duì)從該地址開始的指令進(jìn)行反匯編。環(huán)境變量 IDCOUNT 確定要顯示多少行輸出。?
ss 命令單步執(zhí)行指令然后將控制返回給 KDB。該指令的一個(gè)變體是 ssb ,它執(zhí)行從當(dāng)前指令指針地址開始的指令(在屏幕上打印指令),直到它遇到將引起分支轉(zhuǎn)移的指令為止。分支轉(zhuǎn)移指令的典型示例有 call 、 return 和 jump 。?
go 命令讓系統(tǒng)繼續(xù)正常執(zhí)行。一直執(zhí)行到遇到斷點(diǎn)為止(如果已應(yīng)用了一個(gè)斷點(diǎn)的話)。?
reboot 命令立刻重新引導(dǎo)系統(tǒng)。它并沒有徹底關(guān)閉系統(tǒng),因此結(jié)果是不可預(yù)測的。?
ll 命令以地址、偏移量和另一個(gè) KDB 命令作為參數(shù)。它對(duì)鏈表中的每個(gè)元素反復(fù)執(zhí)行作為參數(shù)的這個(gè)命令。所執(zhí)行的命令以列表中當(dāng)前元素的地址作為參數(shù)。?
示例?
反匯編從例程 schedule 開始的指令。所顯示的行數(shù)取決于環(huán)境變量 IDCOUNT :
[0]kdb> id schedule
執(zhí)行指令直到它遇到分支轉(zhuǎn)移條件(在本例中為指令 jne )為止:
[0]kdb> ssb0xc0105355 default_idle+0x25: cli0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax0xc0105359 default_idle+0x29: test %eax, %eax0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31
5 ?技巧和訣竅
調(diào)試一個(gè)問題涉及到:使用調(diào)試器(或任何其它工具)找到問題的根源以及使用源代碼來跟蹤導(dǎo)致問題的根源。單單使用源代碼來確定問題是極其困難的,只有老練的內(nèi)核黑客才有可能做得到。相反,大多數(shù)的新手往往要過多地依靠調(diào)試器來修正錯(cuò)誤。這種方法可能會(huì)產(chǎn)生不正確的問題解決方案。我們擔(dān)心的是這種方法只會(huì)修正表面癥狀而不能解決真正的問題。此類錯(cuò)誤的典型示例是添加錯(cuò)誤處理代碼以處理 NULL 指針或錯(cuò)誤的引用,卻沒有查出無效引用的真正原因。?
結(jié)合研究代碼和使用調(diào)試工具這兩種方法是識(shí)別和修正問題的最佳方案。?
調(diào)試器的主要用途是找到錯(cuò)誤的位置、確認(rèn)癥狀(在某些情況下還有起因)、確定變量的值,以及確定程序是如何出現(xiàn)這種情況的(即,建立調(diào)用堆棧)。有經(jīng)驗(yàn)的黑客會(huì)知道對(duì)于某種特定的問題應(yīng)使用哪一個(gè)調(diào)試器,并且能迅速地根據(jù)調(diào)試獲取必要的信息,然后繼續(xù)分析代碼以識(shí)別起因。?
因此,這里為您介紹了一些技巧,以便您能使用 KDB 快速地取得上述結(jié)果。當(dāng)然,要記住,調(diào)試的速度和精確度來自經(jīng)驗(yàn)、實(shí)踐和良好的系統(tǒng)知識(shí)(硬件和內(nèi)核內(nèi)部機(jī)理等)。?
5.1 ?技巧 #1?
在 KDB 中,在提示處輸入地址將返回與之最為匹配的符號(hào)。這在堆棧分析以及確定全局?jǐn)?shù)據(jù)的地址/值和函數(shù)地址方面極其有用。同樣,輸入符號(hào)名則返回其虛擬地址。?
示例?
表明函數(shù) sys_read 從地址 0xc013db4c 開始:
[0]kdb> 0xc013db4c0xc013db4c = 0xc013db4c (sys_read)
同樣,表明 sys_write 位于地址 0xc013dcc8:
[0]kdb> sys_writesys_write = 0xc013dcc8 (sys_write)
這些有助于在分析堆棧時(shí)找到全局?jǐn)?shù)據(jù)和函數(shù)地址。?
5.2 ?技巧 #2?
在編譯帶 KDB 的內(nèi)核時(shí),只要 CONFIG_FRAME_POINTER 選項(xiàng)出現(xiàn)就使用該選項(xiàng)。為此,需要在配置內(nèi)核時(shí)選擇“Kernel hacking”部分下面的“Compile the kernel with frame pointers”選項(xiàng)。這確保了幀指針寄存器將被用作幀指針,從而產(chǎn)生正確的回溯。實(shí)際上,您可以手工轉(zhuǎn)儲(chǔ)幀指針寄存器的內(nèi)容并跟蹤整個(gè)堆棧。例如,在 i386 機(jī)器上,%ebp 寄存器可以用來回溯整個(gè)堆棧。?
例如,在函數(shù) rmqueue() 上執(zhí)行第一個(gè)指令后,堆??瓷先ヮ愃朴谙旅孢@樣:
[0]kdb> md %ebp0xc74c9f38 c74c9f60 c0136c40 000001f0 000000000xc74c9f48 08053328 c0425238 c04253a8 000000000xc74c9f58 000001f0 00000246 c74c9f6c c0136a250xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc0xc74c9f78 c014fe45 c74c8000 00000000 08053328[0]kdb> 0xc0136c400xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)[0]kdb> 0xc0136a250xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)[0]kdb> 0xc0136d6d0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)
我們可以看到 rmqueue() 被 __alloc_pages 調(diào)用,后者接下來又被 _alloc_pages 調(diào)用,以此類推。?
每一幀的第一個(gè)雙字(double word)指向下一幀,這后面緊跟著調(diào)用函數(shù)的地址。因此,跟蹤堆棧就變成一件輕松的工作了。?
5.3 ?技巧 #3?
go 命令可以有選擇地以一個(gè)地址作為參數(shù)。如果您想在某個(gè)特定地址處繼續(xù)執(zhí)行,則可以提供該地址作為參數(shù)。另一個(gè)辦法是使用rm 命令修改指令指針寄存器,然后只要輸入 go 。如果您想跳過似乎會(huì)引起問題的某個(gè)特定指令或一組指令,這就會(huì)很有用。但是,請(qǐng)注意,該指令使用不慎會(huì)造成嚴(yán)重的問題,系統(tǒng)可能會(huì)嚴(yán)重崩潰。?
5.4 ?技巧 #4?
您可以利用一個(gè)名為 defcmd 的有用命令來定義自己的命令集。例如,每當(dāng)遇到斷點(diǎn)時(shí),您可能希望能同時(shí)檢查某個(gè)特殊變量、檢查某些寄存器的內(nèi)容并轉(zhuǎn)儲(chǔ)堆棧。通常,您必須要輸入一系列命令,以便能同時(shí)執(zhí)行所有這些工作。 defcmd 允許您定義自己的命令,該命令可以包含一個(gè)或多個(gè)預(yù)定義的 KDB 命令。然后只需要用一個(gè)命令就可以完成所有這三項(xiàng)工作。其語法如下:
[0]kdb> defcmd name "usage" "help"[0]kdb> [defcmd] type the commands here[0]kdb> [defcmd] endefcmd
例如,可以定義一個(gè)(簡單的)新命令 hari ,它顯示從地址 0xc000000 開始的一行內(nèi)存、顯示寄存器的內(nèi)容并轉(zhuǎn)儲(chǔ)堆棧:
[0]kdb> defcmd hari "" "no arguments needed"[0]kdb> [defcmd] md 0xc000000 1[0]kdb> [defcmd] rd[0]kdb> [defcmd] md %ebp 1[0]kdb> [defcmd] endefcmd
該命令的輸出會(huì)是:
[0]kdb> hari[hari]kdb> md 0xc000000 10xc000000 00000001 f000e816 f000e2c3 f000e816[hari]kdb> rdeax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000.......[hari]kdb> md %ebp 10xc0467fbc c0467fd0 c01053d2 00000002 000a0200[0]kdb>
5.5 ?技巧 #5?
可以使用 bph 和 bpha 命令(假如體系結(jié)構(gòu)支持使用硬件寄存器)來應(yīng)用讀寫斷點(diǎn)。這意味著每當(dāng)從某個(gè)特定地址讀取數(shù)據(jù)或?qū)?shù)據(jù)寫入該地址時(shí),我們都可以對(duì)此進(jìn)行控制。當(dāng)調(diào)試數(shù)據(jù)/內(nèi)存毀壞問題時(shí)這可能會(huì)極其方便,在這種情況中您可以用它來識(shí)別毀壞的代碼/進(jìn)程。?
示例?
每當(dāng)將四個(gè)字節(jié)寫入地址 0xc0204060 時(shí)就進(jìn)入內(nèi)核調(diào)試器:
[0]kdb> bph 0xc0204060 dataw 4
在讀取從 0xc000000 開始的至少兩個(gè)字節(jié)的數(shù)據(jù)時(shí)進(jìn)入內(nèi)核調(diào)試器:
[0]kdb> bph 0xc000000 datar 2
6 結(jié)束語
對(duì)于執(zhí)行內(nèi)核調(diào)試,KDB 是一個(gè)方便的且功能強(qiáng)大的工具。它提供了各種選項(xiàng),并且使我們能夠分析內(nèi)存內(nèi)容和數(shù)據(jù)結(jié)構(gòu)。最妙的是,它不需要用另一臺(tái)機(jī)器來執(zhí)行調(diào)試。?
參考:?
Linux 內(nèi)核調(diào)試器內(nèi)幕 KDB入門指南?
?
十二 ?Kprobes
Kprobes 是 Linux 中的一個(gè)簡單的輕量級(jí)裝置,讓您可以將斷點(diǎn)插入到正在運(yùn)行的內(nèi)核之中。 Kprobes 提供了一個(gè)強(qiáng)行進(jìn)入任何內(nèi)核例程并從中斷處理器無干擾地收集信息的接口。使用 Kprobes 可以 輕松地收集處理器寄存器和全局?jǐn)?shù)據(jù)結(jié)構(gòu)等調(diào)試信息。開發(fā)者甚至可以使用 Kprobes 來修改 寄存器值和全局?jǐn)?shù)據(jù)結(jié)構(gòu)的值。?
為完成這一任務(wù),Kprobes 向運(yùn)行的內(nèi)核中給定地址寫入斷點(diǎn)指令,插入一個(gè)探測器。 執(zhí)行被探測的指令會(huì)導(dǎo)致斷點(diǎn)錯(cuò)誤。Kprobes 鉤?。╤ook in)斷點(diǎn)處理器并收集調(diào)試信息。Kprobes 甚至可以單步執(zhí)行被探測的指令。?
1 ?安裝
要安裝 Kprobes,需要從 Kprobes 主頁下載最新的補(bǔ)丁。 打包的文件名稱類似于 kprobes-2.6.8-rc1.tar.gz。解開補(bǔ)丁并將其安裝到 Linux 內(nèi)核:?
$tar -xvzf kprobes-2.6.8-rc1.tar.gz $cd /usr/src/linux-2.6.8-rc1 $patch -p1 < ../kprobes-2.6.8-rc1-base.patch
Kprobes 利用了 SysRq 鍵,這個(gè) DOS 時(shí)代的產(chǎn)物在 Linux 中有了新的用武之地。您可以在 Scroll Lock鍵左邊找到 SysRq 鍵;它通常標(biāo)識(shí)為 Print Screen。要為 Kprobes 啟用 SysRq 鍵,需要安裝 kprobes-2.6.8-rc1-sysrq.patch 補(bǔ)丁:
$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch
使用 make xconfig/ make menuconfig/ make oldconfig 配置內(nèi)核,并 啟用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ標(biāo)記。 編譯并引導(dǎo)到新內(nèi)核。您現(xiàn)在就已經(jīng)準(zhǔn)備就緒,可以插入 printk 并通過編寫簡單的 Kprobes 模塊來動(dòng)態(tài)而且無干擾地 收集調(diào)試信息。?
2 ?編寫 Kprobes 模塊
對(duì)于每一個(gè)探測器,您都要分配一個(gè)結(jié)構(gòu)體 struct kprobe kp; (參考 include/linux/kprobes.h 以獲得關(guān)于此數(shù)據(jù)結(jié)構(gòu)的詳細(xì)信息)。?
清單 9. 定義 pre、post 和 fault 處理器
/* pre_handler: this is called just before the probed instruction is * executed. */int handler_pre(struct kprobe *p, struct pt_regs *regs) {printk("pre_handler: p->addr=0x%p, eflags=0x%lx ",p->addr,regs->eflags);return 0;}/* post_handler: this is called after the probed instruction is executed * (provided no exception is generated). */void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {printk("post_handler: p->addr=0x%p, eflags=0x%lx ", p->addr,regs->eflags);}/* fault_handler: this is called if an exception is generated for any * instruction within the fault-handler, or when Kprobes * single-steps the probed instruction. */int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {printk("fault_handler:p->addr=0x%p, eflags=0x%lx ", p->addr,regs->eflags);return 0;}
2.1 ?獲得內(nèi)核例程的地址?
在注冊(cè)過程中,您還需要指定插入探測器的內(nèi)核例程的地址。使用這些方法中的任意一個(gè)來獲得內(nèi)核例程 的地址:?
從 System.map 文件直接得到地址。
例如,要得到 do_fork 的地址,可以在命令行執(zhí)行 $grep do_fork /usr/src/linux/System.map 。
使用 nm 命令。
$nm vmlinuz |grep do_fork
從 /proc/kallsyms 文件獲得地址。
$cat /proc/kallsyms |grep do_fork
使用 kallsyms_lookup_name() 例程。
這個(gè)例程是在 kernel/kallsyms.c 文件中定義的,要使用它,必須啟用 CONFIG_KALLSYMS 編譯內(nèi)核。kallsyms_lookup_name() 接受一個(gè)字符串格式內(nèi)核例程名, 返回那個(gè)內(nèi)核例程的地址。例如:kallsyms_lookup_name("do_fork");
然后在 init_moudle 中注冊(cè)您的探測器:?
清單 10. 注冊(cè)一個(gè)探測器
/* specify pre_handler address */kp.pre_handler=handler_pre;/* specify post_handler address */kp.post_handler=handler_post;/* specify fault_handler address */kp.fault_handler=handler_fault;/* specify the address/offset where you want to insert probe. * You can get the address using one of the methods described above. */kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");/* check if the kallsyms_lookup_name() returned the correct value. */if (kp.add == NULL) {printk("kallsyms_lookup_name could not find addressfor the specified symbol name ");return 1;}/* or specify address directly. * $grep "do_fork" /usr/src/linux/System.map * or * $cat /proc/kallsyms |grep do_fork * or * $nm vmlinuz |grep do_fork */kp.addr = (kprobe_opcode_t *) 0xc01441d0;/* All set to register with Kprobes */ register_kprobe(&kp);
一旦注冊(cè)了探測器,運(yùn)行任何 shell 命令都會(huì)導(dǎo)致一個(gè)對(duì) do_fork 的調(diào)用,您將可以在控制臺(tái)上或者運(yùn)行 dmesg 命令來查看您的 printk。做完后要記得注銷探測器:?
unregister_kprobe(&kp);?
下面的輸出顯示了 kprobe 的地址以及 eflags 寄存器的內(nèi)容:?
$tail -5 /var/log/messages Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202 Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196
2.2 ?獲得偏移量?
您可以在例程的開頭或者函數(shù)中的任意偏移位置插入 printk(偏移量必須在指令范圍之內(nèi))。 下面的代碼示例展示了如何來計(jì)算偏移量。首先,從對(duì)象文件中反匯編機(jī)器指令,并將它們 保存為一個(gè)文件:?
$objdump -D /usr/src/linux/kernel/fork.o > fork.dis
其結(jié)果是:?
清單 11. 反匯編的 fork
000022b0 : 22b0: 55 push %ebp 22b1: 89 e5 mov %esp,%ebp 22b3: 57 push %edi 22b4: 89 c7 mov %eax,%edi 22b6: 56 push %esi 22b7: 89 d6 mov %edx,%esi 22b9: 53 push %ebx 22ba: 83 ec 38 sub $0x38,%esp 22bd: c7 45 d0 00 00 00 00 movl $0x0,0xffffffd0(%ebp) 22c4: 89 cb mov %ecx,%ebx 22c6: 89 44 24 04 mov %eax,0x4(%esp) 22ca: c7 04 24 0a 00 00 00 movl $0xa,(%esp) 22d1: e8 fc ff ff ff call 22d2 0x22> 22d6: b8 00 e0 ff ff mov $0xffffe000,%eax 22db: 21 e0 and %esp,%eax 22dd: 8b 00 mov (%eax),%eax
要在偏移位置 0x22c4 插入探測器,先要得到與例程的開始處相對(duì)的偏移量 0x22c4 - 0x22b0 = 0x14 ,然后將這個(gè)偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14 。(運(yùn)行 $cat /proc/kallsyms | grep do_fork 命令以獲得 do_fork 的地址。)?
您還可以將 do_fork 的相對(duì)偏移量 0x22c4 - 0x22b0 = 0x14 添加到 kallsyms_lookup_name("do_fork"); 的輸入,即:0x14 + kallsyms_lookup_name("do_fork");?
2.3 ?轉(zhuǎn)儲(chǔ)內(nèi)核數(shù)據(jù)結(jié)構(gòu)?
現(xiàn)在,讓我們使用修改過的用來轉(zhuǎn)儲(chǔ)數(shù)據(jù)結(jié)構(gòu)的 Kprobe post_handler 來轉(zhuǎn)儲(chǔ)運(yùn)行在系統(tǒng)上的所有作業(yè)的一些組成部分:?
清單 12. 用來轉(zhuǎn)儲(chǔ)數(shù)據(jù)結(jié)構(gòu)的修改過的 Kprope post_handler
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { struct task_struct *task; read_lock(&tasklist_lock); for_each_process(task) { printk("pid =%x task-info_ptr=%lx ", task->pid, task->thread_info); printk("thread-info element status=%lx,flags=%lx,cpu=%lx ", task->thread_info->status, task->thread_info->flags, task->thread_info->cpu); } read_unlock(&tasklist_lock);}
這個(gè)模塊應(yīng)該插入到 do_fork 的偏移位置。?
清單 13. pid 1508 和 1509 的結(jié)構(gòu)體 thread_info 的輸出
$tail -10 /var/log/messagesJun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000
2.4 ?啟用奇妙的 SysRq 鍵?
為了支持 SysRq 鍵,我們已經(jīng)進(jìn)行了編譯。這樣來啟用它:?
$echo 1 > /proc/sys/kernel/sysrq
現(xiàn)在,您可以使用 Alt+SysRq+W 在控制臺(tái)上或者到 /var/log/messages 中去查看所有插入的內(nèi)核探測器。?
清單 14. /var/log/messages 顯示出在 do_fork 插入了一個(gè) Kprobe
Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobesJun 23 10:24:48 linux-udp4749545uds kernel:Jun 23 10:24:48 linux-udp4749545uds kernel: [] do_fork+0x0/0x1de
3 ?使用 Kprobes 更好地進(jìn)行調(diào)試
由于探測器事件處理器是作為系統(tǒng)斷點(diǎn)中斷處理器的擴(kuò)展來運(yùn)行,所以它們很少或者根本不依賴于系統(tǒng) 工具 —— 這樣可以被植入到大部分不友好的環(huán)境中(從中斷時(shí)間和任務(wù)時(shí)間到禁用的上下文間切換和支持 SMP 的代碼路徑)—— 都不會(huì)對(duì)系統(tǒng)性能帶來負(fù)面影響。?
使用 Kprobes 的好處有很多。不需要重新編譯和重新引導(dǎo)內(nèi)核就可以插入 printk。為了進(jìn)行調(diào)試可以記錄 處理器寄存器的日志,甚至進(jìn)行修改 —— 不會(huì)干擾系統(tǒng)。類似地,同樣可以無干擾地記錄 Linux 內(nèi)核數(shù)據(jù)結(jié)構(gòu)的日志,甚至 進(jìn)行修改。您甚至可以使用 Kprobes 調(diào)試 SMP 系統(tǒng)上的競態(tài)條件 —— 避免了您自己重新編譯和重新引導(dǎo)的所有 麻煩。您將發(fā)現(xiàn)內(nèi)核調(diào)試比以往更為快速和簡單。?
?
評(píng)論