使用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