• <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>
            春暖花開
            雪化了,花開了,春天來了
            posts - 149,comments - 125,trackbacks - 0

            http://www-128.ibm.com/developerworks/cn/linux/l-mleak2/index.html


            內存泄漏對于C/C++程序員來說也可以算作是個永恒的話題了吧。在Windows下,MFC的一個很有用的功能就是能在程序運行結束時報告是否發(fā)生了內存泄漏。在Linux下,相對來說就沒有那么容易使用的解決方案了:像mpatrol之類的現(xiàn)有工具,易用性、附加開銷和性能都不是很理想。本文實現(xiàn)一個極易于使用、跨平臺的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后面跟的是一個類名的話,當然還要調用該類的構造函數(shù))。類似地,對于"delete ptr"和"delete[] ptr",編譯器會產生"operator delete(ptr)"調用和"operator delete[](ptr)"調用(如果ptr的類型是指向對象的指針的話,那在operator delete之前還要調用對象的析構函數(shù))。當用戶沒有提供這些操作符時,編譯系統(tǒng)自動提供其定義;而當用戶自己提供了這些操作符時,就覆蓋了編譯系統(tǒng)提供的版本,從而可獲得對動態(tài)內存分配操作的精確跟蹤和控制。

            同時,我們還可以使用placement new操作符來調整operator new的行為。所謂placement new,是指帶有附加參數(shù)的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 {};"),其唯一目的是提供編譯器一個可根據(jù)重載規(guī)則識別具體調用的類型。用戶一般簡單地使用"new(std::nothrow) 類型"(nothrow是一個nothrow_t類型的常量)來調用這個placement new操作符。它與標準new的區(qū)別是,new在分配內存失敗時會拋出異常,而"new(std::nothrow)"在分配內存失敗時會返回一個空指針。

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

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





            回頁首


            檢測原理

            和其它一些內存泄漏檢測的方式類似,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]",編譯器會據(jù)此產生一個"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均同時包含數(shù)組版本),我可以跟蹤所有的內存分配調用,并在指定的檢查點上對不匹配的new和delete操作進行報警。實現(xiàn)可以相當簡單,用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);
                        }
                        

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

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

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

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

            基本實現(xiàn)大體就是如此。





            回頁首


            可用性改進

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

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

            此外,為了允許程序能和 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
                        

            包含的位置應當盡可能早,除非跟系統(tǒng)的頭文件(典型情況是STL的頭文件)發(fā)生了沖突。在某些情況下,可能會不希望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中的靜態(tài)對象析構時,因此不能保證用戶的全局對象的析構操作發(fā)生在check_leaks調用之前。對于Windows上的MSVC,我使用了"#pragma init_seg(lib)"來調整對象分配釋放的順序,但很遺憾,我不知道在其他的一些編譯器中(特別是,我沒能成功地在GCC中解決這一問題)怎么做到這一點。為了減少誤報警,我采取的方式是在自動調用了check_leaks之后設new_verbose_flag為true;這樣,就算誤報告了內存泄漏,隨后的delete操作還是會被打印顯示出來。只要泄漏報告和delete報告的內容一致,我們仍可以判斷出沒有發(fā)生內存泄漏。

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

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





            回頁首


            構造函數(shù)中的異常

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

            #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編譯的話,編譯器的警告信息已經告訴我們發(fā)生了什么:

            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) 	

            啊哦,內存泄漏了不是!

            當然,這種情況并非很常見??墒?,隨著對象越來越復雜,誰能夠保證一個對象的子對象的構造函數(shù)或者一個對象在構造函數(shù)中調用的所有函數(shù)都不會拋出異常?并且,解決該問題的方法并不復雜,只是需要編譯器對 C++ 標準有較好支持,允許用戶定義 placement delete 算符([C++1998],5.3.4節(jié);網上可以找到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為止的所有版本)則不支持該項特性。在上面的例子中,如果編譯器支持的話,我們就需要聲明并實現(xiàn) operator delete(void*, int) 來回收new分配的內存。編譯器不支持的話,需要使用宏讓編譯器忽略相關的聲明和實現(xiàn)。如果要讓debug_new在Borland C++ Compiler 5.5.1或Digital Mars C++ Compiler下編譯的話,用戶必須定義宏NO_PLACEMENT_DELETE;當然,用戶得自己注意小心構造函數(shù)中拋出異常這個問題了。





            回頁首


            方案比較

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

            優(yōu)點:

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

             

            缺點:

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

             

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





            回頁首


            總結和討論

            以上段落基本上已經說明了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&)

             

            提供的函數(shù):

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

             

            提供的全局變量

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

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

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

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

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



            參考資料

            [C++1998] ISO/IEC 14882. Programming Languages-C++, 1st Edition. International Standardization Organization, International Electrotechnical Commission, American National Standards Institute, and Information Technology Industry Council, 1998

            [Stroustrup1997] Bjarne Stroustrup. The C++ Programming Language, 3rd Edition. Addison-Wesley, 1997

            [洪琨2003] 洪琨。 《如何在 linux 下檢測內存泄漏》,IBM developerWorks 中國網站。

            posted on 2008-11-02 10:56 Sandy 閱讀(348) 評論(0)  編輯 收藏 引用 所屬分類: C++
            国产精品免费福利久久| 久久人人爽人人人人爽AV| 伊人久久大香线蕉亚洲五月天| 亚洲乱亚洲乱淫久久| aaa级精品久久久国产片| 浪潮AV色综合久久天堂| 亚洲国产精品高清久久久| 亚洲国产精品无码久久SM| 色婷婷久久综合中文久久蜜桃av | 久久国产精品一区| 女人香蕉久久**毛片精品| 久久国产精品久久久| 丰满少妇人妻久久久久久4| 国产99久久久久久免费看| 国产激情久久久久影院| 青青草原综合久久大伊人导航| 中文精品99久久国产| 久久人妻少妇嫩草AV无码专区| 99久久免费国产特黄| 久久久久国产视频电影| 久久天天躁狠狠躁夜夜不卡| 1000部精品久久久久久久久| 久久国产精品偷99| 日韩人妻无码一区二区三区久久| 日韩久久久久久中文人妻| 久久综合综合久久97色| 亚洲人成电影网站久久| 精品久久久久久成人AV| 久久国产成人亚洲精品影院| 久久妇女高潮几次MBA| yellow中文字幕久久网 | 午夜精品久久久久久影视riav| 色婷婷综合久久久久中文一区二区| 青青草国产精品久久| 7777精品伊人久久久大香线蕉| 国产精品青草久久久久婷婷| 久久这里有精品视频| 久久婷婷久久一区二区三区 | 久久精品中文字幕无码绿巨人| 国产精品久久久天天影视香蕉| 久久人人爽人人爽人人片av麻烦 |