C++記憶體洩漏檢測工具-Valgrind使用簡介
一 valgrind是什麼?
Valgrind是一套Linux下,開放原始碼(GPL V2)的模擬除錯工具的集合。Valgrind由核心(core)以及基於核心的其他除錯工具組成。核心類似於一個框架(framework),它模擬了一個CPU環境,並提供服務給其他工具;而其他工具則類似於外掛 (plug-in),利用核心提供的服務完成各種特定的記憶體除錯任務。Valgrind的體系結構如下圖所示:
valgrind的結構圖
Valgrind包括如下一些工具:
- Memcheck。這是valgrind應用最廣泛的工具,一個重量級的記憶體檢查器,能夠發現開發中絕大多數記憶體錯誤使用情況,比如:使用未初始化的記憶體,使用已經釋放了的記憶體,記憶體訪問越界等。這也是本文將重點介紹的部分。
- Callgrind。它主要用來檢查程式中函式呼叫過程中出現的問題。
- Cachegrind。它主要用來檢查程式中快取使用出現的問題。
- Helgrind。它主要用來檢查多執行緒程式中出現的競爭問題。
- Massif。它主要用來檢查程式中堆疊使用中出現的問題。
- Extension。可以利用core提供的功能,自己編寫特定的記憶體除錯工具
linux下記憶體空間佈置:
一個典型的Linux C程式記憶體空間由如下幾部分組成:
- 程式碼段(.text)。這裡存放的是CPU要執行的指令。程式碼段是可共享的,相同的程式碼在記憶體中只會有一個拷貝,同時這個段是隻讀的,防止程式由於錯誤而修改自身的指令。
- 初始化資料段(.data)。
- 未初始化資料段(.bss)。位於這一段中的資料,核心在執行該程式前,將其初始化為0或者null。例如出現在任何函式之外的全域性變數:int sum;
- 堆(Heap)。這個段用於在程式中進行動態記憶體申請,例如經常用到的malloc,new系列函式就是從這個段中申請記憶體。
- 棧(Stack)。函式中的區域性變數以及在函式呼叫過程中產生的臨時變數都儲存在此段中。
Memcheck 能夠檢測出記憶體問題,關鍵在於其建立了兩個全域性表。
- Valid-Value 表:
對於程序的整個地址空間中的每一個位元組(byte),都有與之對應的 8 個 bits;對於 CPU 的每個暫存器,也有一個與之對應的 bit 向量。這些 bits 負責記錄該位元組或者暫存器值是否具有有效的、已初始化的值。
- Valid-Address 表
對於程序整個地址空間中的每一個位元組(byte),還有與之對應的 1 個 bit,負責記錄該地址是否能夠被讀寫。
檢測原理:
- 當要讀寫記憶體中某個位元組時,首先檢查這個位元組對應的 A bit。如果該A bit顯示該位置是無效位置,memcheck 則報告讀寫錯誤。
- 核心(core)類似於一個虛擬的 CPU 環境,這樣當記憶體中的某個位元組被載入到真實的 CPU 中時,該位元組對應的 V bit 也被載入到虛擬的 CPU 環境中。一旦暫存器中的值,被用來產生記憶體地址,或者該值能夠影響程式輸出,則 memcheck 會檢查對應的V bits,如果該值尚未初始化,則會報告使用未初始化記憶體錯誤。
Valgrind 使用
用法: 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]
Valgrind 使用舉例(一)
下面是一段有問題的C程式程式碼test.c
#i nclude <stdlib.h>
void f(void)
{
int* x = malloc(10 * sizeof(int));
x[10] = 0; //問題1: 陣列下標越界
} //問題2: 記憶體沒有釋放
int main(void)
{
f();
return 0;
}
1、 編譯程式test.c
gcc -Wall test.c -g -o test
2、 使用Valgrind檢查程式BUG
valgrind --tool=memcheck --leak-check=full ./test
使用未初始化記憶體問題
問題分析:
對於位於程式中不同段的變數,其初始值是不同的,全域性變數和靜態變數初始值為0,而區域性變數和動態申請的變數,其初始值為隨機值。如果程式使用了為隨機值的變數,那麼程式的行為就變得不可預期。
下面的程式就是一種常見的,使用了未初始化的變數的情況。陣列a是區域性變數,其初始值為隨機值,而在初始化時並沒有給其所有陣列成員初始化,如此在接下來使用這個陣列時就潛在有記憶體問題。
結果分析:
假設這個檔名為:badloop.c,生成的可執行程式為badloop。用memcheck對其進行測試,輸出如下。
輸出結果顯示,在該程式第11行中,程式的跳轉依賴於一個未初始化的變數。準確的發現了上述程式中存在的問題。
記憶體讀寫越界
問題分析:
這種情況是指:訪問了你不應該/沒有許可權訪問的記憶體地址空間,比如訪問陣列時越界;對動態記憶體訪問時超出了申請的記憶體大小範圍。下面的程式就是一個典型的陣列越界問題。pt是一個區域性陣列變數,其大小為4,p初始指向pt陣列的起始地址,但在對p迴圈疊加後,p超出了pt陣列的範圍,如果此時再對p進行寫操作,那麼後果將不可預期。
結果分析:
假設這個檔名為badacc.cpp,生成的可執行程式為badacc,用memcheck對其進行測試,輸出如下。
輸出結果顯示,在該程式的第15行,進行了非法的寫操作;在第16行,進行了非法讀操作。準確地發現了上述問題。
記憶體覆蓋
問題分析:
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行,源地址和目標地址設定出現重疊。準確的發現了上述問題。
動態記憶體管理錯誤
問題分析:
常見的記憶體分配方式分三種:靜態儲存,棧上分配,堆上分配。全域性變數屬於靜態儲存,它們是在編譯時就被分配了儲存空間,函式內的區域性變數屬於棧上分配,而最靈活的記憶體使用方式當屬堆上分配,也叫做記憶體動態分配了。常用的記憶體動態分配函式包括:malloc, alloc, realloc, new等,動態釋放函式包括free, delete。
一旦成功申請了動態記憶體,我們就需要自己對其進行記憶體管理,而這又是最容易犯錯誤的。下面的一段程式,就包括了記憶體動態管理中常見的錯誤。
常見的記憶體動態管理錯誤包括:
-
- 申請和釋放不一致
由於 C++ 相容 C,而 C 與 C++ 的記憶體申請和釋放函式是不同的,因此在 C++ 程式中,就有兩套動態記憶體管理函式。一條不變的規則就是採用 C 方式申請的記憶體就用 C 方式釋放;用 C++ 方式申請的記憶體,用 C++ 方式釋放。也就是用 malloc/alloc/realloc 方式申請的記憶體,用 free 釋放;用 new 方式申請的記憶體用 delete 釋放。在上述程式中,用 malloc 方式申請了記憶體卻用 delete 來釋放,雖然這在很多情況下不會有問題,但這絕對是潛在的問題。
-
- 申請和釋放不匹配
申請了多少記憶體,在使用完成後就要釋放多少。如果沒有釋放,或者少釋放了就是記憶體洩露;多釋放了也會產生問題。上述程式中,指標p和pt指向的是同一塊記憶體,卻被先後釋放兩次。
-
- 釋放後仍然讀寫
本質上說,系統會在堆上維護一個動態記憶體連結串列,如果被釋放,就意味著該塊記憶體可以繼續被分配給其他部分,如果記憶體被釋放後再訪問,就可能覆蓋其他部分的資訊,這是一種嚴重的錯誤,上述程式第16行中就在釋放後仍然寫這塊記憶體。
結果分析:
假設這個檔名為badmac.cpp,生成的可執行程式為badmac,用memcheck對其進行測試,輸出如下。
輸出結果顯示,第14行分配和釋放函式不一致;第16行發生非法寫操作,也就是往釋放後的記憶體地址寫值;第17行釋放記憶體函式無效。準確地發現了上述三個問題。
記憶體洩漏
問題描述:
記憶體洩露(Memory leak)指的是,在程式中動態申請的記憶體,在使用完後既沒有釋放,又無法被程式的其他部分訪問。記憶體洩露是在開發大型程式中最令人頭疼的問題,以至於有人說,記憶體洩露是無法避免的。其實不然,防止記憶體洩露要從良好的程式設計習慣做起,另外重要的一點就是要加強單元測試(Unit Test),而memcheck就是這樣一款優秀的工具。
下面是一個比較典型的記憶體洩露案例。main函式呼叫了mk函式生成樹結點,可是在呼叫完成之後,卻沒有相應的函式:nodefr釋放記憶體,這樣記憶體中的這個樹結構就無法被其他部分訪問,造成了記憶體洩露。
在一個單獨的函式中,每個人的記憶體洩露意識都是比較強的。但很多情況下,我們都會對malloc/free 或new/delete做一些包裝,以符合我們特定的需要,無法做到在一個函式中既使用又釋放。這個例子也說明了記憶體洩露最容易發生的地方:即兩個部分的介面部分,一個函式申請記憶體,一個函式釋放記憶體。並且這些函式由不同的人開發、使用,這樣造成記憶體洩露的可能性就比較大了。這需要養成良好的單元測試習慣,將記憶體洩露消滅在初始階段。
結果分析:
假設上述檔名位tree.h, tree.cpp, badleak.cpp,生成的可執行程式為badleak,用memcheck對其進行測試,輸出如下。
該示例程式是生成一棵樹的過程,每個樹節點的大小為12(考慮記憶體對齊),共8個節點。從上述輸出可以看出,所有的記憶體洩露都被發現。Memcheck將記憶體洩露分為兩種,一種是可能的記憶體洩露(Possibly lost),另外一種是確定的記憶體洩露(Definitely lost)。Possibly lost 是指仍然存在某個指標能夠訪問某塊記憶體,但該指標指向的已經不是該記憶體首地址。Definitely lost 是指已經不能夠訪問這塊記憶體。而Definitely lost又分為兩種:直接的(direct)和間接的(indirect)。直接和間接的區別就是,直接是沒有任何指標指向該記憶體,間接是指指向該記憶體的指標都位於記憶體洩露處。在上述的例子中,根節點是directly lost,而其他節點是indirectly lost。
轉載地址: http://blog.csdn.net/sduliulun/article/details/7732906