因爲 C 和 C++ 程序中徹底由程序員自主申請和釋放內存,稍不注意,就會在系統中導入內存錯誤。同時,內存錯誤每每很是嚴重,通常會帶來諸如系統崩潰,內存耗盡這樣嚴重的後果。從歷史上看,來自計算機應急響應小組和供應商的許多最嚴重的安全公告都是由簡單的內存錯誤形成的。自從 70 年代末期以來,C/C++ 程序員就一直討論此類錯誤,但其影響在 2007 年仍然很大。與許多其餘類型的常見錯誤不一樣,內存錯誤一般具備隱蔽性,即它們很難再現,症狀一般不能在相應的源代碼中找到。例如,不管什麼時候何地發生內存泄漏,均可能表現爲應用程序徹底沒法接受,同時內存泄漏不是顯而易見[1]。存在內存錯誤的 C 和 C++ 程序會致使各類問題。若是它們泄漏內存,則運行速度會逐漸變慢,並最終中止運行;若是覆蓋內存,則會變得很是脆弱,很容易受到惡意用戶的攻擊。 html
所以,出於這些緣由,須要特別關注 C 和 C++ 編程的內存問題,特別是內存泄漏。本文先從如何發現內存泄漏,而後是用不一樣的方法和工具定位內存泄漏,最後對這些工具進行了比較,另外還簡單介紹了資源泄漏的處理(以句柄泄漏爲例)。本文使用的測試平臺是:Linux (Redhat AS4)。可是這些方法和工具許多都不僅是侷限於 C/C++ 語言以及 linux 操做系統。 linux
內存泄漏通常指的是堆內存的泄漏。堆內存是指程序從堆中分配的、大小任意的(內存塊的大小能夠在程序運行期決定)、使用完後必須顯示的釋放的內存。應用程序通常使用malloc、realloc、new 等函數從堆中分配到一塊內存,使用完後,程序必須負責相應的調用 free 或 delete 釋放該內存塊。不然,這塊內存就不能被再次使用,咱們就說這塊內存泄漏了。 c++
有些簡單的內存泄漏問題能夠從在代碼的檢查階段肯定。還有些泄漏比較嚴重的,即在很短的時間內致使程序或系統崩潰,或者系統報告沒有足夠內存,也比較容易發現。最困難的就是泄漏比較緩慢,須要觀測幾天、幾周甚至幾個月才能看到明顯異常現象。那麼如何在比較短的時間內檢測出有沒有潛在的內存泄漏問題呢?實際上不一樣的系統都帶有內存監視工具,咱們能夠從監視工具收集一段時間內的堆棧內存信息,觀測增加趨勢,來肯定是否有內存泄漏。在 Linux 平臺能夠用 ps 命令,來監視內存的使用,好比下面的命令 (觀測指定進程的VSZ值): 程序員
ps -aux
回頁首 編程
包括手動檢測和靜態工具分析,這是代價最小的調試方法。 安全
2.1 手動檢測 ide
當使用 C/C++ 進行開發時,採用良好的一致的編程規範是防止內存問題第一道也是最重要的措施。檢測是編碼標準的補充。兩者各有裨益,但結合使用效果特別好。專業的 C 或 C++ 專業人員甚至能夠瀏覽不熟悉的源代碼,並以極低的成本檢測內存問題。經過少許的實踐和適當的文本搜索,您可以快速驗證平衡的 *alloc() 和 free() 或者 new 和 delete 的源主體。人工查看此類內容一般會出現像清單 1 中同樣的問題,能夠定位出在函數 LeakTest 中的堆變量 Logmsg 沒有釋放。 函數
#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 支持如下平臺:
#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,而後編寫程序按期統計內存的分配和釋放,從中找出可能的內存泄漏。或者調用系統函數按期監視程序堆的大小,關鍵要肯定堆的增加是泄漏而不是合理的內存使用。這類方法比較複雜,在這就不給出詳細例子了。
實時檢測工具主要有 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
#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" 選項。
本文介紹了多種內存泄漏,定位方法(包括靜態分析,動態實時檢測)。涉及到了多個工具,詳細描述的它們的用法、用途以及優缺點。對處理其它產品或項目內存泄漏相關的問題有很好的借鑑意義。