Linux下 C/C++程序調試技巧


1、調試工具– cgdb

1.1 cgdb簡介

cgdb可以看作gdb的界面增強版,用來替代gdb的gdb -tui。

cgdb主要功能是在調試時進行代碼的同步顯示,這無疑增加了調試的方便性,提高了調試效率。界面類似vi,符合unix/linux下開發人員習慣,所以如果熟悉gdb和vi,幾乎可以立即使用cgdb。

1.2 cgdb主要功能

1) 相比GDB,增加了語法加亮的代碼窗口,顯示在GDB窗口的上部,隨GDB的調試位置代碼同步顯示。

2) 斷點設置可視化 。

3) 在代碼窗口中可使用GDB常用命令 。

4) 在代碼窗口可進行代碼查找,支持正則表達式 。

通過cgdbtest啟動(gcctest.c -g -o test)后的界面如下:

 

1.3 界面及使用說明

1.3.1 代碼窗口

    調試時同步顯示被調試程序源代碼,自動標記出程序運行到的位置。當焦點在代碼窗口時,可以瀏覽代碼、查找代碼以及執行命令 ,操作方式同vi。常用命令如下:

    i :焦點切換到GDB窗口。

    o :打開文件選擇框,可選擇要顯示的代碼文件。

   空格 :設置/取消斷點。

    k:向上移動

    j:向下移動

    /:查找

1.3.2 狀態條窗口

    同vi的狀態條,一般顯示當前打開的源文件名,當代碼窗口進入命令狀態時,顯示輸入的命令等信息

1.3.3 GDB窗口

GDB的操作界面,同GDB ,按ESC鍵則焦點切換到代碼窗口 。

1.3.4 啟動&退出

    啟動:cgdb   [gdb options]

退出:在代碼窗口或GDB窗口,執行quit命令 。

1.4 cgdb的安裝

       1) 下載cgdb的安裝包cgdb-0.6.6.tar.gz

       2)解壓tarzxvf cgdb-0.6.6.tar.gz

       3) 切換到cgdb-0.6.6目錄下cd cgdb-0.6.6

       4) 設置安裝目錄./configure–prefix=/usr/local/ 回車

       5) 編譯make

       6) 安裝 makeinstall (注意此步驟需要切換到root賬戶下執行,否則會提示沒有權限)

 

2、"段錯誤"跟蹤-core&gdb

有些時候我們在一段C代碼的時候,由於對一個非法內存進行了操作,在程序運行的過程中,出現了"段錯誤"。下面介紹一種方法,可以有效的定位出現"段錯誤的地方"。

當我們的程序崩潰時,內核有可以把該程序當前內存映射到core文件里,方便程序員找到程序出現問題的地方。

2.1 什么是core dump?

core的意思是內存,dump的意思是扔出來,堆出來。

為什么沒有core文件生成呢?

有時候程序down了,但是core文件卻沒有生成.core文件的生成跟當前系統的環境設置有關系,可以用下面的語句設置一下便生成core文件了

ulimit  -c  unlimited

core 文件生成的位置一般於運行程序的路徑相同,在ubuntu下文件名一般為core.

2.2 什么是core文件

當程序崩潰時,在進程當前工作目錄的core文件中復制了該進程的存儲圖像。core文件僅僅是一個內存映像(同時加上調試信息),主要用來調試的。下面介紹怎樣利用core文件來定位我們出現"段錯誤"的地方。

#include <stdio.h>

 

char *str=”test”;

void core_test()

{

    str[1]=’T’;

}

 

int main()

{

core_test();

return 0;

}

程序運行結果:

 

從上面我們可以看出,第一次運行程序出現"段錯誤"並沒有出現core文件,一般linux操作系統默認core文件的大小都是0,需要手動設置一下。

2.3 調試core文件

core文件是個二進制文件,需要用相應的工具來分析程序崩潰時的內存映像。linux下可以用gdb來調試core文件。

 

從上面我們可以清楚的看到我們的程序是在那個地方出現了錯誤。

 

3、仿真調試工具-Valgrind

3.1 Valgrind工具安裝

(1)查看glibc的版本

ldd --version

(2)$sudo apt-get installvalgrind 成功安裝。

3.2 工具簡介

