歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux內核

Linux內核調試器內幕

 原文地址:[url]http://www-900.ibm.com/developerWorks/cn/Linux/l-kdbug/index.sHtml[/url]
內容:
[size=18:6ddc15f4ad]入門
初始化並設置環境變量
激活 KDB
KDB 命令
技巧和訣竅
結束語
參考資料[/size:6ddc15f4ad]

[size=18:6ddc15f4ad]KDB 入門指南[/size:6ddc15f4ad]
Hariprasad Nellitheertha([email protected]
軟件工程師,IBM
2003 年 9 月
調試內核問題時,能夠跟蹤內核執行情況並查看其內存和數據結構是非常有用的。Linux 中的內置內核調試器 KDB 提供了這種功能。在本文中您將了解如何使用 KDB 所提供的功能,以及如何在 Linux 機器上安裝和設置 KDB。您還將熟悉 KDB 中可以使用的命令以及設置和顯示選項。
Linux 內核調試器(KDB)允許您調試 Linux 內核。這個恰如其名的工具實質上是內核代碼的補丁,它允許高手訪問內核內存和數據結構。KDB 的主要優點之一就是它不需要用另一台機器進行調試:您可以調試正在運行的內核。
設置一台用於 KDB 的機器需要花費一些工作,因為需要給內核打補丁並進行重新編譯。KDB 的用戶應當熟悉 Linux 內核的編譯(在一定程度上還要熟悉內核內部機理),但是如果您需要編譯內核方面的幫助,請參閱本文結尾處的參考資料一節。
在本文中,我們將從有關下載 KDB 補丁、打補丁、(重新)編譯內核以及啟動 KDB 方面的信息著手。然後我們將了解 KDB 命令並研究一些較常用的命令。最後,我們將研究一下有關設置和顯示選項方面的一些詳細信息。
入門
KDB 項目是由 Silicon Graphics 維護的(請參閱參考資料以獲取鏈接),您需要從它的 FTP 站點下載與內核版本有關的補丁。(在編寫本文時)可用的最新 KDB 版本是 4.2。您將需要下載並應用兩個補丁。一個是“公共的”補丁,包含了對通用內核代碼的更改,另一個是特定於體系結構的補丁。補丁可作為 bz2 文件獲取。例如,在運行 2.4.20 內核的 x86 機器上,您會需要 kdb-v4.2-2.4.20- common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。
這裡所提供的所有示例都是針對 i386 體系結構和 2.4.20 內核的。您將需要根據您的機器和內核版本進行適當的更改。您還需要擁有 root 許可權以執行這些操作。
將文件復制到 /usr/src/linux 目錄中並從用 bzip2 壓縮的文件解壓縮補丁文件:
[code:1:6ddc15f4ad]#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2

#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2 [/code:1:6ddc15f4ad]
您將獲得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。
現在,應用這些補丁:
[code:1:6ddc15f4ad]#patch -p1 <kdb-v4.2-2.4.20-common-1

#patch -p1 <kdb-v4.2-2.4.20-i386-1 [/code:1:6ddc15f4ad]
這些補丁應該干淨利落地加以應用。查找任何以 .rej 結尾的文件。這個擴展名表明這些是失敗的補丁。如果內核樹沒問題,那麼補丁的應用就不會有任何問題。
接下來,需要構建內核以支持 KDB。第一步是設置 CONFIG_KDB 選項。使用您喜歡的配置機制(xconfig 和 menUConfig 等)來完成這一步。轉到結尾處的“Kernel hacking”部分並選擇“Built- in Kernel Debugger support”選項。
您還可以根據自己的偏好選擇其它兩個選項。選擇“Compile the kernel with frame pointers”選項(如果有的話)則設置 CONFIG_FRAME_POINTER 標志。這將產生更好的堆棧回溯,因為幀指針寄存器被用作幀指針而不是通用寄存器。您還可以選擇“KDB off by default”選項。這將設置 CONFIG_KDB_OFF 標志,並且在缺省情況下將關閉 KDB。我們將在後面一節中對此進行詳細介紹。



保存配置,然後退出。重新編譯內核。建議在構建內核之前執行“make clean”。用常用方式安裝內核並引導它。
[size=18:6ddc15f4ad]初始化並設置環境變量[/size:6ddc15f4ad]
您可以定義將在 KDB 初始化期間執行的 KDB 命令。需要在純文本文件 kdb_cmds 中定義這些命令,該文件位於 Linux 源代碼樹(當然是在打了補丁之後)的 KDB 目錄中。該文件還可以用來定義設置顯示和打印選項的環境變量。文件開頭的注釋提供了編輯文件方面的幫助。使用這個文件的缺點是,在您更改了文件之後需要重新構建並重新安裝內核。
[size=18:6ddc15f4ad]激活 KDB[/size:6ddc15f4ad]
如果編譯期間沒有選中 CONFIG_KDB_OFF,那麼在缺省情況下 KDB 是活動的。否則,您需要顯式地激活它 - 通過在引導期間將 kdb=on 標志傳遞給內核或者通過在掛裝了 /proc 之後執行該工作:
[code:1:6ddc15f4ad]#echo "1" >/proc/sys/kernel/kdb [/code:1:6ddc15f4ad]
倒過來執行上述步驟則會取消激活 KDB。也就是說,如果缺省情況下 KDB 是打開的,那麼將 kdb=off 標志傳遞給內核或者執行下面這個操作將會取消激活 KDB:
[code:1:6ddc15f4ad]#echo "0" >/proc/sys/kernel/kdb[/code:1:6ddc15f4ad]
在引導期間還可以將另一個標志傳遞給內核。kdb=early 標志將導致在引導過程的初始階段就把控制權傳遞給 KDB。如果您需要在引導過程初始階段進行調試,那麼這將有所幫助。
調用 KDB 的方式有很多。如果 KDB 處於打開狀態,那麼只要內核中有緊急情況就自動調用它。按下鍵盤上的 PAUSE 鍵將手工調用 KDB。調用 KDB 的另一種方式是通過串行控制台。當然,要做到這一點,需要設置串行控制台(請參閱參考資料以獲取這方面的幫助)並且需要一個從串行控制台進行讀取的程序。按鍵序列 Ctrl-A 將從串行控制台調用 KDB。
[size=18:6ddc15f4ad]KDB 命令[/size:6ddc15f4ad]
KDB 是一個功能非常強大的工具,它允許進行幾個操作,比如內存和寄存器修改、應用斷點和堆棧跟蹤。根據這些,可以將 KDB 命令分成幾個類別。下面是有關每一類中最常用命令的詳細信息。
[size=18:6ddc15f4ad]內存顯示和修改[/size:6ddc15f4ad]
這一類別中最常用的命令是 md、mdr、mm 和 mmW。
[size=18:6ddc15f4ad]md[/size:6ddc15f4ad] 命令以一個地址/符號和行計數為參數,顯示從該地址開始的 line-count 行的內存。如果沒有指定 line-count,那麼就使用環境變量所指定的缺省值。如果沒有指定地址,那麼 md 就從上一次打印的地址繼續。地址打印在開頭,字符轉換打印在結尾。
[size=18:6ddc15f4ad]mdr[/size:6ddc15f4ad] 命令帶有地址/符號以及字節計數,顯示從指定的地址開始的 byte-count 字節數的初始內存內容。它本質上和 md 一樣,但是它不顯示起始地址並且不在結尾顯示字符轉換。mdr 命令較少使用。
[size=18:6ddc15f4ad]mm[/size:6ddc15f4ad] 命令修改內存內容。它以地址/符號和新內容作為參數,用 new-contents 替換地址處的內容。
[size=18:6ddc15f4ad]mmW[/size:6ddc15f4ad] 命令更改從地址開始的 W 個字節。請注意,mm 更改一個機器字。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]顯示從 0xc000000 開始的 15 行內存:
[0]kdb> md 0xc000000 15
將內存位置為 0xc000000 上的內容更改為 0x10:
[0]kdb> mm 0xc000000 0x10 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]寄存器顯示和修改[/size:6ddc15f4ad]
這一類別中的命令有 rd、rm 和 ef。
[size=18:6ddc15f4ad]rd[/size:6ddc15f4ad] 命令(不帶任何參數)顯示處理器寄存器的內容。它可以有選擇地帶三個參數。如果傳遞了 c 參數,則 rd 顯示處理器的控制寄存器;如果帶有 d 參數,那麼它就顯示調試寄存器;如果帶有 u 參數,則顯示上一次進入內核的當前任務的寄存器組。
[size=18:6ddc15f4ad]rm[/size:6ddc15f4ad] 命令修改寄存器的內容。它以寄存器名稱和 new- contents 作為參數,用 new-contents 修改寄存器。寄存器名稱與特定的體系結構有關。目前,不能修改控制寄存器。
[size=18:6ddc15f4ad]ef[/size:6ddc15f4ad] 命令以一個地址作為參數,它顯示指定地址處的異常幀。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
顯示通用寄存器組:
[code:1:6ddc15f4ad][0]kdb> rd
將寄存器 ebx 的內容設置成 0x25:
[0]kdb> rm %ebx 0x25 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]斷點[/size:6ddc15f4ad]
常用的斷點命令有 bp、bc、bd、be 和 bl。
[size=18:6ddc15f4ad]bp[/size:6ddc15f4ad] 命令以一個地址/符號作為參數,它在地址處應用斷點。當遇到該斷點時則停止執行並將控制權交予 KDB。該命令有幾個有用的變體。[size=18:6ddc15f4ad]bpa[/size: 6ddc15f4ad] 命令對 SMP 系統中的所有處理器應用斷點。bph 命令強制在支持硬件寄存器的系統上使用它。bpha 命令類似於 bpa 命令,差別在於它強制使用硬件寄存器。
[size=18:6ddc15f4ad]bd[/size:6ddc15f4ad] 命令禁用特殊斷點。它接收斷點號作為參數。該命令不是從斷點表中除去斷點,而只是禁用它。斷點號從 0 開始,根據可用性順序分配給斷點。
[size=18:6ddc15f4ad]be[/size:6ddc15f4ad] 命令啟用斷點。該命令的參數也是斷點號。


