Linux調(diào)試技術(shù)介紹 對于任何編寫內(nèi)核代碼的人來說,最吸引他們注意的問題之一就是如何完成調(diào)試,。由于內(nèi)核是一個不與某個進程相關(guān)的功能集,,其代碼不能很輕松地放在調(diào)試器中執(zhí)行,而且也不能跟蹤,。 本章介紹你可以用來監(jiān)視內(nèi)核代碼和跟蹤錯誤的技術(shù),。 用打印信息調(diào)試 最一般的調(diào)試技術(shù)就是監(jiān)視,,就是在應(yīng)用內(nèi)部合適的點加上printf調(diào)用,。當你調(diào)試內(nèi)核代碼的時候,你可以用printk完成這個任務(wù),。 Printk 在前些章中,,我們簡單假設(shè)printk工作起來和printf很類似?,F(xiàn)在是介紹一下它們之間不同的時候了。 其中一個不同點就是,,printk允許你根據(jù)它們的嚴重程度,,通過附加不同的“記錄級”來對消息分類,或賦予消息優(yōu)先級,。你可以用宏來指示記錄級,。例如,KERN_INFO,,我們前面已經(jīng)看到它被加在打印語句的前面,,它就是一種可能的消息記錄級。記錄級宏展開為一個字串,,在編譯時和消息文本拼接在一起,;這也就是為什么下面的例子中優(yōu)先級和格式字串間沒有逗號。這有兩個printk的例子,,一個是調(diào)試信息,,一個是關(guān)鍵信息: (代碼) 在<linux/kernel.h>中定義了8種記錄級別串。沒有指定優(yōu)先級的printk語句默認使用DEFAULT_MESSAGE_LOGLEVEL優(yōu)先級,,它是一個在kernel/printk.c中定義的整數(shù),。默認記錄級的具體數(shù)值在Linux的開發(fā)期間曾變化過若干次,所以我建議你最好總是指定一個合適的記錄級,。 根據(jù)記錄級,,內(nèi)核將消息打印到當前文本控制臺上:如果優(yōu)先級低于console_loglevel這個數(shù)值的話,該消息就顯示在控制臺上,。如果系統(tǒng)同時運行了klogd和syslogd,,無論console_loglevel為何值,內(nèi)核都將消息追加到/var/log/messages中,。 變量console_loglevel最初初始化為DEFAULT_CONSOLE_LOGLEVEL,,但可以通過sys_syslog系統(tǒng)調(diào)用修改。如klogd的手冊所示,,可以在啟動klogd時指定-c開關(guān)來修改這個變量,。此外,你還可以寫個程序來改變控制臺記錄級,。你可以在O’Reilly站點上的源文件中找到我寫的一個這種功能的程序,,miscprogs/setlevel.c。新優(yōu)先級是通過一個1到8之間的整數(shù)值指定的,。 你也許需要在內(nèi)核失效后降低記錄級(見“調(diào)試系統(tǒng)故障”),,這是因為失效處理代碼會將console_loglevel提升到15,之后所有的消息都會出現(xiàn)在控制臺上,。為看到你的調(diào)試信息,,如果你運行的是內(nèi)核2.0.x話,,你需要提升記錄級。內(nèi)核2.0發(fā)行降低了MINIMUM_CONSOLE_LOGLEVEL,,而舊版本的klogd默認情況下要打印很多控制消息,。如果你碰巧使用了這個舊版本的守護進程,除非你提升記錄級,,內(nèi)核2.0會比你預(yù)期的打印出更少的消息,。這就是為什么hello.c中使用了<1>標記,這樣可以保證消息顯示在控制臺上,。 從1.3.43一來的內(nèi)核版本通過允許你向指定虛控制臺發(fā)送消息,,藉此提供一個靈活的記錄策略。默認情況下,,“控制臺”是當前虛終端,。也可以選擇不同的虛終端接收消息,你只需向所選的虛終端調(diào)用ioctl(TIOCLINUX),。如下程序,,setconsole,可以用來選擇哪個虛終端接收內(nèi)核消息,;它必須以超級用戶身份運行,。如果你對ioctl還不有把握,你可以跳過這至下一節(jié),,等到讀完第5章“字符設(shè)備驅(qū)動程序的擴展操作”的“ioctl”一節(jié)后,,再回到這里讀這段代碼。 (代碼) setconsole使用了用于Linux專用功能的特殊的ioctl命令TIOCLINUX,。為了使用TIOCLINUX,,你要傳遞給它一個指向字節(jié)數(shù)組的指針。數(shù)組的第一個字節(jié)是所請求的子命令的編碼,,隨后的字節(jié)依命令而不同,。在setconsole中使用了子命令11,后一個字節(jié)(存放在bytes[1]中)標別虛擬控制臺,。TIOCLINUX的完成介紹可以在內(nèi)核源碼drivers/char/tty_io.c中找到,。 消息是如何記錄的 printk函數(shù)將消息寫到一個長度為LOG_BUF_LEN個字節(jié)的循環(huán)緩沖區(qū)中。然后喚醒任何等待消息的進程,,即那些在調(diào)用syslog系統(tǒng)調(diào)用或讀取/proc/kmesg過程中睡眠的進程,。這兩個訪問記錄引擎的接口是等價的。不過/proc/kmesg文件更象一個FIFO文件,,從中讀取數(shù)據(jù)更容易些,。一條簡單的cat命令就可以讀取消息。 如果循環(huán)緩沖區(qū)填滿了,printk就繞到緩沖區(qū)的開始處填寫新數(shù)據(jù),,覆蓋舊數(shù)據(jù)。于是記錄進程就丟失了最舊的數(shù)據(jù),。這個問題與利用循環(huán)緩沖區(qū)所獲得的好處相比可以忽略不計,。例如,循環(huán)緩沖區(qū)可以使系統(tǒng)在沒有記錄進程的情況下照樣運行,,同時又不浪費內(nèi)存,。Linux處理消息的方法的另一個特點是,可以在任何地方調(diào)用printk,,甚至在中斷處理函數(shù)里也可以調(diào)用,,而且對數(shù)據(jù)量的大小沒有限制。這個方法的唯一缺點就是可能丟失某些數(shù)據(jù),。 如果klogd正在運行,,它讀取內(nèi)核消息并將它們分派到syslogd,它隨后檢查/etc/syslog.conf找到處理這些數(shù)據(jù)的方式,。syslogd根據(jù)一個“設(shè)施”和“優(yōu)先級”切分消息,;可以使用的值定義在<sys/syslog.h>中。內(nèi)核消息根據(jù)相應(yīng)printk中指定的優(yōu)先級記錄到LOG_KERN設(shè)施中,。如果klogd沒有運行,,數(shù)據(jù)將保存在循環(huán)緩沖區(qū)中直到有進程來讀取數(shù)據(jù)或數(shù)據(jù)溢出。 如果你不希望因監(jiān)視你的驅(qū)動程序的消息而把你的系統(tǒng)記錄搞亂,,你給klogd指定-f(文件)選項或修改/etc/syslog.conf將記錄寫到另一個文件中,。另一種方法是一種強硬方法:殺掉klogd,將消息打印到不用的虛終端上*,,或者在一個不用的xterm上執(zhí)行cat /proc/kmesg顯示消息,。 使用預(yù)處理方便監(jiān)視處理 在驅(qū)動程序開發(fā)早期,printk可 以對調(diào)試和測試新代碼都非常有幫助,。然而當你正式發(fā)行驅(qū)動程序時,,你應(yīng)該去掉,或者至少關(guān)閉,,這些打印語句,。很不幸,你可能很快就發(fā)現(xiàn),,隨著你想不再需要 那些消息并去掉它們時,,你可能又要加新功能,你又需要這些消息了,。解決這些問題有幾種方法――如何從全局打開和關(guān)閉消息以及如何打開和關(guān)閉個別消息,。 下面給出了我處理消息所用的大部分代碼,它有如下一些功能:
下面這些直接來自scull.h的代碼片斷實現(xiàn)了這些功能,。 (代碼) 符合PDEBUG和PDEBUGG依賴于是否定義了SCULL_DEBUG,,它們都和printf調(diào)用很類似。 為了進一步方便這個過程,,在你的Makefile加上如下幾行,。 (代碼) 本節(jié)所給出的代碼依賴于gcc對ANSI C預(yù)編譯器的擴展,gcc可以支持帶可變數(shù)目參數(shù)的宏,。這種對gcc的依賴并不是什么問題,,因為內(nèi)核對gcc特性的依賴更強。此外,,Makefile依賴于GNU的gmake,;基于同樣的道理,這也不是什么問題,。 如果你很熟悉C預(yù)編譯器,,你可以將上面的定義擴展為可以支持“調(diào)試級”概念的,可以為每級賦一個整數(shù)(或位圖),,說明這一級打印多么瑣碎的消息,。 但是每一個驅(qū)動程序都有它自己的功能和監(jiān)視需求。好的編程技巧會在靈活性和高效之間找到一個權(quán)衡點,,這個我就不能說哪個對你最好了,。記住,預(yù)編譯器條件(還有代碼中的常量表達式)只到編譯時運行,,你必須重新編譯程序來打開或關(guān)閉消息,。另一種方法就是使用C條件語句,它在運行時運行,,因此可以讓你在程序執(zhí)行期間打開或關(guān)閉消息,。這個功能很好,但每次代碼執(zhí)行系統(tǒng)都要進行額外的處理,,甚至在消息關(guān)閉后仍然會影響性能,。有時這種性能損失是無法接受的。 個人觀點,,盡管上面給出的宏迫使你每次要增加或去掉消息時都要重新編譯,,重新加載模塊,但我覺得用這些宏已經(jīng)很好了,。 通過查詢調(diào)試 上一節(jié)談到了printk是如何工作的以及如何使用它,。但沒有談及它的缺點,。 由于syslogd會一直保持刷新它的輸出文件,每打印一行都會引起一次磁盤操作,,因此過量使用printk會嚴重降低系統(tǒng)性能,。至少從syslogd的角度看是這樣的。它會將所有的數(shù)據(jù)都一股腦地寫到磁盤上,,以防在打印消息后系統(tǒng)崩潰,;然而,你不想因為調(diào)試信息的緣故而降低系統(tǒng)性能,。這個問題可以通過在/etc/syslogd.conf中記錄文件的名字前加一個波折號解決,,但有時你不想修改你的配置文件,。如果不這樣,,你還可以運行一個非klogd的程序(如前面介紹的cat /proc/kmesg),但這樣并不能為正常操作提供一個合適的環(huán)境,。 與這相比,,最好的方法就是在你需要信息的時候,通過查詢系統(tǒng)獲得相關(guān)信息,,而不是持續(xù)不斷地產(chǎn)生數(shù)據(jù),。事實上,每一個Unix系統(tǒng)都提供了很多工具用來獲得系統(tǒng)信息:ps,,netstat,,vmstat等等。 有許多技術(shù)適合與驅(qū)動程序開發(fā)人員查詢系統(tǒng),,簡而言之就是,,在/proc下創(chuàng)建文件和使用ioctl驅(qū)動程序方法。 使用/proc文件系統(tǒng) Linux中的/proc文件系統(tǒng)與任何設(shè)備都沒有關(guān)系――/proc中的文件都在被讀取時有核心創(chuàng)建的,。這些文件都是普通的文本文件,,它們基本上可由普通人理解,也可被工具程序理解,。例如,,對于大多數(shù)Linux的ps實現(xiàn)而言,它都通過讀取/proc文件系統(tǒng)獲得進程表信息的,。/proc虛擬文件的創(chuàng)意已由若干現(xiàn)代操作系統(tǒng)使用,,且非常成功。 /proc的當前實現(xiàn)可以動態(tài)創(chuàng)建i節(jié)點,,允許用戶模塊為方便信息檢索創(chuàng)建如何入口點,。 為了在/proc中創(chuàng)建一個健全的文件節(jié)點(可以read,write,,seek等等),,你需要定義file_operations結(jié)構(gòu)和inode_operations結(jié)構(gòu),,后者與前者有類似的作用和尺寸。創(chuàng)建這樣一個i節(jié)點比起創(chuàng)建整個字符設(shè)備并沒有什么不同,。我們這里不討論這個問題,,如果你感興趣,你可以在源碼樹fs/proc中獲得進一步細節(jié),。 與大多數(shù)/proc文件一樣,,如果文件節(jié)點僅僅用來讀,創(chuàng)建它們是比較容易的,,我將這里介紹這一技術(shù),。很不幸,這一技術(shù)只能在Linux 2.0及其后續(xù)版本中使用,。 這里是創(chuàng)建一個稱為/proc/scullmem文件的scull代碼,,這個文件用來獲取scull使用的內(nèi)存信息。 (代碼) 填寫/proc文件非常容易,。你的函數(shù)獲取一個空閑頁面填寫數(shù)據(jù),;它將數(shù)據(jù)寫進緩沖區(qū)并返回所寫數(shù)據(jù)的長度。其他事情都由/proc文件系統(tǒng)處理,。唯一的限制就是所寫的數(shù)據(jù)不能超過PAGE_SIZE個字節(jié)(宏PAGE_SIZE定義在頭文件<asm/page.h>中,;它是與體系結(jié)構(gòu)相關(guān)的,但你至少可以它有4KB大?。?。 如果你需要寫多于一個頁面的數(shù)據(jù),你必須實現(xiàn)功能健全的文件,。 注意,,如果一個正在讀你的/proc文件的進程發(fā)出了若干read調(diào)用,每一個都獲取新數(shù)據(jù),,盡管只有少量數(shù)據(jù)被讀取,,你的驅(qū)動程序每次都要重寫整個緩沖區(qū)。這些額外的工作會使系統(tǒng)性能下降,,而且如果文件產(chǎn)生的數(shù)據(jù)與下一次的不同,,以后的read調(diào)用要重新裝配不相關(guān)的部分,這一會造成數(shù)據(jù)錯位,。事實上,,由于每個使用C庫的應(yīng)用程序都大塊地讀取數(shù)據(jù),性能并不是什么問題,。然而,,由于錯位時有發(fā)生,它倒是一個值得考慮的問題,。在獲取數(shù)據(jù)后,,庫調(diào)用至少要調(diào)用1次read――只有當read返回0時才報告文件尾,。如果驅(qū)動程序碰巧比前面產(chǎn)生了更多的數(shù)據(jù),系統(tǒng)就返回到用戶空間額外的字節(jié)并且與前面的數(shù)據(jù)塊是錯位的,。我們將在第6章“時間流”的“任務(wù)隊列”一節(jié)中涉及/proc/jiq*,,那時我們還會遇到錯位問題。 cleanup_module中應(yīng)該使用下面的語句注銷/proc節(jié)點: (代碼) 傳遞給函數(shù)的參數(shù)是包含要撤銷文件的目錄名和文件的i節(jié)點號,。由于i節(jié)點號是自動分配的,,在編譯時是無法知道的,必須從數(shù)據(jù)結(jié)構(gòu)中讀取,。 ioctl方法 ioctl,,下一章將詳細討論,是一個系統(tǒng)調(diào)用,,它可以操做在文件描述符上,;它接收一個“命令”號和(可選的)一個參數(shù),通常這是一個指針,。 做為替代/proc文件系統(tǒng)的方法,,你可以為調(diào)試實現(xiàn)若干ioctl命令,。這些命令從驅(qū)動程序空間復(fù)制相關(guān)數(shù)據(jù)到進程空間,,在進程空間里檢查這些數(shù)據(jù)。 只有使用ioctl獲取信息比起/proc來要困難一些,,因為你一個程序調(diào)用ioctl并顯示結(jié)果,。必須編寫這樣的程序,還要編譯,,保持與你測試的模塊間的一致性等,。 不過有時候這是最好的獲取信息的方法,因為它比起讀/proc來要快得多,。如果在數(shù)據(jù)寫到屏幕前必須完成某些處理工作,,以二進制獲取數(shù)據(jù)要比讀取文本文件有效得多。此外,,ioctl不限制返回數(shù)據(jù)的大小,。 ioctl方法的一個優(yōu)點是,當調(diào)試關(guān)閉后調(diào)試命令仍然可以保留在驅(qū)動程序中,。/proc文件對任何查看這個目錄的人都是可見的,,然而與/proc文件不同,未公開的ioctl命令通常都不會被注意到,。此外,,如果驅(qū)動程序有什么異常,它們?nèi)匀豢梢杂脕碚{(diào)試,。唯一的缺點就是模塊會稍微大一些,。 通過監(jiān)視調(diào)試 有時你遇到的問題并不特別糟,,通過在用戶空間運行應(yīng)用程序來查看驅(qū)動程序與系統(tǒng)之間的交互過程可以幫助你捕捉到一些小問題,并可以驗證驅(qū)動程序確實工作正常,。例如,,看到scull的read實現(xiàn)如何處理不同數(shù)據(jù)量的read請求后,我對scull更有信心,。 有許多方法監(jiān)視一個用戶態(tài)程序的工作情況,。你可以用調(diào)試器一步步跟蹤它的函數(shù),插入打印語句,,或者用strace運行程序,。在實際目的是查看內(nèi)核代碼時,最后一項技術(shù)非常有用,。 strace命令是一個功能非常強大的工具,,它可以現(xiàn)實程序所調(diào)用的所有系統(tǒng)調(diào)用。它不僅可以顯示調(diào)用,,而且還能顯示調(diào)用的參數(shù),,以符號方式顯示返回值。當系統(tǒng)調(diào)用失敗時,,錯誤的符號值(如,,ENOMEM)和對應(yīng)的字串(Out of memory)同時顯示。strace還有許多命令行選項,;最常用的是-t,,它用來顯示調(diào)用發(fā)生的時間,-T,,顯示調(diào)用所花費的時間,,以及-o,將輸出重定向到一個文件中,。默認情況下,,strace將所有跟蹤信息打印到stderr上。 strace從內(nèi)核接收信息,。這意味著一個程序無論是否按調(diào)試方式編譯(用gcc的-g選項)或是被去掉了符號信息都可以被跟蹤,。與調(diào)試器可以連接到一個運行進程并控制它類似,你還可以跟蹤一個已經(jīng)運行的進程,。 跟蹤信息通常用來生成錯誤報告報告給應(yīng)用開發(fā)人員,,但是對內(nèi)核編程人員來說也一樣非常有用。我們可以看到系統(tǒng)調(diào)用是如何執(zhí)行驅(qū)動程序代碼的,;strace允許我們檢查每一次調(diào)用輸入輸出的一致性,。 例如,下面的屏幕輸出給出了命令ls /dev > /dev/scull0的最后幾行: (代碼) 很明顯,,在ls完成目標目錄的檢索后首次對write的調(diào)用中,,它試圖寫4KB,。很奇怪,只寫了4000個字節(jié),,接著重試這一操作,。然而,我們知道scull的write實現(xiàn)每次只寫一個量子,,我在這里看到了部分寫,。經(jīng)過若干步驟之后,所有的東西都清空了,,程序正常退出,。 另一個例子,讓我們來讀scull設(shè)備: (代碼) 正如所料,,read每次只能讀到4000個字節(jié),,但是數(shù)據(jù)總量是不變的。注意本例中重試工作是如何組織的,,注意它與上面寫跟蹤的對比,。wc專門為快速讀數(shù)據(jù)進行了優(yōu)化,它繞過了標準庫,,以便每次用一個系統(tǒng)調(diào)用讀取更多的數(shù)據(jù),。你可以從跟蹤的read行中看到wc每次要讀16KB。 Unix專家可以在strace的輸出中找到很多有用信息,。如果你被這些符號搞得滿頭霧水,,我可以只看文件方法(open,,read等等)是如何工作的,。 個人認為,跟蹤工具在查明系統(tǒng)調(diào)用的運行時錯誤過程中最有用,。通常應(yīng)用或演示程序中的perror調(diào)用不足以用來調(diào)試,,而且對于查明到底是什么樣的參數(shù)觸發(fā)了系統(tǒng)調(diào)用的錯誤也很有幫助。 調(diào)試系統(tǒng)故障 即便你用了所有監(jiān)視和調(diào)試技術(shù),,有時候驅(qū)動程序中依然有錯誤,,當這樣的驅(qū)動程序執(zhí)行會造成系統(tǒng)故障。當這種情況發(fā)生時,,獲取足夠多的信息來解決問題是至關(guān)重要的,。 注意,“故障”不意味著“panic”,。Linux代碼非常魯棒,,可以很好地響應(yīng)大部分錯誤:故障通常會導(dǎo)致當前進程的終止,但系統(tǒng)繼續(xù)運行,。如果在進程上下文之外發(fā)生故障,,或是組成系統(tǒng)的重要部件發(fā)生故障時,,系統(tǒng)可能panic。但問題出在驅(qū)動程序時,,通常只會導(dǎo)致產(chǎn)生故障的進程終止――即那個使用驅(qū)動程序的進程,。唯一不可恢復(fù)的損失就是當進程被終止時,進程上下文分配的內(nèi)存丟失了,;例如,,由驅(qū)動程序通過kmalloc分配的動態(tài)鏈表可能丟失。然而,,由于內(nèi)核會對尚是打開的設(shè)備調(diào)用close,,你的驅(qū)動程序可以釋放任何有open方法分配的資源。 我們已經(jīng)說過,,當內(nèi)核行為異常時會在控制臺上顯示一些有用的信息,。下一節(jié)將解釋如何解碼和使用這些消息。盡管它們對于初學者來說相當晦澀,,處理器的給出數(shù)據(jù)都是些很有意思的信息,,通常無需額外測試就可以查明程序錯誤。 Oops消息 大部分錯誤都是NULL指針引用或使用其他不正確的指針數(shù)值,。這些錯誤通常會導(dǎo)致一個oops消息,。 由處理器使用的地址都是“虛”地址,而且通過一個復(fù)雜的稱為頁表(見第13章“Mmap和DMA”中的“頁表”一節(jié))的結(jié)構(gòu)映射為物理地址,。當引用一個非法指針時,,頁面映射機制就不能將地址映射到物理地址,并且處理器向操作系統(tǒng)發(fā)出一個“頁面失效”,。如果地址確實是非法的,,內(nèi)核就無法從失效地址上“換頁”;如果此時處理在超級用戶太,,系統(tǒng)于是就產(chǎn)生一個“oops”,。值得注意的是,在版本2.1中內(nèi)核處理失效的方式有所變化,,它可以處理在超級用戶態(tài)的非法地址引用了,。新實現(xiàn)將在第17章“最近發(fā)展”的“處理內(nèi)核空間失效”中介紹。 oops顯示故障時的處理器狀態(tài),,模塊CPU寄存器內(nèi)容,,頁描述符表的位置,以及其他似乎不能理解的信息,。這些是由失效處理函數(shù)(arch/*/kernel/traps.c)中的printk語句產(chǎn)生的,,而且象前面“Printk”一節(jié)介紹的那樣進行分派。 讓我們看看這樣一個消息。這里給出的是傳統(tǒng)個人電腦(x86平臺),,運行Linux 2.0或更新版本的oops――版本1.2的輸出稍有不同,。 (代碼) 上面的消息是在一個有意加入錯誤的失效模塊上運行cat所至。fault.c崩潰如下代碼: (代碼) 由于read從它的小緩沖區(qū)(faulty_buf)復(fù)制數(shù)據(jù)到用戶空間,,我們希望讀一小塊文件能夠工作,。然而,每次讀出多于1KB的數(shù)據(jù)會跨越頁面邊界,,如果訪問了非法頁面read就會失敗,。事實上,前面給出的oops是在請求一個4KB大小的read時發(fā)生的,,這條消息在/var/log/messages(syslogd默認存放內(nèi)核消息的文件)的oops消息前給出了: (代碼) 同樣的cat命令卻不能在Alpha上產(chǎn)生oops,,這是因為從faulty_buf讀取4KB字節(jié)沒有超出頁邊界(Alpha上的頁面大小是8KB,緩沖區(qū)正好在頁面的起始位置附近),。如果在你的系統(tǒng)上讀取faulty沒有產(chǎn)生oops,,試試wc,或者給dd顯式地指定塊大小,。 使用ksymoops oops消息的最大問題就是十六進制數(shù)值對于程序員來說沒什么意義,;需要將它們解析為符號。 內(nèi)核源碼通過其所包含的ksymoops工具幫助開發(fā)人員――但是注意,,版本1.2的源碼中沒有這個程序,。該工具將oops消息中的數(shù)值地址解析為內(nèi)核符號,但只限于PC機產(chǎn)生的oops消息,。由于消息本身就是處理器相關(guān)的,,每一體系結(jié)構(gòu)都有其自身的消息格式。 ksymoops從標準輸入獲得oops消息,,并從命令行內(nèi)核符號表的名字,。符號表通常就是/usr/src/linux/System.map。程序以更可讀的方式打印調(diào)用軌跡和程序代碼,,而不是最原始的oops消息,。下面的片斷就是用上一節(jié)的oops消息得出的結(jié)果: (代碼) 由ksymoops反匯編出的代碼給出了失效的指令和其后的指令。很明顯――對于那些知道一點匯編的人――repz movsl指令(REPeat till cx is Zero, MOVe a String of Longs)用源索引(esi,,是0x202e000)訪問了一個未映射頁面。用來獲得模塊信息的ksymoops -m命令給出,,模塊映射到一個在0x0202dxxx的頁面上,,這也確認樂esi確實超出了范圍。 由于faulty模塊所占用的內(nèi)存不在系統(tǒng)表中,,被解碼的調(diào)用軌跡還給出了兩個數(shù)值地址,。這些值可以手動補充,或是通過ksyms命令的輸出,或是在/proc/ksyms中查詢模塊的名字,。 然而對于這個失效,,這兩個地址并不對應(yīng)與代碼地址。如果你看了arch/i386/kernel/traps.c,,你就發(fā)現(xiàn),,調(diào)用軌跡是從整個堆棧并利用一些啟發(fā)式方法區(qū)分數(shù)據(jù)值(本地變量和函數(shù)參數(shù))和返回地址獲得的。調(diào)用軌跡中只給出了引用內(nèi)核代碼的地址和引用模塊的地址,。由于模塊所占頁面既有代碼也有數(shù)據(jù),,錯綜復(fù)雜的棧可能會漏掉啟發(fā)式信息,,這就是上面兩個0x202xxxx地址的情況,。 如果你不愿手動查看模塊地址,下面這組管道可以用來創(chuàng)建一個既有內(nèi)核又有模塊符號的符號表,。無論何時你加載模塊,,你都必須重新創(chuàng)建這個符號表。 (代碼) 這個管道將完整的系統(tǒng)表與/proc/ksyms中的公開內(nèi)核符號混合在一起,,后者除了內(nèi)核符號外,,還包括了當前內(nèi)核里的模塊符號。這些地址在insmod重定位代碼后就出現(xiàn)在/proc/ksyms中,。由于這兩個文件的格式不同,,使用了sed和awk將所有的文本行轉(zhuǎn)換為一種合適的格式。然后對這張表排序,,去除重復(fù)部分,,這樣ksymoops就可以用了。 如果我們重新運行ksymoops,,它從新的符號表中截取出如下信息: (代碼) 正如你所見到的,,當跟蹤與模塊有關(guān)的oops消息時,創(chuàng)建一個修訂的系統(tǒng)表是很有助益的:現(xiàn)在ksymoops能夠?qū)χ噶钪羔樈獯a并完成整個調(diào)用軌跡了,。還要注意,,顯式反匯編碼的格式和objdump所使用的格式一樣。objdump也是一個功能強大的工具,;如果你需要查看失敗前的指令,,你調(diào)用命令objdump d faulty.o。 在文件的匯編列表中,,字串faulty_read+45/60標記為失效行,。有關(guān)objdump的更多的信息和它的命令行選項可以參見該命令的手冊。 即便你構(gòu)建了你自己的修訂版符號表,,上面提到的有關(guān)調(diào)用軌跡的問題仍然存在:雖然0x202xxxx指針被解碼了,,但仍然是假的。 學會解碼oops消息需要一定的經(jīng)驗,但是確實值得一做,。用來學習的時間很快就會有所回報,。不過由于機器指令的Unix語法與Intel語法不同,唯一的問題在于從哪獲得有關(guān)匯編語言的文檔,;盡管你了解PC匯編語言,,但你的經(jīng)驗都是用Intel語法的編程獲得的。在參考書目中,,我給一些有所補益的書籍,。 使用oops 使用ksymoops有些繁瑣。你需要C++編譯器編譯它,,你還要構(gòu)建你自己的符號表來充分發(fā)揮程序的能力,,你還要將原始消息和ksymoops輸出合在一起組成可用的信息。 如果你不想找這么多麻煩,,你可以使用oops程序,。oops在本書的O’Reilly FTP站點給出的源碼中。它源自最初的ksymoops工具,,現(xiàn)在它的作者已經(jīng)不維護這個工具了,。oops是用C語言寫成的,而且直接查看/proc/ksyms而無需用戶每次加載模塊后構(gòu)建新的符號表,。 該程序試圖解碼所有的處理器寄存器并堆棧軌跡解析為符號值,。它的缺點是,它要比ksymoops羅嗦些,,但通常你所有的信息越多,,你發(fā)現(xiàn)錯誤也就越快。oops的另一個優(yōu)點是,,它可以解析x86,,Alpha和Sparc的oops消息。與內(nèi)核源碼相同,,這個程序也按GPL發(fā)行,。 oops產(chǎn)生的輸出與ksymoops的類似,但是更完全,。這里給出前一個oops輸出的開始部分由于在這個oops消息中堆棧沒保存什么有用的東西,,我不認為應(yīng)該顯示整個堆棧軌跡: (代碼) 當你調(diào)試“真正的”模塊(faulty太短了,沒有什么意義)時,,將寄存器和堆棧解碼是非常有益的,,而且如果被調(diào)試的所有模塊符號都開放出來時更有幫助。在失效時,,處理器寄存器一般不會指向模塊的符號,只有當符號表開放給/proc/ksyms時,你才能輸出中標別它們,。 我們可以用一下步驟制作一張更完整的符號表,。首先,我們不應(yīng)在模塊中聲明靜態(tài)變量,,否則我們就無法用insmod開放它們了,。第二,如下面的截取自scull的init_module函數(shù)的代碼所示,,我們可以用#ifdef SCULL_DEBUG或類似的宏屏蔽register_symtab調(diào)用,。 (代碼) 我們在第2章“編寫和運行模塊”的“注冊符號表”一節(jié)中已經(jīng)看到了類似內(nèi)容,那里說,,如果模塊不注冊符號表,,所有的全局符號就都開放。盡管這一功能僅在SCULL_DEBUG被激活時才有效,,為了避免內(nèi)核中的名字空間污染,,所有的全局符號有合適的前綴(參見第2章的“模塊與應(yīng)用程序”一節(jié))。 使用klogd klogd守護進程的近期版本可以在oops存放到記錄文件前對oops消息解碼,。解碼過程只由版本1.3或更新版本的守護進程完成,,而且只有將-k /usr/src/linux/System.map做為參數(shù)傳遞給守護進程時才解碼。(你可以用其他符號表文件代替System.map) 有新的klogd給出的faulty的oops如下所示,,它寫到了系統(tǒng)記錄中: (代碼) 我想能解碼的klogd對于調(diào)試一般的Linux安裝的核心來說是很好的工具,。由klogd解碼的消息包括大部分ksymoops的功能,而且也要求用戶編譯額外的工具,,或是,,當系統(tǒng)出現(xiàn)故障時,為了給出完整的錯誤報告而合并兩個輸出,。當oops發(fā)生在內(nèi)核時,,守護進程還會正確地解碼指令指針。它并不反匯編代碼,,但這不是問題,,當錯誤報告給出消息時,二進制數(shù)據(jù)仍然存在,,可以離線反匯編代碼,。 守護進程的另一個功能就是,如果符號表版本與當前內(nèi)核不匹配,,它會拒絕解析符號,。如果在系統(tǒng)記錄中解析出了符號,你可以確信它是正確的解碼,。 然而,,盡管它對Linux用戶很有幫助,,這個工具在調(diào)試模塊時沒有什么幫助。我個人沒有在開放軟件的電腦里使用解碼選項,。klogd的問題是它不解析模塊中的符號,;因為守護進程在程序員加載模塊前就已經(jīng)運行了,即使讀了/proc/ksyms也不會有什么幫助,。記錄文件中存在解析后的符號會使oops和ksymoops混淆,,造成進一步解析的困難。 如果你需要使用klogd調(diào)試你的模塊,,最新版本的守護進程需要加入一些新的特殊支持,,我期待它的完成,只要給內(nèi)核打一個小補丁就可以了,。 系統(tǒng)掛起 盡管內(nèi)核代碼中的大多數(shù)錯誤僅會導(dǎo)致一個oops消息,,有時它們困難完全將系統(tǒng)掛起。如果系統(tǒng)掛起了,,沒有消息能夠打印出來,。例如,如果代碼遇到一個死循環(huán),,內(nèi)核停止了調(diào)度過程,,系統(tǒng)不會再響應(yīng)任何動作,包括魔法鍵Ctrl-Alt-Del組合,。 處理系統(tǒng)掛起有兩個選擇――一個是防范與未然,,另一個就是亡羊補牢,在發(fā)生掛起后調(diào)試代碼,。 通過在策略點上插入schedule調(diào)用可以防止死循環(huán),。schedule調(diào)用(正如你所猜想到的)調(diào)用調(diào)度器,因此允許其他進程偷取當然進程的CPU時間,。如果進程因你的驅(qū)動程序中的錯誤而在內(nèi)核空間循環(huán),,你可以在跟蹤到這種情況后殺掉這個進程。 在驅(qū)動程序代碼中插入schedule調(diào)用會給程序員帶來新的“問題”:函數(shù),,,以及調(diào)用軌跡中的所有函數(shù),,必須是可重入的。在正常環(huán)境下,,由于不同的進程可能并發(fā)地訪問設(shè)備,,驅(qū)動程序做為整體是可重入的,但由于Linux內(nèi)核是不可搶占的,,不必每個函數(shù)都是可重入的,。但如果驅(qū)動程序函數(shù)允許調(diào)度器中斷當前進程,另一個不同的進程可能會進入同一個函數(shù),。如果schedule調(diào)用僅在調(diào)試期間打開,,如果你不允許,,你可以避免兩個并發(fā)進程訪問驅(qū)動程序,所以并發(fā)性倒不是什么非常重要的問題,。在介紹阻塞型操作時(第5章的“寫可重入代碼”)我們再詳細介紹并發(fā)性問題,。 如果要調(diào)試死循環(huán),,你可以利用Linux鍵盤的特殊鍵,。默認情況下,如果和修飾鍵一起按了PrScr鍵(鍵碼是70),,系統(tǒng)會向當前控制臺打印有關(guān)機器狀態(tài)的有用信息,。這一功能在x86和Alpha系統(tǒng)都有。Linux的Sparc移植也有同樣的功能,,但它使用了標記為“Break/Scroll Lock”的鍵(鍵碼是30),。 每一個特殊函數(shù)都有一個名字,并如下面所示都有一個按鍵事件與之對應(yīng),。組合鍵之后的括號里是函數(shù)名,。 Shift-PrScr(Show_Memory) 打印若干行關(guān)于內(nèi)存使用的信息,尤其是有關(guān)緩沖區(qū)高速緩存的使用情況,。 Control-PrScr(Show_State) 針對系統(tǒng)里的每一個處理器打印一行信息,,同時還打印內(nèi)部進程樹。對當前進程進行標記,。 RightAlt-PrScr(Show_Registers) 由于它可以打印按鍵時的處理器寄存器內(nèi)容,,它是系統(tǒng)掛起時最重要的一個鍵了。如果有當前內(nèi)核的系統(tǒng)表的話,,查看指令計數(shù)器以及它如何隨時間變化,,對了解代碼在何處循環(huán)非常有幫助。 如果想將這些函數(shù)映射到不同的鍵上,,每一個函數(shù)名都可以做為參數(shù)傳遞給loadkeys,。鍵盤映射表可以任意修改(這是“策略無關(guān)的”)。 如果console_loglevel足夠到的話,,這些函數(shù)打印的消息會出現(xiàn)在控制臺上,。如果不是你運行了一個舊klogd和一個新內(nèi)核的話,默認記錄級應(yīng)該足夠了,。如果沒有出現(xiàn)消息,,你可以象以前說的那樣提升記錄級。“足夠高”的具體值與你使用的內(nèi)核版本有關(guān),。對于Linux 2.0或更新的版本來說是5,。 即便當系統(tǒng)掛起時,消息也會打印到控制臺上,,確認記錄級足夠高是非常重要的,。消息是在產(chǎn)生中斷時生成的,,因此即便有錯的進程不釋放CPU也可以運行――當然,除非中斷被屏蔽了,,不過如果發(fā)生這種情況既不太可能也非常不幸,。 有時系統(tǒng)看起來象是掛起了,但其實不是,。例如,,如果鍵盤因某種奇怪的原因被鎖住了就會發(fā)生這種情況。這種假掛起可以通過查看你為探明此種情況而運行的程序輸出來判斷,。我有一個程序會不斷地更新LED顯示器上的時鐘,,我發(fā)現(xiàn)這個對于驗證調(diào)度器尚在運行非常有用。你可以不必依賴外部設(shè)備就可以檢查調(diào)度器,,你可以實現(xiàn)一個程序讓鍵盤LED閃爍,,或是不斷地打開關(guān)閉軟盤馬達,或是不斷觸動揚聲器――不過我個人認為,,通常的蜂鳴聲很煩人,,應(yīng)該盡量避免??纯?/font>ioctl命令KDMKTONE,。O’Reilly FTP站點上的例子程序(misc-progs/heartbeat.c)中有一個是讓鍵盤LED不斷閃爍的。 如果鍵盤不接收輸入了,,最佳的處理手段是從網(wǎng)絡(luò)登錄在系統(tǒng)中,,殺掉任何違例的進程,或是重新設(shè)置鍵盤(用kdb_mode -a),。然而,,如果你沒有網(wǎng)絡(luò)可用來恢復(fù)的話,發(fā)現(xiàn)系統(tǒng)掛起是由鍵盤鎖死造成的一點兒用也沒有,。如果情況確實是這樣,,你應(yīng)該配置一種替代輸入設(shè)備,至少可以保證正常地重啟系統(tǒng),。對于你的計算機來說,,關(guān)閉系統(tǒng)或重啟比起所謂的按“大紅鈕”要更方便一些,至少它可以免去長時間地fsck掃描磁盤,。 這種替代輸入設(shè)備可以是游戲桿或是鼠標,。在sunsite.edu.cn上有一個游戲桿重啟守護進程,gpm-1.10或更新的鼠標服務(wù)器可以通過命令行選項支持類似的功能,。如果鍵盤沒有鎖死,,但是卻誤入“原始”模式,你可以看看kdb包中文檔介紹的一些小技巧,。我建議最好在問題出現(xiàn)以前就看看這些文檔,,否則就太晚了,。另一種可能是配置gpm-root菜單,增添一個“reboot”或“reset keyboard”菜單項,;gpm-root一個響應(yīng)控制鼠標事件的守護進程,,它用來在屏幕上顯示菜單和執(zhí)行所配置的動作。 最好,,你會可以按“留意安全鍵”(SAK),,一個用于將系統(tǒng)恢復(fù)為可用狀態(tài)的特殊鍵。由于不是所有的實現(xiàn)都能用,,當前Linux版本的默認鍵盤表中沒有為此鍵特設(shè)一項,。不過你還是可以用loadkeys將你的鍵盤上的一個鍵映射為SAK。你應(yīng)該看看drivers/char目錄中的SAK實現(xiàn),。代碼中的注釋解釋了為什么這個鍵在Linux 2.0中不是總能工作,這里我就不多說了,。 不過,,如果你運行版本2.1.9或是更新的版本,你就可以使用非??煽康亓粢獍踩I了,。此外,2.1.43及后續(xù)版本內(nèi)核還有一個編譯選項選擇是否打開“SysRq魔法鍵”,;我建議你看一看drivers/char/sysrq.c中的代碼并使用這項新技術(shù),。 如果你的驅(qū)動程序真的將系統(tǒng)掛起了,而且你有不知道在哪插入schedule調(diào)用,,最佳的處理方法就是加一些打印消息,,并將它們打印到控制臺上(通過修改console_loglevel變量值)。在重演掛起過程時,,最好將所有的磁盤都以只讀方式安裝在系統(tǒng)上,。如果磁盤是只讀的或沒有安裝,就不會存在破壞文件系統(tǒng)或使其進入不一致狀態(tài)的危險,。至少你可以避免在復(fù)位系統(tǒng)后運行fsck,。另一中方法就是使用NFS根計算機來測試模塊。在這種情況下,,由于NFS服務(wù)器管理文件系統(tǒng)的一致性,,而它又不會受你的驅(qū)動程序的影響,你可以避免任何的文件系統(tǒng)崩潰,。 使用調(diào)試器 最 后一種調(diào)試模塊的方法就是使用調(diào)試器來一步步地跟蹤代碼,,查看變量和機器寄存器的值。這種方法非常耗時,,應(yīng)該盡可能地避免,。不過,,某些情況下通過調(diào)試器對 代碼進行細粒度的分析是非常有益的。在這里,,我們所說的被調(diào)試的代碼運行在內(nèi)核空間――除非你遠程控制內(nèi)核,,否則不可能一步步跟蹤內(nèi)核,這會使很多事情變 得更加困難,。由于遠程控制很少用到,,我們最后介紹這項技術(shù)。所幸的是,,在當前版本的內(nèi)核中可以查看和修改變量,。 在這一級上熟練地使用調(diào)試器需要精通gdb命令,對匯編碼有一定了解,,并且有能夠?qū)⒃创a與優(yōu)化后的匯編碼對應(yīng)起來的能力,。 不幸的是,gdb更適合與調(diào)試核心而不是模塊,,調(diào)試模塊化的代碼需要更多的技術(shù),。這更多的技術(shù)就是kdebug包,它利用gdb的“遠程調(diào)試”接口控制本地內(nèi)核,。我將在介紹普通調(diào)試器后介紹kdebug,。 使用gdb gdb在探究系統(tǒng)內(nèi)部行為時非常有用。啟動調(diào)試器時必須假想內(nèi)核就是一個應(yīng)用程序,。除了指定內(nèi)核文件名外,,你還應(yīng)該在命令行中提供內(nèi)存鏡象文件的名字。典型的gdb調(diào)用如下所示: (代碼) 第一個參數(shù)是未經(jīng)壓縮的內(nèi)核可執(zhí)行文件(在你編譯完內(nèi)核后,,這個文件在/usr/src/linux目錄中)的名字,。只有x86體系結(jié)構(gòu)有zImage文件(有時稱為vmlinuz),它是一種解決Intel處理器實模式下只有640KB限制的一種技巧,;而無論在哪個平臺上,,vmlinux都是你所編譯的未經(jīng)壓縮的內(nèi)核。 gdb命令行的第二個參數(shù)是是內(nèi)存鏡象文件的名字,。與其他在/proc下的文件類似,,/proc/kcore也是在被讀取時產(chǎn)生的。當read系統(tǒng)調(diào)用在/proc文件系統(tǒng)執(zhí)行時,,它映射到一個用于數(shù)據(jù)生成而不是數(shù)據(jù)讀取的函數(shù)上,;我們已在“使用/proc文件系統(tǒng)”一節(jié)中介紹了這個功能。系統(tǒng)用kcore來表示按內(nèi)存鏡象文件格式存儲的內(nèi)核“可執(zhí)行文件”,;由于它要表示整個內(nèi)核地址空間,,它是一個非常巨大的文件,對應(yīng)所有的物理內(nèi)存。利用gdb,,你可以通過標準gdb命令查看內(nèi)核標量,。例如,p jiffies可以打印從系統(tǒng)啟動到當前時刻的時鐘滴答數(shù),。 當你從gdb打印數(shù)據(jù)時,,內(nèi)核還在運行,不同數(shù)據(jù)項會在不同時刻有不同的數(shù)值,;然而,,gdb為了優(yōu)化對內(nèi)存鏡象文件的訪問會將已經(jīng)讀到的數(shù)據(jù)緩存起來。如果你再次查看jiffies變量,,你會得到和以前相同的值,。緩存變量值防止額外的磁盤操作對普通內(nèi)存鏡象文件來說是對的,但對“動態(tài)”內(nèi)存鏡象文件來說就不是很方便了,。解決方法是在你想刷新gdb緩存的時候執(zhí)行core-file /proc/kcore命令,;調(diào)試器將使用新的內(nèi)存鏡象文件并廢棄舊信息。但是,,讀新數(shù)據(jù)時你并不總是需要執(zhí)行core-file命令,;gdb以1KB的尺度讀取內(nèi)存鏡象文件,僅僅緩存它所引用的若干塊,。 你不能用普通gdb做的是修改內(nèi)核數(shù)據(jù);由于調(diào)試器需要在訪問內(nèi)存鏡象前運行被調(diào)試程序,,它是不會去修改內(nèi)存鏡象文件的,。當調(diào)試內(nèi)核鏡象時,執(zhí)行run命令會導(dǎo)致在執(zhí)行若干指令后導(dǎo)致段違例,。出于這個原因,,/proc/kcore都沒有實現(xiàn)write方法。 如果你用調(diào)試選項(-g)編譯了內(nèi)核,,結(jié)果產(chǎn)生的vmlinux比沒有用-g選項的更適合于gdb,。不過要注意,用-g選項編譯內(nèi)核需要大量的磁盤空間――支持網(wǎng)絡(luò)和很少幾個設(shè)備和文件系統(tǒng)的2.0內(nèi)核在PC上需要11KB,。不過不管怎樣,,你都可以生成zImage文件并用它來其他系統(tǒng):在生成可啟動鏡象時由于選項-g而加入的調(diào)試信息最終都被去掉了。如果我有足夠的磁盤空間,,我會一致打開-g選項的,。 在非PC計算機上則有不同的方法。在Alpha上,,make boot會在生成可啟動鏡象前將調(diào)試信息去掉,,所以你最終會獲得vmlinux和vmlinux.gz兩個文件。gdb可以使用前者,但你只能用后者啟動,。在Sparc上,,默認情況下內(nèi)核(至少是2.0內(nèi)核)不會被去掉調(diào)試信息,所以你需要在將其傳遞給silo(Sparc的內(nèi)核加載器)前將調(diào)試信息去掉,,這樣才能啟動,。由于尺寸的問題,無論milo(Alpha的內(nèi)核加載器)還是silo都不能啟動未去掉調(diào)試信息的內(nèi)核,。 當你用-g選項編譯內(nèi)核并且用vmlinux和/proc/kcore一起使用調(diào)試器,,gdb可以返回很多有關(guān)內(nèi)核內(nèi)部結(jié)構(gòu)的信息。例如,,你可以使用類似于這樣的命令,,p *module_list,p *module_list->next和p *chrdevs[4]->fops等顯示這些結(jié)構(gòu)的內(nèi)容,。如果你手頭有內(nèi)核映射表和源碼的話,,這些探測命令是非常有用的。 另一個gdb可以在當前內(nèi)核上執(zhí)行的有用任務(wù)是,,通過disassemble命令(它可以縮寫)或是“檢查指令”(x/i)命令反匯編函數(shù),。disassemble命令的參數(shù)可以是函數(shù)名或是內(nèi)存區(qū)范圍,而x/i則使用一個內(nèi)存地址做為參數(shù),,也可以用符號名,。例如,你可以用x/20i反匯編20條指令,。注意,,你不能反匯編一個模塊的函數(shù),這是因為調(diào)試器處理vmlinux,,它并不知道你的模塊的信息,。如果你試圖用模塊的地址反匯編代碼,gdb很有可能會報告“不能訪問xxxx處的內(nèi)存(Cannot access memory at xxxx)”,?;谕瑯拥脑颍悴徊榭磳儆谀K的數(shù)據(jù)項,。如果你知道你的變量的地址,,你可以從/dev/mem中讀出它的值,但很難弄明白從系統(tǒng)內(nèi)存中分解出的數(shù)據(jù)是什么含義,。 如果你需要反匯編模塊函數(shù),,你最好對用objdump工具處理你的模塊文件。很不幸,,該工具只能對磁盤上的文件進行處理,,而不能對運行中的模塊進行處理,;因此,objdump中給出的地址都是未經(jīng)重定位的地址,,與模塊的運行環(huán)境無關(guān),。 如你所見,當你的目的是查看內(nèi)核的運行情況時,,gdb是一個非常有用的工具,,但它缺少某些功能,最重要的一些功能就是修改內(nèi)核項和訪問模塊的功能,。這些空白將由kdebug包填補,。 使用kdebug 你可用從一般的FTP站點下的pcmcia/extras目錄下拿到kdebug,但是如果你想確保拿到的是最新的版本,,你最好到ftp://hyper.stanford.edu/pub/pcmcia/extras/去找,。該工具與pcmcia沒有什么關(guān)系,但是這兩個包是同一個作者寫的,。 kdebug是一個使用gdb“遠程調(diào)試”接口與內(nèi)核通信的小工具,。使用時首先向內(nèi)核加載一個模塊,調(diào)試器通過/dev/kdebug訪問內(nèi)核數(shù)據(jù),。gdb將該設(shè)備當成一個與被調(diào)試“應(yīng)用”通信的串口設(shè)備,,但它僅僅是一個用于訪問內(nèi)核空間的通信通道。由于模塊本身運行在內(nèi)核空間,,它可以看到普通調(diào)試器無法訪問的內(nèi)核空間地址,。正如你所猜想到的,模塊是一個字符設(shè)備驅(qū)動程序,,并且使用了主設(shè)備號動態(tài)分配技術(shù),。 kdebug的優(yōu)點在于,你無需打補丁或重新編譯:無論是內(nèi)核還是調(diào)試器都無需修改,。你所需要做的就是編譯和安裝軟件包,然后調(diào)用kgdb,,kgdb是一個完成某些配置并調(diào)用gdb,,通過新接口訪問內(nèi)核部件結(jié)構(gòu)的腳本程序。 但是,,即便是kdebug也沒有提供單步跟蹤內(nèi)核代碼和設(shè)置斷點的功能,。這幾乎是不可避免的,因為內(nèi)核必須保持運行狀態(tài)以保證系統(tǒng)的出于運行狀態(tài),,跟蹤內(nèi)核代碼的唯一方法就是后面將要談到的從另外一臺計算機上通過串口控制系統(tǒng),。不過kgdb的實現(xiàn)允許用戶修改被調(diào)試應(yīng)用(即當前內(nèi)核)的數(shù)據(jù)項,可以傳遞給內(nèi)核任意數(shù)目的參數(shù),,并以讀寫方式訪問模塊所屬的內(nèi)存區(qū),。 最后一個功能就是通過gdb命令將模塊符號表增加到調(diào)試器內(nèi)部的符號表中。這個工作是由kgdb完成的。然后當用戶請求訪問某個符號時,,gdb就知道它的地址是哪了,。最終的訪問是由模塊里的內(nèi)核代碼完成的。不過要注意,,kdebug的當前版本(1.6)在映射模塊化代碼地址方面還有些問題,。你最好通過打印一些符號并與/proc/ksyms中的值進行比較來做些檢查。如果地址沒有匹配,,你可以使用數(shù)值,,但必須將它們強行轉(zhuǎn)換為正確的類型。下面就是一個強制類型轉(zhuǎn)換的例子: (代碼) kdebug的另一個強于gdb的優(yōu)點是,,它允許你在數(shù)據(jù)結(jié)構(gòu)被修改后讀取到最新的值,,而不必刷新調(diào)試器的緩存;gdb命令set remotecache 0可以用來關(guān)閉數(shù)據(jù)緩存,。 由于kdebug與gdb使用起來很相似,,這里我就不過多地羅列使用這個工具的例子了。對于知道如何使用調(diào)試器的人來說,,這種例子很簡單,,但對于那些對調(diào)試器一無所知的人來說就很晦澀了。能夠熟練地使用調(diào)試器需要時間和經(jīng)驗,,我不準備在這里承擔老師的責任,。 總而言之,kdebug是一個非常好的程序,。在線修改數(shù)據(jù)結(jié)構(gòu)對于開發(fā)人員來說是一個非常大的進步(而且一種將系統(tǒng)掛起的最簡單方法)?,F(xiàn)在有許多工具可以使你的開發(fā)工作更輕松――例如,在開發(fā)scull期間,,當模塊的使用計數(shù)器增長后*,,我可以使用kdebug來將其復(fù)位為0。這就不必每次都麻煩我重啟機器,,登錄,,再次啟動我的應(yīng)用程序等等。 遠程調(diào)試 調(diào)試內(nèi)核鏡象的最后一個方法是使用gdb的遠程調(diào)試能力,。 當執(zhí)行遠程調(diào)試的時候,,你需要兩臺計算機:一臺運行gdb;另一臺運行你要調(diào)試的內(nèi)核,。這兩臺計算機間用普通串口連接起來,。如你所料,控制gdb必須能夠理解它所控制的內(nèi)核的二進制格式,。如果這兩臺計算機是不同的體系結(jié)構(gòu),,必須將調(diào)試器編譯為可以支持目標平臺的,。 在2.0中,Linux內(nèi)核的Intel版本不支持遠程調(diào)試,,但是Alpha和Sparc版本都支持,。在Alpha版本中,你必須在編譯時包含對遠程調(diào)試的支持,,并在啟動時通過傳遞給內(nèi)核命令行參數(shù)kgdb=1或只有kgdb打開這個功能,。在Sparc上,始終包含了對遠程調(diào)試的支持,。啟動選項kgdb=ttyx可以用來選擇在哪個串口上控制內(nèi)核,,x可以是a或b。如果沒有使用kgdb=選項,,內(nèi)核就按正常方式啟動,。 如果在內(nèi)核中打開了遠程調(diào)試功能,系統(tǒng)在啟動時就會調(diào)用一個特殊的初始化函數(shù),,配置被調(diào)試內(nèi)核處理它自己的斷點,,并且跳轉(zhuǎn)到一個編譯自程序中的斷點。這會暫停內(nèi)核的正常執(zhí)行,,并將控制轉(zhuǎn)移給斷點服務(wù)例程,。這一處理函數(shù)在串口線上等待來自于gdb的命令,當它獲得gdb的命令后,,就執(zhí)行相應(yīng)的功能,。通過這一配置,程序員可以單步跟蹤內(nèi)核代碼,,設(shè)置斷點,,并且完成gdb所允許的其他任務(wù)。 在控制端,,需要一個目標鏡象的副本(我們假設(shè)它是linux.img),,還需要一個你要調(diào)試的模塊副本。如下命令必須傳遞給gdb: file linux.img file命令告訴gdb哪個二進制文件需要調(diào)試,。另一種方法是在命令行中傳遞鏡象文件名,。這個文件本身必須和運行在另一端的內(nèi)核一模一樣。 target remote /dev/ttyS1 這條命令通知gdb使用遠程計算機做為調(diào)試過程的目標,。/dev/ttyS1是用來通信的本地串口,你可以指定任一設(shè)備,。例如,,前面介紹的kdebug軟件包中的kgdb腳本使用target remote /dev/kdebug。 add-symbol-file module.o address 如果你要調(diào)試已經(jīng)加載到被控內(nèi)核的模塊的話,,在控制系統(tǒng)上你需要一個模塊目標文件的副本,。add-symbol-file通知gdb處理模塊文件,,假定模塊代碼被定位在地址address上了。 盡管遠程調(diào)試可以用于調(diào)試模塊,,但你還是要加載模塊,,并且在模塊上插入斷點前還需要觸發(fā)另一個斷點,調(diào)試模塊還是需要很多技巧的,。我個人不會使用遠程調(diào)試去跟蹤模塊,,除非異步運行的代碼,如中斷處理函數(shù),,出了問題,。 |
|