簡介
内存洩漏(Memory Leak)是指程序中已動态分配的堆内存由于某種原因程序未釋放或無法釋放,造成系統内存的浪費,導緻程序運行速度減慢甚至系統崩潰等嚴重後果。
内存洩漏缺陷具有隐蔽性、積累性的特征,比其他内存非法訪問錯誤更難檢測。因為内存洩漏的産生原因是内存塊未被釋放,屬于遺漏型缺陷而不是過錯型缺陷。此外,内存洩漏通常不會直接産生可觀察的錯誤症狀,而是逐漸積累,降低系統整體性能,極端的情況下可能使系統崩潰。
随着計算機應用需求的日益增加,應用程序的設計與開發也相應的日趨複雜,開發人員在程序實現的過程中處理的變量也大量增加,如何有效進行内存分配和釋放,防止内存洩漏的問題變得越來越突出。例如服務器應用軟件,需要長時間的運行,不斷的處理由客戶端發來的請求,如果沒有有效的内存管理,每處理一次請求信息就有一定的内存洩漏。這樣不僅影響到服務器的性能,還可能造成整個系統的崩潰。因此,内存管理成為軟件設計開發人員在設計中考慮的主要方面 。
洩漏原因
在C語言中,從變量存在的時間生命周期角度上,把變量分為靜态存儲變量和動态存儲變量兩類。靜态存儲變量是指在程序運行期間分配了固定存儲空間的變量,而動态存儲變量是指在程序運行期間根據實際需要進行動态地分配存儲空間的變量。在内存中供用戶使用的内存空間分為三部分:
程序存儲區
靜态存儲區
動态存儲區
程序中所用的數據分别存放在靜态存儲區和動态存儲區中。靜态存儲區數據在程序的開始就分配好内存區,在整個程序執行過程中它們所占的存儲單元是固定的,在程序結束時就釋放,因此靜态存儲區數據一般為全局變量。動态存儲區數據則是在程序執行過程中根據需要動态分配和動态釋放的存儲單元,動态存儲區數據有三類函數形參變量、局部變量和函數調用時的現場保護與返回地址。由于動态存儲變量可以根據函數調用的需要,動态地分配和釋放存儲空間,大大提高了内存的使用效率,使得動态存儲變量在程序中被廣泛使用。
開發人員進行程序開發的過程使用動态存儲變量時,不可避免地面對内存管理的問題。程序中動态分配的存儲空間,在程序執行完畢後需要進行釋放。沒有釋放動态分配的存儲空間而造成内存洩漏,是使用動态存儲變量的主要問題。一般情況下,開發人員使用系統提供的内存管理基本函數,如malloc、realloc、calloc、free等,完成動态存儲變量存儲空間的分配和釋放。但是,當開發程序中使用動态存儲變量較多和頻繁使用函數調用時,就會經常發生内存管理錯誤,例如:
分配一個内存塊并使用其中未經初始化的内容;
釋放一個内存塊,但繼續引用其中的内容;
子函數中分配的内存空間在主函數出現異常中斷時、或主函數對子函數返回的信息使用結束時,沒有對分配的内存進行釋放;
程序實現過程中分配的臨時内存在程序結束時,沒有釋放臨時内存。内存錯誤一般是不可再現的,開發人員不易在程序調試和測試階段發現,即使花費了很多精力和時間,也無法徹底消除。
産生方式的分類
以産生的方式來分類,内存洩漏可以分為四類:
常發性内存洩漏:發生内存洩漏的代碼會被多次執行到,每次被執行時都會導緻一塊内存洩漏。
偶發性内存洩漏:發生内存洩漏的代碼隻有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測内存洩漏至關重要。
一次性内存洩漏:發生内存洩漏的代碼隻會被執行一次,或者由于算法上的缺陷,導緻總會有一塊且僅有一塊内存發生洩漏。
隐式内存洩漏:程序在運行過程中不停的分配内存,但是直到結束的時候才釋放内存。嚴格的說這裡并沒有發生内存洩漏,因為最終程序釋放了所有申請的内存。但是對于一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放内存也可能導緻最終耗盡系統的所有内存。所以,我們稱這類内存洩漏為隐式内存洩漏。從用戶使用程序的角度來看,内存洩漏本身不會産生什麼危害,作為一般的用戶,根本感覺不到内存洩漏的存在。真正有危害的是内存洩漏的堆積,這會最終耗盡系統所有的内存。從這個角度來說,一次性内存洩漏并沒有什麼危害,因為它不會堆積,而隐式内存洩漏危害性則非常大,因為較之于常發性和偶發性内存洩漏它更難被檢測到。
檢測方法
無論是C還是C++程序,運行時候的變量主要有三種分配方式:堆分配、棧分配、全局和靜态内存分配。内存洩漏主要是發生在堆内存分配方式中,即“配置了内存後,所有指向該内存的指針都遺失了”,若缺乏語言這樣的垃圾回收機制,這樣的内存片就無法歸還系統。因為内存洩漏屬于程序運行中的問題,無法通過編譯識别,所以隻能在程序運行過程中來判别和診斷。下面将介紹幾種常用的内存檢測方法,每種方法均以現有的内存檢測工具為分析範例,并對各種方法進行比較。
靜态分析技術
靜态分析技術就是直接分析程序的源代碼或機器代碼,獲得一些有用的信息,而并不運行程序本身。目前有許多靜态分析的工具,編譯器就屬于這一類,它讀入源程序代碼,對源程序進行詞法和語法分析,進行數據類型的檢查以及一些優化的分析等,以此來提高程序的質量與運行效率。這類靜态的分析工具僅僅是讀入程序代碼進行相關的分析,而并不進行其它額外的操作,如修改源程序代碼等。
LCLink是一種通過對源代碼及添加到源代碼中特定格式的注釋說明進行靜态分析的程序理解和檢錯工具,的檢查對象是源程序,能檢查出的内存錯誤有内存分配釋放故障、空指針的錯誤使用、使用未定義或已被釋放的内存等程序錯誤。
LCLink重點分析兩類内存釋放錯誤:
試圖釋放某内存塊,該内存塊有兩個或兩個以上的有效指針指向它。
試圖釋放某内存塊,該内存塊沒有任何有效指針指向它。
解決此類内存錯誤的方法是規定分配某塊内存時返回的指針必須釋放該内存。使用注釋表示某指針是唯一指向某内存塊的指針,使用注釋表示被調用函數可能釋放函數參數指向的内存塊或創建新的指針指向該内存塊。
源代碼插裝技術
為了獲得被測程序的動态執行信息,需要對其進行跟蹤,一般使用插裝方法。所謂插裝就是在保持被測程序的邏輯完整性的基礎上,在被測程序的特定部位插入一段檢測程序又稱探針函數,通過探針的執行抛出程序的運行特征數據。基于這些特征數據分析,可以獲得程序的控制流及數據流信息,進而獲得邏輯覆蓋等動态信息,這樣就可以在被測程序執行的過程中動态地同步執行程序的檢測工作。插裝方法又分為源代碼級程序插裝和目标代碼級程序插裝。源代碼插裝測試必須在靜态測試部分獲得的被測程序的結構信息、靜态數據信息、控制流信息等基礎上,應用插裝技術向被測程序中的适當位置植入相應類型的探針,通過運行帶有探針的被測程序而獲得程序運行的動态數據。源代碼插裝要通過運行被測程序來測定程序的各種指标,如覆蓋率、時間性能、内存使用等等,實現源代碼插裝的關鍵技術是借助于插入到源程序中的監控語句來收集執行信息,以達到揭示程序内部行為和特性的目的,如圖1所示。
基于源代碼插裝的動态測試框架分為個主要的階段:
插裝交互與動态測試信息分析;
插裝階段;
插裝庫制作階段;
測試實施階段。
插裝交互與動态測試信息分析是軟件測試工具與用戶交互的界面。用戶通過該界面選擇要進行動态測試的程序模塊,拓撲産生相應的插裝選擇記錄文件。用戶還可以通過該交互界而浏覽動态測試結果信息,在軟件測試工具的實現上,采用可視化的方式顯示這些動态信息。插裝階段實現了在被測程序中植入探針,并生成帶有插裝信息的源文件。在此過程中,首先将被測程序經過預處理展開為不包含宏、條件編譯和頭文件的文件格式。然後,按照一定的插裝策略,根據前面生成的插裝選擇記錄文件,将探針函數加載到該文件中,最後生成插裝後的程序。插裝庫制作階段的目的是生成插裝庫中的探針函數,它含有插裝語句調用的函數及其函數的定義。顯然,插裝過程中生成的目标文件中含有探針函數的樁,而探針函數的實現恰恰在本過程完成。需要指出的是,插裝庫的制作過程是獨立于動态測試過程之外的,可以與軟件測試工具開發同步。測試實施階段将插裝過程生成的文件與插裝庫制作過程生成的插裝靜态庫連接生成帶有插裝信息的可執行文件,選取測試用例,運行該程序,可以獲得被測程序的動态跟蹤信息。
在以上四個階段中,其中的插裝交互與動态測試信息分析與測試實施階段是測試人員的可視部分,通過這兩部分,用戶與系統交互,完成測試工作。而插裝階段與插裝庫制作階段對測試人員是不可見的,在後台完成,對于用戶而言,這兩部分是完全透明的。在性能方面,采用插裝方法應盡量減少插裝開銷。為了達到不同的統計目的如語句覆蓋、分支覆蓋等,應盡量減少插裝次數。若能僅僅插裝一次就能完成多種類型的統計,則可使插裝代碼得到優化。此外,應盡量減少插裝代碼的數量,減少插裝代碼的運行次數,從而達到減小插裝代碼運行開銷的目的。特别是對于一些實時系統的測試,在這方面的要求尤為苛刻。一個運行時錯誤檢測工具,能夠自動檢測一應用中大量的編程和運行時錯誤。通過使用源碼插裝和運行時指針跟蹤的專利技術,在編譯時,附十插入測試和分析代碼,它建立一個有關程序中各種對象的數據庫。然後在運行時通過檢查數據值和内存引用驗證對象的一緻性和正确性。使用這些技術,包括變異測試技術等,一能夠檢查和測試用戶的代碼,精确定位錯誤的準确位置并給出詳細的診斷信息。能夠可視化實時内存操作,優化内存算法。還能執行覆蓋性分析,清楚地指示那些代碼已經測試過。将集成到開發環境中,能夠極大地減少調試時間并有效地防止錯誤。檢驗每一次内存操作的有效性,包括靜态全局和堆棧以及動态分配内存的操作。葉有兩種運行模式。監護模式下用戶可以快速檢測代碼中的錯誤,不需要對代碼作任何插裝和處理源碼插裝模式則進行徹底地代碼檢測。
目标代碼插裝技術
目标代碼插裝實現主要分為預處理、測試執行和結果彙總個階段,工作流程如圖2所示,系統主要工作是圍繞斷點而進行的。在預處理階段,首先靜态分析被測程序的目标代碼,查找待測程序中源代碼各語句、函數入口點在目标代碼中的對`應位置,然後在相應位置插入斷點在測試執行階段,啟動調試進程,當被測程序執行到斷點處時,響應斷點信息,在相應的斷點處完成相應的統計操作在結果彙總階段,根據各斷點處的統計結果,按不同的統計角度進行歸并、綜合得到最終的統計數據。
被測代碼預處理
在測試預處理階段對被測程序的目标代碼進行分析,可以獲得目标代碼與源代碼中語句、函數的對應關系。在目标代碼中為相對應的源代碼的每條語句及每個函數的入口點插入斷點。對于第三方代碼,隻要其目标代碼格式與下生成的目标代碼格式一緻,我們就可以用與分析用戶代碼同樣的方法獲取信息。獲取斷點的信息後,為所有的斷點建立斷點鍊表,同時建立語句及函數的信息鍊表,供随後的測試執行階段存儲信息。預處理流程如圖3所示。
測試執行階段
利用OCI技術,我們把測試執行看作是一個在被測進程和檢測進程間不斷切換的過程。每當被測進程遇到斷點,就會将自身挂起,同時發送消息喚醒檢測進程,檢測進程根據當前斷點的地址在斷點鍊表中查找相應節點,并查找對應的語句或函數信息,記錄該語句或函數的執行次數、到達或離開的時刻,供以後統計之用。然後,将插入的斷點信息去除,恢複原來的指令,轉入被測進程繼續執行。在轉入被測進程之前,必須将上一個斷點處的斷點恢複上一個斷點處的斷點在指令運行時被去除了。具體流程如圖4所示。
數據統計與結果彙總
根據各斷點處的統計結果,按不同的統計角度進行歸并、綜合,進行覆蓋率及各種時間的計算,得到最終的統計數據。是公司出品的一種軟件測試和質量保證工具,它能檢測程序内存洩漏和内存訪問沖突等錯誤。使用目标碼插裝技術,在編譯器生成的目标碼中直接插入特殊的檢查指令實現對内存錯誤的檢測。在程序的所有代碼中插入這些檢查邏輯,包括第三方目标碼庫,并且驗證系統調用的接口。目标碼插裝技術分為鍊接前插裝和鍊接後插裝兩種插裝方法。使用如圖5所示的鍊接前插裝法。檢查插裝後程序的每個内存讀寫動作,跟蹤内存使用情況,使用類似垃圾收集器的技術來檢查内存洩漏。垃圾收集機制分為兩階段垃圾檢測和垃圾回收。為了不影響程序的執行速度,提供了一個可調用的垃圾檢測器,使用類似于保守式垃圾收集算法,即标記一清除算法。在标記階段,遞歸地從數據段、堆棧段到數據堆跟蹤分析指針,并使用标準保守式方法為所有被引用的内存塊做标記。在清除階段,逐步訪問數據堆,并報告已分配但程序不再引用的内存塊,即程序的内存洩漏。
檢測工具
部分工具
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硬件上獲得最高的性能。



