[size=18:6ddc15f4ad]bl [/size:6ddc15f4ad]命令列出當前的斷點集。它包含了啟用的和禁用的斷點。
[size=18:6ddc15f4ad]bc[/size:6ddc15f4ad] 命令從斷點表中除去斷點。它以具體的斷點號或 * 作為參數,在後一種情況下它將除去所有斷點。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]對函數 sys_write() 設置斷點:
[0]kdb> bp sys_write
列出斷點表中的所有斷點:
[0]kdb> bl
清除斷點號 1:
[0]kdb> bc 1[/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]堆棧跟蹤[/size:6ddc15f4ad]
主要的堆棧跟蹤命令有 BT、btp、btc 和 bta。
[size=18:6ddc15f4ad]bt [/size:6ddc15f4ad]命令設法提供有關當前線程的堆棧的信息。它可以有選擇地將堆棧幀地址作為參數。如果沒有提供地址,那麼它采用當前寄存器來回溯堆棧。否則,它假定所提供的地址是有效的堆棧幀起始地址並設法進行回溯。如果內核編譯期間設置了 CONFIG_FRAME_POINTER 選項,那麼就用幀指針寄存器來維護堆棧,從而就可以正確地執行堆棧回溯。如果沒有設置 CONFIG_FRAME_POINTER,那麼 bt 命令可能會產生錯誤的結果。
[size=18:6ddc15f4ad]btp[/size:6ddc15f4ad] 命令將進程標識作為參數,並對這個特定進程進行堆棧回溯。
[size=18:6ddc15f4ad]btc[/size:6ddc15f4ad] 命令對每個活動 CPU 上正在運行的進程執行堆棧回溯。它從第一個活動 CPU 開始執行 bt,然後切換到下一個活動 CPU,以此類推。
[size=18:6ddc15f4ad]bta[/size:6ddc15f4ad] 命令對處於某種特定狀態的所有進程執行回溯。若不帶任何參數,它就對所有進程執行回溯。可以有選擇地將各種參數傳遞給該命令。將根據參數處理處於特定狀態的進程。選項以及相應的狀態如下:
?D:不可中斷狀態
?R:正運行
?S:可中斷休眠
?T:已跟蹤或已停止
?Z:僵死
?U:不可運行
這類命令中的每一個都會打印出一大堆信息。請查閱下面的參考資料以獲取這些字段的詳細文檔。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]跟蹤當前活動線程的堆棧:
[0]kdb> bt
跟蹤標識為 575 的進程的堆棧:
[0]kdb> btp 575 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]其它命令[/size:6ddc15f4ad]
下面是在內核調試過程中非常有用的其它幾個 KDB 命令。
[size=18:6ddc15f4ad]id [/size:6ddc15f4ad]命令以一個地址/符號作為參數,它對從該地址開始的指令進行反匯編。環境變量 IDCOUNT 確定要顯示多少行輸出。
[size=18:6ddc15f4ad]ss [/size:6ddc15f4ad]命令單步執行指令然後將控制返回給 KDB。該指令的一個變體是 ssb,它執行從當前指令指針地址開始的指令(在屏幕上打印指令),直到它遇到將引起分支轉移的指令為止。分支轉移指令的典型示例有 call、 return 和 jump。
[size=18:6ddc15f4ad]go[/size:6ddc15f4ad] 命令讓系統繼續正常執行。一直執行到遇到斷點為止(如果已應用了一個斷點的話)。
[size=18:6ddc15f4ad]reboot [/size:6ddc15f4ad]命令立刻重新引導系統。它並沒有徹底關閉系統,因此結果是不可預測的。
[size=18:6ddc15f4ad]ll[/size:6ddc15f4ad] 命令以地址、偏移量和另一個 KDB 命令作為參數。它對鏈表中的每個元素反復執行作為參數的這個命令。所執行的命令以列表中當前元素的地址作為參數。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]反匯編從例程 schedule 開始的指令。所顯示的行數取決於環境變量 IDCOUNT:
[0]kdb> id schedule
執行指令直到它遇到分支轉移條件(在本例中為指令 jne)為止:
[0]kdb> ssb

