柳 青,楊英豪,孫永超
(中國電子科技集團公司第四十五研究所,北京101601)
隨著計算機在各個領(lǐng)域內(nèi)的廣泛應(yīng)用,IT 行業(yè)得到突飛猛進的發(fā)展。但程序畢竟是由人的思想產(chǎn)生,所以總會存在一些隱患。內(nèi)存泄漏就是一個常見的問題,其隱蔽性讓人不易察覺;如何找到并解決內(nèi)存泄漏成為程序設(shè)計中的關(guān)鍵。
在計算機里程序通常以進程的方式運行,而任何的進程都需要開辟內(nèi)存,內(nèi)存就是存放數(shù)據(jù)的介質(zhì)。程序員在編寫程序時都會和內(nèi)存打交道,常用的有數(shù)組、類等。數(shù)組和普通變量一樣可以被聲明為靜態(tài)或動態(tài)的;靜態(tài)數(shù)組在程序加載時定位于數(shù)據(jù)段;動態(tài)數(shù)組在程序運行時定位于堆棧之中。
一般進程由3 個部分組成:文本區(qū)域,數(shù)據(jù)區(qū)域和堆棧區(qū)域。如圖1所示。
文本區(qū)域由程序本身自己確定,它包括代碼和數(shù)據(jù)。這個區(qū)域通常是只讀的,任何對它的寫操作都會導(dǎo)致段錯誤。
數(shù)據(jù)區(qū)域包括初始化和未初始化的數(shù)據(jù)。bss段用來存放未初始化的數(shù)據(jù),data 段用來存放以初始化的數(shù)據(jù)。從C 語言的角度來說數(shù)據(jù)區(qū)域主要用來存放靜態(tài)變量。
圖1 內(nèi)存的組織形式
堆棧在高級語言中起到很大的作用,高級語言主要是面向過程和函數(shù)的,當(dāng)一個過程調(diào)用完可以用簡單的跳轉(zhuǎn)指令;因函數(shù)之間可以嵌套調(diào)用,這樣使得程序的邏輯很簡單,但調(diào)用之后釋放控制權(quán)就不能用簡單的跳轉(zhuǎn)指令,這時就必須使用堆棧了。
堆棧是一種抽象的數(shù)據(jù)類型,堆棧的顯著特性是后進先出(LIFO)。堆棧定義了兩種操作進棧(PUSH)和出棧(POP)。進棧時操作是從堆棧頂部加入一個元素;出棧操作是從堆棧頂部減去一個元素。
堆棧既可以向下增長(向內(nèi)存低地址)也可以向上增長,這依賴于具體的實現(xiàn)。此外有一個指針始終指向堆棧稱為堆棧指針(SP),它也是依賴于具體實現(xiàn)的;它可以指向堆棧的最后地址,或者指向堆棧之后的下一個空閑可用地址。在我們的討論當(dāng)中,SP 指向堆棧的最后地址。
除了堆棧指針(SP 指向堆棧頂部的低地址)之外,為了使用方便還有指向棧內(nèi)固定地址的指針叫做幀指針(FP),或者局部基指針(LB-local base pointer)。從理論上來說,局部變量可以用SP 加偏移量來引用。然而,當(dāng)有字被壓棧和出棧后,這些偏移量就變了。盡管在某些情況下編譯器能夠跟蹤棧中的字操作,由此可以修正偏移量,但是在某些情況下是不能的;而且在所有情況下,要引入可觀的管理開銷。
因此,許多編譯器使用第二個寄存器存放FP,對于局部變量和函數(shù)參數(shù)都可以引用,因為它們到FP 的距離不會受到壓棧和出棧操作的影響。
內(nèi)存泄漏(memory leak)指由于疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。內(nèi)存泄漏并非指內(nèi)存在物理上的消失,而是應(yīng)用程序在分配某段內(nèi)存后,由于設(shè)計錯誤,在程序使用完這段內(nèi)存時,未能釋放給操作系統(tǒng),從而失去了對該段內(nèi)存的控制,因此造成了內(nèi)存的浪費。
內(nèi)存泄漏的分類:
(1)常發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會被多次執(zhí)行到,每次被執(zhí)行的時候都會導(dǎo)致一塊內(nèi)存泄漏。
(2)偶發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會發(fā)生。常發(fā)性和偶發(fā)性是相對的。對于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以測試環(huán)境和測試方法對檢測內(nèi)存泄漏至關(guān)重要。
(3)一次性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只會被執(zhí)行一次,或者由于算法上的缺陷,總會導(dǎo)致且僅有一塊內(nèi)存發(fā)生泄漏。比如,在一個類的構(gòu)造函數(shù)中分配內(nèi)存,但在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存。而該類只存在一個實例,所以內(nèi)存泄漏只會發(fā)生一次。
(4)隱式內(nèi)存泄漏。程序在運行過程中不停的分配內(nèi)存,但是直到結(jié)束的時候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因為最終程序釋放了所有申請的內(nèi)存。但是對于一個服務(wù)器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏。
在高級語言編程中,易造成內(nèi)存泄漏的情況通常是動態(tài)分配的堆棧,即程序動態(tài)申請內(nèi)存,使用完后,未釋放給堆棧。下面是高級語言中幾種動態(tài)深淺堆棧的函數(shù)。
(1)void *malloc(size_t size)
此函數(shù)在堆棧中動態(tài)分配一塊size 大小的內(nèi)存。
void free(void *memblock)
此函數(shù)與malloc 對應(yīng)的函數(shù),用來釋放其分配的內(nèi)存。
(2)new [placement]type-name [initializer]
此函數(shù)是用于在堆棧動態(tài)分配一塊type-name 大小的內(nèi)存,type-name 可以是類也可以是數(shù)組等類型。
delete [pointer]
此函數(shù)與new 是對應(yīng)的函數(shù),用來釋放其分配的內(nèi)存。
此外還有一些標(biāo)準(zhǔn)函數(shù)在使用不當(dāng)時也會造成溢出。包括strcat(),strcpy(),sprintf(),vsprintf()。這些函數(shù)對一個NULL 結(jié)尾的字符串進行操作,并不檢查溢出情況。gets()函數(shù)從標(biāo)準(zhǔn)輸入中讀取一行到緩沖區(qū)中,直到換行或EOF,它也不檢查緩沖區(qū)溢出。scanf()函數(shù)族在匹配一系列非空格字符(%s),或從指定集合(%[])中匹配非空字符時,使用字符指針指向數(shù)組,并且沒有定義最大字段寬度這個可選項,就可能出現(xiàn)問題.如果這些函數(shù)的目標(biāo)地址是一個固定大小的緩沖區(qū),函數(shù)的另外參數(shù)是由用戶以某種形式輸入,則很有可能利用緩沖區(qū)溢出來破解它。
Visual Studio 調(diào)試器和C 運行時(CRT) 庫中為我們提供了一些檢測和識別內(nèi)存泄漏的有效方法。如調(diào)試堆棧函數(shù)和輸入調(diào)試信息等函數(shù)。但默認(rèn)總是關(guān)閉的,所以我們要手動打開。
分以下兩個步驟:
(1)使用調(diào)試堆棧函數(shù)
#include
#include
#define _CRTDBG_MAP_ALLOC
(2)輸出內(nèi)存泄漏信息
_CrtDumpMemoryLeaks();在需要檢測內(nèi)存泄漏的地方添加此函數(shù)用來輸出內(nèi)存泄漏的信息。如圖2所示。
圖2 使用C 標(biāo)準(zhǔn)函數(shù)輸出內(nèi)存泄漏信息
我們可以得到內(nèi)存泄漏的地址和內(nèi)存泄漏的內(nèi)容,但我們無法知道內(nèi)存泄漏的具體函數(shù)。
Visual Leak Detector 是一款用于Visual C++的免費內(nèi)存泄露檢測工具。它在每次內(nèi)存分配時將其上下文記錄下來,當(dāng)程序退出時,對于檢測到的內(nèi)存泄漏,查找其記錄下來的上下文信息,并將其轉(zhuǎn)換成報告輸出。
相比較其它的內(nèi)存泄露檢測工具,它在檢測到內(nèi)存泄漏的同時,還具有如下特點:
(1)可以得到內(nèi)存泄漏點的調(diào)用堆棧,如果可以的話,還可以得到其所在文件及行號;
(2)可以得到泄露內(nèi)存的完整數(shù)據(jù);
(3)可以設(shè)置內(nèi)存泄露報告的級別;
(4)它是一個已經(jīng)打包的lib,使用時無須編譯它的源代碼。而對于使用者自己的代碼,也只需要做很小的改動;
(5)它的源代碼使用GNU 許可發(fā)布,并有詳盡的文檔及注釋。對于想深入了解堆內(nèi)存管理的讀者,是一個不錯的選擇。
在http://www.codeproject.com/KB/applications/visualleakdetector.aspx 可以下載到 Visual Leak Detector 的源碼,編譯后安裝;或直接下載安裝包進行安裝。如圖3所示:
安裝完成后,我們還有配置一些選項才能使用Visual Leak Detector。
(1)拷貝Visual Leak Detector 的lib 文件至Visual C++安裝目錄下的lib 子文件夾內(nèi)
(2)拷貝Visual Leak Detector 頭文件(vld.h and vldapi.h)至Visual C++ 安裝目錄下的“include”子文件夾
圖3 安裝Visual Leak Detector
(3)在程序入口點所在的源文件內(nèi)包含vld.h。最好將此頭文件包含在其他頭文件之前,stdafx.h 之后,但這并不是必須的。如果這個源文件包含了stdafx.h,那么vld.h 應(yīng)該在其后包含。
(4)如果運行環(huán)境是windows2000 或更新,則需要拷貝dbghelp.dll 至被調(diào)試的可執(zhí)行文件目錄下。
編譯測試程序運行,我們可以得到內(nèi)存泄漏的詳細(xì)信息,內(nèi)存泄漏的地址,函數(shù)調(diào)用的堆棧及泄漏內(nèi)存的內(nèi)容,如圖4所示。
圖中第二行表示56 號塊有4 字節(jié)的內(nèi)存泄漏,地址為0x003F3ED8。我們可以看到堆棧調(diào)用的結(jié)果,第四行表示運行到程序的第12 行的f()函數(shù)里產(chǎn)生內(nèi)存泄漏;在該地址處分配了4 字節(jié)的堆內(nèi)存空間,并賦值為0x12345678;在第九行我們看到了這4 字節(jié)同樣的內(nèi)容,即內(nèi)存泄漏的堆棧數(shù)據(jù)。
圖4 使用Visual Leak Detector 檢測內(nèi)存泄漏
可以看出,對于每一個內(nèi)存泄漏,這個報告列出了它的泄漏點、長度、分配該內(nèi)存時的調(diào)用堆棧和泄露內(nèi)存的內(nèi)容(分別以16 進制和文本格式列出)。雙擊該堆棧報告的某一行,會自動在代碼編輯器中跳到其所指文件的對應(yīng)行。這些信息對于我們查找內(nèi)存泄露將有很大的幫助。
綜上所述內(nèi)存泄漏有一定的隱蔽性,所以給我們查找?guī)砹艘恍╇y度。盡管C++提供了標(biāo)準(zhǔn)的庫函數(shù)用來檢測內(nèi)存泄漏,但它不能產(chǎn)生具體的堆棧調(diào)用結(jié)果。而Visual Leak Detector 不但使用簡單,也能報告堆棧調(diào)用的詳細(xì)結(jié)果,使內(nèi)存使用情況一目了然,為我們檢測內(nèi)存泄漏提供了方便可靠的方法。
[1]孫鑫.VC++ 深入詳解[M].北京:電子工業(yè)出版社,2008.
[2]林銳.高質(zhì)量C++編程指南[Z].2001.