1. 程式人生 > >Linux核心除錯技術

Linux核心除錯技術

Linux除錯技術介紹 

對於任何編寫核心程式碼的人來說,最吸引他們注意的問題之一就是如何完成除錯。由於核心是一個不與某個程序相關的功能集,其程式碼不能很輕鬆地放在偵錯程式中執行,而且也不能跟蹤。 

本章介紹你可以用來監視核心程式碼和跟蹤錯誤的技術。 

用列印資訊除錯 

最一般的除錯技術就是監視,就是在應用內部合適的點加上printk呼叫。當你除錯核心程式碼的時候,你可以用printk完成這個任務。 

Printk 

在前些章中,我們簡單假設printk工作起來和printf很類似。現在是介紹一下它們之間不同的時候了。 

其中一個不同點就是,printk允許你根據它們的嚴重程度,通過附加不同的“記錄級”來對訊息分類,或賦予訊息優先順序。你可以用巨集來指示記錄級。例如,KERN_INFO,我們前面已經看到它被加在列印語句的前面,它就是一種可能的訊息記錄級。記錄級巨集展開為一個字串,在編譯時和訊息文字拼接在一起;這也就是為什麼下面的例子中優先順序和格式字串間沒有逗號。這有兩個printk的例子,一個是除錯資訊,一個是關鍵資訊: 


(程式碼) 

中定義了8種記錄級別串。沒有指定優先順序的printk語句預設使用DEFAULT_MESSAGE_LOGLEVEL優先順序,它是一個在kernel/printk.c中定義的整數。預設記錄級的具體數值在Linux的開發期間曾變化過若干次,所以我建議你最好總是指定一個合適的記錄級。 

根據記錄級,核心將訊息列印到當前文字控制檯上:如果優先順序低於console_loglevel這個數值的話,該訊息就顯示在控制檯上。如果系統同時運行了klogd和syslogd,無論console_loglevel為何值,核心都將訊息追加到/var/log/messages中。 

變數console_loglevel最初初始化為DEFAULT_CONSOLE_LOGLEVEL,但可以通過sys_syslog系統呼叫修改。如klogd的手冊所示,可以在啟動klogd時指定-c開關來修改這個變數。此外,你還可以寫個程式來改變控制檯記錄級。你可以在O’Reilly站點上的原始檔中找到我寫的一個這種功能的程式,miscprogs/setlevel.c。新優先順序是通過一個1到8之間的整數值指定的。 

你也許需要在核心失效後降低記錄級(見“除錯系統故障”),這是因為失效處理程式碼會將console_loglevel提升到15,之後所有的訊息都會出現在控制檯上。為看到你的除錯資訊,如果你執行的是核心2.0.x話,你需要提升記錄級。核心2.0發行降低了MINIMUM_CONSOLE_LOGLEVEL,而舊版本的klogd預設情況下要列印很多控制訊息。如果你碰巧使用了這個舊版本的守護程序,除非你提升記錄級,核心2.0會比你預期的打印出更少的訊息。這就是為什麼hello.c中使用了<1>標記,這樣可以保證訊息顯示在控制檯上。 

從1.3.43一來的核心版本通過允許你向指定虛控制檯傳送訊息,藉此提供一個靈活的記錄策略。預設情況下,“控制檯”是當前虛終端。也可以選擇不同的虛終端接收訊息,你只需向所選的虛終端呼叫ioctl(TIOCLINUX)。如下程式,setconsole,可以用來選擇哪個虛終端接收核心訊息;它必須以超級使用者身份執行。如果你對ioctl還不有把握,你可以跳過這至下一節,等到讀完第5章“字元裝置驅動程式的擴充套件操作”的“ioctl”一節後,再回到這裡讀這段程式碼。 

(程式碼) 

setconsole使用了用於Linux專用功能的特殊的ioctl命令TIOCLINUX。為了使用TIOCLINUX,你要傳遞給它一個指向位元組陣列的指標。陣列的第一個位元組是所請求的子命令的編碼,隨後的位元組依命令而不同。在setconsole中使用了子命令11,後一個位元組(存放在bytes[1]中)標別虛擬控制檯。TIOCLINUX的完成介紹可以在核心原始碼drivers/char/tty_io.c中找到。 

訊息是如何記錄的 

printk函式將訊息寫到一個長度為LOG_BUF_LEN個位元組的迴圈緩衝區中。然後喚醒任何等待訊息的程序,即那些在呼叫syslog系統呼叫或讀取/proc/kmesg過程中睡眠的程序。這兩個訪問記錄引擎的介面是等價的。不過/proc/kmesg檔案更象一個FIFO檔案,從中讀取資料更容易些。一條簡單的cat命令就可以讀取訊息。 

如果迴圈緩衝區填滿了,printk就繞到緩衝區的開始處填寫新資料,覆蓋舊資料。於是記錄程序就丟失了最舊的資料。這個問題與利用迴圈緩衝區所獲得的好處相比可以忽略不計。例如,迴圈緩衝區可以使系統在沒有記錄程序的情況下照樣執行,同時又不浪費記憶體。Linux處理訊息的方法的另一個特點是,可以在任何地方呼叫printk,甚至在中斷處理函式裡也可以呼叫,而且對資料量的大小沒有限制。這個方法的唯一缺點就是可能丟失某些資料。 

如果klogd正在執行,它讀取核心訊息並將它們分派到syslogd,它隨後檢查/etc/syslog.conf找到處理這些資料的方式。syslogd根據一個“設施”和“優先順序”切分訊息;可以使用的值定義在中。核心訊息根據相應printk中指定的優先順序記錄到LOG_KERN設施中。如果klogd沒有執行,資料將儲存在迴圈緩衝區中直到有程序來讀取資料或資料溢位。 

如果你不希望因監視你的驅動程式的訊息而把你的系統記錄搞亂,你給klogd指定-f(檔案)選項或修改/etc/syslog.conf將記錄寫到另一個檔案中。另一種方法是一種強硬方法:殺掉klogd,將訊息列印到不用的虛終端上*,或者在一個不用的xterm上執行cat /proc/kmesg顯示訊息。 

