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