Linux kernel的調試技術


內核中的調試支持

內核開發者在內核中建立了很多調試功能。但是這些調試功能會造成額外的輸出,並且導致性能下降,因此發行版廠商通常會禁止發行版內核中的這些功能。但是作為一名內核開發者,調試需求具有更高優先級,從而樂意接受額外的調試支持而帶來的性能損失。

這里列出內核開發的幾個配置選項,除特殊指出,所有這些選項均出現在“kernel hacking”菜單。並非所有體系架構都支持其中的某些選線。更多的調試功可能查看lib/Kconfig.debug文件,或者在menuconfig中搜索關鍵字debug。

  • CONFIG_DEBUG_KERNEL

該選項僅僅使得其他的調試選項可用。我們應該打開該選項,但它本身不會打開所有的調試功能。

  • CONFIG_DEBUG_SLAB

這是一個非常重要的選項,它打開內核分配函數中的多個類型的檢查;打開該檢查后,就可以檢測許多內存溢出以及忘記初始化的錯誤。在將已分配內存返回給調用者之前,內核將其中的每個字節設置為0xa5,而在釋放后將其設置為0x6b。如果讀者在自己驅動程序的輸出中,或者在oops信息中看到上述“毒劑”字符,則可輕松判斷問題所在。在打開該選項后,內核還會在每個已分配內存對象的前面和后面放置一些特殊的防護值,這樣當這些防護值發生變化時,內核就可以知道有些代碼超出了內存的正常訪問范圍。同時,該選項還會檢查更多的隱藏錯誤。

  • CONFIG_DEBUG_PAGEALLOC

在釋放時,全部內存頁從內核地址空間中移除。該選項將大大降低運行速度,但可以快速定位特定的內存損壞錯誤的所在位置。

  • CONFIG_DEBUG_SPINLOCK

內核將捕獲自旋鎖的錯誤操作,比如操作未初始化自旋鎖、兩次解開同一鎖的操作等其他錯誤。

  • CONFIG_DEBUG_ATOMIC_SLEEP

該選項將會檢查擁有原子鎖的休眠企圖

  • CONFIG_DEBUG_INFO

該選項將使內核的構造包含完整的調試信息。如果讀者打算用gdb調試內核,將需要這些信息,還需要打開CONFIG_FRAME_POINTER。

  • CONFIG_DEBUG_STACKOVERFLOW
  • CONFIG_DEBUG_STACK_USAGE

這些選項幫助跟蹤內核棧溢出問題。棧溢出的確切信號是不包含任何合理的反向跟蹤信息的oops清單。第一個選項將在內核中增加明確的溢出檢查;第二個選項將讓內核監視棧的使用(打印最大棧深度),並通過sysrq按鍵輸出一些統計信息。

  • CONFIG_DEBUG_KMEMLEAK

kmemleak是內核提供的一種檢測內存泄露工具,啟動一個內核線程掃描內存,每隔一定時間掃描內存(默認10分鍾),並打印發現新的未引用對象數量。

CONFIG_HAVE_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE=16000
CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=y---關閉此選項,則不需要在命令行添加kmemleak=on

使用方法:

立即觸發保存掃描結果
echo scan > /sys/kernel/debug/kmemleak

顯示可能的內存泄漏的詳細信息,需要先掛載debugfs文件系統:
mount -t debugfs nodev /sys/kernel/debug
cat /sys/kernel/debug/kmemleak

  • CONFIG_KALLSYMS

該選項將在內核中包含符號信息,默認是打開的,符號信息用於調試上下文,沒有此符號,oops清單只能給出十六進制的內核反向跟蹤信息,這通常沒有多少用處。

  • ONFIG_IKCONFIG //放到鏡像中
  • CONFIG_IKCONFIG_PROC //放到 proc目錄

