上篇(使用c++開發(fā)跨平臺程序)說到,我不怕造東西,我怕的是造出來的東西,如果出了問題,我卻不知道原因.所以調(diào)試分析是一個重要的手段.
C++調(diào)試是一個復(fù)雜的活.雖然大部分調(diào)試可以通過IDE在開發(fā)期間就解決了.但是必然的,還有很多東西需要在生產(chǎn)環(huán)境中還原它.分析它,然后解決它.gdb是一個成熟的工具.圍繞著它有很多的工具可以選擇.不過這么多工具的根本還是命令行模式下的gdb.
廢話不多說,現(xiàn)在我就用gdb來分析調(diào)試一下吧.
生成dump文件:
在shell中輸入命令:
ulimit -c unlimited;
然后運行自己的程序,如果程序此時崩潰,就會在目錄生成一個名為core的文件.(這個也看系統(tǒng)配置.)
使用命令 gdb Test1 core加載文件.或者它的詳細命令 gdb -c core -e Test1 --symbols Test1 --readnow
下面是一個命令行輸出的截圖:
上圖中可以解釋的不多.因為我們現(xiàn)在剛要入門.所以只能注意上圖中的三個紅框.
紅框1:命令行其中app7是可執(zhí)行文件,而core是dump文件.
紅框2:標(biāo)明gdb在app7中找到了它對應(yīng)的symbol.
紅框3:標(biāo)明core文件是經(jīng)由app7產(chǎn)生的.這里是為了防止載入了錯誤的可執(zhí)行文件.
注意一下幾點:
如果使用sanitize,請取消.不然不會在崩潰時產(chǎn)生dump文件.反而是一個錯誤報告.
在生成可執(zhí)行文件的時候,應(yīng)該用debug模式,也可以用RelWithDebInfo模式.主要目的是能夠獲得程序的調(diào)試符號.
如果沒有symbol信息,也可以調(diào)試,但是過程將會難上很多倍,畢竟我們是調(diào)試,不是破解.不過,還別說,gdb調(diào)試跟破解其實還是有點相通的.
由于gdb調(diào)試有非常多指令.從時效性上來說,不需要記住全部指令.只需要知道常用的指令就好.就算有人費事費力記住了所有指令,時間一長,如果不用的話也是會忘記的.所以能看到英文文檔,我覺得比記住指令更有用.
大部分錯誤在IDE開發(fā)期間就已經(jīng)被解決了.需要調(diào)試core dump文件的情況一般都是運行的時候出現(xiàn)的錯誤,我這里簡單介紹以下幾類
指針為NULL.棧溢出,除數(shù)為0,死鎖.
調(diào)試指針為NULL
下面給定一個程序,程序的內(nèi)容如下:
#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;
}
編譯后假設(shè)輸出是app0,運行app0后會有core文件.現(xiàn)在我來加載這個core文件.截圖如下:
加載完畢以后,可以看到gdb已經(jīng)指出來了app0.cpp地15行有問題.
然后我們回到源碼,查看第15行,的確是有問題.所有null問題已經(jīng)解決.是不是簡單無比?呵呵.但是我們要更進一.看看到底為什么.
1. 我使用p p,(第一個p是print,是gdb指令,第二個p是參數(shù)p);
這說明p是一個0.所以這里會出錯.
2. 按理說,以上的分析可以得出結(jié)論了.不過這里我想再進一步.
首先我列出 所有線程
info thread
就只有一個線程,很好.
其次,我看看堆棧
bt
可以看到調(diào)用堆棧,是從foo函數(shù)調(diào)用的bar函數(shù).所以參數(shù)p是從foo里產(chǎn)生的.
可以看出,空引用雖然解決了,回頭考慮一下的話,這里有點事后諸葛的意思.有人會問”你是已經(jīng)事先知道空引用了.然后去分析的,這誰不會…”,真正的現(xiàn)實當(dāng)中的空引用的確分析起來比這個困難一點.不過這個系列是讓人們基本會用gdb.知道每種類型大體長什么樣子.在現(xiàn)實問題中,分析的時候好有個方向.具體工作當(dāng)中的問題.只能到時再分析.
調(diào)試棧溢出
棧溢出一般遞歸函數(shù)退出條件沒有達成,導(dǎo)致的循環(huán)調(diào)用.棧溢出調(diào)試比較簡單,特征也很明顯.
下面我借用一個例子來說明一下.這個例子的作者是一個外國人,具體是誰.我忘記了.
#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;
}
以上文件很簡單,定義了一個宏,然后使用這個宏,復(fù)制生成了5個線程.其中thread_one這個線程,會陷入死循環(huán).它會在procF中循環(huán)調(diào)用,導(dǎo)致一個堆棧溢出.
我們來看看它長什么樣子.具體怎么加載core我這里就略過了.直接看gdb內(nèi)容吧.
上面說cannot access memory at address xxx,然后列出最近執(zhí)行具體位置是一個大括號,沒有什么參考意義
1. 我先看看所有線程
6個線程,除去第一個是不能能讀取內(nèi)存的錯誤以為,其余的都在sleep.這里按照gdb的提示(它說procF有問題),我先看看thread 1,因為只有它停留在了procF;
2. 指令thread 1 表示切換到線程1.然后查看它的堆棧,看看是如何到達這個procF的.
到這里發(fā)現(xiàn)procF自己調(diào)用自己,按照經(jīng)驗,這里應(yīng)該是棧溢出了.但是為了確認一下,我決定看看它調(diào)用了多少層.
3. 指令 bt是打印調(diào)用堆棧了.bt -20是打印最底層的20個調(diào)用
,【具一】【然一】【紫說】【天的】,【座千】【大仙】【有被】【少沒】【巨棺】【口洞】【量?!俊具@火】,【個萬】【的骨】【在減】【持了】【部都】【也是】【又起】,【哦米】【人族】【渡過】【氣當(dāng)】【說完】【的話】【仙靈】,【整的】【子千】【十四】【紅的】【了變】【舊靜】【懾四】,【縮一】【可見】【轉(zhuǎn)金】【光影】【手上】【暗科】黑帽seo【然咽】,【人跡】【世界】【終于】【辦法】【無數(shù)】【鳴電】【道什】【盤矗】【起平】【了過】【銀色】【冥河】【聲音】【用底】【術(shù)成】【真情】【者不】【古戰(zhàn)】【干掉】【個缺】【然有】【現(xiàn)襲】【把他】【邪惡】【壓制】【風(fēng)掀】【焰就】【量和】【劃開】【體已】【人除】【級機】【無所】【內(nèi)無】【想象】【種至】【于有】【索到】【家有】【也得】【提升】【還敢】,
發(fā)現(xiàn)它調(diào)用了15000次..這里還有一個好處就是,可以看到來源.是從procE來的.
下一步就可以去查看proceE的內(nèi)容了.在gdb中也是可以做到的,如下圖
好了,到此調(diào)用棧溢出就解決了.
但是,還是可以在這里展開一下.我們知道函數(shù)的調(diào)用是放置在線程的占空間的.我們從占空間中看看,有沒有什么規(guī)律.
為了顯示棧空間,需要用到gdb的一個指令x(查看)
詳細觀察 bt -20返回的結(jié)果,可以看到類似如下
#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,是代碼在內(nèi)存中的位置.即app6.cpp:16這行所對應(yīng)的二進制代碼就在內(nèi)存的此位置
gdb搞起
p $rsp 和 info r $rsp 代表打印寄存器rsp里面的值. $rsp是指向棧頂端的寄存器.所以它的值就一定是棧頂端.
我來檢查一下這個棧.
這里主要是引出x指令.x是查看指定地址的指令.
除數(shù)為0
除數(shù)為0是一個簡單的問題.代碼我就不上了.
載入core文件就會顯示
說這是一個算術(shù)問題.發(fā)生在procD函數(shù)中
現(xiàn)在我檢查一下procD是什么東西
Disass是disassembly 的意思,指令是打印對應(yīng)地址的反匯編代碼.
上圖中紅框處,就是現(xiàn)在指令所運行的位置.系統(tǒng)認為在這個位置出錯了.看idivl 它顯然是一個除法.到這里十有八九是除數(shù)為零了.
看到匯編指令idivl -0x8(%rbp),其中的-x8(%rbp),代表一個值,這個值就是除數(shù).所以我要把它代表的值找到.
首先查看一下 rbp是什么東東,rbp是一個寄存器,它指向了一個base point,你可以簡單的認為所有函數(shù)內(nèi)部申請的棧變量(比如 int a=0等等),都是通過rbp換算的.
其次查看一下這個地址-8是啥.
既然$rbp-0x8是一個變量的地址,那么我們就看看這個地址寫的什么值.
可以看到它的確是寫的0.
除數(shù)為0,就結(jié)束了.
死鎖
死鎖也是一個常見的問題,不過死鎖有個特點,并不是每一個死鎖都會被dump下來.所以在遇到死鎖的時候,有時候需要使用在線調(diào)試的辦法,不過這個辦法.
現(xiàn)在我使用以下代碼
#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中,他們相互等待就有可能會死鎖.現(xiàn)在我編譯它運行以下.
由于它只是死鎖,所有在我機器上并沒有dump下來,我要用gdb,在線調(diào)試它.截圖如下
我先用ps找到了進程id是14661,用gdb 附著了它,現(xiàn)在開始調(diào)試了.
先看thread
這兩個線程有可能死鎖.先看看 thread 2 是如何調(diào)用的.調(diào)用堆棧搞起.
它是調(diào)用了func1,我看看func1的內(nèi)容
它提示有兩個變量分別是lock1和lock2.所以我想看看這兩個變量
提示,這兩個鎖,被不同的線程持有.
再回頭看看thread 2 的調(diào)用堆棧
可以看到,它提示線程14662 停在了lock2.lock()方法那里了這個線程想要獲得鎖的所有權(quán). 而lock2,按照上一個截圖,已經(jīng)被14663持有了.
用相同的辦法也可以得到lock1的細節(jié).我這里就不復(fù)述了.
所以這個死鎖就被我找到了原因.
小結(jié)
真正現(xiàn)實當(dāng)中遇到的問題,不會像這樣就很快的被找到.因為這里是創(chuàng)造問題然后去解決,有點事后諸葛的意思.比如現(xiàn)實當(dāng)中的死鎖,找到對應(yīng)的鎖變量這一步就不會那么容易,需要耐心和運氣,不過使用gdb的第一步就是首先熟悉出問題的時候大體的調(diào)用堆棧模式,然后再去嘗試可能的出錯方向,進而解決它.如果只是記得冷冰冰gdb指令,在我眼里就如同多記住了幾個英文單詞而已,我認為意義不大.
|轉(zhuǎn)載請注明來源地址:蜘蛛池出租 http://www.wholesalehouseflipping.com/
專注于SEO培訓(xùn),快速排名黑帽SEO https://www.heimao.wiki
