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