Thursday, December 14, 2006

Memory Debugging Tool: Valgrind

介紹一下 Valgrind 這套 Debugging Tool,在 Valgrind Home 裡面介紹這套軟體除了可以 Detect memory 的 bug 之外,還可以 Detect threads 的 bug,功能非常強大,但此篇文章主要介紹 Memory 的部分。

Getting Valgrind
首先,一開始當然是先取得 Valgrind 這套軟體,可以到 Valgrind Home 下載 GPL Source Code 下來編譯,官網上提供的 Source Code 都是 bz2 的壓縮格式,因此使用下面的指令來解開他:

tar jxvf valgrind-X.Y.Z.tar.bz2
cd valgrind-X.Y.Z

上面的 X.Y.Z 是要看您下載的哪個版本,以我的例子中,我下載了 3.2.1 的版本,因此檔案全名就是 valgrind-3.2.1.tar.bz2 ,請自行更改。完成後要開始進行編譯與安裝的動作,一般情況使用下面的指令就可以順利的編譯完成並安裝到系統中:

./configure
make
make install

在這接步驟都沒有任何 Error messages 的話,那恭喜您,已經將 Valgrind 安裝好了,接下來儘管使用他來 detect memory bug 就對了。當然使用 FC4 以上的 Linux Distribution 都已經包含 Valgrind,可以不需要再安裝。

Valgrind Future
Valgrind 在 detect memory bug 主要使用的是 Memcheck 這個 tool 來完成,Valgrind 裡面可以看成包含了許多的 tool modules,而這裡介紹的 Memcheck 只是其中的一個,其他的 tool 如:Cachegrind, Callgrind, Massif...等等,有機會在介紹,詳細使用可以參考 Valgrind on-line manual,也可以下載 off-line 的版本。

在 Memcheck 的功能大致上包含 Memory Leaks, Invalid Pointer, Uninitialized Variables 等三個主要功能,下面我將分別介紹與舉個簡單的範例。

Finding Memory Leaks With Valgrind
Memory leaks 可以說是在 memory bugs 中佔最大宗的比例,而且不像其他的 bugs 容易顯現出來,通常都是執行一段長時間直到 out of memory 產生 memory 不夠用時才會發現,但現在 memory 越來越便宜的情況之下,許多錯誤的 coding concept 就無形中被接受了,當然這是非常不正確的。

Memory leaks 問題尤其在 C/C++ 最為嚴重,因為 C/C++ 不像 Java 或者 .NET 一樣擁有 Garbage collection 機制,可以自動回收沒有使用到的 memory space,不會花太多時間處理在正確的時間點 free 不必要的 memory space。

使用 Valgrind 並配合 Memcheck tool 來執行要 debug 的程式,在執行過程中 Valgrind 就會即時檢查 memory 的使用是否正確,並且即時列出檢查的結果,最後還有個 free/malloc 的 Summary 統計。最簡單的使用方式,我們可以使用下面指令並指定使用 memcheck 這個 tool 來執行程式:

$ valgrind --tool=memcheck ./program


...
==20631== malloc/free: in use at exit: 1,000 bytes in 1 blocks.
==20631== malloc/free: 1 allocs, 0 frees, 1,000 bytes allocated.
...
==20631== LEAK SUMMARY:
==20631== definitely lost: 1,000 bytes in 1 blocks.
==20631== possibly lost: 0 bytes in 0 blocks.
==20631== still reachable: 0 bytes in 0 blocks.
==20631== suppressed: 0 bytes in 0 blocks.

其中訊息最前面的 20631 是程式在執行執行的 Process ID,所以每此執行不會一樣,程式結束後就會列出這些統計數據,因為訊息過多已將不必要的簡化了,上面就可以看到 allocate 一次但沒有任何 free 動作,最後面 Summary 就列出了 lost 1,000 bytes 這樣的資訊。當然這些訊息只能告訴我們有 memory leaks 存在,仍不足以輔助我們修正程式,僅能夠初步瞭解因為 malloc/new 和 free/delete 沒有 match 的關係造成 leak 出現,當然也有可能並非是我們的 Code 沒寫好,也可能因為 System Library 造成(機率非常低,大部分都是自己造成的)。

我們可以使用 leak-check 這個參數列出更詳細的 messages 如下:

$ valgrind --tool=memcheck --leak-check=yes ./program

如此就會在執行過程中一發現有問題的 memory bugs 會馬上 printout 到螢幕上,以剛剛的例子再跑一次就會看到一個沒有 free 的 memory bug 列抓出來了:

==20706== 1,000 bytes in 1 blocks are definitely lost in loss record 1 of 1
==20706== at 0x40044F5: malloc (vg_replace_malloc.c:149)
==20706== by 0x80483A4: main (in /home/andy/program)

從上面訊息 Valgrind 幫我們抓到了一個 lost record ,並且將 stack 狀況 dump 出來,可以看到 program 這隻程式會呼叫 System Library 的 malloc 但沒有 free 動作,但這樣還是無法知道到底是 program 哪裡 call malloc,所以在編譯 program 時需要對 gcc 加上 -g 的參數,並重新執行一次:

$ gcc -g program.c -o program
$ valgrind --tool=memcheck --leak-check=yes ./program


...
==20734== 1,000 bytes in 1 blocks are definitely lost in loss record 1 of 1
==20734== at 0x40044F5: malloc (vg_replace_malloc.c:149)
==20734== by 0x80483A4: main (program.c:5)
...

