內核開發比用戶空間開發更難的一個因素就是內核調試艱難。內核錯誤往往會導致系統宕機,很難保留出錯時的現場。調試內核的關鍵在于你的對內核的深刻理解。
嵌入式進階教程分門別類整理好了,看的時候十分方便,由于內容較多,這里就截取一部分圖吧。
在調試一個bug之前,我們所要做的準備工作有:
有一個被確認的bug,包含這個bug的內核版本號,需要分析出這個bug在哪一個版本被引入,這個對于解決問題有極大的幫助。可以采用二分查找法來逐步鎖定bug引入版本號。
對內核代碼理解越深刻越好,同時還需要一點點運氣,該bug可以復現。如果能夠找到規律,那么離找到問題的原因就不遠了;最小化系統。把可能產生bug的因素逐一排除掉。
內核中的bug
內核中的bug也是多種多樣的。它們的產生有無數的原因,同時表象也變化多端。從隱藏在源代碼中的錯誤到展現在目擊者面前的bug,其發作往往是一系列連鎖反應的事件才可能觸發的。雖然內核調試有一定的困難,但是通過你的努力和理解,說不定你會喜歡上這樣的挑戰。
內核調試配置選項
學習編寫驅動程序要構建安裝自己的內核(標準主線內核)。最重要的原因之一是:內核開發者已經建立了多項用于調試的功能。但是由于這些功能會造成額外的輸出,并導致能量下降,因此發行版廠商通常會禁止發行版內核中的調試功能。
內核配置
為了實現內核調試,在內核配置上增加了幾項:
Kernel hacking ---> [*] Magic SysRq key [*] Kernel debugging [*]
Debug slab memory allocaTIons [*]
Spinlock and rw-lock debugging: basic checks [*]
Spinlock debugging: sleep-inside-spinlock checking [*]
Compile the kernel with debug info Device Drivers ---> Generic Driver Options ---> [*]
Driver Core verbose debug messages General setup ---> [*]
Configure standard kernel features (for small systems) ---> [*]
Load all symbols for debugging/ksymoops
調試原子操作
從內核2.5開發,為了檢查各類由原子操作引發的問題,內核提供了極佳的工具。 內核提供了一個原子操作計數器,它可以配置成,一旦在原子操作過程中,經常進入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追蹤線索。 所以,包括在使用鎖的時候調用schedule(),正使用鎖的時候以阻塞方式請求分配內存等,各種潛在的bug都能夠被探測到。
下面這些選項可以最大限度地利用該特性:
CONFIG_PREEMPT = y
CONFIG_DEBUG_KERNEL = y
CONFIG_KLLSYMS = y CONFIG_SPINLOCK_SLEEP = y
引發bug并打印信息
BUG()和BUG_ON()
一些內核調用可以用來方便標記bug,提供斷言并輸出信息。最常用的兩個是BUG()和BUG_ON()。定義在中:
#ifndef HAVE_ARCH_BUG #define BUG() do {
printk("BUG: failure at %s:%d/%s()! ", __FILE__, __LINE__, __FUNCTION__); panic("BUG!"); /* 引發更嚴重的錯誤,不但打印錯誤消息,而且整個系統業會掛起 */ } while (0) #endif #ifndef HAVE_ARCH_BUG_ON #define BUG_ON(condiTIon)
do {
if (unlikely(condiTIon)) BUG();
}while(0)
#endif
當調用這兩個宏的時候,它們會引發OOPS,導致棧的回溯和錯誤消息的打印。
※ 可以把這兩個調用當作斷言使用,如:BUG_ON(bad_thing);
WARN(x) 和 WARN_ON(x)
而WARN_ON則是調用dump_stack,打印堆棧信息,不會OOPS。定義在中:
#ifndef __WARN_TAINT#ifndef __ASSEMBLY__extern void warn_slowpath_fmt(
const char *file, const int line, const char *fmt, ...) __attribute__((format(printf, 3, 4)));extern void warn_slowpath_fmt_taint(const char *file, const int line, unsigned taint, const char *fmt, ...) __attribute__((format(printf, 4, 5)));extern void warn_slowpath_null(const char *file, const int line);
#define WANT_WARN_ON_SLOWPATH
#endif
#define __WARN() warn_slowpath_null(__FILE__, __LINE__)
#define __WARN_printf(arg...) warn_slowpath_fmt(__FILE__, __LINE__, arg)
#define __WARN_printf_taint(taint, arg...) warn_slowpath_fmt_taint(__FILE__, __LINE__, taint, arg)
#else
#define __WARN() __WARN_TAINT(TAINT_WARN)
#define __WARN_printf(arg...) do { printk(arg); __WARN();
} while (0)
#define __WARN_printf_taint(taint, arg...) do { printk(arg); __WARN_TAINT(taint);
}
while (0)
#endif#ifndef WARN_ON#define WARN_ON(condition) ({
int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on)) __WARN();
unlikely(__ret_warn_on); })
#endif
#ifndef WARN#define WARN(condition, format...) ({
int __ret_warn_on = !!(condition); if (unlikely(__ret_warn_on)) __WARN_printf(format);
unlikely(__ret_warn_on); })
#endif
dump_stack()
有些時候,只需要在終端上打印一下棧的回溯信息來幫助你調試。這時可以使用dump_stack()。這個函數只是在終端上打印寄存器上下文和函數的跟蹤線索。
if (!debug_check) { printk(KERN_DEBUG “provide some information…/n”); dump_stack(); }
printk()
內核提供的格式化打印函數。
printk函數的健壯性
健壯性是printk最容易被接受的一個特質,幾乎在任何地方,任何時候內核都可以調用它(中斷上下文、進程上下文、持有鎖時、多處理器處理時等)。
printk函數脆弱之處
在系統啟動過程中,終端初始化之前,在某些地方是不能調用的。如果真的需要調試系統啟動過程最開始的地方,有以下方法可以使用:
使用串口調試,將調試信息輸出到其他終端設備。
使用early_printk(),該函數在系統啟動初期就有打印能力。但它只支持部分硬件體系。
LOG等級
printk和printf一個主要的區別就是前者可以指定一個LOG等級。內核根據這個等級來判斷是否在終端上打印消息。內核把比指定等級高的所有消息顯示在終端。 可以使用下面的方式指定一個LOG級別:
printk(KERN_CRIT “Hello, world! ”);
注意,第一個參數并不一個真正的參數,因為其中沒有用于分隔級別(KERN_CRIT)和格式字符的逗號(,)。KERN_CRIT本身只是一個普通的字符串(事實上,它表示的是字符串 "<2>";表 1 列出了完整的日志級別清單)。
作為預處理程序的一部分,C 會自動地使用一個名為 字符串串聯 的功能將這兩個字符串組合在一起。組合的結果是將日志級別和用戶指定的格式字符串包含在一個字符串中。
內核使用這個指定LOG級別與當前終端LOG等級console_loglevel來決定是不是向終端打印。下面是可使用的LOG等級:
#define KERN_EMERG "<0>" /* system is unusable */#define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */#define KERN_ERR "<3>" /* error conditions */#define KERN_WARNING "<4>" /* warning conditions */#define KERN_NOTICE "<5>" /* normal but significant condition */#define KERN_INFO "<6>" /* informational */#define KERN_DEBUG "<7>" /* debug-level messages */#define KERN_DEFAULT "" /* Use the default kernel loglevel */
注意,如果調用者未將日志級別提供給 printk,那么系統就會使用默認值 KERN_WARNING "<4>"(表示只有KERN_WARNING 級別以上的日志消息會被記錄)。由于默認值存在變化,所以在使用時最好指定LOG級別。有LOG級別的一個好處就是我們可以選擇性的輸出LOG。
比如平時我們只需要打印KERN_WARNING級別以上的關鍵性LOG,但是調試的時候,我們可以選擇打印KERN_DEBUG等以上的詳細LOG。而這些都不需要我們修改代碼,只需要通過命令修改默認日志輸出級別:
mtj@ubuntu :~$ cat /proc/sys/kernel/printk4 4 1 7mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay0mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit5mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst10
第一項定義了 printk API 當前使用的日志級別。這些日志級別表示了控制臺的日志級別、默認消息日志級別、最小控制臺日志級別和默認控制臺日志級別。printk_delay 值表示的是 printk 消息之間的延遲毫秒數(用于提高某些場景的可讀性)。
注意,這里它的值為 0,而它是不可以通過的 /proc 設置的。
printk_ratelimit 定義了消息之間允許的最小時間間隔(當前定義為每 5 秒內的某個內核消息數)。消息數量是由 printk_ratelimit_burst 定義的(當前定義為 10)。
如果您擁有一個非正式內核而又使用有帶寬限制的控制臺設備(如通過串口), 那么這非常有用。注意,在內核中,速度限制是由調用者控制的,而不是在printk 中實現的。如果一個 printk 如果用戶要求進行速度限制,那么該用戶就需要調用printk_ratelimit 函數。
記錄緩沖區
內核消息都被保存在一個LOG_BUF_LEN大小的環形隊列中。
關于LOG_BUF_LEN定義:
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
※ 變量CONFIG_LOG_BUF_SHIFT在內核編譯時由配置文件定義,對于i386平臺,其值定義如下(在
linux26/arch/i386/defconfig中):
CONFIG_LOG_BUF_SHIFT=18
記錄緩沖區操作:
① 消息被讀出到用戶空間時,此消息就會從環形隊列中刪除。
② 當消息緩沖區滿時,如果再有printk()調用時,新消息將覆蓋隊列中的老消息。
③ 在讀寫環形隊列時,同步問題很容易得到解決。
※ 這個紀錄緩沖區之所以稱為環形,是因為它的讀寫都是按照環形隊列的方式進行操作的。
syslogd/klogd
在標準的Linux系統上,用戶空間的守護進程klogd從紀錄緩沖區中獲取內核消息,再通過syslogd守護進程把這些消息保存在系統日志文件中。klogd進程既可以從/proc/kmsg文件中,也可以通過syslog()系統調用讀取這些消息。默認情況下,它選擇讀取/proc方式實現。klogd守護進程在消息緩沖區有新的消息之前,一直處于阻塞狀態。一旦有新的內核消息,klogd被喚醒,讀出內核消息并進行處理。默認情況下,處理例程就是把內核消息傳給syslogd守護進程。syslogd守護進程一般把接收到的消息寫入/var/log/messages文件中。不過,還是可以通過/etc/syslog.conf文件來進行配置,可以選擇其他的輸出文件。
dmesg
dmesg 命令也可用于打印和控制內核緩沖區。這個命令使用 klogctl 系統調用來讀取內核環緩沖區,并將它轉發到標準輸出(stdout)。這個命令也可以用來清除內核環緩沖區(使用 -c 選項),設置控制臺日志級別(-n 選項),以及定義用于讀取內核日志消息的緩沖區大小(-s 選項)。注意,如果沒有指定緩沖區大小,那么 dmesg 會使用 klogctl 的SYSLOG_ACTION_SIZE_BUFFER 操作確定緩沖區大小。
注意
a) 雖然printk很健壯,但是看了源碼你就知道,這個函數的效率很低:做字符拷貝時一次只拷貝一個字節,且去調用console輸出可能還產生中斷。所以如果你的驅動在功能調試完成以后做性能測試或者發布的時候千萬記得盡量減少printk輸出,做到僅在出錯時輸出少量信息。否則往console輸出無用信息影響性能。
b) printk的臨時緩存printk_buf只有1K,所有一次printk函數只能記錄<1K的信息到log buffer,并且printk使用的“ringbuffer”.
內核printk和日志系統的總體結構
動態調試
動態調試是通過動態的開啟和禁止某些內核代碼來獲取額外的內核信息。
首先內核選項CONFIG_DYNAMIC_DEBUG應該被設置。所有通過pr_debug()/dev_debug()打印的信息都可以動態地顯示或不顯示。
可以通過簡單的查詢語句來篩選需要顯示的信息。
-源文件名
-函數名
-行號(包括指定范圍的行號)
-模塊名
-格式化字符串
將要打印信息的格式寫入/dynamic_debug/control中。
nullarbor:~ # echo 'file svcsock.c line 1603 +p' > /dynamic_debug/control
參考:
1 內核日志及printk結構淺析 -- Tekkaman Ninja
2 內核日志:API 及實現
3 printk實現分析
4 dynamic-debug-howto.txt
內存調試工具
MEMWATCH
MEMWATCH 由 Johan Lindh 編寫,是一個開放源代碼 C 語言內存錯誤檢測工具,您可以自己下載它。只要在代碼中添加一個頭文件并在 gcc 語句中定義了 MEMWATCH 之后,您就可以跟蹤程序中的內存泄漏和錯誤了。MEMWATCH 支持ANSIC,它提供結果日志紀錄,能檢測雙重釋放(double-free)、錯誤釋放(erroneous free)、沒有釋放的內存(unfreedmemory)、溢出和下溢等等。
清單 1. 內存樣本(test1.c)
#include #include #include "memwatch.h"int main(void){ char *ptr1; char *ptr2; ptr1 = malloc(512); ptr2 = malloc(512); ptr2 = ptr1; free(ptr2); free(ptr1);}
清單 1 中的代碼將分配兩個 512 字節的內存塊,然后指向第一個內存塊的指針被設定為指向第二個內存塊。結果,第二個內存塊的地址丟失,從而產生了內存泄漏。
現在我們編譯清單 1 的 memwatch.c。
下面是一個 makefile 示例:
test1gcc -DMEMWATCH -DMW_STDIO test1.c memwatchc -o test1
當您運行 test1 程序后,它會生成一個關于泄漏的內存的報告。清單 2 展示了示例 memwatch.log 輸出文件。
清單 2. test1 memwatch.log 文件
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
MEMWATCH 為您顯示真正導致問題出現的信息。如果您釋放一個已經釋放過的指針,它會告訴您。對于沒有釋放的內存也一樣。日志結尾部分顯示統計信息,包括泄露了多少內存,使用了多少內存,以及總共分配了多少內存。
YAMD
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 輸出
YAMD version 0.32Executable: /usr/src/test/yamd-0.32/test1...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal deallocation of this blockAddress 0x40025e00, size 512...ERROR: Multiple freeing Atfree of pointer already freedAddress 0x40025e00, size 512...WARNING: Memory leakAddress 0x40028e00, size 512WARNING: Total memory leaks:1 unfreed allocations totaling 512 bytes*** Finished at Tue ... 1015 2002Allocated a grand total of 1024 bytes 2 allocationsAverage of 512 bytes per allocationMax bytes allocated at one time: 102424 K alloced internally / 12 K mapped now / 8 K maxVirtual program size is 1416 KEnd.
YAMD 顯示我們已經釋放了內存,而且存在內存泄漏。讓我們在清單 4 中另一個樣本程序上試試 YAMD。
清單 4. 內存代碼(test2.c)
#include #include 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);}
您可以使用下面的命令來啟動 YAMD:
./run-yamd /usr/src/test/test2/test2
清單 5 顯示了該樣本程序 test2 上使用 YAMD 得到的輸出。YAMD 告訴我們在 for 循環中有“越界(out-of-bounds)”的情況。
清單 5. 使用 YAMD 的 test2 輸出
Running /usr/src/test/test2/test2Temp output to /tmp/yamd-out.1243*********./run-yamd: line 101: 1248 Segmentation fault (core dumped)YAMD version 0.32Starting run: /usr/src/test/test2/test2Executable: /usr/src/test/test2/test2Virtual program size is 1380 K...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal allocation of this blockAddress 0x4002be00, size 512ERROR: Crash...Tried to write address 0x4002c000Seems 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.
MEMWATCH 和 YAMD 都是很有用的調試工具,不過它們的使用方法有所不同。對于 MEMWATCH,您需要添加包含文件memwatch.h 并打開兩個編譯時間標記。對于鏈接(link)語句,YAMD 只需要 -g 選項。
Electric Fence
多數 Linux 分發版包含一個 Electric Fence 包,不過您也可以選擇下載它。Electric Fence 是一個由 Bruce Perens 編寫的malloc()調試庫。它就在您分配內存后分配受保護的內存。如果存在 fencepost 錯誤(超過數組末尾運行),程序就會產生保護錯誤,并立即結束。通過結合 Electric Fence 和 gdb,您可以精確地跟蹤到哪一行試圖訪問受保護內存。ElectricFence 的另一個功能就是能夠檢測內存泄漏。
strace
strace 命令是一種強大的工具,它能夠顯示所有由用戶空間程序發出的系統調用。strace 顯示這些調用的參數并返回符號形式的值。strace 從內核接收信息,而且不需要以任何特殊的方式來構建內核。將跟蹤信息發送到應用程序及內核開發者都很有用。在清單 6 中,分區的一種格式有錯誤,清單顯示了 strace 的開頭部分,內容是關于調出創建文件系統操作(mkfs )的。strace 確定哪個調用導致問題出現。
清單 6. mkfs 上 strace 的開頭部分
execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &...open("/dev/test1", O_RDWR|O_LARGEFILE) = 4stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0ioctl(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 ) = 98stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)write(2, "mkfs.jfs: can't determine device"..., ..._exit(1) = ?
清單 6 顯示 ioctl 調用導致用來格式化分區的 mkfs 程序失敗。ioctl BLKGETSIZE64 失敗。( BLKGET-SIZE64 在調用 ioctl的源代碼中定義。) BLKGETSIZE64 ioctl 將被添加到 Linux 中所有的設備,而在這里,邏輯卷管理器還不支持它。因此,如果BLKGETSIZE64 ioctl 調用失敗,mkfs 代碼將改為調用較早的 ioctl 調用;這使得 mkfs 適用于邏輯卷管理器。
OOPS
OOPS(也稱 Panic)消息包含系統錯誤的細節,如?CPU?寄存器的內容等。是內核告知用戶有不幸發生的最常用的方式。
內核只能發布OOPS,這個過程包括向終端上輸出錯誤消息,輸出寄存器保存的信息,并輸出可供跟蹤的回溯線索。通常,發送完OOPS之后,內核會處于一種不穩定的狀態。
OOPS的產生有很多可能原因,其中包括內存訪問越界或非法的指令等。
※ 作為內核的開發者,必定將會經常處理OOPS。
※ OOPS中包含的重要信息,對所有體系結構的機器都是完全相同的:寄存器上下文和回溯線索(回溯線索顯示了導致錯誤發生的函數調用鏈)。
ksymoops
在 Linux 中,調試系統崩潰的傳統方法是分析在發生崩潰時發送到系統控制臺的 Oops 消息。一旦您掌握了細節,就可以將消息發送到 ksymoops 使用程序,它將試圖將代碼轉換為指令并將堆棧值映射到內核符號。
※ 如:回溯線索中的地址,會通過ksymoops轉化成名稱可見的函數名。
ksymoops需要幾項內容:Oops 消息輸出、來自正在運行的內核的 System.map 文件,還有 /proc/ksyms、vmlinux和/proc/modules。
關于如何使用 ksymoops,內核源代碼
/usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊頁上有完整的說明可以參考。Ksymoops 返回編代碼部分,指出發生錯誤的指令,并顯示一個跟蹤部分表明代碼如何被調用。
首先,將 Oops 消息保存在一個文件中以便通過 ksymoops 使用程序運行它。清單 7 顯示了由安裝 JFS 文件系統的 mount命令創建的 Oops 消息。
清單 7. ksymoops 處理后的 Oops 消息
ksymoops 2.4.0 on i686 2.4.17. Options used... 1537 sfb1 kernel: Unable to handle kernel NULL pointer dereference atvirtual address 0000000... 1537 sfb1 kernel: c01588fc... 1537 sfb1 kernel: *pde = 0000000... 1537 sfb1 kernel: Oops: 0000... 1537 sfb1 kernel: CPU: 0... 1537 sfb1 kernel: EIP: 0010:[jfs_mount+60/704]... 1537 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]... 1537 sfb1 kernel: Call Trace: []...... 1537 sfb1 kernel: [>EIP; c01588fc <=====...Trace; c0106cf3 33/40>Code; c01588fc 00000000 <_EIP>:Code; c01588fc <===== 0: 8b 2d 00 00 00 00 mov 0x0,%ebp <=====Code; c0158902 42/2c0> 6: 55 push %ebp
接下來,您要確定 jfs_mount 中的哪一行代碼引起了這個問題。Oops 消息告訴我們問題是由位于偏移地址 3c 的指令引起的。做這件事的辦法之一就是對 jfs_mount.o 文件使用 objdump 使用程序,然后查看偏移地址 3c。Objdump 用來反匯編模塊函數,看看您的 C 源代碼會產生什么匯編指令。清單 8 顯示了使用 objdump 后您將看到的內容,接著,我們查看jfs_mount 的 C 代碼,可以看到空值是第 109 行引起的。偏移地址 3c 之所以這么重要,是因為 Oops 消息將該處標識為引起問題的位置。
清單 8. jfs_mount 匯編程序清單
109 printk("%d ",*ptr);objdump jfs_mount.ojfs_mount.o: file format elf32-i386Disassembly of section .text:00000000 : 0:55 push %ebp ... 2c: e8 cf 03 00 00 call 400 31: 89 c3 mov %eax,%ebx 33: 58 pop %eax 34: 85 db test %ebx,%ebx 36: 0f 85 55 02 00 00 jne 291 0x291> 3c: 8b 2d 00 00 00 00 mov 0x0,%ebp << problem line above 42: 55 push %ebp
kallsyms
開發版2.5內核引入了kallsyms特性,它可以通過定義CONFIG_KALLSYMS編譯選項啟用。該選項可以載入內核鏡像所對應的內存地址的符號名稱(即函數名),所以內核可以打印解碼之后的跟蹤線索。相應,解碼OOPS也不再需要System.map和ksymoops工具了。另外, 這樣做,會使內核變大些,因為地址對應符號名稱必須始終駐留在內核所在內存上。
#cat /proc/kallsyms c0100240 T _stext c0100240 t run_init_process c0100240 T stext c0100269 t init …
Kdump
什么是 kexec ?
Kexec 是實現 kdump 機制的關鍵,它包括 2 一是組成部分:一是內核空間的系統調用 kexec_load,負責在生產內核(production kernel 或 first kernel)啟動時將捕獲內核(capture kernel 或 sencond kernel)加載到指定地址。二是用戶空間的工具 kexec-tools,他將捕獲內核的地址傳遞給生產內核,從而在系統崩潰的時候能夠找到捕獲內核的地址并運行。沒有 kexec 就沒有 kdump。先有 kexec 實現了在一個內核中可以啟動另一個內核,才讓 kdump 有了用武之地。kexec 原來的目的是為了節省時間 kernel 開發人員重啟系統的時間,誰能想到這個“偷懶”的技術卻孕育了最成功的內存轉存機制呢?
什么是 kdump ?
Kdump 的概念出現在 2005 左右,是迄今為止最可靠的內核轉存機制,已經被主要的 linux 廠商選用。kdump是一種先進的基于 kexec 的內核崩潰轉儲機制。當系統崩潰時,kdump 使用 kexec 啟動到第二個內核。第二個內核通常叫做捕獲內核,以很小的內存啟動以捕獲轉儲鏡像。第一個內核保留了內存的一部分給第二個內核啟動用。由于 kdump 利用 kexec 啟動捕獲內核,繞過了 BIOS,所以第一個內核的內存得以保留。這是內核崩潰轉儲的本質。
kdump 需要兩個不同目的的內核,生產內核和捕獲內核。生產內核是捕獲內核服務的對象。捕獲內核會在生產內核崩潰時啟動起來,與相應的 ramdisk 一起組建一個微環境,用以對生產內核下的內存進行收集和轉存。
如何使用 kdump
構建系統和 dump-capture 內核,此操作有 2 種方式可選:
1)構建一個單獨的自定義轉儲捕獲內核以捕獲內核轉儲;
2) 或者將系統內核本身作為轉儲捕獲內核,這就不需要構建一個單獨的轉儲捕獲內核。
方法(2)只能用于可支持可重定位內核的體系結構上;目前 i386,x86_64,ppc64 和 ia64 體系結構支持可重定位內核。構建一個可重定位內核使得不需要構建第二個內核就可以捕獲轉儲。但是可能有時想構建一個自定義轉儲捕獲內核以滿足特定要求。
如何訪問捕獲內存
在內核崩潰之前所有關于核心映像的必要信息都用 ELF 格式編碼并存儲在保留的內存區域中。ELF 頭所在的物理地址被作為命令行參數(fcorehdr=)傳遞給新啟動的轉儲內核。
在 i386 體系結構上,啟動的時候需要使用物理內存開始的 640K,而不管操作系統內核轉載在何處。因此,這個640K 的區域在重新啟動第二個內核的時候由 kexec 備份。
在第二個內核中,“前一個系統的內存”可以通過兩種方式訪問:
1) 通過 /dev/oldmem 這個設備接口。
一個“捕捉”設備可以使用“raw”(裸的)方式 “讀”這個設備文件并寫出到文件。這是關于內存的 “裸”的數據轉儲,同時這些分析 / 捕捉工具應該足夠“智能”從而可以知道從哪里可以得到正確的信息。ELF 文件頭(通過命令行參數傳遞過來的 elfcorehdr)可能會有幫助。
2) 通過 /proc/vmcore。
這個方式是將轉儲輸出為一個 ELF 格式的文件,并且可以使用一些文件拷貝命令(比如 cp,scp 等)將信息讀出來。同時,gdb 可以在得到的轉儲文件上做一些調試(有限的)。這種方式保證了內存中的頁面都以正確的途徑被保存 ( 注意內存開始的 640K 被重新映射了 )。
kdump 的優勢
1) 高可靠性
崩潰轉儲數據可從一個新啟動內核的上下文中獲取,而不是從已經崩潰內核的上下文。
2) 多版本支持
LKCD(Linux Kernel Crash Dump),netdump,diskdump 已被納入 LDPs(Linux Documen-tation Project) 內核。SUSE 和 RedHat 都對 kdump 有技術支持。
配置 kdump
安裝軟件包和實用程序
Kdump 用到的各種工具都在 kexec-tools 中。kernel-debuginfo 則是用來分析 vmcore 文件。從 rhel5 開始,kexec-tools 已被默認安裝在發行版。而 novell 也在 sles10 發行版中把 kdump 集成進來。所以如果使用的是rhel5 和 sles10 之后的發行版,那就省去了安裝 kexec-tools 的步驟。而如果需要調試 kdump 生成的 vmcore文件,則需要手動安裝 kernel-debuginfo 包。檢查安裝包操作:
3.3.2 參數相關設置 uli13lp1:/ # rpm -qa|grep kexec kexec-tools-2.0.0-53.43.10 uli13lp1:/ # rpm -qa 'kernel*debuginfo*' kernel-default-debuginfo-3.0.13-0.27.1 kernel-ppc64-debuginfo-3.0.13-0.27.1
系統內核設置選項和轉儲捕獲內核配置選擇在《使用 Crash 工具分析 Linux dump 文件》一文中已有說明,在此不再贅述。僅列出內核引導參數設置以及配置文件設置。
1) 修改內核引導參數,為啟動捕獲內核預留內存
通過下面的方法來配置 kdump 使用的內存大小。添加啟動參數"crashkernel=Y@X",這里,Y 是為 kdump 捕捉內核保留的內存,X 是保留部分內存的開始位置。
對于 i386 和 x86_64, 編輯 /etc/grub.conf, 在內核行的最后添加"crashkernel=128M" 。
對于 ppc64,在 /etc/yaboot.conf 最后添加"crashkernel=128M"。
在 ia64, 編輯 /etc/elilo.conf,添加"crashkernel=256M"到內核行。
2) kdump 配置文件
kdump 的配置文件是 /etc/kdump.conf(RHEL6.2);/etc/sysconfig/kdump(SLES11 sp2)。每個文件頭部都有選項說明,可以根據使用需求設置相應的選項。
啟動 kdump 服務
在設置了預留內存后,需要重啟機器,否則 kdump 是不可使用的。啟動 kdump 服務:
Rhel6.2:
# chkconfig kdump on # service kdump status Kdump is operational # service kdump start
SLES11SP2:
# chkconfig boot.kdump on # service boot.kdump start
測試配置是否有效
可以通過 kexec 加載內核鏡像,讓系統準備好去捕獲一個崩潰時產生的 vmcore。可以通過 sysrq 強制系統崩潰。
# echo c > /proc/sysrq-trigger
這造成內核崩潰,如配置有效,系統將重啟進入 kdump 內核,當系統進程進入到啟動 kdump 服務的點時,vmcore 將會拷貝到你在 kdump 配置文件中設置的位置。RHEL 的缺省目錄是 : /var/crash;SLES 的缺省目錄是 : /var/log/dump。然后系統重啟進入到正常的內核。一旦回復到正常的內核,就可以在上述的目錄下發現 vmcore 文件,即內存轉儲文件。可以使用之前安裝的 kernel-debuginfo 中的 crash 工具來進行分析(crash 的更多詳細用法將在本系列后面的文章中有介紹)。
# crash /usr/lib/debug/lib/modules/2.6.17-1.2621.el5/vmlinux /var/crash/2006-08-23-15:34/vmcore crash> bt
載入“轉儲捕獲”內核
需要引導系統內核時,可使用如下步驟和命令載入“轉儲捕獲”內核:
kexec -p --initrd=for-dump-capture-kernel> --args-linux --append="root= init 1 irqpoll"
裝載轉儲捕捉內核的注意事項:
轉儲捕捉內核應當是一個 vmlinux 格式的映像(即是一個未壓縮的 ELF 映像文件),而不能是 bzImage 格式;
默認情況下,ELF 文件頭采用 ELF64 格式存儲以支持那些擁有超過 4GB 內存的系統。但是可以指定“--elf32-core-headers”標志以強制使用 ELF32 格式的 ELF 文件頭。這個標志是有必要注意的,一個重要的原因就是:當前版本的 GDB 不能在一個 32 位系統上打開一個使用 ELF64 格式的 vmcore 文件。ELF32 格式的文件頭不能使用在一個“沒有物理地址擴展”(non-PAE)的系統上(即:少于 4GB 內存的系統);
一個“irqpoll”的啟動參數可以減低由于在“轉儲捕獲內核”中使用了“共享中斷”技術而導致出現驅動初始化失敗這種情況發生的概率 ;
必須指定 ,指定的格式是和要使用根設備的名字。具體可以查看 mount 命令的輸出;“init 1”這個命令將啟動“轉儲捕捉內核”到一個沒有網絡支持的單用戶模式。如果你希望有網絡支持,那么使用“init 3”。
后記
Kdump 是一個強大的、靈活的內核轉儲機制,能夠在生產內核上下文中執行捕獲內核是非常有價值的。本文僅介紹在 RHEL6.2 和 SLES11 中如何配置 kdump。?
?
審核編輯 :李倩
評論
查看更多