1. 程式人生 > >使用gdb除錯c++程式

使用gdb除錯c++程式

 

上篇(使用c++開發跨平臺程式)說到,我不怕造東西,我怕的是造出來的東西,如果出了問題,我卻不知道原因.所以除錯分析是一個重要的手段.

C++除錯是一個複雜的活.雖然大部分除錯可以通過IDE在開發期間就解決了.但是必然的,還有很多東西需要在生產環境中還原它.分析它,然後解決它.gdb是一個成熟的工具.圍繞著它有很多的工具可以選擇.不過這麼多工具的根本還是命令列模式下的gdb.

廢話不多說,現在我就用gdb來分析除錯一下吧.

 

生成dump檔案:

在shell中輸入命令:

ulimit -c unlimited;

然後執行自己的程式,如果程式此時崩潰,就會在目錄生成一個名為core的檔案.(這個也看系統配置.)

使用命令 gdb Test1 core載入檔案.或者它的詳細命令 gdb -c core -e Test1 --symbols Test1 --readnow

 

下面是一個命令列輸出的截圖:

上圖中可以解釋的不多.因為我們現在剛要入門.所以只能注意上圖中的三個紅框.

紅框1:命令列其中app7是可執行檔案,而core是dump檔案.

紅框2:標明gdb在app7中找到了它對應的symbol.

紅框3:標明core檔案是經由app7產生的.這裡是為了防止載入了錯誤的可執行檔案.

 

注意一下幾點:

如果使用sanitize,請取消.不然不會在崩潰時產生dump檔案.反而是一個錯誤報告.

在生成可執行檔案的時候,應該用debug模式,也可以用RelWithDebInfo模式.主要目的是能夠獲得程式的除錯符號.

如果沒有symbol資訊,也可以除錯,但是過程將會難上很多倍,畢竟我們是除錯,不是破解.不過,還別說,gdb除錯跟破解其實還是有點相通的.

由於gdb除錯有非常多指令.從時效性上來說,不需要記住全部指令.只需要知道常用的指令就好.就算有人費事費力記住了所有指令,時間一長,如果不用的話也是會忘記的.所以能看到英文文件,我覺得比記住指令更有用.

大部分錯誤在IDE開發期間就已經被解決了.需要除錯core dump檔案的情況一般都是執行的時候出現的錯誤,我這裡簡單介紹以下幾類

指標為NULL.棧溢位,除數為0,死鎖.

除錯指標為NULL

下面給定一個程式,程式的內容如下:

#include <stdlib.h>
void bar(int* p)
{
    int aa=*p;
}
void foo()
{
    int* p=NULL;
    bar(p);
}
int main(int argc, const char * argv[])
{
    foo();
    return 0;
}

  

編譯後假設輸出是app0,執行app0後會有core檔案.現在我來載入這個core檔案.截圖如下:

載入完畢以後,可以看到gdb已經指出來了app0.cpp地15行有問題.

然後我們回到原始碼,檢視第15行,的確是有問題.所有null問題已經解決.是不是簡單無比?呵呵.但是我們要更進一.看看到底為什麼.

1.       我使用p p,(第一個p是print,是gdb指令,第二個p是引數p);

這說明p是一個0.所以這裡會出錯.

2.       按理說,以上的分析可以得出結論了.不過這裡我想再進一步.

首先我列出 所有執行緒

info thread

 

 

 

就只有一個執行緒,很好.

其次,我看看堆疊

bt

 

 

可以看到呼叫堆疊,是從foo函式呼叫的bar函式.所以引數p是從foo裡產生的.

   可以看出,空引用雖然解決了,回頭考慮一下的話,這裡有點事後諸葛的意思.有人會問”你是已經事先知道空引用了.然後去分析的,這誰不會…”,真正的現實當中的空引用的確分析起來比這個困難一點.不過這個系列是讓人們基本會用gdb.知道每種型別大體長什麼樣子.在現實問題中,分析的時候好有個方向.具體工作當中的問題.只能到時再分析.

 

除錯棧溢位

棧溢位一般遞迴函式退出條件沒有達成,導致的迴圈呼叫.棧溢位除錯比較簡單,特徵也很明顯.