使用預處理方便監視處理 

在驅動程式開發早期,printk可以對除錯和測試新程式碼都非常有幫助。然而當你正式發行驅動程式時,你應該去掉,或者至少關閉,這些列印語句。很不幸,你可能很快就發現,隨著你想不再需要那些訊息並去掉它們時,你可能又要加新功能,你又需要這些訊息了。解決這些問題有幾種方法――如何從全域性開啟和關閉訊息以及如何開啟和關閉個別訊息。 

下面給出了我處理訊息所用的大部分程式碼,它有如下一些功能: 


可以通過在巨集名字加一個字母或去掉一個字母開啟或關閉每一條語句。 


通過在編譯前修改CFLAGS變數,可以一次關閉所有訊息。 


同樣的列印語句既可以用在核心態(驅動程式)也可以用在使用者態(演示或測試程式)。 

下面這些直接來自scull.h的程式碼片斷實現了這些功能。 

(程式碼) 

符合PDEBUG和PDEBUGG依賴於是否定義了SCULL_DEBUG,它們都和printf呼叫很類似。 

為了進一步方便這個過程,在你的Makefile加上如下幾行。 

(程式碼) 

本節所給出的程式碼依賴於gcc對ANSI C預編譯器的擴充套件,gcc可以支援帶可變數目引數的巨集。這種對gcc的依賴並不是什麼問題,因為核心對gcc特性的依賴更強。此外,Makefile依賴於GNU的gmake;基於同樣的道理,這也不是什麼問題。 

如果你很熟悉C預編譯器,你可以將上面的定義擴充套件為可以支援“除錯級”概念的,可以為每級賦一個整數(或點陣圖),說明這一級列印多麼瑣碎的訊息。 

但是每一個驅動程式都有它自己的功能和監視需求。好的程式設計技巧會在靈活性和高效之間找到一個權衡點,這個我就不能說哪個對你最好了。記住,預編譯器條件(還有程式碼中的常量表達式)只到編譯時執行,你必須重新編譯程式來開啟或關閉訊息。另一種方法就是使用C條件語句,它在執行時執行,因此可以讓你在程式執行期間開啟或關閉訊息。這個功能很好,但每次程式碼執行系統都要進行額外的處理,甚至在訊息關閉後仍然會影響效能。有時這種效能損失是無法接受的。 

個人觀點,儘管上面給出的巨集迫使你每次要增加或去掉訊息時都要重新編譯,重新載入模組,但我覺得用這些巨集已經很好了。 

通過查詢除錯 

上一節談到了printk是如何工作的以及如何使用它。但沒有談及它的缺點。 

由於syslogd會一直保持重新整理它的輸出檔案,每列印一行都會引起一次磁碟操作,因此過量使用printk會嚴重降低系統性能。至少從syslogd的角度看是這樣的。它會將所有的資料都一股腦地寫到磁碟上,以防在列印訊息後系統崩潰;然而,你不想因為除錯資訊的緣故而降低系統性能。這個問題可以通過在/etc/syslogd.conf中記錄檔案的名字前加一個波折號解決,但有時你不想修改你的配置檔案。如果不這樣,你還可以執行一個非klogd的程式(如前面介紹的cat /proc/kmesg),但這樣並不能為正常操作提供一個合適的環境。 

與這相比,最好的方法就是在你需要資訊的時候,通過查詢系統獲得相關資訊,而不是持續不斷地產生資料。事實上,每一個Unix系統都提供了很多工具用來獲得系統資訊:ps,netstat,vmstat等等。 

有許多技術適合與驅動程式開發人員查詢系統,簡而言之就是,在/proc下建立檔案和使用ioctl驅動程式方法。 

使用/proc檔案系統 

Linux中的/proc檔案系統與任何裝置都沒有關係――/proc中的檔案都在被讀取時有核心建立的。這些檔案都是普通的文字檔案,它們基本上可由普通人理解,也可被工具程式理解。例如,對於大多數Linux的ps實現而言,它都通過讀取/proc檔案系統獲得程序表資訊的。/proc虛擬檔案的創意已由若干現代作業系統使用,且非常成功。 

/proc的當前實現可以動態建立i節點,允許使用者模組為方便資訊檢索建立如何入口點。 

為了在/proc中建立一個健全的檔案節點(可以read,write,seek等等),你需要定義file_operations結構和inode_operations結構,後者與前者有類似的作用和尺寸。建立這樣一個i節點比起建立整個字元裝置並沒有什麼不同。我們這裡不討論這個問題,如果你感興趣,你可以在原始碼樹fs/proc中獲得進一步細節。 

與大多數/proc檔案一樣,如果檔案節點僅僅用來讀,建立它們是比較容易的,我將這裡介紹這一技術。很不幸,這一技術只能在Linux 2.0及其後續版本中使用。 

這裡是建立一個稱為/proc/scullmem檔案的scull程式碼,這個檔案用來獲取scull使用的記憶體資訊。 

(程式碼) 

填寫/proc檔案非常容易。你的函式獲取一個空閒頁面填寫資料;它將資料寫進緩衝區並返回所寫資料的長度。其他事情都由/proc檔案系統處理。唯一的限制就是所寫的資料不能超過PAGE_SIZE個位元組(巨集PAGE_SIZE定義在標頭檔案中;它是與體系結構相關的,但你至少可以它有4KB大小)。 

如果你需要寫多於一個頁面的資料,你必須實現功能健全的檔案。 