Valgrind是一套Linux下,開放源代碼(GPL V2)的仿真調試工具的集合。Valgrind由內核(core)以及基於內核的其他調試工具組成。內核類似於一個框架(framework),它模擬了 一個CPU環境,並提供服務給其他工具;而其他工具則類似於插件 (plug-in),利用內核提供的服務完成各種特定的內存調試任務。

Valgrind支持很多工具:memcheck,addrcheck,cachegrind,Massif,helgrind和Callgrind等。在運行Valgrind時,你必須指明想用的工具,如果省略工具名,默認運行memcheck。

3.2.1 Memcheck

最常用的工具,用來檢測程序中出現的內存問題,所有對內存的讀寫都會被檢測到,一切對malloc()/free()/new/delete的調用都會被捕獲。所以,它能檢測以下問題:

1).對未初始化內存的使用;

2).讀/寫釋放后的內存塊;

3).讀/寫超出malloc分配的內存塊;

4).讀/寫不適當的棧中內存塊;

5).內存泄漏,指向一塊內存的指針永遠丟失;

6).不正確的malloc/free或new/delete匹配;

7).memcpy()相關函數中的dst和src指針重疊。

 

Memcheck 能夠檢測出內存問題,關鍵在於其建立了兩個全局表。

(1)    Valid-Value 表:

對於進程的整個地址空間中的每一個字節(byte),都有與之對應的 8 個 bits;對於 CPU 的每個寄存器,也有一個與之對應的 bit 向量。這些 bits 負責記錄該字節或者寄存器值是否具有有效的、已初始化的值。

(2)    Valid-Address 表

對於進程整個地址空間中的每一個字節(byte),還有與之對應的 1 個 bit,負責記錄該地址是否能夠被讀寫。

檢測原理:

當要讀寫內存中某個字節時,首先檢查這個字節對應的 A bit。如果該A bit顯示該位置是無效位置,memcheck 則報告讀寫錯誤。

內核(core)類似於一個虛擬的 CPU環境,這樣當內存中的某個字節被加載到真實的 CPU 中時,該字節對應的 V bit 也被加載到虛擬的 CPU 環境中。一旦寄存器中的值,被用來產生內存地址,或者該值能夠影響程序輸出,則 memcheck 會檢查對應的V bits,如果該值尚未初始化,則會報告使用未初始化內存錯誤。

 

一個典型的Linux C程序內存空間由如下幾部分組成:

·         代碼段(.text)。這里存放的是CPU要執行的指令。代碼段是可共享的,相同的代碼在內存中只會有一個拷貝,同時這個段是只讀的,防止程序由於錯誤而修改自身的指令。

·         初始化數據段(.data)。這里存放的是程序中需要明確賦初始值的變量,例如位於所有函數之外的全局變量:int val="100"。需要強調的是,以上兩段都是位於程序的可執行文件中,內核在調用exec函數啟動該程序時從源程序文件中讀入。

·         未初始化數據段(.bss)。位於這一段中的數據,內核在執行該程序前,將其初始化為0或者null。例如出現在任何函數之外的全局變量:int sum;

·         堆(Heap)。這個段用於在程序中進行動態內存申請,例如經常用到的malloc,new系列函數就是從這個段中申請內存。

·         棧(Stack)。函數中的局部變量以及在函數調用過程中產生的臨時變量都保存在此段中。

linux下內存空間布置

3.2.2 Callgrind

和gprof類似的分析工具,但它對程序的運行觀察更是入微,能給我們提供更多的信息。和gprof不同,它不需要在編譯源代碼時附加特殊選項,但加上調試選項是推薦的。Callgrind收集程序運行時的一些數據,建立函數調用關系圖,還可以有選擇地進行cache模擬。在運行結束時,它會把分析數據寫入一個文件。callgrind_annotate可以把這個文件的內容轉化成可讀的形式。

3.2.3 Cachegrind

Cache分析器,它模擬CPU中的一級緩存I1,Dl和二級緩存,能夠精確地指出程序中cache的丟失和命中。如果需要,它還能夠為我們提供cache丟失次數,內存引用次數,以及每行代碼,每個函數,每個模塊,整個程序產生的指令數。這對優化程序有很大的幫助。

3.2.4 Helgrind

