在 Linux下調(diào)試內(nèi)存泄漏的方法
由于內(nèi)存泄漏不是顯而易見,而且存在內(nèi)存錯誤的 C 和 C++ 程序會導致各種問題,所以需要特別關注 C 和 C++ 編程的內(nèi)存問題,特別是內(nèi)存泄漏。本文先從如何發(fā)現(xiàn)內(nèi)存泄漏,然后是用不同的方法和工具定位內(nèi)存泄漏,最后對這些工具進行了比較,另外還簡單介紹了資源泄漏的處理(以句柄泄漏為例)。本文使用的測試平臺是:Linux (Redhat AS4)。但是這些方法和工具許多都不只是局限于 C/C++ 語言以及 linux 操作系統(tǒng)。
內(nèi)存泄漏一般指的是堆內(nèi)存的泄漏。堆內(nèi)存是指程序從堆中分配的、大小任意的(內(nèi)存塊的大小可以在程序運行期決定)、使用完后必須顯示的釋放的內(nèi)存。應用程序一般使用malloc、realloc、new 等函數(shù)從堆中分配到一塊內(nèi)存,使用完后,程序必須負責相應的調(diào)用 free 或 delete 釋放該內(nèi)存塊。否則,這塊內(nèi)存就不能被再次使用,我們就說這塊內(nèi)存泄漏了。
1. 如何發(fā)現(xiàn)內(nèi)存泄漏
有些簡單的內(nèi)存泄漏問題可以從在代碼的檢查階段確定。還有些泄漏比較嚴重的,即在很短的時間內(nèi)導致程序或系統(tǒng)崩潰,或者系統(tǒng)報告沒有足夠內(nèi)存,也比較容易發(fā)現(xiàn)。最困難的就是泄漏比較緩慢,需要觀測幾天、幾周甚至幾個月才能看到明顯異常現(xiàn)象。那么如何在比較短的時間內(nèi)檢測出有沒有潛在的內(nèi)存泄漏問題呢?實際上不同的系統(tǒng)都帶有內(nèi)存監(jiān)視工具,我們可以從監(jiān)視工具收集一段時間內(nèi)的堆棧內(nèi)存信息,觀測增長趨勢,來確定是否有內(nèi)存泄漏。在 Linux 平臺可以用 ps 命令,來監(jiān)視內(nèi)存的使用,比如下面的命令 (觀測指定進程的VSZ值):
ps -aux
2. 靜態(tài)分析
包括手動檢測和靜態(tài)工具分析,這是代價最小的調(diào)試方法。
(1)手動檢測
當使用 C/C++ 進行開發(fā)時,采用良好的一致的編程規(guī)范是防止內(nèi)存問題第一道也是最重要的措施。檢測是編碼標準的補充。二者各有裨益,但結合使用效果特別好。專業(yè)的 C 或 C++ 專業(yè)人員甚至可以瀏覽不熟悉的源代碼,并以極低的成本檢測內(nèi)存問題。通過少量的實踐和適當?shù)奈谋舅阉?,您能夠快速驗證平衡的 *alloc() 和 free() 或者 new 和 delete 的源主體。人工查看此類內(nèi)容通常會出現(xiàn)像清單 1 中一樣的問題,可以定位出在函數(shù) LeakTest 中的堆變量 Logmsg 沒有釋放。
清單1. 簡單的內(nèi)存泄漏
- #include
- #include
- #include
- 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)靜態(tài)代碼分析工具
代碼靜態(tài)掃描和分析的工具比較多,比如 splint, PC-LINT, BEAM 等。因為 BEAM 支持的平臺比較多,這以 BEAM 為例,做個簡單介紹,其它有類似的處理過程。
BEAM 可以檢測四類問題: 沒有初始化的變量;廢棄的空指針;內(nèi)存泄漏;冗余計算。而且支持的平臺比較多。
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
- #include
- #include
- 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,具體的環(huán)境如下:
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++ 編譯器,按下面的命令進行編譯 (前面兩個命令是設置編譯器環(huán)境變量):
- ./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
從下面的編譯報告中,我們可以看到這段程序中有三個錯誤:”內(nèi)存泄漏”;“變量未初始化”;“ 空指針操作”
- "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
#p#
(3) 內(nèi)嵌程序
可以重載內(nèi)存分配和釋放函數(shù) new 和 delete,然后編寫程序定期統(tǒng)計內(nèi)存的分配和釋放,從中找出可能的內(nèi)存泄漏。或者調(diào)用系統(tǒng)函數(shù)定期監(jiān)視程序堆的大小,關鍵要確定堆的增長是泄漏而不是合理的內(nèi)存使用。這類方法比較復雜,在這就不給出詳細例子了。
3. 動態(tài)運行檢測
實時檢測工具主要有 valgrind, Rational purify 等。
(1) Valgrind
valgrind 是幫助程序員尋找程序里的 bug 和改進程序性能的工具。程序通過 valgrind 運行時,valgrind 收集各種有用的信息,通過這些信息可以找到程序中潛在的 bug 和性能瓶頸。
Valgrind 現(xiàn)在提供多個工具,其中最重要的是 Memcheck,Cachegrind,Massif 和 Callgrind。Valgrind 是在 Linux 系統(tǒng)下開發(fā)應用程序時用于調(diào)試內(nèi)存問題的工具。它尤其擅長發(fā)現(xiàn)內(nèi)存管理的問題,它可以檢查程序運行時的內(nèi)存泄漏問題。其中的 memecheck 工具可以用來尋找 c、c++ 程序中內(nèi)存管理的錯誤。可以檢查出下列幾種內(nèi)存操作上的錯誤:
讀寫已經(jīng)釋放的內(nèi)存
讀寫內(nèi)存塊越界(從前或者從后)
使用還未初始化的變量
將無意義的參數(shù)傳遞給系統(tǒng)調(diào)用
內(nèi)存泄漏
(2) Rational purify
Rational Purify 主要針對軟件開發(fā)過程中難于發(fā)現(xiàn)的內(nèi)存錯誤、運行時錯誤。在軟件開發(fā)過程中自動地發(fā)現(xiàn)錯誤,準確地定位錯誤,提供完備的錯誤信息,從而減少了調(diào)試時間。同時也是市場上唯一支持多種平臺的類似工具,并且可以和很多主流開發(fā)工具集成。Purify 可以檢查應用的每一個模塊,甚至可以查出復雜的多線程或進程應用中的錯誤。另外不僅可以檢查 C/C++,還可以對 Java 或 .NET 中的內(nèi)存泄漏問題給出報告。
在 Linux 系統(tǒng)中,使用 Purify 需要重新編譯程序。通常的做法是修改 Makefile 中的編譯器變量。下面是用來編譯本文中程序的 Makefile:
CC=purify gcc
首先運行 Purify 安裝目錄下的 purifyplus_setup.sh 來設置環(huán)境變量,然后運行 make 重新編譯程序。
./purifyplus_setup.sh
下面給出編譯一個代碼文件的示例,源代碼文件命名為 test3.cpp. 用 purify 和 g++ 的編譯命令如下,‘-g’是編譯時加上調(diào)試信息。
purify g++ -g test3.cpp –o test
運行編譯生成的可執(zhí)行文件 test,就可以得到圖1,可以定位出內(nèi)存泄漏的具體位置。
./test
清單3. Purify 分析的代碼
- #include
- 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;
- }
需要指出的是,程序必須編譯成調(diào)試版本才可以定位到具體哪行代碼發(fā)生了內(nèi)存泄漏。即在 gcc 或者 g++ 中,必須使用 "-g" 選項。
圖 1 purify 的輸出結果
以上就是幾種內(nèi)存泄露,以及調(diào)試方法。對程序內(nèi)存泄露的問題有著一定的幫助。
【編輯推薦】
- 在iPhone應用中如何避免內(nèi)存泄露
- Linux 內(nèi)存監(jiān)控內(nèi)存泄露和回收內(nèi)存的方法
- Windows 7被曝內(nèi)存泄露缺陷 可導致系統(tǒng)崩潰
- Linux kernel多個內(nèi)存泄露本地拒絕服務漏洞
- Linux Kernel 2.4 RTC處理函數(shù)內(nèi)存泄露漏洞
- Linux內(nèi)核本地整數(shù)溢出和內(nèi)存泄露漏洞