什么是coredump
Coredump叫做核心轉儲,它是進程運行時在突然崩潰的那一刻的一個內存快照。操作系統(tǒng)在程序發(fā)生異常而異常在進程內部又沒有被捕獲的情況下,會把進程此刻內存、寄存器狀態(tài)、運行堆棧等信息轉儲保存在一個文件里。
該文件也是二進制文件,可以使用gdb、elfdump、objdump或者windows下的windebug、solaris下的mdb進行打開分析里面的具體內容。
注:core是在半導體作為內存材料前的線圈,當時用線圈當做內存材料,線圈叫做core。用線圈做的內存叫做core memory。
ulimit
雖然我們知道進程在coredump的時候會產生core文件,但是有時候卻發(fā)現(xiàn)進程雖然core了,但是我們卻找不到core文件。
在linux和Solaris下是需要進行設置的。
ulimit -c 可以設置core文件的大小,如果這個值為0.則不會產生core文件,這個值太小,則core文件也不會產生,因為core文件一般都比較大。
使用ulimit -c unlimited來設置無限大,則任意情況下都會產生core文件。
Windows下miniDump和FullDump的設置
Windows下需要在下面的注冊表,
[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsWindowsError Reporting]
下面加一項LocalDumps,并做如下項配置:
上面的配置信息,可以通過打開注冊表來手動添加。
首先,打開cmd或者運行程序(微軟圖標+R)
如上截圖,可以通過圖形界面手動添加這些注冊表信息,然后windows系統(tǒng)在有進程crash的時候就會保存fulldump的文件。
或者通過reg文件的方式來進行注冊
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsWindows Error ReportingLocalDumps]
"DumpFolder"="F:\study_Test\Dump"
"DumpCount"=dword:a
"DumpType"=dword:1
如上,通過新建一個fulldump.reg的文件,保存上面內容,雙擊后,這些信息就會注冊到注冊表中。
gdb 調試coredump的簡單示例
#include "stdio.h"
#include "stdlib.h"
void dumpCrash()
{
char *pStr = "test_content";
free(pStr);
}
int main()
{
dumpCrash();
return 0;
}
如上代碼,pStr指針指向的是字符串常量,字符串常量是保存在常量區(qū)的,free釋放常量區(qū)的內存肯定會導致coredump。
首先把上面的代碼拷貝到linux機器上,保存為dumpTest.c文件,gcc編譯
gcc -o dumpTestdumpTest.c
運行dumpTest產生core文件
生成core文件
如上,運行dumpTest的時候進程coredump了,但是沒有產生core文件
如截圖所示,系統(tǒng)設置的core文件大小為0,此時即使產生了coredump,也不會產生core文件。
如截圖所示,ulimit -c unlimited設置core文件大小后,產生了名字為core的core文件。
此時生成的core文件名稱都是統(tǒng)一的”core”命名。
自定義core文件的文件名
上面的設置只是使能了core dump功能,缺省情況下,內核在coredump時所產生的core文件放在與該程序相同的目錄中,并且文件名固定為core。很顯然,如果有多個程序產生core文件,或者同一個程序多次崩潰,就會重復覆蓋同一個core文件。
我們通過修改kernel的參數(shù),可以指定內核所生成的coredump文件的文件名。例如,Easwy使用下面的命令使kernel生成名字為core_filename_time_pid格式的core dump文件:
echo /usr/core_log/core_%e_%t_%p > /proc/sys/kernel/core_pattern
echo后面內容最好不要帶上引號,有的系統(tǒng)會把引號也帶入,如下:
這樣,系統(tǒng)是不識別該內容的,也就會導致程序coredump而不會生成core文件。
如上截圖,通過設置core文件的名稱以及路徑,程序coredump的時候就會在指定路徑按照指定的規(guī)則命名生成core文件。
可以在core_pattern模板中使用變量見下面的列表:
%%單個%字符
%p所dump進程的進程ID
%u所dump進程的實際用戶ID
%g所dump進程的實際組ID
%s導致本次core dump的信號
%t core dump的時間 (由1970年1月1日計起的秒數(shù))
%h主機名
%e程序文件名
設置永久保存
上面截圖可以看到,我后面再次執(zhí)行生成coredump文件的時候實際上又再次設置了ulimit-c unlimited的,因為中間機器重啟了。上面的設置都只是臨時的,重啟之后就需要重新設置,如何設置永久生效呢?
打開/etc/security/limits.conf 文件,在該文件的最后加上兩行
#下面是我的配置
@root soft core unlimited
@root hard core unlimited
配置好后,放回原目錄,重啟reboot。
命名規(guī)則的修改在/proc/sys/kernel/core_pattern中也只是臨時的,這個也是動態(tài)加載和生成的。永久修改在/etc/sysctl.conf文件中,在該文件的最后加上兩行:
kernel.core_pattern = /var/core_log/core_%e_%t_%p
kernel.core_uses_pid = 0
可以使用以下命令,使修改結果馬上生效。
#sysctl –p
如上截圖,當前生成的core文件命名按照上面定義的規(guī)則加上了程序名稱、coredump時間,進程ID等信息,并放到了指定目錄/var/core_log
gdb調試coredump初步嘗試
gdb打開core文件的格式為
gdb程序名(包含路徑) core*(core文件名和路徑),如下截圖
如上,gdb打開core文件時,有顯示沒有調試信息,因為之前編譯的時候沒有帶上-g選項,沒有調試信息是正常的,實際上它也不影響調試core文件。因為調試core文件時,符號信息都來自符號表,用不到調試信息。如下為加上調試信息的效果。
查看coredump時的堆棧
查看堆棧使用bt或者where命令
如上,在帶上調試信息的情況下,我們實際上是可以看到core的地方和代碼行的匹配位置。
但往往正常發(fā)布環(huán)境是不會帶上調試信息的,因為調試信息通常會占用比較大的存儲空間,一般都會在編譯的時候把-g選項去掉。
沒有調試信息的情況下找core的代碼行
如上截圖,沒有調試信息的情況下,打開coredump堆棧,并不會直接顯示core的代碼行。
此時,frame addr(幀數(shù))或者簡寫如上,f 1 跳轉到core堆棧的第1幀。因為第0幀是libc的代碼,已經不是我們自己代碼了。
disassemble打開該幀函數(shù)的反匯編代碼。
#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
=> 0x080483ec <+24>: leave
0x080483ed <+25>: ret
End of assembler dump.
如上箭頭位置表示coredump時該函數(shù)調用所在的位置
如上截圖,shell echo free@plt |c++filt 去掉函數(shù)的名詞修飾
不過上面的free使用去掉名詞修飾效果和之前還是一樣的。但是我們可以推測到這里是在調用free函數(shù)。
如此,我們就能知道我們coredump的位置,從而進一步能推斷出coredump的原因。
當然,現(xiàn)實環(huán)境中,coredump的場景肯定遠比這個復雜,都是邏輯都是一樣的,我們需要先找到coredump的位置,再結合代碼以及core文件推測coredump的原因。
尋找this指針和虛指針
#include "stdio.h"
#include
#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<
}
void dumpTest::test()
{
cout<<"dumpTest"<
delete childPStr;
}
void dumpCrash()
{
char *pStr = "test_content";
free(pStr);
}
int main()
{
dumpTest dump;
dump.test();
return 0;
}
如上代碼,實現(xiàn)了一個簡單的基類和一個子類。在main函數(shù)里定義一個子類的實例化對象,并調用它的虛函數(shù)方法test,test里由于直接delete沒有初始化的指針childPStr,肯定會造成coredump。本次我們就希望通過dump文件,找到子類dumpTest的this指針和虛函數(shù)指針。
和gcc一樣,使用g++ -o DumpCppTest dumpTest.cpp編譯cpp文件生成可執(zhí)行程序。
./DumpCppTest 執(zhí)行該程序,程序因為直接delete未初始化的指針,肯定會coredump。生成core文件如下
如上,使用gdb打開core文件,同時bt打開core的堆棧信息。
從堆??梢钥吹?,最后兩幀為我們程序自己的函數(shù),其他的都是libc的代碼。
f 6 調到第6幀上,之后info frame查看堆棧寄存器信息。
如上截圖所示,前一幀的棧寄存器地址是0xbf8cdb50,它的前一幀也就是main函數(shù)的位置,main函數(shù)里調用dump.test()的位置,那我們在這個地址上應該可以找到dump的this指針和它的虛指針,以及虛指針指向的虛函數(shù)表
如圖所示,0xbf8cdb50地址指向的是前一幀保存dump信息的位置,0xbf8cdc14bf8cdb64就表示dump的this指針,而this指針指向的第一個8字節(jié)0x0804893008048958就表示虛指針,如上,通過x 0x0804893008048958看到_ZTV8dumpTest+8的內容。
shell echo_ZTV8dumpTest|c++filt 可以看到“vtable for dumpTest”的內容。這個就表示dumpTest的虛函數(shù)表。
從上面也可以看到,這個地址指向的是虛函數(shù)表+8的偏移位置,而這個位置0x000000000804876a 通過x 0x000000000804876a 可以看到,存儲的內容就是
dumpTest::test() 函數(shù)。
這里也印證了,在繼承關系里,基類的虛函數(shù)是在子類虛函數(shù)的前面。
如上,x 0x000000000804876a-4 就可以看到dumpTest的基類base的虛函數(shù)test的位置。
如上,在實際問題中,C++程序的很多coredump問題都是和指針相關的,很多segmentfault都是由于指針被誤刪或者訪問空指針、或者越界等造成的,而這些都一般意味著正在訪問的對象的this指針可能已經被破壞了,此時,我們通過去尋找函數(shù)對應的對象的this指針、虛指針能驗證我們的推測。之后再結合代碼尋找問題所在。
gdb 查看core進程的所有線程堆棧
#include
#include
#include
using namespace std;
#define NUM_THREADS 5 //線程數(shù)
int count = 0;
void* say_hello( void *args )
{
while(1)
{
sleep(1);
cout<<"hello..."<
if(NUM_THREADS == count)
{
char *pStr = "";
delete pStr;
}
}
} //函數(shù)返回的是函數(shù)指針,便于后面作為參數(shù)
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); //參數(shù):創(chuàng)建的線程id,線程參數(shù),線程運行函數(shù)的起始地址,運行函數(shù)的參數(shù)
if( ret != 0 ) //創(chuàng)建線程成功返回0
{
cout << "pthread_create error:error_code=" << ret << endl;
}
}
pthread_exit( NULL ); //等待各個線程退出后,進程才結束,否則進程強制結束,線程處于未終止的狀態(tài)
}
如上代碼,簡單示意C++多線程。
在linux下使用g++直接編譯該cpp文件會報錯,報錯信息如下:
會報 undefined reference to `pthread_create' 的錯誤信息,解決辦法如下:
使用 g++ -o MultiThreadDump MultiThread.cpp -lpthread 編譯,編譯參數(shù)上帶上-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文件中找出問題的所在。
歡迎工作一到五年的Java工程師朋友們加入Java架構開發(fā): 854393687
群內提供免費的Java架構學習資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!