它主要用來檢查多線程程序中出現的競爭問題。Helgrind尋找內存中被多個線程訪問,而又沒有一貫加鎖的區域,這些區域往往是線程之間失去同步的地方,而且會導致難以發掘的錯誤。Helgrind實現了名為“Eraser”的競爭檢測算法,並做了進一步改進,減少了報告錯誤的次數。不過,Helgrind仍然處於實驗階段。

3.2.5 Massif

堆棧分析器,它能測量程序在堆棧中使用了多少內存,告訴我們堆塊,堆管理塊和棧的大小。Massif能幫助我們減少內存的使用,在帶有虛擬內存的現代系統中,它還能夠加速我們程序的運行,減少程序停留在交換區中的幾率。

3.3 基本使用

用法: valgrind [options] prog-and-args [options]: 常用選項,適用於所有Valgrind工具

   -tool=<name> 最常用的選項。運行 valgrind中名為toolname的工具。默認memcheck。

    h–help 顯示幫助信息。

   -version 顯示valgrind內核的版本,每個工具都有各自的版本。

    q–quiet 安靜地運行,只打印錯誤信息。

    v–verbose 更詳細的信息, 增加錯誤數統計。

   -trace-children=no|yes 跟蹤子線程? [no]

   -track-fds=no|yes 跟蹤打開的文件描述?[no]

   -time-stamp=no|yes 增加時間戳到LOG信息? [no]

   -log-fd=<number> 輸出LOG到描述符文件 [2=stderr]

   -log-file=<file> 將輸出的信息寫入到filename.PID的文件里,PID是運行程序的進程ID

   -log-file-exactly=<file> 輸出LOG信息到 file

   -log-file-qualifier=<VAR> 取得環境變量的值來做為輸出信息的文件名。[none]

   -log-socket=ipaddr:port 輸出LOG到socket ,ipaddr:port

 

LOG信息輸出

   -xml=yes 將信息以xml格式輸出,只有memcheck可用

   -num-callers=<number> show <number> callers in stack traces[12]

   -error-limit=no|yes 如果太多錯誤,則停止顯示新錯誤? [yes]

   -error-exitcode=<number> 如果發現錯誤則返回錯誤代碼[0=disable]

   -db-attach=no|yes 當出現錯誤,valgrind會自動啟動調試器gdb。[no]

   -db-command=<command> 啟動調試器的命令行選項[gdb -nw%f %p]

 

適用於Memcheck工具的相關選項:

   -leak-check=no|summary|full 要求對leak給出詳細信息?[summary]

   -leak-resolution=low|med|high how much bt merging in leak check [low]

   -show-reachable=no|yes show reachable blocks in leak check? [no]

3.4 Valgrind-memcheck 使用舉例

下面是一段有問題的C程序代碼test.c

#include <stdlib.h>

void f(void)

{

   int* x = malloc(10 * sizeof(int));

   x[10] = 0;  //問題1: 數組下標越界

}            //問題2: 內存沒有釋放

 

int main(void)

{

   f();

   return 0;

 }

首先編譯程序test.c

gcc -Wall test.c -g -otest

然后使用Valgrind檢查程序BUG

valgrind --tool=memcheck--leak-check=full ./test

3.4.1使用未初始化內存問題

問題分析:對於位於程序中不同段的變量,其初始值是不同的,全局變量和靜態變量初始值為0,而局部變量和動態申請的變量,其初始值為隨機值。如果程序使用了為隨機值的變量,那么程序的行為就變得不可預期。

下面的程序就是一種常見的,使用了未初始化的變量的情況。數組a是局部變量,其初始值為隨機值,而在初始化時並沒有給其所有數組成員初始化,如此在接下來使用這個數組時就潛在有內存問題。

 

結果分析:假設這個文件名為:badloop.c,生成的可執行程序為badloop。用memcheck對其進行測試,輸出如下:

 

輸出結果顯示,在該程序第11行中,程序的跳轉依賴於一個未初始化的變量。准確的發現了上述程序中存在的問題。

3.4.2內存讀寫越界

問題分析:這種情況是指:訪問了你不應該/沒有權限訪問的內存地址空間,比如訪問數組時越界;對動態內存訪問時超出了申請的內存大小范圍。下面的程序就是一個典型的數組越界問題。pt是一個局部數組變量,其大小為4,p初始指向pt數組的起始地址,但在對p循環疊加后,p超出了pt數組的范圍,如果此時再對p進行寫操作,那么后果將不可預期。

 

