標籤:linux panic 調試
問題描述
核心調試中最常見的一個問題是:核心Panic後,如何快速定位到出錯的程式碼?
就是這樣一個常見的問題,面試過的大部分同學都未能很好地回答,這裡希望能夠做很徹底地解答。
問題分析
核心Panic時,一般會列印回調,並列印出當前出錯的地址:
kernel/panic.c:panic():
#ifdef CONFIG_DEBUG_BUGVERBOSE /* * Avoid nested stack-dumping if a panic occurs during oops processing */ if (!test_taint(TAINT_DIE) && oops_in_progress <= 1) dump_stack(); #endif
而dump_stack()
調用關係如下:
dump_stack() --> __dump_stack() --> show_stack() --> dump_backtrace()
dump_backtrace()
會列印整個回調,例如:
[<001360ac>] (unwind_backtrace+0x0/0xf8) from [<00147b7c>] (warn_slowpath_common+0x50/0x60)[<00147b7c>] (warn_slowpath_common+0x50/0x60) from [<00147c40>] (warn_slowpath_null+0x1c/0x24)[<00147c40>] (warn_slowpath_null+0x1c/0x24) from [<0014de44>] (local_bh_enable_ip+0xa0/0xac)[<0014de44>] (local_bh_enable_ip+0xa0/0xac) from [<0019594c>] (bdi_register+0xec/0x150)
通常,上面的回調會列印出出錯的地址。
解決方案
通過分析,要快速定位出錯的程式碼,其實就是快速尋找到出錯的地址對應的代碼?
情況一
在代碼編譯串連時,每個函數都有起始地址和長度,這個地址是程式運行時的地址,而函數內部,每條指令相對於函數開始地址會有位移。那麼有了地址以後,就可以定位到該地址落在哪個函數的區間內,然後找到該函數,進而通過計算位移,定位到程式碼。
情況二
但是,如果拿到的記錄檔所在的系統版本跟當前的代碼版本不一致,那麼編譯後的地址就會有差異。那麼簡單地直接通過地址就可能找不到原來的位置,這個就可能需要回調裡頭的函數名資訊。先通過函數名定位到所在函數,然後通過位移定位到程式碼。
相應的工具有addr2line, gdb, objdump等,這幾個工具在[How to read a Linux kernel panic?](http://stackoverflow.com/questions/13468286/how-to-read-a-linux-kernel-panic)都有介紹,我們將針對上面的執行個體做更具體的分析。需要提到的是,代碼的實際運行是不需要符號的,只需要地址就行。所以如果要調試代碼,必須確保偵錯符號已經編譯到核心中,不然,回調裡頭列印的是一堆地址,根本看不到符號,那麼對於上面提到的情況二而言,將無法準確定位問題。如果要擷取到足夠多的調試資訊,請根據需要開啟如下選項:CONFIG_KALLSYMS=yCONFIG_KALLSYMS_ALL=yCONFIG_DEBUG_BUGVERBOSE=yCONFIG_STACKTRACE=y
下面分別介紹各種用法。
addr2line
如果出錯的核心跟當前需要調試的核心一致,而且編譯器等都一致,那麼可以通過addr2line直接擷取到出錯的程式碼,假設出錯地址為0019594c:
$ addr2line -e vmlinux_with_debug_info 0x0019594cmm/backing-dev.c:335
然後用vim就可以直接找到代碼出錯的位置:
$ vim mm/backing-dev.c +335
如果是情況二,可以先通過nm擷取到當前的vmlinux中bdi_register
函數的真實位置。
$ nm vmlinux | grep bdi_register0x00195860 T bdi_register
然後,加上0xec的位移,即可算出真真實位址:
$ echo "obase=16;ibase=10;$((0x00195860+0xec))" | bc -l19594C
gdb
這個也適用情況二,因為可以直接用 符號+位移 的方式,因此,即使其他地方有改動,這個相對的位置是不變的。
$ gdb vmlinux_with_debug_info$ list *(bdi_register+0xec)0x0019594c is in bdi_register (/path/to/mm/backing-dev.c:335).330 bdi->dev = dev;331332 bdi_debug_register(bdi, dev_name(dev));333 set_bit(BDI_registered, &bdi->state);334335 spin_lock_bh(&bdi_lock);336 list_add_tail_rcu(&bdi->bdi_list, &bdi_list);337 spin_unlock_bh(&bdi_lock);338339 trace_writeback_bdi_register(bdi);
如果是情況一,則可以直接用地址:list *0x0019594c
。
objdump
如果是情況一,直接用地址dump出來。咱們回頭看一下Backtrace資訊:bdi_register+0xec/0x150
,這裡的0xec是位移,而0x150是該函數的大小。用objdump預設可以擷取整個vmlinux的代碼,但是咱們其實只擷取一部分,這個可以通過--start-address
和--stop-address
來指定。另外-d
可以彙編代碼,-S
則可以併入原始碼。
$ objdump -dS vmlinux_with_debug_info --start-address=0x0019594c --end-address=$((0x0019594c+0x150))
如果是情況二,也可以跟addr2line一樣先算出真真實位址,然後再通過上面的方法匯出。
總地來看,gdb還是來得簡單方便,無論是情況下和情況二都適用,而且很快捷地就顯示出了出錯的代碼位置,並且能夠顯示代碼的內容。
對於使用者態來說,分析的方式類似。如果要在應用中擷取Backtrace,可以參考Generating backtraces。其例子如下:
#include <execinfo.h>#define BACKTRACE_SIZ 64void show_backtrace (void){ void *array[BACKTRACE_SIZ]; size_t size, i; char **strings; size = backtrace(array, BACKTRACE_SIZ); strings = backtrace_symbols(array, size); for (i = 0; i < size; i++) { printf("%p : %s\n", array[i], strings[i]); } free(strings); // malloced by backtrace_symbols}
編譯代碼時需要加上:-funwind-tables
,-g
和-rdynamic
。
如何快速定位 Linux Panic 出錯的程式碼