如何快速定位Linux Panic出錯(cuò)的代碼行 問題描述 內(nèi)核調(diào)試中最常見的一個(gè)問題是:內(nèi)核Panic后,,如何快速定位到出錯(cuò)的代碼行,? 就是這樣一個(gè)常見的問題,面試過的大部分同學(xué)都未能很好地回答,,這里希望能夠做很徹底地解答,。 問題分析 內(nèi)核Panic時(shí),,一般會(huì)打印回調(diào),并打印出當(dāng)前出錯(cuò)的地址: 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() 調(diào)用關(guān)系如下: | dump_stack() --> __dump_stack() --> show_stack() --> dump_backtrace() | dump_backtrace() 會(huì)打印整個(gè)回調(diào),,例如: | [<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) | 通常,,上面的回調(diào)會(huì)打印出出錯(cuò)的地址。 解決方案 通過分析,,要快速定位出錯(cuò)的代碼行,,其實(shí)就是快速查找到出錯(cuò)的地址對(duì)應(yīng)的代碼? 情況一 在代碼編譯連接時(shí),,每個(gè)函數(shù)都有起始地址和長(zhǎng)度,,這個(gè)地址是程序運(yùn)行時(shí)的地址,而函數(shù)內(nèi)部,,每條指令相對(duì)于函數(shù)開始地址會(huì)有偏移,。那么有了地址以后,就可以定位到該地址落在哪個(gè)函數(shù)的區(qū)間內(nèi),,然后找到該函數(shù),,進(jìn)而通過計(jì)算偏移,定位到代碼行,。 情況二 但是,,如果拿到的日志文件所在的系統(tǒng)版本跟當(dāng)前的代碼版本不一致,那么編譯后的地址就會(huì)有差異,。那么簡(jiǎn)單地直接通過地址就可能找不到原來的位置,,這個(gè)就可能需要回調(diào)里頭的函數(shù)名信息。先通過函數(shù)名定位到所在函數(shù),,然后通過偏移定位到代碼行,。 相應(yīng)的工具有addr2line, gdb, objdump等,這幾個(gè)工具在How to read a Linux kernel panic?都有介紹,,我們將針對(duì)上面的實(shí)例做更具體的分析,。 需要提到的是,代碼的實(shí)際運(yùn)行是不需要符號(hào)的,,只需要地址就行,。所以如果要調(diào)試代碼,,必須確保調(diào)試符號(hào)已經(jīng)編譯到內(nèi)核中,,不然,回調(diào)里頭打印的是一堆地址,,根本看不到符號(hào),,那么對(duì)于上面提到的情況二而言,將無法準(zhǔn)確定位問題,。 如果要獲取到足夠多的調(diào)試信息,,請(qǐng)根據(jù)需要打開如下選項(xiàng): | CONFIG_KALLSYMS=y CONFIG_KALLSYMS_ALL=y CONFIG_DEBUG_BUGVERBOSE=y CONFIG_STACKTRACE=y | 下面分別介紹各種用法。 addr2line 如果出錯(cuò)的內(nèi)核跟當(dāng)前需要調(diào)試的內(nèi)核一致,而且編譯器等都一致,,那么可以通過addr2line直接獲取到出錯(cuò)的代碼行,,假設(shè)出錯(cuò)地址為0019594c: | $ addr2line -e vmlinux_with_debug_info 0x0019594c mm/backing-dev.c:335 | 然后用vim就可以直接找到代碼出錯(cuò)的位置: | $ vim mm/backing-dev.c +335 | 如果是情況二,可以先通過nm獲取到當(dāng)前的vmlinux中bdi_register 函數(shù)的真實(shí)位置,。 | $ nm vmlinux | grep bdi_register 0x00195860 T bdi_register | 然后,,加上0xec的偏移,即可算出真實(shí)地址: | $ echo "obase=16;ibase=10;$((0x00195860+0xec))" | bc -l 19594C | gdb 這個(gè)也適用情況二,,因?yàn)榭梢灾苯佑?符號(hào)+偏移 的方式,,因此,即使其他地方有改動(dòng),,這個(gè)相對(duì)的位置是不變的,。 | $ 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; 331 332 bdi_debug_register(bdi, dev_name(dev)); 333 set_bit(BDI_registered, &bdi->state); 334 335 spin_lock_bh(&bdi_lock); 336 list_add_tail_rcu(&bdi->bdi_list, &bdi_list); 337 spin_unlock_bh(&bdi_lock); 338 339 trace_writeback_bdi_register(bdi); | 如果是情況一,則可以直接用地址:list *0x0019594c ,。 objdump 如果是情況一,,直接用地址dump出來。咱們回頭看一下Backtrace信息:bdi_register+0xec/0x150 ,,這里的0xec是偏移,,而0×150是該函數(shù)的大小。用objdump默認(rèn)可以獲取整個(gè)vmlinux的代碼,,但是咱們其實(shí)只獲取一部分,,這個(gè)可以通過--start-address 和--stop-address 來指定。另外-d 可以匯編代碼,,-S 則可以并入源代碼,。 | $ objdump -dS vmlinux_with_debug_info --start-address=0x0019594c --end-address=$((0x0019594c+0x150)) | 如果是情況二,也可以跟addr2line一樣先算出真實(shí)地址,,然后再通過上面的方法導(dǎo)出,。 總地來看,gdb還是來得簡(jiǎn)單方便,,無論是情況下和情況二都適用,,而且很快捷地就顯示出了出錯(cuò)的代碼位置,并且能夠顯示代碼的內(nèi)容,。 對(duì)于用戶態(tài)來說,,分析的方式類似。如果要在應(yīng)用中獲取Backtrace,,可以參考Generating backtraces,。其例子如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <execinfo.h> #define BACKTRACE_SIZ 64 void 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 } | 編譯代碼時(shí)需要加上:-funwind-tables ,-g 和-rdynamic ,。
|