上面訊息就更詳細了,除了 stack 之外還告訴我們在 program.c 第5行的位置 call malloc,方便我們修正 bugs,同理,在其他功能的支援也是一樣,編譯時需要加上 -g 參數才可以得到更詳細的 stack 資訊。

在 Valgrind 裡面,對於 Memory leaks 的問題分成下面幾類類:

  • definitely lost: 這種 leaks 是一般我們最常見的,如下例子:

    int main() {
        char *ptr = malloc(100);
        ptr = NULL;
        return (0);
    }

    原來已經 allocate 的那塊 memory 已經沒有任何 pointer 指向他,使得他已經完全被 lost 了
  • possibly lost: 這是屬於 Valgrind 無法精準判斷 lost 的一個分類,主要發生在當一個 pointer 已經指向一個 memory space,但因為某些指標操作的關係使得 pointer 已經被移動了,因此 Valgrind 無法正確判斷原本的 memory 是否真的 lost 了
  • still reachable: 這種問題也經常出現,如下例子:

    int main() {
        char *ptr = malloc(100);
        return (0);
    }

    在離開前沒有 free 已經配置的 memory 就離開,但離開前仍有 pointer 指向這塊 memory 就屬於這種類型
但只是用 leak-check 參數並不會列出 still reachable 的情況,所以還要使用 --show-reachable=yes 參數才會列出來。

Finding Invalid Pointer Use With Valgrind
這功能就有趣了,透過 Memcheck tool 可以協助我們尋找 invalid heap memory 的 bugs,如下:

#include <stdlib.h>

int main()
{
    char *x = malloc(10);
    x[10] = 'a';
    return (0);
}

並使用 Valgrind 執行這隻程式

$ valgrind --tool=memcheck --leak-check=yes ./ex2

就會得到下面結果:

==20916== Invalid write of size 1
==20916== at 0x80483AE: main (ex2.c:7)
==20916== Address 0x4014032 is 0 bytes after a block of size 10 alloc'd
==20916== at 0x40044F5: malloc (vg_replace_malloc.c:149)
==20916== by 0x80483A1: main (ex2.c:5)

Valgrind 告訴我們第7行只 allocate 10 bytes 的空間,但是程式存取了在這 10 之後的第 1 個 bytes 的位置(index 0),而上面的 Invalid write of size X 其中 X 代表的就是存取的 size,因為程式以 char 變數來存取,所以是 1bytes,當然這隻程式也是有 memory leak。不過這功能只能夠在偵測 heap memory ,對於 static 宣告方式的 stack memory 是無法使用的。

Detecting The Use Of Uninitialized Variables

這問題也是典型初學者會犯的錯誤,簡單的程式如下:

#include <stdio.h>

int main()
{
    int x;
    if (x == 0)
        printf("X is zero");

    return (0);
}

透過 Valgrind 就會得到下面訊息

==20972== Conditional jump or move depends on uninitialised value(s)
==20972== at 0x804839C: main (ex3.c:6)

告訴我們第六行判斷式所使用的變數並沒有初始化,上面這寫法或許大部分情況都是 work,但 memory 大量使用之下一宣告出來的 variables 裡面所存放的值都是原本 memory 裡面已經存在的值,所以就不會剛好是我們所假定的初始值,非常需要注意。

對於 array 或是 struct 可以使用 memset 一次將整塊 memory 設定自己所預設的值(一般我都是以 0 作為初始值),當然如果有其他初始值應用考量,使用其他方法亦可,重點是初始化的這個動作的目的要達到。

現在 GCC 都可以協助我們在 compiler 階段抓到這些問題,且我自己測試過 memcheck 針對 uninitialized 的功能,發現在某些情況會有誤判情況,因此建議還是使用 GCC 的功能為主,warning messages 全打開就對了。

目前自己測試發現會發生誤判的情況,當我 function A 有個 variable 透過 malloc 取得 memory space,但沒有 在同個 function 進行 assign 動作,而是利用 call by reference 方式 call function B ,進而在 function B 完成 assign 後再回到 function A 將 pointer 內的 value 印出來,這種 case 就會被 Valgrind 判斷該 variable 沒有 initialized,不過事實是是在 function B 完成了。

What else will Valgrind Find

Valgrind with memcheck 不只可以抓到上面三種 memory bugs,但我們最主要使用的是 Memory leaks 的功能,其他功能包括可以協助我們判斷是否有 Illegal frees 存在,或是 Mismatched free/delete/delete[] 存在,因為 C/C++ 裡面不同的 allocate 方式就應該對應不同的 free 方式雖然大多人會把 delete 和 free 混用,但這是極為不好的習慣。甚至可以檢查 memcpy, strcpy, strncpy, strcat 等這類的 string function 是否有 source 和 destination overlap 的情況發生,當然也有可能因為 coding 考量故意這麼使用,但還是得小心使用。

更詳細的使用可以參考 on-line manual 手冊

Drawback

這種程式既然功能這麼強大一樣會有某些缺點,最主要的缺點就是會使用更多的 memory space,幾乎是使用了兩倍原本程式使用的 memory 量,而且執行時速度也會變的慢多了,會花更多的時間來等待程式結束。

Summary

Valgrind 同時也是套可以 support x86 and AMD64 architecture 的 tool,目前僅能夠在 Linux 之下使用。不僅可以測試 Memory leaks 、使用了 Invalid pointer 或是 Invalid memory operations 等問題,非常方便且實用(但不支援 statically allocated arrays)

...《消息來源》

HEMiDEMi Technorati Del.icio.us MyShare個人書籤 Yahoo

0 Comments: