gdb除錯coredump(使用篇)

什麼是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檔案。

 

Windows下miniDump和FullDump的設定

 

Windows下需要在下面的登錄檔,

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\WindowsError Reporting]

下面加一項LocalDumps,並做如下項配置:

Value

描述

Type

預設值

DumpFolder

檔案儲存路徑

REG_EXPAND_SZ

%LOCALAPPDATA%CrashDumps

DumpCount

dump檔案的最大數目

REG_DWORD

10

DumpType

指定生成的dump型別:
0:Custom dump
1:Mini dump
2:Full dump

REG_DWORD

1

CustomDumpFlags

僅在DumpType為0時使用
為MINIDUMP_TYPE的組合

REG_DWORD

MiniDumpWithDataSegs|
MiniDumpWithUnloadedModules|
MiniDumpWithProcessThreadData

上面的配置資訊,可以通過開啟登錄檔來手動新增。

首先,開啟cmd或者執行程式(微軟圖示 R)

 

如上截圖,可以通過圖形介面手動新增這些登錄檔資訊,然後windows系統在有程序crash的時候就會儲存fulldump的檔案。

 

或者通過reg檔案的方式來進行註冊

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps]
"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指標指向的是字串常量,字串常量是儲存在常量區的,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 <[email protected]>
=> 0x080483ec < 24>:    leave
   0x080483ed < 25>:    ret
End of assembler dump.

 

如上箭頭位置表示coredump時該函式呼叫所在的位置

如上截圖,shell echo [email protected] |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檔案中找出問題的所在。