打開這些選項(在“General setup”菜單下)使完整的內核配置信息編譯進內核,並可通過/proc/config.gz訪問(zcat免解壓顯示內容;gzip解壓文件;adb pull proc/config.gz)。大多數的內核開發者了解他們使用的配置,所以不需要這個選項(它使內核變得更大)。如果你嘗試調試其它人編譯的內核中的問題,它會非常有幫助。也可以在boot查看配置信息:cat /boot/config-$(uname -r) | grep IKCONFIG

  • CONFIG_DEBUG_DRIVER

位於“Device drivers”。打開驅動程序核心的調試信息,跟蹤底層支持代碼產生的問題時很有用

通過打印調試printk

printk介紹

  • printk是打印內核消息的函數
  • printk通過附加不同日志級別(loglevel)或者說消息優先級,讓printk對消息進行分類,這是與printf最大的區別
  • 在編譯時,日志級別宏會被展開為一個字符串,然后與消息本文拼接在一起,因此printk中優先級和格式字符串之間沒有逗號。

如果編譯內核時選擇了CONFIG_PRINTK=y,則會增加這個功能,否則所有printk都會被替換成空語句。
rsyslog從/proc/kmsg中持續讀取,並寫入/var/log/messages文件(通過/etc/rsyslog.conf配置)。
dmesg從/dev/kmsg中讀取。
cat /dev/kmsg可以獲得和dmesg幾乎同樣的效果,區別是命令最后會阻塞住等待新日志並持續打印。

這里是prink命令的兩個例子,一條調試信息和一個臨界信息:
printk(KERN_DEBUG "Here I am: %s:%i\n", FILE, LINE);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

在頭文件<linux/kernel.h>中定義了8中可用的日志級別字符串,下面以嚴重程序的降序方式排列:

  • KERN_EMEGR:用於緊急事件消息,一般是系統崩潰之前提示的消息
  • KERN_ALERT:用於需要立即采取動作的情況
  • KERN_CRIT:臨界狀態,通常設計嚴重的硬件或軟件操作失敗
  • KERN_ERR:用於報告錯誤狀態。設備驅動程序會經常使用該宏來報告來自硬件的問題
  • KERN_WARNING:對可能出現問題的情況進行警告,但這類情況通常不會對系統造成嚴重問題
  • KERN_NOTICE:有必要進行提示的正常情形。許多與安全相關的狀態用這個級別進行匯報
  • KERN_INFO:提示性信息。很多驅動程序在啟動的時候以這個級別來打印出它們找到的硬件信息
  • KERN_DEBUG:用於調試信息

上面每個字符串(以宏的形式展開)表示一個尖括號中的整數。范圍分別為0~7。數值越小,優先級越高。

打印級別

  • 默認打印機別

MESSAGE_LOGLEVEL_DEFAULT、CONSOLE_LOGLEVEL_DEFAULT宏

  • 未指定優先級的printk語句采用的默認級別是MESSAGE_LOGLEVEL_DEFAULT;該宏在kernel/printk/printk.c中被指定為另一個宏CONFIG_MESSAGE_LOGLEVEL_DEFAULT,該宏通過config配置

  • 在Linux 2.6.10內核中,MESSAGE_LOGLEV_ELDEFAULT就是KERN_WARNGIN(從config.gz中看到其值為4,說明為KERN_WARNING)

  • 根據日志級別,內核會把消息打印到控制台上或者保存到dmesg中。這個控制台可以是一個字符串終端、一個打印機。當優先級小於console_loglevel這個整數變量時,消息才會打印到控制台,而且每次輸出一行。

  • 動態修改打印級別

我們可以通過對文本文件/proc/sys/kernel/printk的訪問來讀取和修改控制台的日志級別。文件中分別有4個數值字段,從左到右分別為:當前的日志級別未明確指定日志級別時的默認消息級別最小允許的日志級別引導時的默認日志級別。向該文件中寫入單個整數值,將會把當前日志級別修改為這個值。

~/$ cat /proc/sys/kernel/printk
4 4 1 7

消息如何被記錄

