在 Linux 平臺中調試 C/C++ 內存泄漏方法

https://www.ibm.com/developerworks/cn/linux/l-cn-memleak/

在 Linux 平臺中調試 C/C++ 內存泄漏方法

韓 兆兵 ([email protected]), 軟件工程師, IBM

劉 盈, 軟件工程師, IBM

強 晟 ([email protected]), 軟件工程師, IBM

2008 年 5 月 15 日

由於 C 和 C++ 程序中完全由程序員自主申請和釋放內存,稍不注意,就會在系統中導入內存錯誤。同時,內存錯誤往往非常嚴重,一般會帶來諸如系統崩潰,內存耗盡這樣嚴重的後果。本文將從靜態分析和動態檢測兩個角度介紹在 Linux 環境進行內存泄漏檢測的方法,並重點介紹靜態分析工具 BEAM、動態監測工具 Valgrind 和 rational purify 的使用方法。相信通過本文的介紹,能給大家對處理其它產品或項目內存泄漏相關的問題時提供借鑑。

內容

由於 C 和 C++ 程序中完全由程序員自主申請和釋放內存,稍不注意,就會在系統中導入內存錯誤。同時,內存錯誤往往非常嚴重,一般會帶來諸如系統崩潰,內存耗盡這樣嚴重的後果。從歷史上看,來自計算機應急響應小組和供應商的許多最嚴重的安全公告都是由簡單的內存錯誤造成的。自從 70 年代末期以來,C/C++ 程序員就一直討論此類錯誤,但其影響在 2007 年仍然很大。與許多其他類型的常見錯誤不同,內存錯誤通常具有隱蔽性,即它們很難再現,症狀通常不能在相應的源代碼中找到。例如,無論何時何地發生內存泄漏,都可能表現爲應用程序完全無法接受,同時內存泄漏不是顯而易見[1]。存在內存錯誤的 C 和 C++ 程序會導致各種問題。如果它們泄漏內存,則運行速度會逐漸變慢,並最終停止運行;如果覆蓋內存,則會變得非常脆弱,很容易受到惡意用戶的攻擊。

因此,出於這些原因,需要特別關注 C 和 C++ 編程的內存問題,特別是內存泄漏。本文先從如何發現內存泄漏,然後是用不同的方法和工具定位內存泄漏,最後對這些工具進行了比較,另外還簡單介紹了資源泄漏的處理(以句柄泄漏爲例)。本文使用的測試平臺是:Linux (Redhat AS4)。但是這些方法和工具許多都不只是侷限於 C/C++ 語言以及 linux 操作系統。

內存泄漏一般指的是堆內存的泄漏。堆內存是指程序從堆中分配的、大小任意的(內存塊的大小可以在程序運行期決定)、使用完後必須顯示的釋放的內存。應用程序一般使用malloc、realloc、new 等函數從堆中分配到一塊內存,使用完後,程序必須負責相應的調用 free 或 delete 釋放該內存塊。否則,這塊內存就不能被再次使用,我們就說這塊內存泄漏了。

1. 如何發現內存泄漏

有些簡單的內存泄漏問題可以從在代碼的檢查階段確定。還有些泄漏比較嚴重的,即在很短的時間內導致程序或系統崩潰,或者系統報告沒有足夠內存,也比較容易發現。最困難的就是泄漏比較緩慢,需要觀測幾天、幾周甚至幾個月才能看到明顯異常現象。那麼如何在比較短的時間內檢測出有沒有潛在的內存泄漏問題呢?實際上不同的系統都帶有內存監視工具,我們可以從監視工具收集一段時間內的堆棧內存信息,觀測增長趨勢,來確定是否有內存泄漏。在 Linux 平臺可以用 ps 命令,來監視內存的使用,比如下面的命令 (觀測指定進程的VSZ值):

ps -aux

2. 靜態分析

包括手動檢測和靜態工具分析,這是代價最小的調試方法。

2.1 手動檢測

當使用 C/C++ 進行開發時,採用良好的一致的編程規範是防止內存問題第一道也是最重要的措施。檢測是編碼標準的補充。二者各有裨益,但結合使用效果特別好。專業的 C 或 C++ 專業人員甚至可以瀏覽不熟悉的源代碼,並以極低的成本檢測內存問題。通過少量的實踐和適當的文本搜索,您能夠快速驗證平衡的 *alloc() 和 free() 或者 new 和 delete 的源主體。人工查看此類內容通常會出現像清單 1 中一樣的問題,可以定位出在函數 LeakTest 中的堆變量 Logmsg 沒有釋放。