結果分析:假設這個文件名為badacc.cpp,生成的可執行程序為badacc,用memcheck對其進行測試,輸出如下:

 

輸出結果顯示,在該程序的第15行,進行了非法的寫操作;在第16行,進行了非法讀操作。准確地發現了上述問題。

3.4.3內存覆蓋

問題分析:C 語言的強大和可怕之處在於其可以直接操作內存,C 標准庫中提供了大量這樣的函數,比如 strcpy, strncpy, memcpy,strcat 等,這些函數有一個共同的特點就是需要設置源地址 (src),和目標地址(dst),src 和 dst 指向的地址不能發生重疊,否則結果將不可預期。

下面就是一個 src 和 dst 發生重疊的例子。在 15 與17 行中,src 和 dst 所指向的地址相差 20,但指定的拷貝長度卻是 21,這樣就會把之前的拷貝值覆蓋。第 24 行程序類似,src(x+20) 與 dst(x) 所指向的地址相差 20,但 dst 的長度卻為 21,這樣也會發生內存覆蓋。

 

結果分析:假設這個文件名為badlap.cpp,生成的可執行程序為 badlap,用memcheck 對其進行測試,輸出如下:

 

輸出結果顯示上述程序中第15,17,24行,源地址和目標地址設置出現重疊。准確的發現了上述問題。

3.4.4動態內存管理錯誤

問題分析:常見的內存分配方式分三種:靜態存儲,棧上分配,堆上分配。全局變量屬於靜態存儲,它們是在編譯時就被分配了存儲空間,函數內的局部變量屬於棧上分配,而最靈活的內存使用方式當屬堆上分配,也叫做內存動態分配了。常用的內存動態分配函數包 括:malloc, alloc, realloc, new等,動態釋放函數包括free,delete。

一旦成功申請了動態內存,我們就需要自己對其進行內存管理,而這又是最容易犯錯誤的。下面的一段程序,就包括了內存動態管理中常見的錯誤。

 

常見的內存動態管理錯誤包括:

a)申請和釋放不一致

由於 C++ 兼容 C,而 C 與 C++ 的內存申請和釋放函數是不同的,因此在 C++ 程序中,就有兩套動態內存管理函數。一條不變的規則就是采用 C 方式申請的內存就用 C 方式釋放;用 C++ 方式申請的內存,用 C++ 方式釋放。也就是用 malloc/alloc/realloc 方式申請的內存,用 free 釋放;用 new 方式申請的內存用 delete 釋放。在上述程序中,用 malloc 方式申請了內存卻用 delete 來釋放,雖然這在很多情況下不會有問題,但這絕對是潛在的問題。

b)申請和釋放不匹配

申請了多少內存,在使用完成后就要釋放多少。如果沒有釋放,或者少釋放了就是內存泄露;多釋放了也會產生問題。上述程序中,指針p和pt指向的是同一塊內存,卻被先后釋放兩次。

c)釋放后仍然讀寫

本質上說,系統會在堆上維護一個動態內存鏈表,如果被釋放,就意味着該塊內存可以繼續被分配給其他部分,如果內存被釋放后再訪問,就可能覆蓋其他部分的信息,這是一種嚴重的錯誤,上述程序第16行中就在釋放后仍然寫這塊內存。

結果分析:假設這個文件名為badmac.cpp,生成的可執行程序為badmac,用memcheck對其進行測試,輸出如下:

 

輸出結果顯示,第14行分配和釋放函數不一致;第16行發生非法寫操作,也就是往釋放后的內存地址寫值;第17行釋放內存函數無效。准確地發現了上述三個問題。

3.4.5內存泄漏

問題描述:內存泄露(Memoryleak)指的是,在程序中動態申請的內存,在使用完后既沒有釋放,又無法被程序的其他部分訪問。內存泄露是在開發大型程序中最令人頭疼的問題,以至於有人說,內存泄露是無法避免的。其實不然,防止內存泄露要從良好的編程習慣做起,另外重要的一點就是要加強單元測試(Unit Test),而memcheck就是這樣一款優秀的工具。

#include <stdlib.h>

int main()

{

char *x = (char*)malloc(20);

char *y = (char*)malloc(20);

x=y;

free(x);

free(y);

return 0;

}

 

結果分析:

Valgrind提示如下

==19013== Invalid free() / delete /delete[]

