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