快速衝內存定位的方法

今天遇到一個衝內存的問題,但是沒看出來在哪裏衝的,網上搜了一下有一個非常簡單的方法可以快速抓住第一現場。

http://www.brucesky.com/articles/604

程序BUG往往因爲無知和無意識悄然埋下。在網絡庫中,我寫了這麼一段關閉socket的代碼:

01 void CTcpSocket::Destroy(BOOL bNotifyClosed)
02 {
03     if (m_nLinkStatus != LinkNotOpen)
04     {
05         m_nLinkStatus = LinkNotOpen;
06  
07         ::shutdown(m_hSock, SD_SEND);
08         ::closesocket(m_hSock);
09         if (bNotifyClosed)
10             OnClose(::WSAGetLastError());  // Tag#1:通知應用層socket已關閉
11         m_hSock = INVALID_SOCKET;          // Tag#2:socket句柄置爲無效
12     }
13 }

因爲這段代碼,服務器程序沒跑多久就出現異常而Crash掉。現在要討論的主題就是與Tag1、Tag2的兩處代碼相關。

一、關於delete this

應用層創建和釋放socket對象,而socket對象生命期由引用計數類託管,網絡庫在調用OnClose通知應用層socket句柄關閉的時候,socket對象開始做清理工作並遞減引用計數,發現引用計數爲0,進行delete this(即“自殺”)。delete this是一個”飽含爭議“的操作,有認爲It’s usefull,也有認爲It’s a bad practice,甚至有認爲這是面試時唯一可以用來考驗C++程序員的問題(The Best C++ Interview Question – Ever!)。暫不管是usefull還是bad idea,先來看delete this的合法性問題(這裏不討論delete的語意,可以參考《Inside The C++ Object Model》)。C++ Faqs中是這麼闡述:

只要你小心,一個對象通過成員函數請求自殺(delete this)是沒有問題的。下面是對“小心”的定義。

  1. 你必須100%確定this對象是通過new分配的(而不是通過new[]、placement new、棧上局部對象、全局對象、或是另一個對象的成員)。
  2. 你必須100%確定這個成員函數是this對象調用的最後一個成員函數。
  3. 你必須100%確定這個成員函數餘下的代碼(delete this之後)不會再訪問this對象的任何一塊內存(包括調用任何其他成員函數或訪問任何成員數據)。
  4. 你必須100%確定在delete this之後,不再去訪問該this指針。換句話說,你不能對它做檢驗操作,用來和其他指針比較(包括NULL),用來打印,做轉換(cast)等任何操作。

通常如果this指針指向的是一個不具有virtual析構函數的基類對象時往往會出現警告。

既然delete this有其合法性,我當且認爲delete this本身並非一個bad practice,而要看delete this是否得當(這裏我想起電影《錢學森》中的一句話:手上沒有劍和有劍不用不是一回事。C++就是這麼一柄利劍,很多強大特性需要去權衡考慮用或不用)。正如人也會有自殺一樣,有些是因爲萬千煩惱而自尋短見,有些則是捨身取義而自我犧牲,我們惋惜前者,敬佩後者。如果C++對象自殺能避免以上“忌諱”而達到了資源安全釋放的目的,也就可以爲之。

二、堆破壞檢測

說完了delete this,接下來要說堆破壞的問題。上面Tag2處的代碼即犯了“小心”delete this的第3條忌諱,OnClose觸發應用層socket對象delete this,而網絡庫卻還在該對象的成員(m_hSock)進行寫入操作,另外應用層還有別處在申請堆內存,結果發生堆破壞而造成程序Crash。堆破壞是開發過程中常見的一個問題(尤其對於這種多人模塊開發),可以藉助PageHeap(頁堆)工具來檢測堆破壞。

1、什麼是頁堆

從Windows2000開始系統在堆管理器(即PageHeap管理器)引入“校驗層”,該層處於Ntdll.dll模塊內,可以驗證程序所有的動態內存操作(分配、釋放及其他堆操作)。當啓用頁堆管理器,讓應用層序在調試器下啓動時,如果遇到問題,調試器將會中斷,但不指名是什麼錯誤(如果不是在調試器下啓動,則遇到問題只會崩潰而無任何反饋)。