清單1. 簡單的內存泄漏
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int LeakTest(char * Para)
{
        if(NULL==Para){
                //local_log("LeakTest Func: empty parameter\n");
                return -1;
        }
        char * Logmsg = new char[128];
        if(NULL == Logmsg){
                //local_log("memeory allocation failed\n");
                return -2;
        }
        sprintf(Logmsg,"LeakTest routine exit: '%s'.\n", Para);
        //local_log(Logmsg);
        return 0;
}
int   main(int argc,char **argv )
{
        char szInit [] = "testcase1";
        LeakTest(szInit);
        return 0;
}

2.2 靜態代碼分析工具

代碼靜態掃描和分析的工具比較多,比如 splint, PC-LINT, BEAM 等。因爲 BEAM 支持的平臺比較多,這以 BEAM 爲例,做個簡單介紹,其它有類似的處理過程。

BEAM 可以檢測四類問題: 沒有初始化的變量;廢棄的空指針;內存泄漏;冗餘計算。而且支持的平臺比較多。

BEAM 支持以下平臺:

  • Linux x86 (glibc 2.2.4)
  • Linux s390/s390x (glibc 2.3.3 or higher)
  • Linux (PowerPC, USS) (glibc 2.3.2 or higher)
  • AIX (4.3.2+)
  • Window2000 以上
清單2. 用作 Beam 分析的代碼
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *p;

void
foo(int a)
{
  int b, c;

  b = 0;
  if(!p) 
    c = 1;

  if(c > a)
    c += p[1];
}

int LeakTest(char * Para)
{
        char * Logmsg = new char[128];
        if((Para==NULL)||(Logmsg == NULL))
                return -1;        
        sprintf(Logmsg,"LeakTest routine exit: '%s'.\n", Para);        
        return 0;
}

int   main(int argc,char **argv )
{
        char szInit [] = "testcase1";
        LeakTest(szInit);
        return 0;
}

下面以 X86 Linux 爲例,代碼如清單 2,具體的環境如下:

OS: Red Hat Enterprise Linux AS release 4 (Nahant Update 2)

GCC: gcc version 3.4.4

BEAM: 3.4.2; https://w3.eda.ibm.com/beam/

可以把 BEAM 看作一個 C/C++ 編譯器,按下面的命令進行編譯 (前面兩個命令是設置編譯器環境變量):

./beam-3.4.2/bin/beam_configure  --c gcc
./beam-3.4.2/bin/beam_configure  --cpp g++
./beam-3.4.2/bin/beam_compile  --beam::compiler=compiler_cpp_config.tcl  -cpp code2.cpp

從下面的編譯報告中,我們可以看到這段程序中有三個錯誤:”內存泄漏”;“變量未初始化”;“ 空指針操作”

"code2.cpp", line 10: warning: variable "b" was set but never used
    int b, c;
        ^

BEAM_VERSION=3.4.2
BEAM_ROOT=/home/hanzb/memdetect
BEAM_DIRECTORY_WRITE_INNOCENTS=
BEAM_DIRECTORY_WRITE_ERRORS=

-- ERROR23(heap_memory)     /*memory leak*/     >>>ERROR23_LeakTest_7b00071dc5cbb458
"code2.cpp", line 24: memory leak
ONE POSSIBLE PATH LEADING TO THE ERROR:
 "code2.cpp", line 22: allocating using `operator new[]' (this memory will not be freed)
 "code2.cpp", line 22: assigning into `Logmsg'
 "code2.cpp", line 24: deallocating `Logmsg' because exiting its scope 
                       (losing last pointer to the memory)

-- ERROR1     /*uninitialized*/     >>>ERROR1_foo_60c7889b2b608
"code2.cpp", line 16: uninitialized `c'
ONE POSSIBLE PATH LEADING TO THE ERROR:
 "code2.cpp", line 10: allocating `c'
 "code2.cpp", line 13: the if-condition is false
 "code2.cpp", line 16: getting the value of `c'

 VALUES AT THE END OF THE PATH:
  p != 0 


-- ERROR2     /*operating on NULL*/     >>>ERROR2_foo_af57809a2b615
"code2.cpp", line 17: invalid operation involving NULL pointer
ONE POSSIBLE PATH LEADING TO THE ERROR:
 "code2.cpp", line 13: the if-condition is true (used as evidence that error is possible)
 "code2.cpp", line 16: the if-condition is true
 "code2.cpp", line 17: invalid operation `[]' involving NULL pointer `p'

 VALUES AT THE END OF THE PATH:
  c = 1 
  p = 0 
  a <= 0