下面我借用一個例子來說明一下.這個例子的作者是一個外國人,具體是誰.我忘記了.

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void procF(int i)
{
    int buffer[128] = {-1, 0, i+1, 0, -1};
    procF(buffer[2]);
}
void procE()
{
    procF(1);
}
#define THREAD_DECLARE(num,func) void bar_##num()\
{\
sleep(3);\
func;\
}\
\
void foo_##num()\
{\
bar_##num();\
}\
\
void * thread_##num (void *arg)\
{\
foo_##num();\
\
return 0;\
}
THREAD_DECLARE(one,procE())
THREAD_DECLARE(two,sleep(-1))
THREAD_DECLARE(three,sleep(-1))
THREAD_DECLARE(four,sleep(-1))
THREAD_DECLARE(five,sleep(-1))
#define THREAD_CREATE(num) {pthread_t threadID_##num; pthread_create (&threadID_##num, NULL,thread_##num, NULL);}
int main(int argc, const char * argv[])
{
    THREAD_CREATE(one)
    THREAD_CREATE(two)
    THREAD_CREATE(three)
    THREAD_CREATE(four)
    THREAD_CREATE(five)
    sleep(-1);
    return 0;
}

 

  

 

以上檔案很簡單,定義了一個巨集,然後使用這個巨集,複製生成了5個執行緒.其中thread_one這個執行緒,會陷入死迴圈.它會在procF中迴圈呼叫,導致一個堆疊溢位.

我們來看看它長什麼樣子.具體怎麼載入core我這裡就略過了.直接看gdb內容吧.

上面說cannot access memory at address xxx,然後列出最近執行具體位置是一個大括號,沒有什麼參考意義

1.       我先看看所有執行緒

6個執行緒,除去第一個是不能能讀取記憶體的錯誤以為,其餘的都在sleep.這裡按照gdb的提示(它說procF有問題),我先看看thread 1,因為只有它停留在了procF;

2.       指令thread 1 表示切換到執行緒1.然後檢視它的堆疊,看看是如何到達這個procF的.

到這裡發現procF自己呼叫自己,按照經驗,這裡應該是棧溢位了.但是為了確認一下,我決定看看它呼叫了多少層.

3.       指令 bt是列印呼叫堆疊了.bt -20是列印最底層的20個呼叫

發現它呼叫了15000次..這裡還有一個好處就是,可以看到來源.是從procE來的.

下一步就可以去檢視proceE的內容了.在gdb中也是可以做到的,如下圖

好了,到此呼叫棧溢位就解決了.

 

但是,還是可以在這裡展開一下.我們知道函式的呼叫是放置線上程的佔空間的.我們從佔空間中看看,有沒有什麼規律.

為了顯示棧空間,需要用到gdb的一個指令x(檢視)

詳細觀察 bt -20返回的結果,可以看到類似如下

 

#14971 0x00005636f87b2c91 in procF (i=1) at /root/clionproject/Test1/dump/app6.cpp:16

#14972 0x00005636f87b2cb6 in procE () at /root/clionproject/Test1/dump/app6.cpp:20

其中#14971是frame的編號.

後邊的0x00005636f87b2c91,是程式碼在記憶體中的位置.即app6.cpp:16這行所對應的二進位制程式碼就在記憶體的此位置

gdb搞起

 

p $rsp 和 info r $rsp 代表列印暫存器rsp裡面的值. $rsp是指向棧頂端的暫存器.所以它的值就一定是棧頂端.

我來檢查一下這個棧.

這裡主要是引出x指令.x是檢視指定地址的指令.

 

除數為0

除數為0是一個簡單的問題.程式碼我就不上了.

載入core檔案就會顯示

說這是一個算術問題.發生在procD函式中

現在我檢查一下procD是什麼東西

Disass是disassembly 的意思,指令是列印對應地址的反彙編程式碼.

上圖中紅框處,就是現在指令所執行的位置.系統認為在這個位置出錯了.看idivl 它顯然是一個除法.到這裡十有八九是除數為零了.

看到彙編指令idivl  -0x8(%rbp),其中的-x8(%rbp),代表一個值,這個值就是除數.所以我要把它代表的值找到.