0xc0105355 default_idle+0x25: cli
0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax
0xc0105359 default_idle+0x29: test %eax, %eax
0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]技巧和訣竅[/size:6ddc15f4ad]
調試一個問題涉及到:使用調試器(或任何其它工具)找到問題的根源以及使用源代碼來跟蹤導致問題的根源。單單使用源代碼來確定問題是極其困難的,只有老練的內核黑客才有可能做得到。相反,大多數的新手往往要過多地依靠調試器來修正錯誤。這種方法可能會產生不正確的問題解決方案。我們擔心的是這種方法只會修正表面症狀而不能解決真正的問題。此類錯誤的典型示例是添加錯誤處理代碼以處理 NULL 指針或錯誤的引用,卻沒有查出無效引用的真正原因。
結合研究代碼和使用調試工具這兩種方法是識別和修正問題的最佳方案。
調試器的主要用途是找到錯誤的位置、確認症狀(在某些情況下還有起因)、確定變量的值,以及確定程序是如何出現這種情況的(即,建立調用堆棧)。有經驗的黑客會知道對於某種特定的問題應使用哪一個調試器,並且能迅速地根據調試獲取必要的信息,然後繼續分析代碼以識別起因。
因此,這裡為您介紹了一些技巧,以便您能使用 KDB 快速地取得上述結果。當然,要記住,調試的速度和精確度來自經驗、實踐和良好的系統知識(硬件和內核內部機理等)。


[size=18:6ddc15f4ad]技巧 #1[/size:6ddc15f4ad]
在 KDB 中,在提示處輸入地址將返回與之最為匹配的符號。這在堆棧分析以及確定全局數據的地址/值和函數地址方面極其有用。同樣,輸入符號名則返回其虛擬地址。
示例
[code:1:6ddc15f4ad]表明函數 sys_read 從地址 0xc013db4c 開始:
[0]kdb> 0xc013db4c

0xc013db4c = 0xc013db4c (sys_read)
同樣,
同樣,表明 sys_write 位於地址 0xc013dcc8:
[0]kdb> sys_write

sys_write = 0xc013dcc8 (sys_write)
這些有助於在分析堆棧時找到全局數據和函數地址。[/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]技巧 #2[/size:6ddc15f4ad]在編譯帶 KDB 的內核時,只要 CONFIG_FRAME_POINTER 選項出現就使用該選項。為此,需要在配置內核時選擇“Kernel hacking”部分下面的 “Compile the kernel with frame pointers”選項。這確保了幀指針寄存器將被用作幀指針,從而產生正確的回溯。實際上,您可以手工轉儲幀指針寄存器的內容並跟蹤整個堆棧。例如,在 i386 機器上,%ebp 寄存器可以用來回溯整個堆棧。
例如,在函數 rmqueue() 上執行第一個指令後,堆棧看上去類似於下面這樣:
[code:1:6ddc15f4ad][0]kdb> md %ebp

0xc74c9f38 c74c9f60 c0136c40 000001f0 00000000
0xc74c9f48 08053328 c0425238 c04253a8 00000000
0xc74c9f58 000001f0 00000246 c74c9f6c c0136a25
0xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc
0xc74c9f78 c014fe45 c74c8000 00000000 08053328

[0]kdb> 0xc0136c40

0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)

[0]kdb> 0xc0136a25

0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)

[0]kdb> 0xc0136d6d

0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)[/code:1:6ddc15f4ad]
我們可以看到 rmqueue() 被 __alloc_pages 調用,後者接下來又被 _alloc_pages 調用,以此類推。
每一幀的第一個雙字(double Word)指向下一幀,這後面緊跟著調用函數的地址。因此,跟蹤堆棧就變成一件輕松的工作了。
[size=18:6ddc15f4ad]技巧 #3[/size:6ddc15f4ad]
go 命令可以有選擇地以一個地址作為參數。如果您想在某個特定地址處繼續執行,則可以提供該地址作為參數。另一個辦法是使用 rm 命令修改指令指針寄存器,然後只要輸入 go。如果您想跳過似乎會引起問題的某個特定指令或一組指令,這就會很有用。但是,請注意,該指令使用不慎會造成嚴重的問題,系統可能會嚴重崩潰。
[size=18:6ddc15f4ad]技巧 #4[/size:6ddc15f4ad]
您可以利用一個名為 defcmd 的有用命令來定義自己的命令集。例如,每當遇到斷點時,您可能希望能同時檢查某個特殊變量、檢查某些寄存器的內容並轉儲堆棧。通常,您必須要輸入一系列命令,以便能同時執行所有這些工作。defcmd 允許您定義自己的命令,該命令可以包含一個或多個預定義的 KDB 命令。然後只需要用一個命令就可以完成所有這三項工作。其語法如下:
[code:1:6ddc15f4ad][0]kdb> defcmd name "usage" "help"

[0]kdb> [defcmd] type the commands here

[0]kdb> [defcmd] endefcmd [/code:1:6ddc15f4ad]
例如,可以定義一個(簡單的)新命令 hari,它顯示從地址 0xc000000 開始的一行內存、顯示寄存器的內容並轉儲堆棧:
[code:1:6ddc15f4ad][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 [/code:1:6ddc15f4ad]
該命令的輸出會是:
[code:1:6ddc15f4ad][0]kdb> hari

[hari]kdb> md 0xc000000 1

0xc000000 00000001 f000e816 f000e2c3 f000e816

[hari]kdb> rd

eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
....
...

[hari]kdb> md %ebp 1

0xc0467fbc c0467fd0 c01053d2 00000002 000a0200

[0]kdb> [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]技巧 #5[/size:6ddc15f4ad]
可以使用 bph 和 bpha 命令(假如體系結構支持使用硬件寄存器)來應用讀寫斷點。這意味著每當從某個特定地址讀取數據或將數據寫入該地址時,我們都可以對此進行控制。當調試數據/內存毀壞問題時這可能會極其方便,在這種情況中您可以用它來識別毀壞的代碼/進程。


示例
[code:1:6ddc15f4ad]每當將四個字節寫入地址 0xc0204060 時就進入內核調試器:
[0]kdb> bph 0xc0204060 dataw 4
在讀取從 0xc000000 開始的至少兩個字節的數據時進入內核調試器:
[0]kdb> bph 0xc000000 datar 2[/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]結束語[/size:6ddc15f4ad]
對於執行內核調試,KDB 是一個方便的且功能強大的工具。它提供了各種選項,並且使我們能夠分析內存內容和數據結構。最妙的是,它不需要用另一台機器來執行調試。
[size=18:6ddc15f4ad]參考資料[/size:6ddc15f4ad]
?請在 Documentation/kdb 目錄中查找 KDB 手冊頁。
?有關設置串行控制台的信息,請查找 Documentation 目錄中的 serial-console.txt。
?請在 SGI 的內核調試器項目網站上下載 KDB。
?有關幾個基於方案的 Linux 調試技術的概述,請閱讀“掌握 Linux 調試技術”(developerWorks,2002 年 8 月)。
?教程“編譯 Linux 內核”(developerWorks,2000 年 8 月)讓您完整地了解配置、編譯和安裝內核的過程。
?IBM AIX 用戶可以在 KDB Kernel Debugger and Command 頁面上獲取有關用於 AIX 的 KDB 的使用幫助。
?那些尋求有關調試 OS/2 信息的讀者應該閱讀 IBM 紅皮書 The OS/2 Debugging Handbook(共四卷)的第 II 卷。
?在 developerWorks Linux 專區中查找更多針對 Linux 開發人員的參考資料。

【發表回復】【查看CU論壇原帖】【關閉】
zhchhui 回復於:2003-09-15 10:38:56
掌握 Linux 調試技術
內容:
常見調試方法
第 1 種情況:內存調試工具
MEMWATCH
YAMD
Electric Fence
第 2 種情況:使用 strace
第 3 種情況:使用 gdb 和 Oops
kgdb
Oops 分析
kdb
第 4 種情況:使用魔術鍵控順序獲取反跟蹤
結束語

zhchhui 回復於:2003-09-15 10:42:18
在 Linux 上找出並解決程序錯誤的主要方法
Steve Best([email protected]
JFS 核心小組成員,IBM
2002 年 8 月
您可以用各種方法來監控運行著的用戶空間程序:可以為其運行調試器並單步調試該程序,添加打印語句,或者添加工具來分析程序。本文描述了幾種可以用來調試在 Linux 上運行的程序的方法。我們將回顧四種調試問題的情況,這些問題包括段錯誤,內存溢出和洩漏,還有掛起。
本文討論了四種調試 Linux 程序的情況。在第 1 種情況中,我們使用了兩個有內存分配問題的樣本程序,使用 MEMWATCH 和 Yet Another Malloc Debugger(YAMD)工具來調試它們。在第 2 種情況中,我們使用了 Linux 中的 strace 實用程序,它能夠跟蹤系統調用和信號,從而找出程序發生錯誤的地方。在第 3 種情況中,我們使用 Linux 內核的 Oops 功能來解決程序的段錯誤,並向您展示如何設置內核源代碼級調試器(kernel source level debugger,kgdb),以使用 GNU 調試器(GNU debugger,gdb)來解決相同的問題;kgdb 程序是使用串行連接的 Linux 內核遠程 gdb。在第 4 種情況中,我們使用 Linux 上提供的魔術鍵控順序(magic key sequence)來顯示引發掛起問題的組件的信息。
[size=18:b0b26de3a8][b:b0b26de3a8]常見調試方法[/b:b0b26de3a8][/size:b0b26de3a8]
當您的程序中包含錯誤時,很可能在代碼中某處有一個條件,您認為它為真(true),但實際上是假(false)。找出錯誤的過程也就是在找出錯誤後推翻以前一直確信為真的某個條件過程。
以下幾個示例是您可能確信成立的條件的一些類型:
?在源代碼中的某處,某變量有特定的值。
?在給定的地方,某個結構已被正確設置。
?對於給定的 if-then-else 語句,if 部分就是被執行的路徑。
?當子例程被調用時,該例程正確地接收到了它的參數。
找出錯誤也就是要確定上述所有情況是否存在。如果您確信在子例程被調用時某變量應該有特定的值,那麼就檢查一下情況是否如此。如果您相信 if 結構會被執行,那麼也檢查一下情況是否如此。通常,您的假設都會是正確的,但最終您會找到與假設不符的情況。結果,您就會找出發生錯誤的地方。
調試是您無法逃避的任務。進行調試有很多種方法,比如將消息打印到屏幕上、使用調試器,或只是考慮程序執行的情況並仔細地揣摩問題所在。
在修正問題之前,您必須找出它的源頭。舉例來說,對於段錯誤,您需要了解段錯誤發生在代碼的哪一行。一旦您發現了代碼中出錯的行,請確定該方法中變量的值、方法被調用的方式以及關於錯誤如何發生的詳細情況。使用調試器將使找出所有這些信息變得很簡單。如果沒有調試器可用,您還可以使用其它的工具。(請注意,產品環境中可能並不提供調試器,而且 Linux 內核沒有內建的調試器。)
[size=18:b0b26de3a8][b:b0b26de3a8]實用的內存和內核工具[/b:b0b26de3a8][/size:b0b26de3a8]
您可以使用 Linux 上的調試工具,通過各種方式跟蹤用戶空間和內核問題。請使用下面的工具和技術來構建和調試您的源代碼:
[size=18:b0b26de3a8][b:b0b26de3a8]用戶空間工具[/b:b0b26de3a8][/size:b0b26de3a8]:
?內存工具:MEMWATCH 和 YAMD
?strace
?GNU 調試器(gdb)
?魔術鍵控順序
[size=18:b0b26de3a8][b:b0b26de3a8]內核工具[/b:b0b26de3a8][/size:b0b26de3a8]:
?內核源代碼級調試器(kgdb)
?內建內核調試器(kdb)
?Oops
本文將討論一類通過人工檢查代碼不容易找到的問題,而且此類問題只在很少見的情況下存在。內存錯誤通常在多種情況同時存在時出現,而且您有時只能在部署程序之後才能發現內存錯誤。

zhchhui 回復於:2003-09-15 10:46:42


[size=18:ff78191c7b][b] 第 1 種情況:內存調試工具[/b[/size:ff78191c7b]]
C 語言作為 Linux 系統上標准的編程語言給予了我們對動態內存分配很大的控制權。然而,這種自由可能會導致嚴重的內存管理問題,而這些問題可能導致程序崩潰或隨時間的推移導致性能降級。
內存洩漏(即 malloc() 內存在對應的 free() 調用執行後永不被釋放)和緩沖區溢出(例如對以前分配到某數組的內存進行寫操作)是一些常見的問題,它們可能很難檢測到。這一部分將討論幾個調試工具,它們極大地簡化了檢測和找出內存問題的過程。
[color=blue:ff78191c7b]MEMWATCH[/color:ff78191c7b]
MEMWATCH 由 Johan Lindh 編寫,是一個開放源代碼 C 語言內存錯誤檢測工具,您可以自己下載它(請參閱本文後面部分的參考資料)。只要在代碼中添加一個頭文件並在 gcc 語句中定義了 MEMWATCH 之後,您就可以跟蹤程序中的內存洩漏和錯誤了。MEMWATCH 支持 ANSI C,它提供結果日志紀錄,能檢測雙重釋放(double-free)、錯誤釋放(erroneous free)、沒有釋放的內存(unfreed memory)、溢出和下溢等等。
清單 1. 內存樣本(test1.c)

[code:1:ff78191c7b]#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"

int main(void)
{
char *ptr1;
char *ptr2;

ptr1 = malloc(512);
ptr2 = malloc(512);

ptr2 = ptr1;
free(ptr2);
free(ptr1);
}[/code:1:ff78191c7b]
清單 1 中的代碼將分配兩個 512 字節的內存塊,然後指向第一個內存塊的指針被設定為指向第二個內存塊。結果,第二個內存塊的地址丟失,從而產生了內存洩漏。
現在我們編譯清單 1 的 memwatch.c。下面是一個 makefile 示例:
test1

[code:1:ff78191c7b]gcc -DMEMWATCH -DMW_STDIO test1.c memwatch c -o test1[/code:1:ff78191c7b]
當您運行 test1 程序後,它會生成一個關於洩漏的內存的報告。清單 2 展示了示例 memwatch.log 輸出文件。
清單 2. test1 memwatch.log 文件

[code:1:ff78191c7b]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[/code:1:ff78191c7b]
MEMWATCH 為您顯示真正導致問題的行。如果您釋放一個已經釋放過的指針,它會告訴您。對於沒有釋放的內存也一樣。日志結尾部分顯示統計信息,包括洩漏了多少內存,使用了多少內存,以及總共分配了多少內存。
[color=blue:ff78191c7b]YAMD[/color:ff78191c7b]
YAMD 軟件包由 Nate Eldredge 編寫,可以查找 C 和 C++ 中動態的、與內存分配有關的問題。在撰寫本文時,YAMD 的最新版本為 0.32。請下載 yamd-0.32.tar.gz(請參閱參考資料)。執行 make 命令來構建程序;然後執行 make install 命令安裝程序並設置工具。
一旦您下載了 YAMD 之後,請在 test1.c 上使用它。請刪除 #include memwatch.h 並對 makefile 進行如下小小的修改:
使用 YAMD 的 test1

gcc -g test1.c -o test1
清單 3 展示了來自 test1 上的 YAMD 的輸出。
清單 3. 使用 YAMD 的 test1 輸出

[code:1:ff78191c7b]YAMD version 0.32
Executable: /usr/src/test/yamd-0.32/test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes

*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation


Max bytes allocated at one time: 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.[/code:1:ff78191c7b]
YAMD 顯示我們已經釋放了內存,而且存在內存洩漏。讓我們在清單 4 中另一個樣本程序上試試 YAMD。
清單 4. 內存代碼(test2.c)

[code:1:ff78191c7b]#include <stdlib.h>
#include <stdio.h>

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);
}[/code:1:ff78191c7b]
您可以使用下面的命令來啟動 YAMD:
[code:1:ff78191c7b]./run-yamd /usr/src/test/test2/test2 [/code:1:ff78191c7b]
清單 5 顯示了在樣本程序 test2 上使用 YAMD 得到的輸出。YAMD 告訴我們在 for 循環中有“越界(out-of-bounds)”的情況。
清單 5. 使用 YAMD 的 test2 輸出

[code:1:ff78191c7b]Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out.1243
*********
./run-yamd: line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems 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.[/code:1:ff78191c7b]
MEMWATCH 和 YAMD 都是很有用的調試工具,它們的使用方法有所不同。對於 MEMWATCH,您需要添加包含文件 memwatch.h 並打開兩個編譯時間標記。對於鏈接(link)語句,YAMD 只需要 -g 選項。
[color=blue:ff78191c7b]Electric Fence[/color:ff78191c7b]
多數 Linux 分發版包含一個 Electric Fence 包,不過您也可以選擇下載它。Electric Fence 是一個由 Bruce Perens 編寫的 malloc() 調試庫。它就在您分配內存後分配受保護的內存。如果存在 fencepost 錯誤(超過數組末尾運行),程序就會產生保護錯誤,並立即結束。通過結合 Electric Fence 和 gdb,您可以精確地跟蹤到哪一行試圖訪問受保護內存。 Electric Fence 的另一個功能就是能夠檢測內存洩漏。

zhchhui 回復於:2003-09-15 10:49:18
[b:95b8e28830] [size=18:95b8e28830]第 2 種情況:使用 strace[/size:95b8e28830][/b:95b8e28830]
strace 命令是一種強大的工具,它能夠顯示所有由用戶空間程序發出的系統調用。strace 顯示這些調用的參數並返回符號形式的值。 strace 從內核接收信息,而且不需要以任何特殊的方式來構建內核。將跟蹤信息發送到應用程序及內核開發者都很有用。在清單 6 中,分區的一種格式有錯誤,清單顯示了 strace 的開頭部分,內容是關於調出創建文件系統操作(mkfs)的。strace 確定哪個調用導致問題出現。
清單 6. mkfs 上 strace 的開頭部分

[code:1:95b8e28830]execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &
...
open("/dev/test1", O_RDWRO_LARGEFILE) = 4
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
ioctl(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 )
= 98
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
open("/dev/test1", O_RDONLYO_LARGEFILE) = 5
ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)


write(2, "mkfs.jfs: can't determine device"..., ..._exit(1)
= ?[/code:1:95b8e28830]
清單 6 顯示 ioctl 調用導致用來格式化分區的 mkfs 程序失敗。ioctl BLKGETSIZE64 失敗。(BLKGET- SIZE64 在調用 ioctl 的源代碼中定義。) BLKGETSIZE64 ioctl 將被添加到 Linux 中所有的設備,而在這裡,邏輯卷管理器還不支持它。因此,如果 BLKGETSIZE64 ioctl 調用失敗,mkfs 代碼將改為調用較早的 ioctl 調用;這使得 mkfs 適用於邏輯卷管理器。

zhchhui 回復於:2003-09-15 10:57:11
[b:627becdd94][size=18:627becdd94] 第 3 種情況:使用 gdb 和 Oops[/size:627becdd94][/b:627becdd94]
您可以從命令行使用 gdb 程序(Free Software Foundation 的調試器)來找出錯誤,也可以從諸如 Data Display Debugger(DDD)這樣的幾個圖形工具之一使用 gdb 程序來找出錯誤。您可以使用 gdb 來調試用戶空間程序或 Linux 內核。這一部分只討論從命令行運行 gdb 的情況。
使用 gdb program name 命令啟動 gdb。gdb 將載入可執行程序符號並顯示輸入提示符,讓您可以開始使用調試器。您可以通過三種方式用 gdb 查看進程:
?使用 attach 命令開始查看一個已經運行的進程;attach 將停止進程。
?使用 run 命令執行程序並從頭開始調試程序。
?查看已有的核心文件來確定進程終止時的狀態。要查看核心文件,請用下面的命令啟動 gdb。
gdb programname corefilename
要用核心文件進行調試,您不僅需要程序的可執行文件和源文件,還需要核心文件本身。要用核心文件啟動 gdb,請使用 -c 選項:
gdb -c core programname
gdb 顯示哪行代碼導致程序發生核心轉儲。
在運行程序或連接到已經運行的程序之前,請列出您覺得有錯誤的源代碼,設置斷點,然後開始調試程序。您可以使用 help 命令查看全面的 gdb 在線幫助和詳細的教程。
[color=blue:627becdd94]kgdb[/color:627becdd94]
kgdb 程序(使用 gdb 的遠程主機 Linux 內核調試器)提供了一種使用 gdb 調試 Linux 內核的機制。kgdb 程序是內核的擴展,它讓您能夠在遠程主機上運行 gdb 時連接到運行用 kgdb 擴展的內核機器。您可以接著深入到內核中、設置斷點、檢查數據並進行其它操作(類似於您在應用程序上使用 gdb 的方式)。這個補丁的主要特點之一就是運行 gdb 的主機在引導過程中連接到目標機器(運行要被調試的內核)。這讓您能夠盡早開始調試。請注意,補丁為 Linux 內核添加了功能,所以 gdb 可以用來調試 Linux 內核。
使用 kgdb 需要兩台機器:一台是開發機器,另一台是測試機器。一條串行線(空調制解調器電纜)將通過機器的串口連接它們。您希望調試的內核在測試機器上運行;gdb 在開發機器上運行。gdb 使用串行線與您要調試的內核通信。
請遵循下面的步驟來設置 kgdb 調試環境:
1.下載您的 Linux 內核版本適用的補丁。
2.將組件構建到內核,因為這是使用 kgdb 最簡單的方法。(請注意,有兩種方法可以構建多數內核組件,比如作為模塊或直接構建到內核中。舉例來說,日志紀錄文件系統(Journaled File System,JFS)可以作為模塊構建,或直接構建到內核中。通過使用 gdb 補丁,我們就可以將 JFS 直接構建到內核中。)
3.應用內核補丁並重新構建內核。
4.創建一個名為 .gdbinit 的文件,並將其保存在內核源文件子目錄中(換句話說就是 /usr/src/linux)。文件 .gdbinit 中有下面四行代碼:
[code:1:627becdd94]oset remotebaud 115200
osymbol-file vmlinux
otarget remote /dev/ttyS0
oset output-radix 16 [/code:1:627becdd94]

5.將 append=gdb 這一行添加到 lilo,lilo 是用來在引導內核時選擇使用哪個內核的引導載入程序。
[code:1:627becdd94]oimage=/boot/bzImage-2.4.17
olabel=gdb2417
oread-only
oroot=/dev/sda8
oappend="gdb gdbttyS=1 gdb-baud=115200 nmi_watchdog=0" [/code:1:627becdd94]
清單 7 是一個腳本示例,它將您在開發機器上構建的內核和模塊引入測試機器。您需要修改下面幾項:
?best@sfb:用戶標識和機器名。
?/usr/src/linux-2.4.17:內核源代碼樹的目錄。
?bzImage-2.4.17:測試機器上將引導的內核名。
?rcp 和 rsync:必須允許它在構建內核的機器上運行。
清單 7. 引入測試機器的內核和模塊的腳本

[code:1:627becdd94]set -x
rcp best@sfb: /usr/src/linux-2.4.17/arch/i386/boot/bzImage /boot/bzImage-2.4.17
rcp best@sfb:/usr/src/linux-2.4.17/System.map /boot/System.map-2.4.17
rm -rf /lib/modules/2.4.17
rsync -a best@sfb:/lib/modules/2.4.17 /lib/modules
chown -R root /lib/modules/2.4.17
lilo[/code:1:627becdd94]
現在我們可以通過改為使用內核源代碼樹開始的目錄來啟動開發機器上的 gdb 程序了。在本示例中,內核源代碼樹位於 /usr/src/linux-2.4.17。輸入 gdb 啟動程序。
如果一切正常,測試機器將在啟動過程中停止。輸入 gdb 命令 cont 以繼續啟動過程。一個常見的問題是,空調制解調器電纜可能會被連接到錯誤的串口。如果 gdb 不啟動,將端口改為第二個串口,這會使 gdb 啟動。
[color=darkblue:627becdd94]使用 kgdb 調試內核問題[/color:627becdd94]


清單 8 列出了 jfs_mount.c 文件的源代碼中被修改過的代碼,我們在代碼中創建了一個空指針異常,從而使代碼在第 109 行產生段錯誤。
清單 8. 修改過後的 jfs_mount.c 代碼

[code:1:627becdd94]int jfs_mount(struct super_block *sb)
{
...
int ptr; /* line 1 added */
jFYI(1, ("
Mount JFS
"));
/ *
* read/validate superblock
* (initialize mount inode from the superblock)
* /
if ((rc = chkSuper(sb))) {
goto errout20;
}
108 ptr=0; /* line 2 added */
109 printk("%d
",*ptr); /* line 3 added */[/code:1:627becdd94]
清單 9 在向文件系統發出 mount 命令之後顯示一個 gdb 異常。kgdb 提供了幾條命令,如顯示數據結構和變量值以及顯示系統中的所有任務處於什麼狀態、它們駐留在何處、它們在哪些地方使用了 CPU 等等。清單 9 將顯示回溯跟蹤為該問題提供的信息;where 命令用來執行反跟蹤,它將告訴被執行的調用在代碼中的什麼地方停止。
清單 9. gdb 異常和反跟蹤

[code:1:627becdd94]mount -t jfs /dev/sdb /jfs

Program received signal SIGSEGV, Segmentation fault.
jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
109 printk("%d
",*ptr);
(gdb)where
#0 jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
#1 0xc01a0dbb in jfs_read_super ... at super.c:280
#2 0xc0149ff5 in get_sb_bdev ... at super.c:620
#3 0xc014a89f in do_kern_mount ... at super.c:849
#4 0xc0160e66 in do_add_mount ... at namespace.c:569
#5 0xc01610f4 in do_mount ... at namespace.c:683
#6 0xc01611ea in sys_mount ... at namespace.c:716
#7 0xc01074a7 in system_call () at af_packet.c:1891
#8 0x0 in ?? ()
(gdb)[/code:1:627becdd94]
下一部分還將討論這個相同的 JFS 段錯誤問題,但不設置調試器,如果您在非 kgdb 內核環境中執行清單 8 中的代碼,那麼它使用內核可能生成的 Oops 消息。
[color=darkblue:627becdd94]Oops 分析[/color:627becdd94]
Oops(也稱 panic,慌張)消息包含系統錯誤的細節,如 CPU 寄存器的內容。在 Linux 中,調試系統崩潰的傳統方法是分析在發生崩潰時發送到系統控制台的 Oops 消息。一旦您掌握了細節,就可以將消息發送到 ksymoops 實用程序,它將試圖將代碼轉換為指令並將堆棧值映射到內核符號。在很多情況下,這些信息就足夠您確定錯誤的可能原因是什麼了。請注意,Oops 消息並不包括核心文件。
讓我們假設系統剛剛創建了一條 Oops 消息。作為編寫代碼的人,您希望解決問題並確定什麼導致了 Oops 消息的產生,或者您希望向顯示了 Oops 消息的代碼的開發者提供有關您的問題的大部分信息,從而及時地解決問題。Oops 消息是等式的一部分,但如果不通過 ksymoops 程序運行它也於事無補。下面的圖顯示了格式化 Oops 消息的過程。
[color=darkblue:627becdd94]格式化 Oops 消息[/color:627becdd94]見附圖

ksymoops 需要幾項內容:Oops 消息輸出、來自正在運行的內核的 System.map 文件,還有 /proc/ksyms、 vmlinux 和 /proc/modules。關於如何使用 ksymoops,內核源代碼 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊頁上有完整的說明可以參考。Ksymoops 反匯編代碼部分,指出發生錯誤的指令,並顯示一個跟蹤部分表明代碼如何被調用。
首先,將 Oops 消息保存在一個文件中以便通過 ksymoops 實用程序運行它。清單 10 顯示了由安裝 JFS 文件系統的 mount 命令創建的 Oops 消息,問題是由清單 8 中添加到 JFS 安裝代碼的那三行代碼產生的。
清單 10. ksymoops 處理後的 Oops 消息

[code:1:627becdd94]ksymoops 2.4.0 on i686 2.4.17. Options used
... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference at
virtual 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: [<c0155d4f>]...
... 15:59:37 sfb1 kernel: [<c0106e04 ...


Copyright © Linux教程網 All Rights Reserved