==19013== at 0x4A0541E: free(vg_replace_malloc.c:233)

==19013== by 0x4004F5: main (sample5.c:8)

==19013== Address 0x4C2E078 is 0 bytesinside a block of size 20 free'd

==19013== at 0x4A0541E: free(vg_replace_malloc.c:233)

==19013== by 0x4004EC: main (sample5.c:7)

==19013==

==19013== ERROR SUMMARY: 1 errors from 1contexts (suppressed: 5 from 1)

==19013== malloc/free: in use at exit: 20bytes in 1 blocks.

==19013== malloc/free: 2 allocs, 2 frees,40 bytes allocated.

==19013== For counts of detected errors,rerun with: -v

==19013== searching for pointers to 1not-freed blocks.

==19013== checked 66,584 bytes.

==19013==

==19013== LEAK SUMMARY:

==19013== definitely lost: 20 bytes in 1blocks.

==19013== possibly lost: 0 bytes in 0blocks.

==19013== still reachable: 0 bytes in 0blocks.

==19013== suppressed: 0 bytes in 0 blocks.

==19013== Use --leak-check=full to seedetails of leaked memory.

3.4.6非法寫/讀

代碼如下:

int main()

{

int i, *x;

x = (int *)malloc(10*sizeof(int));

for (i=0; i<11; i++)

x[i] = i;

free(x);

}

Valgrind提示如下

==21483== Invalid write of size 4

==21483== at 0x4004EA: main (sample6.c:6)

==21483== Address 0x4C2E058 is 0 bytesafter a block of size 40 alloc'd

==21483== at 0x4A05809: malloc(vg_replace_malloc.c:149)

==21483== by 0x4004C9: main (sample6.c:4)

==21483==

==21483== ERROR SUMMARY: 1 errors from 1contexts (suppressed: 5 from 1)

==21483== malloc/free: in use at exit: 0bytes in 0 blocks.

==21483== malloc/free: 1 allocs, 1 frees,40 bytes allocated.

==21483== For counts of detected errors,rerun with: -v

==21483== All heap blocks were freed -- noleaks are possible.

3.4.7無效指針

代碼如下:

#include <stdlib.h>

int main()

{

char *x = malloc(10);

x[10] = 'a';

free(x);

return 0;

}

Valgrind提示如下

==15262== Invalid write of size 1

==15262== at 0x4004D6: main (sample7.c:5)

==15262== Address 0x4C2E03A is 0 bytesafter a block of size 10 alloc'd

==15262== at 0x4A05809: malloc(vg_replace_malloc.c:149)

==15262== by 0x4004C9: main (sample7.c:4)

==15262==

==15262== ERROR SUMMARY: 1 errors from 1contexts (suppressed: 5 from 1)

==15262== malloc/free: in use at exit: 0bytes in 0 blocks.

==15262== malloc/free: 1 allocs, 1 frees,10 bytes allocated.

==15262== For counts of detected errors,rerun with: -v

==15262== All heap blocks were freed -- noleaks are possible.

3.4.8重復釋放

代碼如下:

#include <stdlib.h>

int main()

{

char *x = malloc(10);

free(x);

free(x);

return 0;

}

 

Valgrind提示如下

==15005== Invalid free() / delete /delete[]

==15005== at 0x4A0541E: free(vg_replace_malloc.c:233)

==15005== by 0x4004DF: main (sample8.c:6)

==15005== Address 0x4C2E030 is 0 bytesinside a block of size 10 free'd

==15005== at 0x4A0541E: free(vg_replace_malloc.c:233)

==15005== by 0x4004D6: main (sample8.c:5)

==15005==

==15005== ERROR SUMMARY: 1 errors from 1contexts (suppressed: 5 from 1)

==15005== malloc/free: in use at exit: 0bytes in 0 blocks.

==15005== malloc/free: 1 allocs, 2 frees,10 bytes allocated.

==15005== For counts of detected errors,rerun with: -v

==15005== All heap blocks were freed -- noleaks are possible.

3.4.8局限性

Valgrind不對靜態數組(分配在棧上)進行邊界檢查。如果在程序中聲明了一個數組:

int main()

{

char x[10];

x[11] = 'a';

}

Valgrind則不會警告你,你可以把數組改為動態在堆上分配的數組,這樣就可能進行邊界檢查了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM