• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            牽著老婆滿街逛

            嚴以律己,寬以待人. 三思而后行.
            GMail/GTalk: yanglinbo#google.com;
            MSN/Email: tx7do#yahoo.com.cn;
            QQ: 3 0 3 3 9 6 9 2 0 .

            一個跨平臺的 C++ 內存泄漏檢測器

            級別: 初級

            吳詠煒

            2004 年 3 月 01 日

            內存泄漏對于C/C++程序員來說也可以算作是個永恒的話題了吧。在Windows下,MFC的一個很有用的功能就是能在程序運行結束時報告是否發生了內存泄漏。在Linux下,相對來說就沒有那么容易使用的解決方案了:像mpatrol之類的現有工具,易用性、附加開銷和性能都不是很理想。本文實現一個極易于使用、跨平臺的C++內存泄漏檢測器。并對相關的技術問題作一下探討。

            基本使用

            對于下面這樣的一個簡單程序test.cpp:

            												
            														int main()
            {
            	int* p1 = new int;
            	char* p2 = new char[10];
            	return 0;
            }
            
            												
            										

            我們的基本需求當然是對于該程序報告存在兩處內存泄漏。要做到這點的話,非常簡單,只要把debug_new.cpp也編譯、鏈接進去就可以了。在Linux下,我們使用:

            												
            														g++ test.cpp debug_new.cpp -o test
            
            												
            										

            輸出結果如下所示:

            												
            														Leaked object at 0x805e438 (size 10, <Unknown>:0)
            Leaked object at 0x805e410 (size 4, <Unknown>:0)
            
            												
            										

            如果我們需要更清晰的報告,也很簡單,在test.cpp開頭加一行

            												
            														#include "debug_new.h"
            
            												
            										

            即可。添加該行后的輸出如下:

            												
            														Leaked object at 0x805e438 (size 10, test.cpp:5)
            Leaked object at 0x805e410 (size 4, test.cpp:4)
            
            
            												
            										

            非常簡單!



            回頁首


            背景知識

            在new/delete操作中,C++為用戶產生了對operator new和operator delete的調用。這是用戶不能改變的。operator new和operator delete的原型如下所示:

            												
            														void *operator new(size_t) throw(std::bad_alloc);
            void *operator new[](size_t) throw(std::bad_alloc);
            void operator delete(void*) throw();
            void operator delete[](void*) throw();
            
            												
            										

            對于"new int",編譯器會產生一個調用"operator new(sizeof(int))",而對于"new char[10]",編譯器會產生"operator new[](sizeof(char) * 10)"(如果new后面跟的是一個類名的話,當然還要調用該類的構造函數)。類似地,對于"delete ptr"和"delete[] ptr",編譯器會產生"operator delete(ptr)"調用和"operator delete[](ptr)"調用(如果ptr的類型是指向對象的指針的話,那在operator delete之前還要調用對象的析構函數)。當用戶沒有提供這些操作符時,編譯系統自動提供其定義;而當用戶自己提供了這些操作符時,就覆蓋了編譯系統提供的版本,從而可獲得對動態內存分配操作的精確跟蹤和控制。

            同時,我們還可以使用placement new操作符來調整operator new的行為。所謂placement new,是指帶有附加參數的new操作符,比如,當我們提供了一個原型為

            												
            														void* operator new(size_t size, const char* file, int line);
            
            												
            										

            的操作符時,我們就可以使用"new("hello", 123) int"來產生一個調用"operator new(sizeof(int), "hello", 123)"。這可以是相當靈活的。又如,C++標準要求編譯器提供的一個placement new操作符是

            												
            														void* operator new(size_t size, const std::nothrow_t&);
            
            												
            										

            其中,nothrow_t通常是一個空結構(定義為"struct nothrow_t {};"),其唯一目的是提供編譯器一個可根據重載規則識別具體調用的類型。用戶一般簡單地使用"new(std::nothrow) 類型"(nothrow是一個nothrow_t類型的常量)來調用這個placement new操作符。它與標準new的區別是,new在分配內存失敗時會拋出異常,而"new(std::nothrow)"在分配內存失敗時會返回一個空指針。

            要注意的是,沒有對應的"delete(std::nothrow) ptr"的語法;不過后文會提到另一個相關問題。

            要進一步了解以上關于C++語言特性的信息,請參閱[Stroustrup1997],特別是6.2.6、10.4.11、15.6、19.4.5和B.3.4節。這些C++語言特性是理解本實現的關鍵。



            回頁首


            檢測原理

            和其它一些內存泄漏檢測的方式類似,debug_new中提供了operator new重載,并使用了宏在用戶程序中進行替換。debug_new.h中的相關部分如下:

            												
            														void* operator new(size_t size, const char* file, int line);
            void* operator new[](size_t size, const char* file, int line);
            #define new DEBUG_NEW
            #define DEBUG_NEW new(__FILE__, __LINE__)
            
            												
            										

            拿上面加入debug_new.h包含后的test.cpp來說,"new char[10]"在預處理后會變成"new("test.cpp", 4) char[10]",編譯器會據此產生一個"operator new[](sizeof(char) * 10, "test.cpp", 4)"調用。通過在debug_new.cpp中自定義"operator new(size_t, const char*, int)"和"operator delete(void*)"(以及"operator new[]…"和"operator delete[]…";為避免行文累贅,以下不特別指出,說到operator new和operator delete均同時包含數組版本),我可以跟蹤所有的內存分配調用,并在指定的檢查點上對不匹配的new和delete操作進行報警。實現可以相當簡單,用map記錄所有分配的內存指針就可以了:new時往map里加一個指針及其對應的信息,delete時刪除指針及對應的信息;delete時如果map里不存在該指針為錯誤刪除;程序退出時如果map里還存在未刪除的指針則說明有內存泄漏。

            不過,如果不包含debug_new.h,這種方法就起不了作用了。不僅如此,部分文件包含debug_new.h,部分不包含debug_new.h都是不可行的。因為雖然我們使用了兩種不同的operator new --"operator new(size_t, const char*, int)"和"operator new(size_t)"-- 但可用的"operator delete"還是只有一種!使用我們自定義的"operator delete",當我們刪除由"operator new(size_t)"分配的指針時,程序將認為被刪除的是一個非法指針!我們處于一個兩難境地:要么對這種情況產生誤報,要么對重復刪除同一指針兩次不予報警:都不是可接受的良好行為。

            看來,自定義全局"operator new(size_t)"也是不可避免的了。在debug_new中,我是這樣做的:

            												
            														void* operator new(size_t size)
            {
            	return operator new(size, "<Unknown>", 0);
            }
            
            												
            										

            但前面描述的方式去實現內存泄漏檢測器,在某些C++的實現中(如GCC 2.95.3中帶的SGI STL)工作正常,但在另外一些實現中會莫名其妙地崩潰。原因也不復雜,SGI STL使用了內存池,一次分配一大片內存,因而使利用map成為可能;但在其他的實現可能沒這樣做,在map中添加數據會調用operator new,而operator new會在map中添加數據,從而構成一個死循環,導致內存溢出,應用程序立即崩潰。因此,我們不得不停止使用方便的STL模板,而使用手工構建的數據結構:

            												
            														struct new_ptr_list_t
            {
            	new_ptr_list_t*		next;
            	const char*			file;
            	int					line;
            	size_t				size;
            };
            
            												
            										

            我最初的實現方法就是每次在使用new分配內存時,調用malloc多分配 sizeof(new_ptr_list_t) 個字節,把分配的內存全部串成一個一個鏈表(利用next字段),把文件名、行號、對象大小信息分別存入file、line和size字段中,然后返回(malloc返回的指針 + sizeof(new_ptr_list_t))。在delete時,則在鏈表中搜索,如果找到的話((char*)鏈表指針 + sizeof(new_ptr_list_t) == 待釋放的指針),則調整鏈表、釋放內存,找不到的話報告刪除非法指針并abort。

            至于自動檢測內存泄漏,我的做法是生成一個靜態全局對象(根據C++的對象生命期,在程序初始化時會調用該對象的構造函數,在其退出時會調用該對象的析構函數),在其析構函數中調用檢測內存泄漏的函數。用戶手工調用內存泄漏檢測函數當然也是可以的。

            基本實現大體就是如此。



            回頁首


            可用性改進

            上述方案最初工作得相當好,直到我開始創建大量的對象為止。由于每次delete時需要在鏈表中進行搜索,平均搜索次數為(鏈表長度/2),程序很快就慢得像烏龜爬。雖說只是用于調試,速度太慢也是不能接受的。因此,我做了一個小更改,把指向鏈表頭部的new_ptr_list改成了一個數組,一個對象指針放在哪一個鏈表中則由它的哈希值決定。--用戶可以更改宏DEBUG_NEW_HASH和DEBUG_NEW_HASHTABLESIZE的定義來調整debug_new的行為。他們的當前值是我測試下來比較滿意的定義。

            使用中我們發現,在某些特殊情況下(請直接參看debug_new.cpp中關于DEBUG_NEW_FILENAME_LEN部分的注釋),文件名指針會失效。因此,目前的debug_new的缺省行為會復制文件名的頭20個字符,而不只是存儲文件名的指針。另外,請注意原先new_ptr_list_t的長度為16字節,現在是32字節,都能保證在通常情況下內存對齊。

            此外,為了允許程序能和 new(std::nothrow) 一起工作,我也重載了operator new(size_t, const std::nothrow_t&) throw();不然的話,debug_new會認為對應于 new(nothrow) 的delete調用刪除的是一個非法指針。由于debug_new不拋出異常(內存不足時程序直接報警退出),所以這一重載的操作只不過是調用 operator new(size_t) 而已。這就不用多說了。

            前面已經提到,要得到精確的內存泄漏檢測報告,可以在文件開頭包含"debug_new.h"。我的慣常做法可以用作參考:

            												
            														#ifdef _DEBUG
            #include "debug_new.h"
            #endif
            
            												
            										

            包含的位置應當盡可能早,除非跟系統的頭文件(典型情況是STL的頭文件)發生了沖突。在某些情況下,可能會不希望debug_new重定義new,這時可以在包含debug_new.h之前定義DEBUG_NEW_NO_NEW_REDEFINITION,這樣的話,在用戶應用程序中應使用debug_new來代替new(順便提一句,沒有定義DEBUG_NEW_NO_NEW_REDEFINITION時也可以使用debug_new代替new)。在源文件中也許就該這樣寫:

            												
            														#ifdef _DEBUG
            #define DEBUG_NEW_NO_NEW_REDEFINITION
            #include "debug_new.h"
            #else
            #define debug_new new
            #endif
            
            												
            										

            并在需要追蹤內存分配的時候全部使用debug_new(考慮使用全局替換)。

            用戶可以選擇定義DEBUG_NEW_EMULATE_MALLOC,這樣debug_new.h會使用debug_new和delete來模擬malloc和free操作,使得用戶程序中的malloc和free操作也可以被跟蹤。在使用某些編譯器的時候(如Digital Mars C++ Compiler 8.29和Borland C++ Compiler 5.5.1),用戶必須定義NO_PLACEMENT_DELETE,否則編譯無法通過。用戶還可以使用兩個全局布爾量來調整debug_new的行為:new_verbose_flag,缺省為false,定義為true時能在每次new/delete時向標準錯誤輸出顯示跟蹤信息;new_autocheck_flag,缺省為true,即在程序退出時自動調用check_leaks檢查內存泄漏,改為false的話用戶必須手工調用check_leaks來檢查內存泄漏。

            需要注意的一點是,由于自動調用check_leaks是在debug_new.cpp中的靜態對象析構時,因此不能保證用戶的全局對象的析構操作發生在check_leaks調用之前。對于Windows上的MSVC,我使用了"#pragma init_seg(lib)"來調整對象分配釋放的順序,但很遺憾,我不知道在其他的一些編譯器中(特別是,我沒能成功地在GCC中解決這一問題)怎么做到這一點。為了減少誤報警,我采取的方式是在自動調用了check_leaks之后設new_verbose_flag為true;這樣,就算誤報告了內存泄漏,隨后的delete操作還是會被打印顯示出來。只要泄漏報告和delete報告的內容一致,我們仍可以判斷出沒有發生內存泄漏。

            Debug_new也能檢測對同一指針重復調用delete(或delete無效指針)的錯誤。程序將顯示錯誤的指針值,并強制調用abort退出。

            還有一個問題是異常處理。這值得用專門的一節來進行說明。



            回頁首


            構造函數中的異常

            我們看一下以下的簡單程序示例:

            												
            														#include <stdexcept>
            #include <stdio.h>
            
            void* operator new(size_t size, int line)
            {
            	printf("Allocate %u bytes on line %d\\n", size, line);
            	return operator new(size);
            }
            
            class Obj {
            public:
            	Obj(int n);
            private:
            	int _n;
            };
            
            Obj::Obj(int n) : _n(n)
            {
            	if (n == 0) {
            		throw std::runtime_error("0 not allowed");
            	}
            }
            
            int main()
            {
            	try {
            		Obj* p = new(__LINE__) Obj(0);
            		delete p;
            	} catch (const std::runtime_error& e) {
            		printf("Exception: %s\\n", e.what());
            	}
            }
            
            												
            										

            看出代碼中有什么問題了嗎?實際上,如果我們用MSVC編譯的話,編譯器的警告信息已經告訴我們發生了什么:

            												
            														test.cpp(27) : warning C4291: ''''void *__cdecl operator new(unsigned int,int)'''' : 
            no matching operator delete found; memory will not be freed if initialization throws an exception
            
            												
            										

            好,把debug_new.cpp鏈接進去。運行結果如下:

            												
            														Allocate 4 bytes on line 27 Exception: 0 not allowed Leaked object at 00342BE8 (size 4, <Unknown>:0) 	
            												
            										

            啊哦,內存泄漏了不是!

            當然,這種情況并非很常見。可是,隨著對象越來越復雜,誰能夠保證一個對象的子對象的構造函數或者一個對象在構造函數中調用的所有函數都不會拋出異常?并且,解決該問題的方法并不復雜,只是需要編譯器對 C++ 標準有較好支持,允許用戶定義 placement delete 算符([C++1998],5.3.4節;網上可以找到1996年的標準草案,比如下面的網址 http://www.comnets.rwth-aachen.de/doc/c++std/expr.html#expr.new)。在我測試的編譯器中,GCC(2.95.3或更高版本,Linux/Windows)和MSVC(6.0或更高版本)沒有問題,而Borland C++ Compiler 5.5.1和Digital Mars C++ Compiler(到v8.38為止的所有版本)則不支持該項特性。在上面的例子中,如果編譯器支持的話,我們就需要聲明并實現 operator delete(void*, int) 來回收new分配的內存。編譯器不支持的話,需要使用宏讓編譯器忽略相關的聲明和實現。如果要讓debug_new在Borland C++ Compiler 5.5.1或Digital Mars C++ Compiler下編譯的話,用戶必須定義宏NO_PLACEMENT_DELETE;當然,用戶得自己注意小心構造函數中拋出異常這個問題了。



            回頁首


            方案比較

            IBM developerWorks上刊載了洪琨先生設計實現的一個Linux上的內存泄漏檢測方法([洪琨2003])。我的方案與其相比,主要區別如下:

            優點:

            • 跨平臺:只使用標準函數,并且在GCC 2.95.3/3.2(Linux/Windows)、MSVC 6、Digital Mars C++ 8.29、Borland C++ 5.5.1等多個編譯器下調試通過。(雖然Linux是我的主要開發平臺,但我發現,有時候能在Windows下編譯運行代碼還是非常方便的。)
            • 易用性:由于重載了operator new(size_t)--洪琨先生只重載了operator new(size_t, const char*, int)--即使不包含我的頭文件也能檢測內存泄漏;程序退出時能自動檢測內存泄漏;可以檢測用戶程序(不包括系統/庫文件)中malloc/free產生的內存泄漏。
            • 靈活性:有多個靈活的可配置項,可使用宏定義進行編譯時選擇。
            • 可重入性:不使用全局變量,沒有嵌套delete問題。
            • 異常安全性:在編譯器支持的情況下,能夠處理構造函數中拋出的異常而不發生內存泄漏。

            ?

            缺點:

            • 單線程模型:跨平臺的多線程實現較為麻煩,根據項目的實際需要,也為了代碼清晰簡單起見,我的方案不是線程安全的;換句話說,如果多個線程中同時進行new或delete操作的話,后果未定義。
            • 未實現運行中內存泄漏檢測報告機制:沒有遇到這個需求J;不過,如果要手工調用check_leaks函數實現的話也不困難,只是跨平臺性就有點問題了。
            • 不能檢測帶 [] 算符和不帶 [] 算符混用的不匹配:主要也是需求問題(如果要修改實現的話并不困難)。
            • 不能在錯誤的delete調用時顯示文件名和行號:應該不是大問題;由于我重載了operator new(size_t),可以保證delete出錯時程序必然有問題,因而我不只是顯示警告信息,而且會強制程序abort,可以通過跟蹤程序、檢查abort時程序的調用棧知道問題出在哪兒。

            ?

            另外,現在已存在不少商業和Open Source的內存泄漏檢測器,本文不打算一一再做比較。Debug_new與它們相比,功能上總的來說仍較弱,但是,其良好的易用性和跨平臺性、低廉的附加開銷還是具有很大優勢的。



            回頁首


            總結和討論

            以上段落基本上已經說明了debug_new的主要特點。下面做一個小小的總結。

            重載的算符:

            • operator new(size_t, const char*, int)
            • operator new[](size_t, const char*, int)
            • operator new(size_t)
            • operator new[](size_t)
            • operator new(size_t, const std::nothrow_t&)
            • operator new[](size_t, const std::nothrow_t&)
            • operator delete(void*)
            • operator delete[](void*)
            • operator delete(void*, const char*, int)
            • operator delete[](void*, const char*, int)
            • operator delete(void*, const std::nothrow_t&)
            • operator delete[](void*, const std::nothrow_t&)

            ?

            提供的函數:

            • check_leaks()
              檢查是否發生內存泄漏

            ?

            提供的全局變量

            • new_verbose_flag
              是否在new和delete時"羅嗦"地顯示信息
            • new_autocheck_flag
              是否在程序退出是自動檢測一次內存泄漏

            ?

            可重定義的宏:

            • NO_PLACEMENT_DELETE
              假設編譯器不支持placement delete(全局有效)
            • DEBUG_NEW_NO_NEW_REDEFINITION
              不重定義new,假設用戶會自己使用debug_new(包含debug_new.h時有效)
            • DEBUG_NEW_EMULATE_MALLOC
              重定義malloc/free,使用new/delete進行模擬(包含debug_new.h時有效)
            • DEBUG_NEW_HASH
              改變內存塊鏈表哈希值的算法(編譯debug_new.cpp時有效)
            • DEBUG_NEW_HASHTABLE_SIZE
              改變內存塊鏈表哈希桶的大小(編譯debug_new.cpp時有效)
            • DEBUG_NEW_FILENAME_LEN
              如果在分配內存時復制文件名的話,保留的文件名長度;為0時則自動定義DEBUG_NEW_NO_FILENAME_COPY(編譯debug_new.cpp時有效;參見文件中的注釋)
            • DEBUG_NEW_NO_FILENAME_COPY
              分配內存時不進行文件名復制,而只是保存其指針;效率較高(編譯debug_new.cpp時有效;參見文件中的注釋)

            ?

            我本人認為,debug_new目前的一個主要缺陷是不支持多線程。對于某一特定平臺,要加入多線程支持并不困難,難就難在通用上(當然,條件編譯是一個辦法,雖然不夠優雅)。等到C++標準中包含線程模型時,這個問題也許能比較完美地解決吧。另一個辦法是使用像boost這樣的程序庫中的線程封裝類,不過,這又會增加對其它庫的依賴性--畢竟boost并不是C++標準的一部分。如果項目本身并不用boost,單為了這一個目的使用另外一個程序庫似乎并不值得。因此,我自己暫時就不做這進一步的改進了。

            另外一個可能的修改是保留標準operator new的異常行為,使其在內存不足的情況下拋出異常(普通情況)或是返回NULL(nothrow情況),而不是像現在一樣終止程序運行(參見debug_new.cpp的源代碼)。這一做法的難度主要在于后者:我沒想出什么方法,可以保留 new(nothrow) 的語法,同時能夠報告文件名和行號,并且還能夠使用普通的new。不過,如果不使用標準語法,一律使用debug_new和debug_new_nothrow的話,那還是非常容易實現的。

            如果大家有改進意見或其它想法的話,歡迎來信討論。

            debug_new 的源代碼目前可以在 dbg_new.zip處下載。

            在這篇文章的寫完之后,我終于還是實現了一個線程安全的版本。該版本使用了一個輕量級的跨平臺互斥體類fast_mutex(目前支持Win32和POSIX線程,在使用GCC(Linux/MinGW)、MSVC時能通過命令行參數自動檢測線程類型)。有興趣的話可在 http://mywebpage.netscape.com/yongweiwu/dbg_new.tgz下載。

            posted on 2006-07-03 10:51 楊粼波 閱讀(328) 評論(0)  編輯 收藏 引用 所屬分類: 文章收藏

            精品熟女少妇AV免费久久| 久久久久久久国产免费看| 久久亚洲中文字幕精品有坂深雪| 精品熟女少妇AV免费久久| 精品久久久久香蕉网| 久久久久一本毛久久久| 久久99精品久久久大学生| 中文字幕亚洲综合久久2| 一本久久综合亚洲鲁鲁五月天亚洲欧美一区二区 | 久久精品人人做人人爽电影| 青草国产精品久久久久久| 国产亚州精品女人久久久久久 | 九九99精品久久久久久| 四虎影视久久久免费观看| 久久精品www| 久久亚洲精品中文字幕| 亚洲伊人久久综合影院| 国产福利电影一区二区三区久久久久成人精品综合 | 久久久久亚洲AV成人片 | 四虎国产精品免费久久久| 亚洲国产精品无码成人片久久| 亚洲狠狠久久综合一区77777| 久久久久国产精品嫩草影院| 久久精品?ⅴ无码中文字幕| 国产精品青草久久久久婷婷 | 欧美777精品久久久久网| 色8久久人人97超碰香蕉987| 久久青青草原精品国产不卡 | 尹人香蕉久久99天天拍| 久久久久免费视频| 久久久无码精品亚洲日韩软件| 久久99久久99小草精品免视看 | 国产精品免费久久| 久久国产精品99久久久久久老狼| 99精品国产99久久久久久97| 久久午夜夜伦鲁鲁片免费无码影视| 国产一区二区三精品久久久无广告| 国产人久久人人人人爽| 99久久中文字幕| 国产精品99久久久久久宅男| 国产高潮国产高潮久久久91 |