Hello Valgrind!---Valgrind快速入門

原文來自:https://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/

目錄

1. valgrind簡介

1.1 valgrind的結構圖

1.2 linux下內存空間佈置:

2. Valgrind 使用

2.1 Valgrind用法

2.2 Valgrind 使用舉例

2.2.1 示例

2.2.2 使用未初始化內存問題

2.2.3 內存讀寫越界

2.2.4 內存覆蓋

2.2.5 動態內存管理錯誤

2.2.6 內存泄漏


1. valgrind簡介

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

1.1 valgrind的結構圖

點擊看大圖

Valgrind包括如下一些工具:

Memcheck。這是valgrind應用最廣泛的工具,一個重量級的內存檢查器,能夠發現開發中絕大多數內存錯誤使用情況,比如:使用未初始化的內存,使用已經釋放了的內存,內存訪問越界等。這也是本文將重點介紹的部分。

Callgrind。它主要用來檢查程序中函數調用過程中出現的問題。

Cachegrind。它主要用來檢查程序中緩存使用出現的問題。

Helgrind。它主要用來檢查多線程程序中出現的競爭問題。

Massif。它主要用來檢查程序中堆棧使用中出現的問題。

Extension。可以利用core提供的功能,自己編寫特定的內存調試工具

1.2 linux下內存空間佈置:

典型內存空間佈局

 

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

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

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

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

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

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

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,如果該值尚未初始化,則會報告使用未初始化內存錯誤。

2. Valgrind 使用

2.1 Valgrind用法

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

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

  2. h –help 顯示幫助信息。

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

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

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

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

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

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

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

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

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

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

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

LOG信息輸出

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

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

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

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

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

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

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

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

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

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

2.2 Valgrind 使用舉例

2.2.1 示例

下面是一段有問題的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;
 }
1、 編譯程序test.c
gcc -Wall test.c -g -o test
2、 使用Valgrind檢查程序BUG
valgrind --tool=memcheck --leak-check=full ./test

2.2.2 使用未初始化內存問題

問題分析:

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

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

清單 3

結果分析:

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

清單 4

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

2.2.3 內存讀寫越界

問題分析:

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

清單 5

結果分析:

假設這個文件名爲badacc.cpp,生成的可執行程序爲badacc,用memcheck對其進行測試,輸出如下。

清單 6

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

2.2.4 內存覆蓋

問題分析:

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,這樣也會發生內存覆蓋。

清單 7

結果分析:

假設這個文件名爲 badlap.cpp,生成的可執行程序爲 badlap,用 memcheck 對其進行測試,輸出如下。

點擊看大圖

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

2.2.5 動態內存管理錯誤

問題分析:

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

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

清單 9

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

  • 申請和釋放不一致

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

  • 申請和釋放不匹配

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

  • 釋放後仍然讀寫

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

結果分析:

假設這個文件名爲badmac.cpp,生成的可執行程序爲badmac,用memcheck對其進行測試,輸出如下。
清單 10

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

2.2.6 內存泄漏

問題描述:

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

下面是一個比較典型的內存泄露案例。main函數調用了mk函數生成樹結點,可是在調用完成之後,卻沒有相應的函數:nodefr釋放內存,這樣內存中的這個樹結構就無法被其他部分訪問,造成了內存泄露。

在一個單獨的函數中,每個人的內存泄露意識都是比較強的。但很多情況下,我們都會對malloc/free 或new/delete做一些包裝,以符合我們特定的需要,無法做到在一個函數中既使用又釋放。這個例子也說明了內存泄露最容易發生的地方:即兩個部分的接口部分,一個函數申請內存,一個函數釋放內存。並且這些函數由不同的人開發、使用,這樣造成內存泄露的可能性就比較大了。這需要養成良好的單元測試習慣,將內存泄露消滅在初始階段。

清單 1 

清單 11.2 


清單 11.3

結果分析:

假設上述文件名位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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章