頁堆有兩種類型,正常頁堆(Normal Page Heap)完全頁堆(Full-page Heap)

完全頁堆:當分配一塊內存時,通過調整內存塊的起始分配位置,使其結尾恰好與系統分頁邊界對齊,然後在邊界相鄰處再多分配一個不可訪問的頁作爲保護區域。這樣,一旦出現內存讀寫越界時,系統捕獲到這個異常然後中斷執行並將該異常交給調試器處理,從而有機會及時檢查內存越界的位置。

因爲每次分配的內存都需要以這種形式佈局,對於小片內存分配,即使分配1個字節,也要分配一個內存頁和一個保留頁,這就需要大量內存。所以在使用完全頁堆前確保虛擬內存呢能滿足這樣的分配需求。

正常頁堆:類似於CRT調試內存分配函數,通過分配少量的填充信息,在釋放內存塊時檢查填充區域,來檢測內存是否被破壞。此方法的優點是極大的減少了內存耗用量,缺點是隻能在釋放內存塊時檢測,不方便跟蹤出錯代碼的位置。

2、頁堆工具

PageHeap.exe、GFlagsApplication Verifier是三種外殼工具,都是用來方便配置Page Heap選項(也可以手動配置,位於註冊表目錄:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\YourAppName\),當Windows開始啓動一個進程時,通過檢查這個註冊表目錄的設置,對該進程啓用相應的Page Heap選項。

我一般使用GFlags,功能比較全,包含在WinDbg調試器安裝包內。使用GFlags配置頁堆選項的例子:

列出當前啓動了頁堆選項的程序列表

C:\Windows\system32>D:\DebugTools\Debugging_Tools_for_Windows\gflags.exe /p

配置正常頁堆

C:\Windows\system32>D:\DebugTools\Debugging_Tools_for_Windows\gflags.exe /p /enable appname.exe

配置完全頁堆

C:\Windows\system32>D:\DebugTools\Debugging_Tools_for_Windows\gflags.exe /p /enable appname.exe /full

取消頁堆設置

C:\Windows\system32>D:\DebugTools\Debugging_Tools_for_Windows\gflags.exe /p  /disable appname.exe

一些特殊選項

/unaligned

這個選項只能用於完全頁堆。當我們從Windows堆管理器申請一塊內存時,內存總是8字節對齊的(64位上爲16字節),頁堆默認情況下也會遵守這個規則。但是這會導致分配的內存塊的結尾不能跟頁邊界精確對齊,可能存在0-7個字節的間隙,顯然,對於間隙範圍內的訪問不會立即被發現。/unaligned用於修正這個缺陷,它指定頁堆管理器不必遵守8字節對齊規則,保證內存塊尾部精確對齊邊界。

/backwads

這個選項只能用於完全堆,它使得分配的內存塊頭部(而不是尾部)與邊界對齊,通過這個選項來檢測頭部分越界訪問。

3、頁堆檢測的有效範圍

只要是最終(直接或間接的)調入到Ntdll.dll堆管理函數(即RtlAllocateHeap、RtlFreeHeap)分配函數,頁堆檢測功能都是有效的。這些分配函數包括:

kernel32導出的HeapAlloc、HeapFree、HeapReAlloc、GlobalAlloc、GlobalFree、GlobalReAlloc、LocalAlloc、LocalFree、LocalReAlloc;

msvcrt.dll導出的malloc、free、realloc、msize、expand、new、delete、new[]、delete []。

4、頁堆能發現的錯誤類型

錯誤 正常頁堆 整頁堆
堆句柄無效 立即發現 立即發現
堆塊指針無效 立即發現 立即發現
多線程不同步訪問堆 立即發現 立即發現
假設重新分配返回相同地址 90% 在實際釋放後發現 90% 立即發現
內存塊重複釋放 90% 立即發現 90% 立即發現
訪問已釋放的內存塊 90% 在實際釋放後發現 90% 立即發現
訪問塊結尾之後的內容 在釋放後發現 立即發現
訪問塊開始之前的內容 在釋放後發現 立即發現(特殊標誌)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章