printk函數將消息寫到一個長度為__LOG_BUF_LEN字節的循環緩沖區中(ring buff),我們可在配置內核時為__LOG_BUF_LEN指定4KB-1MB之間的值。Linux消息處理方法的另一特點是,可以在任何地方調用printk,甚至在中斷處理函數里,而且對數據的大小沒有限制,唯一缺點是可能會丟失某些數據。

rsyslogd日志記錄器由兩個守護進程(rklogd,rsyslogd)和一個配置文件(/etc/rsyslog.conf)組成。rklogd不使用配置文件,它負責截獲內核消息,它既可以獨立使用也可以作為rsyslogd的客戶端運行。rsyslogd默認使用/etc/syslog.conf作為配置文件,負責截獲應用程序消息,還可以截獲rklogd向其轉發的內核消息,然后根據不同服務產生的消息分別記錄到不同的文件中。

/var/log/messages或/var/log/syslog — 包括整體系統信息,其中也包含系統啟動期間的日志。此外,mail,cron,daemon,kern和auth等內容也記錄在var/log/messages日志中。
/var/log/dmesg — 包含內核緩沖信息(kernel ring buffer)。在系統啟動時,會在屏幕上顯示許多與硬件有關的信息。可以用dmesg查看它們。
/var/log/auth.log — 包含系統授權信息,包括用戶登錄和使用的權限機制等。
/var/log/boot.log — 包含系統啟動時的日志。
/var/log/daemon.log — 包含各種系統后台守護進程日志信息。
/var/log/dpkg.log — 包括安裝或dpkg命令清除軟件包的日志。
/var/log/kern.log — 包含內核產生的日志,有助於在定制內核時解決問題。
/var/log/lastlog — 記錄所有用戶的最近信息。這不是一個ASCII文件,因此需要用lastlog命令查看內容。
/var/log/maillog 與 /var/log/mail.log — 包含來着系統運行電子郵件服務器的日志信息。例如,sendmail日志信息就全部送到這個文件中。
/var/log/user.log — 記錄所有等級用戶信息的日志。
/var/log/Xorg.x.log — 來自X的日志信息。
/var/log/alternatives.log — 更新替代信息都記錄在這個文件中。
/var/log/btmp — 記錄所有失敗登錄信息。使用last命令可以查看btmp文件。例如,last -f /var/log/btmp | more 。
/var/log/cups — 涉及所有打印信息的日志。
/var/log/anaconda.log — 在安裝Linux時,所有安裝信息都儲存在這個文件中。
/var/log/yum.log — 包含使用yum安裝的軟件包信息。
/var/log/cron — 每當cron進程開始一個工作時,就會將相關信息記錄在這個文件中。
/var/log/secure — 包含驗證和授權方面信息。例如,sshd會將所有信息記錄(其中包括失敗登錄)在這里。
/var/log/wtmp或/var/log/utmp — 包含登錄信息。使用wtmp可以找出誰正在登陸進入系統,誰使用命令顯示這個文件或信息等。
/var/log/faillog —包含用戶登錄失敗信息。此外,錯誤登錄命令也會記錄在本文件中。

  • 開啟及關閉消息

在程序開發的初期階段,printk對於調試和測試新代碼是相當有幫助的。不過在正式發布驅動程序時,就得刪除這些打印,或者至少禁用它們。不幸的是,你可能會發生這樣的情況,即在刪除了那些已被認為不再需要的提示消息后,又需要實現一個新的功能(或者發現一個bug),這時,又希望恢復那些log。解決辦法:

  1. 使用條件語句,因此可在運行期間打開或者關閉,這是一個好功能;但是每次都要進行額外的處理,甚至在禁用消息后仍然會影響性能
  2. 定義一個宏,在需要的時候這個宏展開為一個printk調用。麻煩的是需要重新編譯,好處是不影響性能
  • 速度限制