注意,如果一個正在讀你的/proc檔案的程序發出了若干read呼叫,每一個都獲取新資料,儘管只有少量資料被讀取,你的驅動程式每次都要重寫整個緩衝區。這些額外的工作會使系統性能下降,而且如果檔案產生的資料與下一次的不同,以後的read呼叫要重新裝配不相關的部分,這一會造成資料錯位。事實上,由於每個使用C庫的應用程式都大塊地讀取資料,效能並不是什麼問題。然而,由於錯位時有發生,它倒是一個值得考慮的問題。在獲取資料後,庫呼叫至少要呼叫1次read――只有當read返回0時才報告檔案尾。如果驅動程式碰巧比前面產生了更多的資料,系統就返回到使用者空間額外的位元組並且與前面的資料塊是錯位的。我們將在第6章“時間流”的“任務佇列”一節中涉及/proc/jiq*,那時我們還會遇到錯位問題。 

cleanup_module中應該使用下面的語句登出/proc節點: 

(程式碼) 

傳遞給函式的引數是包含要撤銷檔案的目錄名和檔案的i節點號。由於i節點號是自動分配的,在編譯時是無法知道的,必須從資料結構中讀取。 

ioctl方法 

ioctl,下一章將詳細討論,是一個系統呼叫,它可以操做在檔案描述符上;它接收一個“命令”號和(可選的)一個引數,通常這是一個指標。 

做為替代/proc檔案系統的方法,你可以為除錯實現若干ioctl命令。這些命令從驅動程式空間複製相關資料到程序空間,在程序空間裡檢查這些資料。 

只有使用ioctl獲取資訊比起/proc來要困難一些,因為你一個程式呼叫ioctl並顯示結果。必須編寫這樣的程式,還要編譯,保持與你測試的模組間的一致性等。 

不過有時候這是最好的獲取資訊的方法,因為它比起讀/proc來要快得多。如果在資料寫到螢幕前必須完成某些處理工作,以二進位制獲取資料要比讀取文字檔案有效得多。此外,ioctl不限制返回資料的大小。 

ioctl方法的一個優點是,當除錯關閉後除錯命令仍然可以保留在驅動程式中。/proc檔案對任何檢視這個目錄的人都是可見的,然而與/proc檔案不同,未公開的ioctl命令通常都不會被注意到。此外,如果驅動程式有什麼異常,它們仍然可以用來除錯。唯一的缺點就是模組會稍微大一些。 

通過監視除錯 

有時你遇到的問題並不特別糟,通過在使用者空間執行應用程式來檢視驅動程式與系統之間的互動過程可以幫助你捕捉到一些小問題,並可以驗證驅動程式確實工作正常。例如,看到scull的read實現如何處理不同資料量的read請求後,我對scull更有信心。 

有許多方法監視一個使用者態程式的工作情況。你可以用偵錯程式一步步跟蹤它的函式,插入列印語句,或者用strace執行程式。在實際目的是檢視核心程式碼時,最後一項技術非常有用。 

strace命令是一個功能非常強大的工具,它可以現實程式所呼叫的所有系統呼叫。它不僅可以顯示呼叫,而且還能顯示呼叫的引數,以符號方式顯示返回值。當系統呼叫失敗時,錯誤的符號值(如,ENOMEM)和對應的字串(Out of memory)同時顯示。strace還有許多命令列選項;最常用的是-t,它用來顯示呼叫發生的時間,-T,顯示呼叫所花費的時間,以及-o,將輸出重定向到一個檔案中。預設情況下,strace將所有跟蹤資訊列印到stderr上。 

strace從核心接收資訊。這意味著一個程式無論是否按除錯方式編譯(用gcc的-g選項)或是被去掉了符號資訊都可以被跟蹤。與偵錯程式可以連線到一個執行程序並控制它類似,你還可以跟蹤一個已經執行的程序。 

跟蹤資訊通常用來生成錯誤報告報告給應用開發人員,但是對核心程式設計人員來說也一樣非常有用。我們可以看到系統呼叫是如何執行驅動程式程式碼的;strace允許我們檢查每一次呼叫輸入輸出的一致性。 

例如,下面的螢幕輸出給出了命令ls /dev > /dev/scull0的最後幾行: 

(程式碼) 

很明顯,在ls完成目標目錄的檢索後首次對write的呼叫中,它試圖寫4KB。很奇怪,只寫了4000個位元組,接著重試這一操作。然而,我們知道scull的write實現每次只寫一個量子,我在這裡看到了部分寫。經過若干步驟之後,所有的東西都清空了,程式正常退出。 

另一個例子,讓我們來讀scull裝置: 

(程式碼) 

正如所料,read每次只能讀到4000個位元組,但是資料總量是不變的。注意本例中重試工作是如何組織的,注意它與上面寫跟蹤的對比。wc專門為快速讀資料進行了優化,它繞過了標準庫,以便每次用一個系統呼叫讀取更多的資料。你可以從跟蹤的read行中看到wc每次要讀16KB。 

Unix專家可以在strace的輸出中找到很多有用資訊。如果你被這些符號搞得滿頭霧水,我可以只看檔案方法(open,read等等)是如何工作的。 

個人認為,跟蹤工具在查明系統呼叫的執行時錯誤過程中最有用。通常應用或演示程式中的perror呼叫不足以用來除錯,而且對於查明到底是什麼樣的引數觸發了系統呼叫的錯誤也很有幫助。 

除錯系統故障 

即便你用了所有監視和除錯技術,有時候驅動程式中依然有錯誤,當這樣的驅動程式執行會造成系統故障。當這種情況發生時,獲取足夠多的資訊來解決問題是至關重要的。 

注意,“故障”不意味著“panic”。Linux程式碼非常魯棒,可以很好地響應大部分錯誤:故障通常會導致當前程序的終止,但系統繼續執行。如果在程序上下文之外發生故障,或是組成系統的重要部件發生故障時,系統可能panic。但問題出在驅動程式時,通常只會導致產生故障的程序終止――即那個使用驅動程式的程序。唯一不可恢復的損失就是當程序被終止時,程序上下文分配的記憶體丟失了;例如,由驅動程式通過kmalloc分配的動態連結串列可能丟失。然而,由於核心會對尚是開啟的裝置呼叫close,你的驅動程式可以釋放任何有open方法分配的資源。 

我們已經說過,當核心行為異常時會在控制檯上顯示一些有用的資訊。下一節將解釋如何解碼和使用這些訊息。儘管它們對於初學者來說相當晦澀,處理器的給出資料都是些很有意思的資訊,通常無需額外測試就可以查明程式錯誤。 

Oops訊息 

大部分錯誤都是NULL指標引用或使用其他不正確的指標數值。這些錯誤通常會導致一個oops訊息。 

由處理器使用的地址都是“虛”地址,而且通過一個複雜的稱為頁表(見第13章“Mmap和DMA”中的“頁表”一節)的結構對映為實體地址。當引用一個非法指標時,頁面對映機制就不能將地址對映到實體地址,並且處理器向作業系統發出一個“頁面失效”。如果地址確實是非法的,核心就無法從失效地址上“換頁”;如果此時處理在超級使用者太,系統於是就產生一個“oops”。值得注意的是,在版本2.1中核心處理失效的方式有所變化,它可以處理在超級使用者態的非法地址引用了。新實現將在第17章“最近發展”的“處理核心空間失效”中介紹。 

oops顯示故障時的處理器狀態,模組CPU暫存器內容,頁描述符表的位置,以及其他似乎不能理解的資訊。這些是由失效處理函式(arch/*/kernel/traps.c)中的printk語句產生的,而且象前面“Printk”一節介紹的那樣進行分派。 

讓我們看看這樣一個訊息。這裡給出的是傳統個人電腦(x86平臺),執行Linux 2.0或更新版本的oops――版本1.2的輸出稍有不同。 

(程式碼) 

上面的訊息是在一個有意加入錯誤的失效模組上執行cat所至。fault.c崩潰如下程式碼: 

(程式碼) 

由於read從它的小緩衝區(faulty_buf)複製資料到使用者空間,我們希望讀一小塊檔案能夠工作。然而,每次讀出多於1KB的資料會跨越頁面邊界,如果訪問了非法頁面read就會失敗。事實上,前面給出的oops是在請求一個4KB大小的read時發生的,這條訊息在/var/log/messages(syslogd預設存放核心訊息的檔案)的oops訊息前給出了: 

(程式碼) 

同樣的cat命令卻不能在Alpha上產生oops,這是因為從faulty_buf讀取4KB位元組沒有超出頁邊界(Alpha上的頁面大小是8KB,緩衝區正好在頁面的起始位置附近)。如果在你的系統上讀取faulty沒有產生oops,試試wc,或者給dd顯式地指定塊大小。 

使用ksymoops 

oops訊息的最大問題就是十六進位制數值對於程式設計師來說沒什麼意義;需要將它們解析為符號。 

核心原始碼通過其所包含的ksymoops工具幫助開發人員――但是注意,版本1.2的原始碼中沒有這個程式。該工具將oops訊息中的數值地址解析為核心符號,但只限於PC機產生的oops訊息。由於訊息本身就是處理器相關的,每一體系結構都有其自身的訊息格式。 

ksymoops從標準輸入獲得oops訊息,並從命令列核心符號表的名字。符號表通常就是/usr/src/linux/System.map。程式以更可讀的方式列印呼叫軌跡和程式程式碼,而不是最原始的oops訊息。下面的片斷就是用上一節的oops訊息得出的結果: 

(程式碼) 

由ksymoops反彙編出的程式碼給出了失效的指令和其後的指令。很明顯――對於那些知道一點彙編的人――repz movsl指令(REPeat till cx is Zero, MOVe a String of Longs)用源索引(esi,是0x202e000)訪問了一個未對映頁面。用來獲得模組資訊的ksymoops -m命令給出,模組對映到一個在0x0202dxxx的頁面上,這也確認樂esi確實超出了範圍。 

由於faulty模組所佔用的記憶體不在系統表中,被解碼的呼叫軌跡還給出了兩個數值地址。這些值可以手動補充,或是通過ksyms命令的輸出,或是在/proc/ksyms中查詢模組的名字。 

然而對於這個失效,這兩個地址並不對應與程式碼地址。如果你看了arch/i386/kernel/traps.c,你就發現,呼叫軌跡是從整個堆疊並利用一些啟發式方法區分資料值(本地變數和函式引數)和返回地址獲得的。呼叫軌跡中只給出了引用核心程式碼的地址和引用模組的地址。由於模組所佔頁面既有程式碼也有資料,錯綜複雜的棧可能會漏掉啟發式資訊,這就是上面兩個0x202xxxx地址的情況。 

如果你不願手動檢視模組地址,下面這組管道可以用來建立一個既有核心又有模組符號的符號表。無論何時你載入模組,你都必須重新建立這個符號表。 

(程式碼) 

這個管道將完整的系統表與/proc/ksyms中的公開核心符號混合在一起,後者除了核心符號外,還包括了當前核心裡的模組符號。這些地址在insmod重定位程式碼後就出現在/proc/ksyms中。由於這兩個檔案的格式不同,使用了sed和awk將所有的文字行轉換為一種合適的格式。然後對這張表排序,去除重複部分,這樣ksymoops就可以用了。 

如果我們重新執行ksymoops,它從新的符號表中截取出如下資訊: 

(程式碼) 

正如你所見到的,當跟蹤與模組有關的oops訊息時,建立一個修訂的系統表是很有助益的:現在ksymoops能夠對指令指標解碼並完成整個呼叫軌跡了。還要注意,顯式反彙編碼的格式和objdump所使用的格式一樣。objdump也是一個功能強大的工具;如果你需要檢視失敗前的指令,你呼叫命令objdump �d faulty.o。 

在檔案的彙編列表中,字串faulty_read+45/60標記為失效行。有關objdump的更多的資訊和它的命令列選項可以參見該命令的手冊。 

即便你構建了你自己的修訂版符號表,上面提到的有關呼叫軌跡的問題仍然存在:雖然0x202xxxx指標被解碼了,但仍然是假的。 

學會解碼oops訊息需要一定的經驗,但是確實值得一做。用來學習的時間很快就會有所回報。不過由於機器指令的Unix語法與Intel語法不同,唯一的問題在於從哪獲得有關組合語言的文件;儘管你瞭解PC組合語言,但你的經驗都是用Intel語法的程式設計獲得的。在參考書目中,我給一些有所補益的書籍。 

使用oops 

使用ksymoops有些繁瑣。你需要C++編譯器編譯它,你還要構建你自己的符號表來充分發揮程式的能力,你還要將原始訊息和ksymoops輸出合在一起組成可用的資訊。 

如果你不想找這麼多麻煩,你可以使用oops程式。oops在本書的O’Reilly FTP站點給出的原始碼中。它源自最初的ksymoops工具,現在它的作者已經不維護這個工具了。oops是用C語言寫成的,而且直接檢視/proc/ksyms而無需使用者每次載入模組後構建新的符號表。 

該程式試圖解碼所有的處理器暫存器並堆疊軌跡解析為符號值。它的缺點是,它要比ksymoops羅嗦些,但通常你所有的資訊越多,你發現錯誤也就越快。oops的另一個優點是,它可以解析x86,Alpha和Sparc的oops訊息。與核心原始碼相同,這個程式也按GPL發行。 

oops產生的輸出與ksymoops的類似,但是更完全。這裡給出前一個oops輸出的開始部分�由於在這個oops訊息中堆疊沒儲存什麼有用的東西,我不認為應該顯示整個堆疊軌跡: 

(程式碼) 

當你除錯“真正的”模組(faulty太短了,沒有什麼意義)時,將暫存器和堆疊解碼是非常有益的,而且如果被除錯的所有模組符號都開放出來時更有幫助。在失效時,處理器暫存器一般不會指向模組的符號,只有當符號表開放給/proc/ksyms時,你才能輸出中標別它們。 

我們可以用一下步驟製作一張更完整的符號表。首先,我們不應在模組中宣告靜態變數,否則我們就無法用insmod開放它們了。第二,如下面的擷取自scull的init_module函式的程式碼所示,我們可以用#ifdef SCULL_DEBUG或類似的巨集遮蔽register_symtab呼叫。 

(程式碼) 

我們在第2章“編寫和執行模組”的“註冊符號表”一節中已經看到了類似內容,那裡說,如果模組不註冊符號表,所有的全域性符號就都開放。儘管這一功能僅在SCULL_DEBUG被啟用時才有效,為了避免核心中的名字空間汙染,所有的全域性符號有合適的字首(參見第2章的“模組與應用程式”一節)。 

使用klogd 

klogd守護程序的近期版本可以在oops存放到記錄檔案前對oops訊息解碼。解碼過程只由版本1.3或更新版本的守護程序完成,而且只有將-k /usr/src/linux/System.map做為引數傳遞給守護程序時才解碼。(你可以用其他符號表檔案代替System.map) 

有新的klogd給出的faulty的oops如下所示,它寫到了系統記錄中: 

(程式碼) 

我想能解碼的klogd對於除錯一般的Linux安裝的核心來說是很好的工具。由klogd解碼的訊息包括大部分ksymoops的功能,而且也要求使用者編譯額外的工具,或是,當系統出現故障時,為了給出完整的錯誤報告而合併兩個輸出。當oops發生在核心時,守護程序還會正確地解碼指令指標。它並不反彙編程式碼,但這不是問題,當錯誤報告給出訊息時,二進位制資料仍然存在,可以離線反彙編程式碼。 

守護程序的另一個功能就是,如果符號表版本與當前核心不匹配,它會拒絕解析符號。如果在系統記錄中解析出了符號,你可以確信它是正確的解碼。 

然而,儘管它對Linux使用者很有幫助,這個工具在除錯模組時沒有什麼幫助。我個人沒有在開放軟體的電腦裡使用解碼選項。klogd的問題是它不解析模組中的符號;因為守護程序在程式設計師載入模組前就已經運行了,即使讀了/proc/ksyms也不會有什麼幫助。記錄檔案中存在解析後的符號會使oops和ksymoops混淆,造成進一步解析的困難。 

如果你需要使用klogd除錯你的模組,最新版本的守護程序需要加入一些新的特殊支援,我期待它的完成,只要給核心打一個小補丁就可以了。 

系統掛起 

儘管核心程式碼中的大多數錯誤僅會導致一個oops訊息,有時它們困難完全將系統掛起。如果系統掛起了,沒有訊息能夠打印出來。例如,如果程式碼遇到一個死迴圈,核心停止了排程過程,系統不會再響應任何動作,包括魔法鍵Ctrl-Alt-Del組合。 

處理系統掛起有兩個選擇――一個是防範與未然,另一個就是亡羊補牢,在發生掛起後除錯程式碼。 

通過在策略點上插入schedule呼叫可以防止死迴圈。schedule呼叫(正如你所猜想到的)呼叫排程器,因此允許其他程序偷取當然程序的CPU時間。如果程序因你的驅動程式中的錯誤而在核心空間迴圈,你可以在跟蹤到這種情況後殺掉這個程序。 

在驅動程式程式碼中插入schedule呼叫會給程式設計師帶來新的“問題”:函式,,以及呼叫軌跡中的所有函式,必須是可重入的。在正常環境下,由於不同的程序可能併發地訪問裝置,驅動程式做為整體是可重入的,但由於Linux核心是不可搶佔的,不必每個函式都是可重入的。但如果驅動程式函式允許排程器中斷當前程序,另一個不同的程序可能會進入同一個函式。如果schedule呼叫僅在除錯期間開啟,如果你不允許,你可以避免兩個併發程序訪問驅動程式,所以併發性倒不是什麼非常重要的問題。在介紹阻塞型操作時(第5章的“寫可重入程式碼”)我們再詳細介紹併發性問題。 

如果要除錯死迴圈,你可以利用Linux鍵盤的特殊鍵。預設情況下,如果和修飾鍵一起按了PrScr鍵(鍵碼是70),系統會向當前控制檯列印有關機器狀態的有用資訊。這一功能在x86和Alpha系統都有。Linux的Sparc移植也有同樣的功能,但它使用了標記為“Break/Scroll Lock”的鍵(鍵碼是30)。 

每一個特殊函式都有一個名字,並如下面所示都有一個按鍵事件與之對應。組合鍵之後的括號裡是函式名。 

Shift-PrScr(Show_Memory) 

列印若干行關於記憶體使用的資訊,尤其是有關緩衝區快取記憶體的使用情況。 

Control-PrScr(Show_State) 

針對系統裡的每一個處理器列印一行資訊,同時還列印內部程序樹。對當前程序進行標記。 

RightAlt-PrScr(Show_Registers) 

由於它可以列印按鍵時的處理器暫存器內容,它是系統掛起時最重要的一個鍵了。如果有當前核心的系統表的話,檢視指令計數器以及它如何隨時間變化,對了解程式碼在何處迴圈非常有幫助。 

如果想將這些函式對映到不同的鍵上,每一個函式名都可以做為引數傳遞給loadkeys。鍵盤對映表可以任意修改(這是“策略無關的”)。 

如果console_loglevel足夠到的話,這些函式列印的訊息會出現在控制檯上。如果不是你運行了一箇舊klogd和一個新核心的話,預設記錄級應該足夠了。如果沒有出現訊息,你可以象以前說的那樣提升記錄級。“足夠高”的具體值與你使用的核心版本有關。對於Linux 2.0或更新的版本來說是5。 

即便當系統掛起時,訊息也會列印到控制檯上,確認記錄級足夠高是非常重要的。訊息是在產生中斷時生成的,因此即便有錯的程序不釋放CPU也可以執行――當然,除非中斷被遮蔽了,不過如果發生這種情況既不太可能也非常不幸。 

有時系統看起來象是掛起了,但其實不是。例如,如果鍵盤因某種奇怪的原因被鎖住了就會發生這種情況。這種假掛起可以通過檢視你為探明此種情況而執行的程式輸出來判斷。我有一個程式會不斷地更新LED顯示器上的時鐘,我發現這個對於驗證排程器尚在執行非常有用。你可以不必依賴外部裝置就可以檢查排程器,你可以實現一個程式讓鍵盤LED閃爍,或是不斷地開啟關閉軟盤馬達,或是不斷觸動揚聲器――不過我個人認為,通常的蜂鳴聲很煩人,應該儘量避免。看看ioctl命令KDMKTONE。O’Reilly FTP站點上的例子程式(misc-progs/heartbeat.c)中有一個是讓鍵盤LED不斷閃爍的。 

如果鍵盤不接收輸入了,最佳的處理手段是從網路登入在系統中,殺掉任何違例的程序,或是重新設定鍵盤(用kdb_mode -a)。然而,如果你沒有網路可用來恢復的話,發現系統掛起是由鍵盤鎖死造成的一點兒用也沒有。如果情況確實是這樣,你應該配置一種替代輸入裝置,至少可以保證正常地重啟系統。對於你的計算機來說,關閉系統或重啟比起所謂的按“大紅鈕”要更方便一些,至少它可以免去長時間地fsck掃描磁碟。 

這種替代輸入裝置可以是遊戲杆或是滑鼠。在sunsite.edu.cn上有一個遊戲杆重啟守護程序,gpm-1.10或更新的滑鼠伺服器可以通過命令列選項支援類似的功能。如果鍵盤沒有鎖死,但是卻誤入“原始”模式,你可以看看kdb包中文件介紹的一些小技巧。我建議最好在問題出現以前就看看這些文件,否則就太晚了。另一種可能是配置gpm-root選單,增添一個“reboot”或“reset keyboard”選單項;gpm-root一個響應控制滑鼠事件的守護程序,它用來在螢幕上顯示選單和執行所配置的動作。 

最好,你會可以按“留意安全鍵”(SAK),一個用於將系統恢復為可用狀態的特殊鍵。由於不是所有的實現都能用,當前Linux版本的預設鍵盤表中沒有為此鍵特設一項。不過你還是可以用loadkeys將你的鍵盤上的一個鍵對映為SAK。你應該看看drivers/char目錄中的SAK實現。程式碼中的註釋解釋了為什麼這個鍵在Linux 2.0中不是總能工作,這裡我就不多說了。 

不過,如果你執行版本2.1.9或是更新的版本,你就可以使用非常可靠地留意安全鍵了。此外,2.1.43及後續版本核心還有一個編譯選項選擇是否開啟“SysRq魔法鍵”;我建議你看一看drivers/char/sysrq.c中的程式碼並使用這項新技術。 

如果你的驅動程式真的將系統掛起了,而且你有不知道在哪插入schedule呼叫,最佳的處理方法就是加一些列印訊息,並將它們列印到控制檯上(通過修改console_loglevel變數值)。在重演掛起過程時,最好將所有的磁碟都以只讀方式安裝在系統上。如果磁碟是隻讀的或沒有安裝,就不會存在破壞檔案系統或使其進入不一致狀態的危險。至少你可以避免在復位系統後執行fsck。另一中方法就是使用NFS根計算機來測試模組。在這種情況下,由於NFS伺服器管理檔案系統的一致性,而它又不會受你的驅動程式的影響,你可以避免任何的檔案系統崩潰。 

使用偵錯程式 

最後一種除錯模組的方法就是使用偵錯程式來一步步地跟蹤程式碼,檢視變數和機器暫存器的值。這種方法非常耗時,應該儘可能地避免。不過,某些情況下通過偵錯程式對程式碼進行細粒度的分析是非常有益的。在這裡,我們所說的被除錯的程式碼執行在核心空間――除非你遠端控制核心,否則不可能一步步跟蹤核心,這會使很多事情變得更加困難。由於遠端控制很少用到,我們最後介紹這項技術。所幸的是,在當前版本的核心中可以檢視和修改變數。 

在這一級上熟練地使用偵錯程式需要精通gdb命令,對彙編碼有一定了解,並且有能夠將原始碼與優化後的彙編碼對應起來的能力。 

不幸的是,gdb更適合與除錯核心而不是模組,除錯模組化的程式碼需要更多的技術。這更多的技術就是kdebug包,它利用gdb的“遠端除錯”介面控制本地核心。我將在介紹普通偵錯程式後介紹kdebug。 

使用gdb 

gdb在探究系統內部行為時非常有用。啟動偵錯程式時必須假想核心就是一個應用程式。除了指定核心檔名外,你還應該在命令列中提供記憶體鏡象檔案的名字。典型的gdb呼叫如下所示: 

(程式碼) 

第一個引數是未經壓縮的核心可執行檔案(在你編譯完核心後,這個檔案在/usr/src/linux目錄中)的名字。只有x86體系結構有zImage檔案(有時稱為vmlinuz),它是一種解決Intel處理器真實模式下只有640KB限制的一種技巧;而無論在哪個平臺上,vmlinux都是你所編譯的未經壓縮的核心。 

gdb命令列的第二個引數是是記憶體鏡象檔案的名字。與其他在/proc下的檔案類似,/proc/kcore也是在被讀取時產生的。當read系統呼叫在/proc檔案系統執行時,它對映到一個用於資料生成而不是資料讀取的函式上;我們已在“使用/proc檔案系統”一節中介紹了這個功能。系統用kcore來表示按記憶體鏡象檔案格式儲存的核心“可執行檔案”;由於它要表示整個核心地址空間,它是一個非常巨大的檔案,對應所有的實體記憶體。利用gdb,你可以通過標準gdb命令檢視核心標量。例如,p jiffies可以列印從系統啟動到當前時刻的時鐘滴答數。 

當你從gdb列印資料時,核心還在執行,不同資料項會在不同時刻有不同的數值;然而,gdb為了優化對記憶體鏡象檔案的訪問會將已經讀到的資料快取起來。如果你再次檢視jiffies變數,你會得到和以前相同的值。快取變數值防止額外的磁碟操作對普通記憶體鏡象檔案來說是對的,但對“動態”記憶體鏡象檔案來說就不是很方便了。解決方法是在你想重新整理gdb快取的時候執行core-file /proc/kcore命令;偵錯程式將使用新的記憶體鏡象檔案並廢棄舊資訊。但是,讀新資料時你並不總是需要執行core-file命令;gdb以1KB的尺度讀取記憶體鏡象檔案,僅僅快取它所引用的若干塊。 

你不能用普通gdb做的是修改核心資料;由於偵錯程式需要在訪問記憶體鏡象前執行被除錯程式,它是不會去修改記憶體鏡象檔案的。當除錯核心鏡象時,執行run命令會導致在執行若干指令後導致段違例。出於這個原因,/proc/kcore都沒有實現write方法。 

如果你用除錯選項(-g)編譯了核心,結果產生的vmlinux比沒有用-g選項的更適合於gdb。不過要注意,用-g選項編譯核心需要大量的磁碟空間――支援網路和很少幾個裝置和檔案系統的2.0核心在PC上需要11KB。不過不管怎樣,你都可以生成zImage檔案並用它來其他系統:在生成可啟動鏡象時由於選項-g而加入的除錯資訊最終都被去掉了。如果我有足夠的磁碟空間,我會一致開啟-g選項的。 

在非PC計算機上則有不同的方法。在Alpha上,make boot會在生成可啟動鏡象前將除錯資訊去掉,所以你最終會獲得vmlinux和vmlinux.gz兩個檔案。gdb可以使用前者,但你只能用後者啟動。在Sparc上,預設情況下核心(至少是2.0核心)不會被去掉除錯資訊,所以你需要在將其傳遞給silo(Sparc的核心載入器)前將除錯資訊去掉,這樣才能啟動。由於尺寸的問題,無論milo(Alpha的核心載入器)還是silo都不能啟動未去掉除錯資訊的核心。 

當你用-g選項編譯核心並且用vmlinux和/proc/kcore一起使用偵錯程式,gdb可以返回很多有關核心內部結構的資訊。例如,你可以使用類似於這樣的命令,p *module_list,p *module_list->next和p *chrdevs[4]->fops等顯示這些結構的內容。如果你手頭有核心對映表和原始碼的話,這些探測命令是非常有用的。 

另一個gdb可以在當前核心上執行的有用任務是,通過disassemble命令(它可以縮寫)或是“檢查指令”(x/i)命令反彙編函式。disassemble命令的引數可以是函式名或是記憶體區範圍,而x/i則使用一個記憶體地址做為引數,也可以用符號名。例如,你可以用x/20i反彙編20條指令。注意,你不能反彙編一個模組的函式,這是因為偵錯程式處理vmlinux,它並不知道你的模組的資訊。如果你試圖用模組的地址反彙編程式碼,gdb很有可能會報告“不能訪問xxxx處的記憶體(Cannot access memory at xxxx)”。基於同樣的原因,你不檢視屬於模組的資料項。如果你知道你的變數的地址,你可以從/dev/mem中讀出它的值,但很難弄明白從系統記憶體中分解出的資料是什麼含義。 

如果你需要反彙編模組函式,你最好對用objdump工具處理你的模組檔案。很不幸,該工具只能對磁碟上的檔案進行處理,而不能對執行中的模組進行處理;因此,objdump中給出的地址都是未經重定位的地址,與模組的執行環境無關。 

如你所見,當你的目的是檢視核心的執行情況時,gdb是一個非常有用的工具,但它缺少某些功能,最重要的一些功能就是修改核心項和訪問模組的功能。這些空白將由kdebug包填補。 

使用kdebug 

你可用從一般的FTP站點下的pcmcia/extras目錄下拿到kdebug,但是如果你想確保拿到的是最新的版本,你最好到ftp://hyper.stanford.edu/pub/pcmcia/extras/去找。該工具與pcmcia沒有什麼關係,但是這兩個包是同一個作者寫的。 

kdebug是一個使用gdb“遠端除錯”介面與核心通訊的小工具。使用時首先向核心載入一個模組,偵錯程式通過/dev/kdebug訪問核心資料。gdb將該裝置當成一個與被除錯“應用”通訊的串列埠裝置,但它僅僅是一個用於訪問核心空間的通訊通道。由於模組本身執行在核心空間,它可以看到普通偵錯程式無法訪問的核心空間地址。正如你所猜想到的,模組是一個字元裝置驅動程式,並且使用了主裝置號動態分配技術。 

kdebug的優點在於,你無需打補丁或重新編譯:無論是核心還是偵錯程式都無需修改。你所需要做的就是編譯和安裝軟體包,然後呼叫kgdb,kgdb是一個完成某些配置並呼叫gdb,通過新介面訪問核心部件結構的指令碼程式。 

但是,即便是kdebug也沒有提供單步跟蹤核心程式碼和設定斷點的功能。這幾乎是不可避免的,因為核心必須保持執行狀態以保證系統的出於執行狀態,跟蹤核心程式碼的唯一方法就是後面將要談到的從另外一臺計算機上通過串列埠控制系統。不過kgdb的實現允許使用者修改被除錯應用(即當前核心)的資料項,可以傳遞給核心任意數目的引數,並以讀寫方式訪問模組所屬的記憶體區。 

最後一個功能就是通過gdb命令將模組符號表增加到偵錯程式內部的符號表中。這個工作是由kgdb完成的。然後當用戶請求訪問某個符號時,gdb就知道它的地址是哪了。最終的訪問是由模組裡的核心程式碼完成的。不過要注意,kdebug的當前版本(1.6)在對映模組化程式碼地址方面還有些問題。你最好通過列印一些符號並與/proc/ksyms中的值進行比較來做些檢查。如果地址沒有匹配,你可以使用數值,但必須將它們強行轉換為正確的型別。下面就是一個強制型別轉換的例子: 

(程式碼) 

kdebug的另一個強於gdb的優點是,它允許你在資料結構被修改後讀取到最新的值,而不必重新整理偵錯程式的快取;gdb命令set remotecache 0可以用來關閉資料快取。 

由於kdebug與gdb使用起來很相似,這裡我就不過多地羅列使用這個工具的例子了。對於知道如何使用偵錯程式的人來說,這種例子很簡單,但對於那些對偵錯程式一無所知的人來說就很晦澀了。能夠熟練地使用偵錯程式需要時間和經驗,我不準備在這裡承擔老師的責任。 

總而言之,kdebug是一個非常好的程式。線上修改資料結構對於開發人員來說是一個非常大的進步(而且一種將系統掛起的最簡單方法)。現在有許多工具可以使你的開發工作更輕鬆――例如,在開發scull期間,當模組的使用計數器增長後*,我可以使用kdebug來將其復位為0。這就不必每次都麻煩我重啟機器,登入,再次啟動我的應用程式等等。 

遠端除錯 

除錯核心鏡象的最後一個方法是使用gdb的遠端除錯能力。 

當執行遠端除錯的時候,你需要兩臺計算機:一臺執行gdb;另一臺執行你要除錯的核心。這兩臺計算機間用普通串列埠連線起來。如你所料,控制gdb必須能夠理解它所控制的核心的二進位制格式。如果這兩臺計算機是不同的體系結構,必須將偵錯程式編譯為可以支援目標平臺的。 

在2.0中,Linux核心的Intel版本不支援遠端除錯,但是Alpha和Sparc版本都支援。在Alpha版本中,你必須在編譯時包含對遠端除錯的支援,並在啟動時通過傳遞給核心命令列引數kgdb=1或只有kgdb開啟這個功能。在Sparc上,始終包含了對遠端除錯的支援。啟動選項kgdb=ttyx可以用來選擇在哪個串列埠上控制核心,x可以是a或b。如果沒有使用kgdb=選項,核心就按正常方式啟動。 

如果在核心中打開了遠端除錯功能,系統在啟動時就會呼叫一個特殊的初始化函式,配置被除錯核心處理它自己的斷點,並且跳轉到一個編譯自程式中的斷點。這會暫停核心的正常執行,並將控制轉移給斷點服務例程。這一處理函式在串列埠線上等待來自於gdb的命令,當它獲得gdb的命令後,就執行相應的功能。通過這一配置,程式設計師可以單步跟蹤核心程式碼,設定斷點,並且完成gdb所允許的其他任務。 

在控制端,需要一個目標鏡象的副本(我們假設它是linux.img),還需要一個你要除錯的模組副本。如下命令必須傳遞給gdb: 

file linux.img 

file命令告訴gdb哪個二進位制檔案需要除錯。另一種方法是在命令列中傳遞鏡象檔名。這個檔案本身必須和執行在另一端的核心一模一樣。 

target remote /dev/ttyS1 

這條命令通知gdb使用遠端計算機做為除錯過程的目標。/dev/ttyS1是用來通訊的本地串列埠,你可以指定任一裝置。例如,前面介紹的kdebug軟體包中的kgdb指令碼使用target remote /dev/kdebug。 

add-symbol-file module.o address 

如果你要除錯已經載入到被控核心的模組的話,在控制系統上你需要一個模組目標檔案的副本。add-symbol-file通知gdb處理模組檔案,假定模組程式碼被定位在地址address上了。 

儘管遠端除錯可以用於除錯模組,但你還是要載入模組,並且在模組上插入斷點前還需要觸發另一個斷點,除錯模組還是需要很多技巧的。我個人不會使用遠端除錯去跟蹤模組,除非非同步執行的程式碼,如中斷處理函式,出了問題。