2.3 內嵌程序

可以重載內存分配和釋放函數 new 和 delete,然後編寫程序定期統計內存的分配和釋放,從中找出可能的內存泄漏。或者調用系統函數定期監視程序堆的大小,關鍵要確定堆的增長是泄漏而不是合理的內存使用。這類方法比較複雜,在這就不給出詳細例子了。

3. 動態運行檢測

實時檢測工具主要有 valgrind, Rational purify 等。

3.1 Valgrind

valgrind 是幫助程序員尋找程序裏的 bug 和改進程序性能的工具。程序通過 valgrind 運行時,valgrind 收集各種有用的信息,通過這些信息可以找到程序中潛在的 bug 和性能瓶頸。

Valgrind 現在提供多個工具,其中最重要的是 Memcheck,Cachegrind,Massif 和 Callgrind。Valgrind 是在 Linux 系統下開發應用程序時用於調試內存問題的工具。它尤其擅長髮現內存管理的問題,它可以檢查程序運行時的內存泄漏問題。其中的 memecheck 工具可以用來尋找 c、c++ 程序中內存管理的錯誤。可以檢查出下列幾種內存操作上的錯誤:

  • 讀寫已經釋放的內存
  • 讀寫內存塊越界(從前或者從後)
  • 使用還未初始化的變量
  • 將無意義的參數傳遞給系統調用
  • 內存泄漏

3.2 Rational purify

Rational Purify 主要針對軟件開發過程中難於發現的內存錯誤、運行時錯誤。在軟件開發過程中自動地發現錯誤,準確地定位錯誤,提供完備的錯誤信息,從而減少了調試時間。同時也是市場上唯一支持多種平臺的類似工具,並且可以和很多主流開發工具集成。Purify 可以檢查應用的每一個模塊,甚至可以查出複雜的多線程或進程應用中的錯誤。另外不僅可以檢查 C/C++,還可以對 Java 或 .NET 中的內存泄漏問題給出報告。

在 Linux 系統中,使用 Purify 需要重新編譯程序。通常的做法是修改 Makefile 中的編譯器變量。下面是用來編譯本文中程序的 Makefile:

CC=purify gcc

首先運行 Purify 安裝目錄下的 purifyplus_setup.sh 來設置環境變量,然後運行 make 重新編譯程序。

./purifyplus_setup.sh

下面給出編譯一個代碼文件的示例,源代碼文件命名爲 test3.cpp. 用 purify 和 g++ 的編譯命令如下,‘-g’是編譯時加上調試信息。

purify g++ -g test3.cpp –o test

運行編譯生成的可執行文件 test,就可以得到圖1,可以定位出內存泄漏的具體位置。

./test
清單3. Purify 分析的代碼
#include <unistd.h> 
 char * Logmsg;

int LeakTest(char * Para)
{
        if(NULL==Para){
                //local_log("LeakTest Func: empty parameter\n");
                return -1;
        }
        Logmsg = new char[128];
		for (int i = 0 ; i < 128; i++)
			Logmsg[i] = i%64;

        if(NULL == Logmsg){
                //local_log("memeory allocation failed\n");
                return -2;
        }
        sprintf(Logmsg,"LeakTest routine exit: '%s'.\n", Para);
        //local_log(Logmsg);
        return 0;
}

int   main(int argc,char **argv )
{
        char szInit [] = "testcase1";
		int i;
         LeakTest(szInit);
		for (i=0; i < 2; i++){
			if(i%200 == 0)
				LeakTest(szInit);
			sleep(1);
		}        
        return 0;
}

需要指出的是,程序必須編譯成調試版本纔可以定位到具體哪行代碼發生了內存泄漏。即在 gcc 或者 g++ 中,必須使用 "-g" 選項。

圖 1 purify 的輸出結果
圖 1 purify 的輸出結果

結論

本文介紹了多種內存泄漏,定位方法(包括靜態分析,動態實時檢測)。涉及到了多個工具,詳細描述的它們的用法、用途以及優缺點。對處理其它產品或項目內存泄漏相關的問題有很好的借鑑意義。

參考資料

學習

獲得產品和技術

  • 用可直接從 developerWorks 下載的 IBM 試用軟件 構建您的下一個 Linux 開發項目。


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