首先檢視一下 rbp是什麼東東,rbp是一個暫存器,它指向了一個base point,你可以簡單的認為所有函式內部申請的棧變數(比如 int a=0等等),都是通過rbp換算的.

其次檢視一下這個地址-8是啥.

既然$rbp-0x8是一個變數的地址,那麼我們就看看這個地址寫的什麼值.

可以看到它的確是寫的0.

 

除數為0,就結束了.

 

 

死鎖

死鎖也是一個常見的問題,不過死鎖有個特點,並不是每一個死鎖都會被dump下來.所以在遇到死鎖的時候,有時候需要使用線上除錯的辦法,不過這個辦法.

現在我使用以下程式碼  

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <mutex>

static int sequence1 = 0;
static int sequence2 = 0;

std::mutex lock1;
std::mutex lock2;

int func1()
{
    lock1.lock();
    sleep(1);
    sequence1++;
    lock2.lock();
    sequence2++;
    lock1.unlock();
    lock2.unlock();
    return sequence1;
}

int func2()
{
    lock2.lock();
    sleep(1);
    sequence2++;
    lock1.lock();
    sequence1++;
    lock2.unlock();
    lock1.unlock();
    return sequence1;
}


void* thread1(void *arg)
{
    int rev = 0;
    while(1)
    {
        rev = func1();

        if (rev == 100000)
        {
            pthread_exit(NULL);
        }
    }
}

void* thread2(void *arg)
{
    int rev = 0;
    while(1)
    {
        rev = func2();

        if (rev == 100000)
        {
            pthread_exit(NULL);
        }
    }
}

void* thread3(void *arg)
{
    int count = 0;
    while(1)
    {
        sleep(1);
        if ( count++ > 10000)
        {
            pthread_exit(NULL);
        }
    }
}

void* thread4(void *arg)
{
    int count = 0;
    while(1)
    {
        sleep(1);
        if ( count++ > 10000)
        {
            pthread_exit(NULL);
        }
    }
}



int main()
{
    pthread_t tid[4];
    if(pthread_create(&tid[0], NULL, & thread1, NULL) != 0)
    {
        _exit(1);
    }
    if(pthread_create(&tid[1], NULL, & thread2, NULL) != 0)
    {
        _exit(1);
    }

    if(pthread_create(&tid[2], NULL, & thread3, NULL) != 0)
    {
        _exit(1);
    }

    if(pthread_create(&tid[3], NULL, & thread4, NULL) != 0)
    {
        _exit(1);
    }
    sleep(5);
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);
    return 0;
}

 

 

  

以上程式碼主要集中在func1和func2中,他們相互等待就有可能會死鎖.現在我編譯它執行以下.

由於它只是死鎖,所有在我機器上並沒有dump下來,我要用gdb,線上除錯它.截圖如下

我先用ps找到了程序id是14661,用gdb 附著了它,現在開始除錯了.

先看thread

這兩個執行緒有可能死鎖.先看看 thread 2 是如何呼叫的.呼叫堆疊搞起.

它是呼叫了func1,我看看func1的內容

它提示有兩個變數分別是lock1和lock2.所以我想看看這兩個變數

提示,這兩個鎖,被不同的執行緒持有.

再回頭看看thread 2 的呼叫堆疊

可以看到,它提示執行緒14662 停在了lock2.lock()方法那裡了這個執行緒想要獲得鎖的所有權. 而lock2,按照上一個截圖,已經被14663持有了.

用相同的辦法也可以得到lock1的細節.我這裡就不復述了.

 

所以這個死鎖就被我找到了原因.

 

小結

真正現實當中遇到的問題,不會像這樣就很快的被找到.因為這裡是創造問題然後去解決,有點事後諸葛的意思.比如現實當中的死鎖,找到對應的鎖變數這一步就不會那麼容易,需要耐心和運氣,不過使用gdb的第一步就是首先熟悉出問題的時候大體的呼叫堆疊模式,然後再去嘗試可能的出錯方向,進而解決它.如果只是記得冷冰冰gdb指令,在我眼裡就如同多記住了幾個英文單詞而已,我認為意義不大.

&n