有時讀者會一不小心利用printk產生了上千條消息,從而讓日志信息充滿控制台,更可能使系統日志文件溢出。如果使用某個慢速控制台(如串口),過快的消息輸出會導致系統變慢產生其他時序的問題。因此我們應該非常小心的管理我們的打印信息。通常,正式代碼不應該在正常操作下打印任何信息,而打印出的信息應該作為在異常時的提示信息。另一方面,在我們設備異常停止工作時,也許希望產生一條日志信息,但是我們要小心,不能在重試過程中不斷地打印失敗的提示消息,這樣的巨量輸出會阻塞CPU運行。

在許多情況下,最好的辦法是設置一個標志,表示我已經就此聲明過了,並在該標志被設置時不再打印任何消息。但在某些情況下,仍然希望偶爾發出一條該設備停止工作的提示消息。

printk_ratelimit函數(kernel建議用printk_ratelimited代替)通過跟蹤發送到控制台的消息數量工作,如果輸出的速度超過一個閾值,printk_ratelimit函數將返回零。從而避免發送重復消息。printk_ratelimit函數返回非零值表示我們可以繼續打印,否則就應該跳過。

修改/proc/sys/kernel/printk_ratelimit(在重新打開消息之前應該等待的秒數);/proc/sys/kernel/printk_ratelimit_burst(在進行速度限制之前可以接受的消息數)

if (printk_ratelimit()) {
printk(KERN_NOTICE "the printer is still on fire\n");
}

打印設備號

有時侯,當從一個驅動程序中打印信息時,你想打印與硬件結合的設備號以引起注意。打印主次設備號並不是非常難,但是,為了一致性,內核提供了一對工具宏(在<linux/kdev_t.h>中定義)來達成這個目的:
int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);
兩個宏都把設備號編碼到給出的buffer中;唯一的區別是print_dev_t返回的是被打印的字符數目,而format_dev_t返回buffer;因此,它可以直接作為printk調用的參數,雖然必須記住printk在遇到換行符之前不會輸出。緩沖區必須足夠大以能保存一個設備號;64位的設備號在將來的內核中是明顯可能的,緩沖區至少需要20字節長。

通過查詢調試

由於rsyslogd一直保持對輸出文件的同步刷新,即使我們通過console_loglevel控制打印到控制台的信息,但是大量使用printk仍然會顯著降低系統性能。多數情況下,獲取相關信息的最好方法是在需要的時候采取查詢系統信息,而不是持續不斷地產生數據。

使用/proc文件系統

/proc文件系統是一種特殊的、由軟件創建的文件系統,內核使用它向外界導出信息。/proc下面的每個文件都綁定了一個內核函數,用戶讀取其中的文件時,該函數動態地生成文件的“內容”。在Linux系統中對/proc的使用很頻繁,很多系統工具都市通過/proc來獲取它們需要的信息,例如ps、top和uptime。/proc文件大多是只讀文件,不過也可以寫入數據。最初的用途是用於提供系統中進程的信息,所以不鼓勵在/proc目錄下添加過多文件,建議放到/sys目錄。

在老版本內核中, 是通過實現read_proc_t 回調函數,再通過create_proc_read_entry注冊接口來創建節點的讀取

struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void * data);
void remove_dir_entry(const char *name, struct proc_dir_entry *parent);

seq_file接口

由於procfs的默認操作函數只使用一頁的緩存,在處理較大的proc文件時就有點麻煩,並且在輸出一系列結構體中的數據時也比較不靈活,需要自己在read_proc函數中實現迭代,容易出現Bug。所以內核黑客們對一些/proc代碼做了研究,抽象出共性,最終形成了seq_file(Sequence file:序列文件)接口。 這個接口提供了一套簡單的函數來解決以上proc接口編程時存在的問題,使得編程更加容易,降低了Bug出現的機會。

seq相關頭文件linux/seq_file.h,seq相關函數的實現在fs/seq_file.c。seq函數最早是在2001年就引入了,但以前內核中一直用得不多,而到了2.6內核后,許多/proc的只讀文件中大量使用了seq函數處理。

seq_file相關學習: http://blog.chinaunix.net/uid-28253945-id-3382865.html

