簡介
記憶體泄漏(Memory Leak)是指程式中己動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式運行速度減慢甚至系統崩潰等嚴重後果。
記憶體泄漏缺陷具有隱蔽性、積累性的特徵,比其他記憶體非法訪問錯誤更難檢測。因為記憶體泄漏的產生原因是記憶體塊未被釋放,屬於遺漏型缺陷而不是過錯型缺陷。此外,記憶體泄漏通常不會直接產生可觀察的錯誤症狀,而是逐漸積累,降低系統整體性能,極端的情況下可能使系統崩潰。
隨著計算機套用需求的日益增加,應用程式的設計與開發也相應的日趨複雜,開發人員在程式實現的過程中處理的變數也大量增加,如何有效進行記憶體分配和釋放,防止記憶體泄漏的問題變得越來越突出。例如伺服器套用軟體,需要長時間的運行,不斷的處理由客戶端發來的請求,如果沒有有效的記憶體管理,每處理一次請求信息就有一定的記憶體泄漏。這樣不僅影響到伺服器的性能,還可能造成整個系統的崩潰。因此,記憶體管理成為軟體設計開發人員在設計中考慮的主要方面 。
泄漏原因
在C語言中,從變數存在的時間生命周期角度上,把變數分為靜態存儲變數和動態存儲變數兩類。靜態存儲變數是指在程式運行期間分配了固定存儲空間的變數而動態存儲變數是指在程式運行期間根據實際需要進行動態地分配存儲空間的變數。在記憶體中供用戶使用的記憶體空間分為三部分:
•程式存儲區
•靜態存儲區
•動態存儲區。
程式中所用的數據分別存放在靜態存儲區和動態存儲區中。靜態存儲區數據在程式的開始就分配好記憶體區,在整個程式執行過程中它們所占的存儲單元是固定的,在程式結束時就釋放,因此靜態存儲區數據一般為全局變數。動態存儲區數據則是在程式執行過程中根據需要動態分配和動態釋放的存儲單元,動態存儲區數據有三類函式形參變數、局部變數和函式調用時的現場保護與返回地址。由於動態存儲變數可以根據函式調用的需要,動態地分配和釋放存儲空間,大大提高了記憶體的使用效率,使得動態存儲變數在程式中被廣泛使用。
開發人員進行程式開發的過程使用動態存儲變數時,不可避免地面對記憶體管理的問題。程式中動態分配的存儲空間,在程式執行完畢後需要進行釋放。沒有釋放動態分配的存儲空間而造成記憶體泄漏,是使用動態存儲變數的主要問題。一般情況下,開發人員使用系統提供的記憶體管理基本函式,如malloc、recalloc、calloc、free等,完成動態存儲變數存儲空間的分配和釋放。但是,當開發程式中使用動態存儲變數較多和頻繁使用函式調用時,就會經常發生記憶體管理錯誤,例如:
•分配一個記憶體塊並使用其中未經初始化的內容;
•釋放一個記憶體塊,但繼續引用其中的內容;
•子函式中分配的記憶體空間在主函式出現異常中斷時、或主函式對子函式返回的信息使用結束時,沒有對分配的記憶體進行釋放;
•程式實現過程中分配的臨時記憶體在程式結束時,沒有釋放臨時記憶體。記憶體錯誤一般是不可再現的,開發人員不易在程式調試和測試階段發現,即使花費了很多精力和時間,也無法徹底消除。
產生方式的分類
以產生的方式來分類,記憶體泄漏可以分為四類:
常發性記憶體泄漏:發生記憶體泄漏的代碼會被多次執行到,每次被執行時都會導致一塊記憶體泄漏。
偶發性記憶體泄漏:發生記憶體泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測記憶體泄漏至關重要。
一次性記憶體泄漏:發生記憶體泄漏的代碼只會被執行一次,或者由於算法上的缺陷,導致總會有一塊且僅有一塊記憶體發生泄漏。
隱式記憶體泄漏:程式在運行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體泄漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要運行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所以,我們稱這類記憶體泄漏為隱式記憶體泄漏。從用戶使用程式的角度來看,記憶體泄漏本身不會產生什麼危害,作為一般的用戶,根本感覺不到記憶體泄漏的存在。真正有危害的是記憶體泄漏的堆積,這會最終耗盡系統所有的記憶體。從這個角度來說,一次性記憶體泄漏並沒有什麼危害,因為它不會堆積,而隱式記憶體泄漏危害性則非常大,因為較之於常發性和偶發性記憶體泄漏它更難被檢測到。
1.常發性記憶體泄漏:發生記憶體泄漏的代碼會被多次執行到,每次被執行時都會導致一塊記憶體泄漏。
2.偶發性記憶體泄漏:發生記憶體泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測記憶體泄漏至關重要。
3.一次性記憶體泄漏:發生記憶體泄漏的代碼只會被執行一次,或者由於算法上的缺陷,導致總會有一塊且僅有一塊記憶體發生泄漏。
4.隱式記憶體泄漏:程式在運行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體泄漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要運行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所以,我們稱這類記憶體泄漏為隱式記憶體泄漏。從用戶使用程式的角度來看,記憶體泄漏本身不會產生什麼危害,作為一般的用戶,根本感覺不到記憶體泄漏的存在。真正有危害的是記憶體泄漏的堆積,這會最終耗盡系統所有的記憶體。從這個角度來說,一次性記憶體泄漏並沒有什麼危害,因為它不會堆積,而隱式記憶體泄漏危害性則非常大,因為較之於常發性和偶發性記憶體泄漏它更難被檢測到。
檢測方法
無論是C還是C++程式,運行時候的變數主要有三種分配方式:堆分配、棧分配、全局和靜態記憶體分配。記憶體泄漏主要是發生在堆記憶體分配方式中,即“配置了記憶體後,所有指向該記憶體的指針都遺失了”,若缺乏語言這樣的垃圾回收機制,這樣的記憶體片就無法歸還系統。因為記憶體泄漏屬於程式運行中的問題,無法通過編譯識別,所以只能在程式運行過程中來判別和診斷。下面將介紹幾種常用的記憶體檢測方法,每種方法均以現有的記憶體檢測工具為分析範例,並對各種方法進行比較。
靜態分析技術
靜態分析技術就是直接分析程式的原始碼或機器代碼,獲得一些有用的信息,而並不運行程式本身。目前有許多靜態分析的工具,編譯器就屬於這一類,它讀入源程式代碼,對源程式進行詞法和語法分析,進行數據類型的檢查以及一些最佳化的分析等,以此來提高程式的質量與運行效率。這類靜態的分析工具僅僅是讀入程式代碼進行相關的分析,而並不進行其它額外的操作,如修改源程式代碼等。
LCLink是一種通過對原始碼及添加到原始碼中特定格式的注釋說明進行靜態分析的程式理解和檢錯工具,的檢查對象是源程式,能檢查出的記憶體錯誤有記憶體分配釋放故障、空指針的錯誤使用、使用未定義或己被釋放的記憶體等程式錯誤。
LCLink重點分析兩類記憶體釋放錯誤:
•試圖釋放某記憶體塊,該記憶體塊有兩個或兩個以上的有效指針指向它。
•試圖釋放某記憶體塊,該記憶體塊沒有任何有效指針指向它。
解決此類記憶體錯誤的方法是規定分配某塊記憶體時返回的指針必須釋放該記憶體。使用注釋表示某指針是唯一指向某記憶體塊的指針,使用注釋表示被調用函式可能釋放函式參數指向的記憶體塊或創建新的指針指向該記憶體塊。
原始碼插裝技術
為了獲得被測程式的動態執行信息,需要對其進行跟蹤,一般使用插裝方法。所謂插裝就是在保持被測程式的邏輯完整性的基礎上,在被測程式的特定部位插入一段檢測程式又稱探針函式,通過探針的執行拋出程式的運行特徵數據。基於這些特徵數據分析,可以獲得程式的控制流及數據流信息,進而獲得邏輯覆蓋等動態信息,這樣就可以在被測程式執行的過程中動態地同步執行程式的檢測工作。插裝方法又分為原始碼級程式插裝和目標代碼級程式插裝。原始碼插裝測試必須在靜態測試部分獲得的被測程式的結構信息、靜態數據信息、控制流信息等基礎上,套用插裝技術向被測程式中的適當位置植入相應類型的探針,通過運行帶有探針的被測程式而獲得程式運行的動態數據。原始碼插裝要通過運行被測程式來測定程式的各種指標,如覆蓋率、時間性能、記憶體使用等等,實現原始碼插裝的關鍵技術是藉助於插入到源程式中的監控語句來收集執行信息,以達到揭示程式內部行為和特性的目的,如圖所示。
基於原始碼插裝的動態測試框架分為個主要的階段:
•插裝互動與動態測試信息分析;
•插裝階段;
•插裝庫製作階段;
•測試實施階段。
插裝互動與動態測試信息分析是軟體測試工具與用戶互動的界面。用戶通過該界面選擇要進行動態測試的程式模組,拓撲產生相應的插裝選擇記錄檔案。用戶還可以通過該互動界而瀏覽動態測試結果信息,在軟體測試工具的實現上,採用可視化的方式顯示這些動態信息。插裝階段實現了在被測程式中植入探針,並生成帶有插裝信息的源檔案。在此過程中,首先將被測程式經過預處理展開為不包含宏、條件編譯和頭檔案的檔案格式。然後,按照一定的插裝策略,根據前面生成的插裝選擇記錄檔案,將探針函式載入到該檔案中,最後生成插裝後的程式。插裝庫製作階段的目的是生成插裝庫中的探針函式,它含有插裝語句調用的函式及其函式的定義。顯然,插裝過程中生成的目標檔案中含有探針函式的樁,而探針函式的實現恰恰在本過程完成。需要指出的是,插裝庫的製作過程是獨立於動態測試過程之外的,可以與軟體測試工具開發同步。測試實施階段將插裝過程生成的檔案與插裝庫製作過程生成的插裝靜態庫連線生成帶有插裝信息的執行檔,選取測試用例,運行該程式,可以獲得被測程式的動態跟蹤信息。
在以上四個階段中,其中的插裝互動與動態測試信息分析與測試實施階段是測試人員的可視部分,通過這兩部分,用戶與系統互動,完成測試工作。而插裝階段與插裝庫製作階段對測試人員是不可見的,在後台完成,對於用戶而言,這兩部分是完全透明的。在性能方面,採用插裝方法應儘量減少插裝開銷。為了達到不同的統計目的如語句覆蓋、分支覆蓋等,應儘量減少插裝次數。若能僅僅插裝一次就能完成多種類型的統計,則可使插裝代碼得到最佳化。此外,應儘量減少插裝代碼的數量,減少插裝代碼的運行次數,從而達到減小插裝代碼運行開銷的目的。特別是對於一些實時系統的測試,在這方面的要求尤為苛刻。一個運行時錯誤檢測工具,能夠自動檢測一套用中大量的編程和運行時錯誤。通過使用源碼插裝和運行時指針跟蹤的專利技術,在編譯時,附十插入測試和分析代碼,它建立一個有關程式中各種對象的資料庫。然後在運行時通過檢查數據值和記憶體引用驗證對象的一致性和正確性。使用這些技術,包括變異測試技術等,一能夠檢查和測試用戶的代碼,精確定位錯誤的準確位置並給出詳細的診斷信息。十十能夠可視化實時記憶體操作,最佳化記憶體算法。還能執行覆蓋性分析,清楚地指示那些代碼己經測試過。將集成到開發環境中,能夠極大地減少調試時間並有效地防止錯誤。檢驗每一次記憶體操作的有效性,包括靜態全局和堆疊以及動態分配記憶體的操作。葉有兩種運行模式。監護模式下用戶可以快速檢測代碼中的錯誤,不需要對代碼作任何插裝和處理源碼插裝模式則進行徹底地代碼檢測。
目標代碼插裝技術
目標代碼插裝實現主要分為預處理、測試執行和結果匯總個階段,工作流程如圖所示,系統主要工作是圍繞斷點而進行的。在預處理階段,首先靜態分析被測程式的目標代碼,查找待測程式中原始碼各語句、函式入口點在目標代碼中的對`應位置,然後在相應位置插入斷點在測試執行階段,啟動調試進程,當被測程式執行到斷點處時,回響斷點信息,在相應的斷點處完成相應的統計操作在結果匯總階段,根據各斷點處的統計結果,按不同的統計角度進行歸併、綜合得到最終的統計數據。
被測代碼預處理
在測試預處理階段對被測程式的目標代碼進行分析,可以獲得目標代碼與原始碼中語句、函式的對應關係。在目標代碼中為相對應的原始碼的每條語句及每個函式的入口點插入斷點。對於第三方代碼,只要其目標代碼格式與下生成的目標代碼格式一致,我們就可以用與分析用戶代碼同樣的方法獲取信息。獲取斷點的信息後,為所有的斷點建立斷點鍊表,同時建立語句及函式的信息鍊表,供隨後的測試執行階段存儲信息。預處理流程如圖所示。
測試執行階段
利用OCI技術,我們把測試執行看作是一個在被測進程和檢測進程間不斷切換的過程。每當被測進程遇到斷點,就會將自身掛起,同時傳送訊息喚醒檢測進程,檢測進程根據當前斷點的地址在斷點鍊表中查找相應節點,並查找對應的語句或函式信息,記錄該語句或函式的執行次數、到達或離開的時刻,供以後統計之用。然後,將插入的斷點信息去除,恢復原來的指令,轉入被測進程繼續執行。在轉入被測進程之前,必須將上一個斷點處的斷點恢復上一個斷點處的斷點在指令運行時被去除了。具體流程如圖所示。
數據統計與結果匯總
根據各斷點處的統計結果,按不同的統計角度進行歸併、綜合,進行覆蓋率及各種時間的計算,得到最終的統計數據。是公司出品的一種軟體測試和質量保證工具,它能檢測程式記憶體泄漏和記憶體訪問衝突等錯誤。使用目標碼插裝技術,在編譯器生成的目標碼中直接插入特殊的檢查指令實現對記憶體錯誤的檢測。在程式的所有代碼中插入這些檢查邏輯,包括第三方目標碼庫,並且驗證系統調用的接口。目標碼插裝技術分為連結前插裝和連結後插裝兩種插裝方法。使用如圖所示的連結前插裝法。檢查插裝後程式的每個記憶體讀寫動作,跟蹤記憶體使用情況,使用類似垃圾收集器的技術來檢查記憶體泄漏。垃圾收集機制分為兩階段垃圾檢測和垃圾回收。為了不影響程式的執行速度,提供了一個可調用的垃圾檢測器,使用類似於保守式垃圾收集算法,即標記一清除算法。在標記階段,遞歸地從數據段、堆疊段到數據堆跟蹤分析指針,並使用標準保守式方法為所有被引用的記憶體塊做標記。在清除階段,逐步訪問數據堆,並報告已分配但程式不再引用的記憶體塊,即程式的記憶體泄漏。
檢測工具
部分工具
1.ccmalloc-Linux和Solaris下對C和C++程式的簡單的使用記憶體泄漏和malloc調試庫。
2.Dmalloc-Debug Malloc Library.
3.Electric Fence-Linux分發版中由Bruce Perens編寫的malloc()調試庫。
4.Leaky-Linux下檢測記憶體泄漏的程式。
5.LeakTracer-Linux、Solaris和HP-UX下跟蹤和分析C++程式中的記憶體泄漏。
6.MEMWATCH-由Johan Lindh編寫,是一個開放原始碼C語言記憶體錯誤檢測工具,主要是通過gcc的precessor來進行。
7.Valgrind-Debugging and profiling Linux programs, aiming at programs written in C and C++.
8.KCachegrind-A visualization tool for the profiling data generated by Cachegrind and Calltree.
9.IBM Rational PurifyPlus-幫助開發人員查明C/C++、託管.NET、Java和VB6代碼中的性能和可靠性錯誤。PurifyPlus 將記憶體錯誤和泄漏檢測、應用程式性能描述、代碼覆蓋分析等功能組合在一個單一、完整的工具包中。
10.ParasoftInsure++-針對C/C++套用的運行時錯誤自動檢測工具,它能夠自動監測C/C++程式,發現其中存在著的記憶體破壞、記憶體泄漏、指針錯誤和I/O等錯誤。並通過使用一系列獨特的技術(SCI技術和變異測試等),徹底的檢查和測試我們的代碼,精確定位錯誤的準確位置並給出詳細的診斷信息。能作為MicrosoftVisual C++的一個外掛程式運行。
11.Compuware DevPartner for Visual C++ BoundsChecker Suite-為C++開發者設計的運行錯誤檢測和調試工具軟體。作為Microsoft Visual Studio和C++ 6.0的一個外掛程式運行。
12.Electric Software GlowCode-包括記憶體泄漏檢查,code profiler,函式調用跟蹤等功能。給C++和.Net開發者提供完整的錯誤診斷,和運行時性能分析工具包。
13.Compuware DevPartner Java Edition-包含Java記憶體檢測,代碼覆蓋率測試,代碼性能測試,執行緒死鎖,分散式套用等幾大功能模組。
14.Quest JProbe-分析Java的記憶體泄漏。
15.ej-technologies JProfiler-一個全功能的Java剖析工具,專用於分析J2SE和J2EE應用程式。它把CPU、執行緒和記憶體的剖析組合在一個強大的套用中。
16.BEAJRockit-用來診斷Java記憶體泄漏並指出根本原因,專門針對Intel平台並得到最佳化,能在Intel硬體上獲得最高的性能。