看到一個非常好的介紹coredump的文章,做個記錄,
參考鏈接:
https://blog.csdn.net/sunxiaopengsun/article/details/72974548
什么是coredump
Coredump叫做核心轉儲,它是進程運行時在突然崩潰的那一刻的一個內存快照。操作系統在程序發生異常而異常在進程內部又沒有被捕獲的情況下,會把進程此刻內存、寄存器狀態、運行堆棧等信息轉儲保存在一個文件里。
該文件也是二進制文件,可以使用gdb、elfdump、objdump或者windows下的windebug、solaris下的mdb進行打開分析里面的具體內容。
注:core是在半導體作為內存材料前的線圈,當時用線圈當做內存材料,線圈叫做core。用線圈做的內存叫做core memory。
ulimit
雖然我們知道進程在coredump的時候會產生core文件,但是有時候卻發現進程雖然core了,但是我們卻找不到core文件。
在Linux和Solaris下是需要進行設置的。
ulimit -c 可以設置core文件的大小,如果這個值為0.則不會產生core文件,這個值太小,則core文件也不會產生,因為core文件一般都比較大。
使用ulimit -c unlimited來設置無限大,則任意情況下都會產生core文件。
可以通過修改/etc/profile文件:來永久設置文件大小和文件名和存儲位,在文件末尾添加:
ulimit -c unlimited
echo /data/coredump/core.%e.%p> /proc/sys/kernel/core_pattern
同時我們也可以通過ulimit修改棧區的size大小.默認是8M,當程序比較大時,就容易出現段錯誤.可以將其設置為200M
也是在文件末尾添加:ulimit -s 204800 重啟開發板即可.
在嵌入式開發板調試過程.
gdb ./ctest
進入gdb環境后,敲
core-file /data/coredump/core.ctest.6408
敲bt命令,這是gdb查看back trace的命令
gdb 調試coredump的簡單示例
#include "stdio.h" #include "stdlib.h" void dumpCrash() { char *pStr = "test_content"; free(pStr); } int main() { dumpCrash(); return 0; }
如上代碼,pStr指針指向的是字符串常量,字符串常量是保存在常量區的,free釋放常量區的內存肯定會導致coredump。
首先把上面的代碼拷貝到linux機器上,保存為dumpTest.c文件,gcc編譯
gcc -o dumpTestdumpTest.c
運行dumpTest產生core文件
生成core文件
如上,運行dumpTest的時候進程coredump了,但是沒有產生core文件
如截圖所示,系統設置的core文件大小為0,此時即使產生了coredump,也不會產生core文件。
如截圖所示,ulimit -c unlimited設置core文件大小后,產生了名字為core的core文件。
此時生成的core文件名稱都是統一的”core”命名。
自定義core文件的文件名
上面的設置只是使能了core dump功能,缺省情況下,內核在coredump時所產生的core文件放在與該程序相同的目錄中,並且文件名固定為core。很顯然,如果有多個程序產生core文件,或者同一個程序多次崩潰,就會重復覆蓋同一個core文件。
我們通過修改kernel的參數,可以指定內核所生成的coredump文件的文件名。例如,Easwy使用下面的命令使kernel生成名字為core_filename_time_pid格式的core dump文件:
echo /usr/core_log/core_%e_%t_%p > /proc/sys/kernel/core_pattern
echo后面內容最好不要帶上引號,有的系統會把引號也帶入,如下:
這樣,系統是不識別該內容的,也就會導致程序coredump而不會生成core文件。
如上截圖,通過設置core文件的名稱以及路徑,程序coredump的時候就會在指定路徑按照指定的規則命名生成core文件。
可以在core_pattern模板中使用變量見下面的列表:
%%單個%字符
%p所dump進程的進程ID
%u所dump進程的實際用戶ID
%g所dump進程的實際組ID
%s導致本次core dump的信號
%t core dump的時間 (由1970年1月1日計起的秒數)
%h主機名
%e程序文件名
設置永久保存
上面截圖可以看到,我后面再次執行生成coredump文件的時候實際上又再次設置了ulimit-c unlimited的,因為中間機器重啟了。上面的設置都只是臨時的,重啟之后就需要重新設置,如何設置永久生效呢?
打開/etc/security/limits.conf 文件,在該文件的最后加上兩行
#下面是我的配置
@root soft core unlimited
@root hard core unlimited
配置好后,放回原目錄,重啟reboot。
命名規則的修改在/proc/sys/kernel/core_pattern中也只是臨時的,這個也是動態加載和生成的。永久修改在/etc/sysctl.conf文件中,在該文件的最后加上兩行:
kernel.core_pattern = /var/core_log/core_%e_%t_%p
kernel.core_uses_pid = 0
可以使用以下命令,使修改結果馬上生效。
#sysctl –p
如上截圖,當前生成的core文件命名按照上面定義的規則加上了程序名稱、coredump時間,進程ID等信息,並放到了指定目錄/var/core_log
gdb調試coredump初步嘗試
gdb打開core文件的格式為
gdb程序名(包含路徑) core*(core文件名和路徑),如下截圖
如上,gdb打開core文件時,有顯示沒有調試信息,因為之前編譯的時候沒有帶上-g選項,沒有調試信息是正常的,實際上它也不影響調試core文件。因為調試core文件時,符號信息都來自符號表,用不到調試信息。如下為加上調試信息的效果。
查看coredump時的堆棧
查看堆棧使用bt或者where命令
如上,在帶上調試信息的情況下,我們實際上是可以看到core的地方和代碼行的匹配位置。
但往往正常發布環境是不會帶上調試信息的,因為調試信息通常會占用比較大的存儲空間,一般都會在編譯的時候把-g選項去掉。
沒有調試信息的情況下找core的代碼行
如上截圖,沒有調試信息的情況下,打開coredump堆棧,並不會直接顯示core的代碼行。
此時,frame addr(幀數)或者簡寫如上,f 1 跳轉到core堆棧的第1幀。因為第0幀是libc的代碼,已經不是我們自己代碼了。
disassemble打開該幀函數的反匯編代碼。
#1 0x080483ec in dumpCrash () (gdb) disassemble Dump of assembler code for function dumpCrash: 0x080483d4 <+0>: push %ebp 0x080483d5 <+1>: mov %esp,%ebp 0x080483d7 <+3>: sub $0x28,%esp 0x080483da <+6>: movl $0x80484d0,-0xc(%ebp) 0x080483e1 <+13>: mov -0xc(%ebp),%eax 0x080483e4 <+16>: mov %eax,(%esp) 0x080483e7 <+19>: call 0x80482f0 <free@plt> => 0x080483ec <+24>: leave 0x080483ed <+25>: ret End of assembler dump.
如上箭頭位置表示coredump時該函數調用所在的位置
如上截圖,shell echo free@plt |C++filt 去掉函數的名詞修飾
不過上面的free使用去掉名詞修飾效果和之前還是一樣的。但是我們可以推測到這里是在調用free函數。
如此,我們就能知道我們coredump的位置,從而進一步能推斷出coredump的原因。
當然,現實環境中,coredump的場景肯定遠比這個復雜,都是邏輯都是一樣的,我們需要先找到coredump的位置,再結合代碼以及core文件推測coredump的原因。
尋找this指針和虛指針
#include "stdio.h" #include <iostream> #include "stdlib.h" using namespace std; class base { public: base(); virtual void test(); private: char *basePStr; }; class dumpTest : public base { public: void test(); private: char *childPStr; }; base::base() { basePStr = "test_info"; } void base::test() { cout<<basePStr<<endl; } void dumpTest::test() { cout<<"dumpTest"<<endl; delete childPStr; } void dumpCrash() { char *pStr = "test_content"; free(pStr); } int main() { dumpTest dump; dump.test(); return 0; }
如上代碼,實現了一個簡單的基類和一個子類。在main函數里定義一個子類的實例化對象,並調用它的虛函數方法test,test里由於直接delete沒有初始化的指針childPStr,肯定會造成coredump。本次我們就希望通過dump文件,找到子類dumpTest的this指針和虛函數指針。
和gcc一樣,使用g++ -o DumpCppTest dumpTest.cpp編譯cpp文件生成可執行程序。
./DumpCppTest 執行該程序,程序因為直接delete未初始化的指針,肯定會coredump。生成core文件如下
如上,使用gdb打開core文件,同時bt打開core的堆棧信息。
從堆棧可以看到,最后兩幀為我們程序自己的函數,其他的都是libc的代碼。
f 6 調到第6幀上,之后info frame查看堆棧寄存器信息。
如上截圖所示,前一幀的棧寄存器地址是0xbf8cdb50,它的前一幀也就是main函數的位置,main函數里調用dump.test()的位置,那我們在這個地址上應該可以找到dump的this指針和它的虛指針,以及虛指針指向的虛函數表
如圖所示,0xbf8cdb50地址指向的是前一幀保存dump信息的位置,0xbf8cdc14bf8cdb64就表示dump的this指針,而this指針指向的第一個8字節0x0804893008048958就表示虛指針,如上,通過x 0x0804893008048958看到_ZTV8dumpTest+8的內容。
shell echo_ZTV8dumpTest|c++filt 可以看到“vtable for dumpTest”的內容。這個就表示dumpTest的虛函數表。
從上面也可以看到,這個地址指向的是虛函數表+8的偏移位置,而這個位置0x000000000804876a 通過x 0x000000000804876a 可以看到,存儲的內容就是
dumpTest::test() 函數。
這里也印證了,在繼承關系里,基類的虛函數是在子類虛函數的前面。
如上,x 0x000000000804876a-4 就可以看到dumpTest的基類base的虛函數test的位置。
如上,在實際問題中,C++程序的很多coredump問題都是和指針相關的,很多segmentfault都是由於指針被誤刪或者訪問空指針、或者越界等造成的,而這些都一般意味着正在訪問的對象的this指針可能已經被破壞了,此時,我們通過去尋找函數對應的對象的this指針、虛指針能驗證我們的推測。之后再結合代碼尋找問題所在。
gdb 查看core進程的所有線程堆棧
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; #define NUM_THREADS 5 //線程數 int count = 0; void* say_hello( void *args ) { while(1) { sleep(1); cout<<"hello..."<<endl; if(NUM_THREADS == count) { char *pStr = ""; delete pStr; } } } //函數返回的是函數指針,便於后面作為參數 int main() { pthread_t tids[NUM_THREADS]; //線程id for( int i = 0; i < NUM_THREADS; ++i ) { count = i+1; int ret = pthread_create( &tids[i], NULL, say_hello,NULL); //參數:創建的線程id,線程參數,線程運行函數的起始地址,運行函數的參數 if( ret != 0 ) //創建線程成功返回0 { cout << "pthread_create error:error_code=" << ret << endl; } } pthread_exit( NULL ); //等待各個線程退出后,進程才結束,否則進程強制結束,線程處於未終止的狀態 }
如上代碼,簡單示意C++多線程。
在linux下使用g++直接編譯該cpp文件會報錯,報錯信息如下:
會報 undefined reference to `pthread_create' 的錯誤信息,解決辦法如下:
使用 g++ -o MultiThreadDump MultiThread.cpp -lpthread 編譯,編譯參數上帶上-lpthread即可。
運行./MultiThreadDump
由於上面代碼里在count等於5的時候,會delete一個未初始化的指針,肯定會coredump。
如上,gdb打開coredump文件,能看到5個線程LWP的信息。
如何,查看每個線程的堆棧信息呢?
首先,info threads查看所有線程正在運行的指令信息
thread apply all bt打開所有線程的堆棧信息
查看指定線程堆棧信息:threadapply threadID bt,如:
thread apply 5 bt
進入指定線程棧空間
thread threadID如下:
如上截圖所示,可以跳轉到指定的線程中,並查看所在線程的正在運行的堆棧信息和寄存器信息。
總結:
如上,簡單介紹了3種不同情況下的gdb調試coredump文件的情況,基本涵蓋了調試coredump問題時的大部分會用到的gdb命令。
gdb調試coredump,大部分時候還是只能從core文件找出core的直觀原因,但是更根本的原因一般還是需要結合代碼一起分析當時進程的運行上下文場景,才能推測出程序代碼問題所在。
因此gdb調試coredump也是需要經驗的積累,只有有一定的功底和對於基礎知識的掌握才能在一堆二進制符號的core文件中找出問題的所在。
--------------------- 本文來自 sunxiaopengsun 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/sunxiaopengsun/article/details/72974548?utm_source=copy