Linux調試技術介紹(編程開發)

來源:互聯網
上載者:User

對於任何編寫核心代碼的人來說,最吸引他們注意的問題之一就是如何完成調試。由於核心是一個不與某個進程相關的功能集,其代碼不能很輕鬆地放在調試器中執
行,而且也不能跟蹤。本章介紹你可以用來監視核心代碼和跟蹤錯誤的技術。用列印資訊調試最一般的調試技術就是監視,就是在應用內部合適的點加上
printf調用。當你調試核心代碼的時候,你可以用printk完成這個任務。Printk在前些章中,我們簡單假設printk工作起來和
printf很類似。現在是介紹一下它們之間不同的時候了。其中一個不同點就是,printk允許你根據它們的嚴重程度,通過附加不同的“記錄級”來對消
息分類,或賦予訊息優先順序。你可以用宏來指示記錄級。例如,KERN_INFO,我們前面已經看到它被加在列印語句的前面,它就是一種可能的訊息記錄級。
記錄級宏展開為一個字串,在編譯時間和訊息文本拼接在一起;這也就是為什麼下面的例子中優先順序和格式字串間沒有逗號。這有兩個printk的例子,一個是調
試資訊,一個是關鍵資訊:(代碼)在linux/kernel.h中定義了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根據一個“設施”和“優先順序”切分訊息;可以使用的
值定義在sys/syslog.h中。核心訊息根據相應printk中指定的優先順序記錄到LOG_KERN設施中。如果klogd沒有運行,資料將儲存在
迴圈緩衝區中直到有進程來讀取資料或資料溢出。如果你不希望因監視你的驅動程式的訊息而把你的系統記錄搞亂,你給klogd指定-f(檔案)選項或修改
/etc/syslog.conf將記錄寫到另一個檔案中。另一種方法是一種強硬方法:殺掉klogd,將訊息列印到不用的虛終端上*,或者在一個不用的
xterm上執行cat
/proc/kmesg顯示訊息。使用預先處理方便監視處理在驅動程式開發早期,printk可以對調試和測試新代碼都非常有協助。然而當你正式發行驅動程
序時,你應該去掉,或者至少關閉,這些列印語句。很不幸,你可能很快就發現,隨著你想不再需要那些訊息並去掉它們時,你可能又要加新功能,你又需要這些消
息了。解決這些問題有幾種方法�D�D如何從全域開啟和關閉訊息以及如何開啟和關閉個別訊息。下面給出了我處理訊息所用的大部分代碼,它有如下一些功能:
可以通過在宏名字加一個字母或去掉一個字母開啟或關閉每一條語句。通過在編譯前修改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檔案系統與任何
裝置都沒有關係�D�D/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定義在標頭檔asm/page.h中;它是與體繫結構相關的,但你至少可以它
有4KB大小)。如果你需要寫多於一個頁面的資料,你必須實現功能健全的檔案。注意,如果一個正在讀你的/proc檔案的進程發出了若干read調用,每
一個都擷取新資料,儘管只有少量資料被讀取,你的驅動程式每次都要重寫整個緩衝區。這些額外的工作會使系統效能下降,而且如果檔案產生的資料與下一次的不
同,以後的read調用要重新裝配不相關的部分,這一會造成資料錯位。事實上,由於每個使用C庫的應用程式都大塊地讀取資料,效能並不是什麼問題。然而,
由於錯位時有發生,它倒是一個值得考慮的問題。在擷取資料後,庫調用至少要調用1次read�D�D只有當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。但問題出在驅動程式時,通常只會導致產生故障的進程終止�D�D即那個使用驅動程式的進程。唯一不可恢複的損失就是當進程被終止時,
進程上下文分配的記憶體丟失了;例如,由驅動程式通過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�D�D版本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顯式地指定塊大小。使用ksymoopsoops訊息的最大問題就是十六進位數值對於
程式員來說沒什麼意義;需要將它們解析為符號。核心源碼通過其所包含的ksymoops工具協助開發人員�D�D但是注意,版本1.2的源碼中沒有這個程
序。該工具將oops訊息中的數值位址解析為核心符號,但只限於PC機產生的oops訊息。由於訊息本身就是處理器相關的,每一體繫結構都有其自身的訊息
格式。ksymoops從標準輸入獲得oops訊息,並從命令列核心符號表的名字。符號表通常就是/usr/src/linux/System.map。
程式以更可讀的方式列印調用軌跡和程式碼,而不是最原始的oops訊息。下面的片斷就是用上一節的oops訊息得出的結果:(代碼)由ksymoops
反組譯碼出的代碼給出了失效的指令和其後的指令。很明顯�D�D對於那些知道一點彙編的人�D�Drepz 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 #0;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輸出的開始部分#0;由
於在這個oops訊息中堆棧沒儲存什麼有用的東西,我不認為應該顯示整個堆棧軌跡:(代碼)當你調試“真正的”模組(faulty太短了,沒有什麼意義)
時,將寄存器和堆棧解碼是非常有益的,而且如果被調試的所有模組符號都開放出來時更有協助。在失效時,處理器寄存器一般不會指向模組的符號,只有當符號表
開放給/proc/ksyms時,你才能輸出中標別它們。我們可以用一下步驟製作一張更完整的符號表。首先,我們不應在模組中聲明靜態變數,否則我們就無
法用insmod開放它們了。第二,如下面的截取自scull的init_module函數的代碼所示,我們可以用#ifdef
SCULL_DEBUG或類似的宏屏蔽register_symtab調用。(代碼)我們在第2章“編寫和運行模組”的“註冊符號表”一節中已經看到了類
似內容,那裡說,如果模組不註冊符號表,所有的全域符號就都開放。儘管這一功能僅在SCULL_DEBUG被啟用時才有效,為了避免核心中的名字空間汙
染,所有的全域符號有合適的首碼(參見第2章的“模組與應用程式”一節)。使用klogdklogd守護進程的近期版本可以在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組合。處理系統掛起有兩
個選擇�D�D一個是防範與未然,另一個就是亡羊補牢,在發生掛起後調試代碼。通過在策略點上插入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也可以運行�D�D當然,除非中斷被屏蔽了,不過如果發生這種情況既不太可能也非常不幸。有時系統看起來象是掛起了,但其實不是。例如,如果
鍵盤因某種奇怪的原因被鎖住了就會發生這種情況。這種假掛起可以通過查看你為探明此種情況而啟動並執行程式輸出來判斷。我有一個程式會不斷地更新LED顯示器
上的時鐘,我發現這個對於驗證調度器尚在運行非常有用。你可以不必依賴外部裝置就可以檢查調度器,你可以實現一個程式讓鍵盤LED閃爍,或是不斷地開啟關
閉磁碟片馬達,或是不斷觸動擴音器�D�D不過我個人認為,通常的蜂鳴聲很煩人,應該盡量避免。看看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
伺服器管理檔案系統的一致性,而它又不會受你的驅動程式的影響,你可以避免任何的檔案系統崩潰。使用調試器最後一種調試模組的方法就是使用調試器來一步步
地跟蹤代碼,查看變數和機器寄存器的值。這種方法非常耗時,應該儘可能地避免。不過,某些情況下通過調試器對代碼進行細粒度的分析是非常有益的。在這裡,
我們所說的被調試的代碼運行在核心空間�D�D除非你遠端控制核心,否則不可能一步步跟蹤核心,這會使很多事情變得更加困難。由於遠端控制很少用到,我們
最後介紹這項技術。所幸的是,在目前的版本的核心中可以查看和修改變數。在這一級上熟練地使用調試器需要精通gdb命令,對彙編碼有一定瞭解,並且有能夠將
源碼與最佳化後的彙編碼對應起來的能力。不幸的是,gdb更適合與調試核心而不是模組,調試模組化的代碼需要更多的技術。這更多的技術就是kdebug包,
它利用gdb的“遠端偵錯”介面控制本地核心。我將在介紹普通調試器後介紹kdebug。使用gdbgdb在探究系統內部行為時非常有用。啟動調試器時必
須假想核心就是一個應用程式。除了指定核心檔案名稱外,你還應該在命令列中提供記憶體鏡象檔案的名字。典型的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選項編譯核心需要大量的磁碟空
間�D�D支援網路和很少幾個裝置和檔案系統的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
是一個非常好的程式。線上修改資料結構對於開發人員來說是一個非常大的進步(而且一種將系統掛起的最簡單方法)。現在有許多工具可以使你的開發工作更輕鬆
�D�D例如,在開發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.imgfile命令告訴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上了。儘管遠端偵錯可以用於調試模組,但你還是要載入模組,並且在模組上插入斷點前還需要觸發另一個斷
點,調試模組還是需要很多技巧的。我個人不會使用遠端偵錯去跟蹤模組,除非非同步啟動並執行代碼,如中斷處理函數,出了問題 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.