使用觀察來調試

有時小問題可以通過觀察用戶態應用程序的行為來跟蹤。可用調試器單步執行,增加打印語句,或者使用調試工具strace來監視系統調用。

strace是一個有力的工具,常用來跟蹤用戶空間進程執行時的系統調用和所接收的信號。 在Linux世界,進程不能直接訪問硬件設備,當進程需要訪問硬件設備(比如讀取磁盤文件,接收網絡數據等等)時,必須由用戶態模式切換至內核態模式,通過系統調用訪問硬件設備。strace可以跟蹤到一個進程產生的系統調用,包括參數,返回值,執行消耗的時間。

調試系統故障

即使使用了所有的監視和調試技術,有時bugs仍然存在於你的驅動程序中,並在驅動程序被執行時引發系統錯誤。如果這種情況發生,收集盡可能多的信息用於解決問題將非常重要。注意“故障(fault)”並不意味這“驚恐(panic)”。Linux代碼非常健壯,可以很好的響應大部分錯誤:一個錯誤通常導致當前進程被破壞,系統繼續運行。如果錯誤發生在進程上下文之外或系統中關鍵部分被損壞時,系統就會panic。唯一不可恢復的就是分配給進程上下文的內存;舉例來說,驅動程序通過kmalloc分配的動態鏈表可能丟失。然而,內核在進程終止時會對已打開的設備調用close操作,驅動程序仍可以釋放由open方法分配的資源。

盡管oops消息通常不會導致整個系統崩潰,但我們發現遇到此類情況時還是要重新引導系統。一個有bug的驅動程序可能導致硬件不可用,或者導致內核資源處於不一致的狀態,或者更糟的情況是在任意位置破壞內核內存。通常情況下,可以卸載有bug的驅動程序並在oops發生后重試。但是如果看到系統整體出現問題的消息后最好的方法就是立即重新引導系統。

oops消息

大部分錯誤都是因為對NULL指針取值或因為使用了其他不正確的指針值,這些錯誤通常會導致一個oops消息。由處理器使用的地址幾乎都是虛擬地址,這些地址通過MMU映射為物理地址。當引用一個非法指針時,分頁機制無法將該頁映射到物理地址,此時處理器就會向操作系統發出頁面缺失的信號。如果地址非法,內核就無法換入頁面(page in),如果這時處理器處於特權模式,內核就會產生一個oops。

oops顯示發生錯誤時處理器的狀態。比如CPU寄存器的內容以及其他看上去無法理解的信息。這些消息由失效處理函數中的printk語句產生(arch/*/kernel/traps.c),如果必要可深入研究一下traps文件。

讓我們看看系統訪問了一個NULL指針時顯示oops消息的例子。通常,在我們面對一條oops時,首先要觀察的是發生的問題所在的位置,這通常可通過調用棧信息得到。“EIP is at faulty_write+0x4/0x10 [faulty]”表明故障所在函數是faulty_write,該函數位於faulty模塊列在括號內,十六進制的數據表明指令指針在該函數的4字節處,而函數本身是0x10字節長。通常這些信息足以讓我們看到問題所在。調用棧stack可以告訴我們系統是如何到達故障點的,如果內核打開了CONFIG_KALLSYMS選項,就能看到符號化的調用棧而不是裸的16進制清單。

Unable to handle kernel NULL pointer dereference at virtual address 00000000 printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[ ] Not tainted
EFLAGS: 00010246 (2.6.6)
EIP is at faulty_write+0x4/0x10 [faulty]
eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
ds: 007b es: 007b ss: 0068
Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
Stack: c0150558 cf8b2460 080e9408 00000005
cf8b2480 00000000 cf8b2460 cf8b2460
fffffff7 080e9408 c31c4000 c0150682 cf8b2460
080e9408 00000005 cf8b2480
00000000 00000001 00000005 c0103f8f 00000001
080e9408 00000005 00000005
Call Trace:
[ ] vfs_write+0xb8/0x130
[ ] sys_write+0x42/0x70
[ ] syscall_call+0x7/0xb
Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec
0c b8 00 a6 83 d0

系統掛起后sysrq功能

盡管內核代碼中的大多數錯誤只會導致一個oops信息,但有時它們會把系統完全掛起,導致任務消息無法打印出來。有時系統看似掛起了,但其實並沒有,時鍾或系統負載依然在更新。這時一個不可或缺的工具是“sysrq魔法鍵”,通過PC鍵盤上的ALT和sysrq組合鍵來激活系統。根據第三個鍵有不同的功能。其他一些sysrq功能可參考內核源碼Documentation/sysrq.txt。注意sysrq功能必須顯式的在內核配置中啟用,處於安全原因,大多數發行版本並未開啟這一功能。不過可通過寫文件節點來手動打開,“echo 1 > /proc/sys/kernel/sysrq”。還對無法訪問控制台的系統管理員開發了/proc/sysrq-trigger,寫入對應的字符,則觸發相應的sysrq動作,這個sysrq入口點始終可用。

b 理解重啟系統。注意先要執行同步並重新掛裝磁盤

p 打印當前的處理器寄存器信息。

t 打印當前的任務列表

m 打印內存信息。

調試器相關工具

最后一種調試模塊的方法就是使用一個調試器來單步執行代碼,監視變量和寄存器的值。這種方法非常耗時,應該盡量避免,不過。在某些情況下通過調試器對代碼進行細粒度的分析是很有價值的。在內核中使用交互式調試器是一個很復雜的問題,出於對系統所有進程的整體利益的考慮,內核在它自己的地址空間中運行。其結果就是許多用戶空間的調試器所提供的常用功能很難用於內核之中,例如斷點和單步執行,在內核中很難獲得。本節我們討論許多調試內核的方法;它們各有優缺點。

使用gdb

gdb在探究系統內部行為非常有用。在這個層次上進行調試需要具備以下基本素養:

  1. 熟練使用調試器,掌握gdb命令
  2. 了解目標平台的匯編代碼
  3. 具備對源代碼和優化后的匯編代碼進行匹配的能力

啟動調試器時必須把內核看作一個應用程序。除了指定未壓縮的內核映像文件名以外,還應在命令中提供core文件。對於正在運行的內核,所謂的core文件就是這個內核在內存中的核心映像,即/proc/kcore。典型的gdb命令如下gdb /usr/src/linux/vmlinux /proc/kcore。第一個參數是未經壓縮的內核ELF可執行文件,而不是zImage或bzImage以及其他特殊的內核映像。第二個參數是core文件。與其他/proc中的文件類似,/proc/kcore也是在被讀取時產生的。由於它要表示對應與所有物理內存的整個內核地址空間,所以是一個非常巨大的文件。

對內核調試時,gdb很多功能都不可用,例如不能修改內核數據,不能設置斷點或者觀察點,也不能單步蹤內核。其原因是內核不信任交互式的調試器,擔心調試器有不良修改導致系統異常。只能簡單的查看信息。而且必須打開CONFIG_DEBUG_INFO選項編譯的內核才能看到變量。在調試信息可用的情況下,我們可了解到許多內核內部的工作情況。但是困難在於處理模塊,因為模塊不是傳遞給gdb的vmlinux映像的一部分,因此調試器不知道模塊的存在。可以通過一條gdb命令告訴調試器有關模塊的信息,這條命令就是add-symbol-file,該命令指定目標文件的名稱,代碼段基地址、數據段基地址以及其他參數。

kdb內核調試器

kdb是一個內核調試器,但是是以非正式的補丁形式提供,要使用kdb必須先獲得補丁,然后對內核源代碼進行patch操作,再重新編譯並安裝內核。其用法是一旦支持kdb就可以在命令行進入kdb調試模式,從而支持查看修改變量、設置斷點、單步調試等功能。目前kdb被收購不再開源,對我們來說用處不大,不對其做過多介紹。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM