delete this及堆破壞檢測方法

2022-05-22 01:00:11 字數 3599 閱讀 9045

1,837 人閱讀

程式bug往往因為無知和無意識悄然埋下。在網路庫中,我寫了這麼一段關閉socket的**:

view source

print

?01

voidctcpsocket::destroy(boolbnotifyclosed)

02

13

}

因為這段**,伺服器程式沒跑多久就出現異常而crash掉。現在要討論的主題就是與tag1、tag2的兩處**相關。

應用層建立和釋放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)是沒有問題的。下面是對「小心」的定義。

你必須100%確定this物件是通過new分配的(而不是通過new、placement new

、棧上區域性物件、全域性物件、或是另乙個物件的成員)。

你必須100%確定這個成員函式是this物件呼叫的最後乙個成員函式。

你必須100%確定這個成員函式餘下的**(delete this之後)不會再訪問this物件的任何一塊記憶體(包括呼叫任何其他成員函式或訪問任何成員資料)。

你必須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(頁堆)工具來檢測堆破壞。

從windows2000開始系統在堆管理器(即pageheap管理器)引入「校驗層」,該層處於ntdll.dll模組內,可以驗證程式所有的動態記憶體操作(分配、釋放及其他堆操作)。當啟用頁堆管理器,讓應用層序在偵錯程式下啟動時,如果遇到問題,偵錯程式將會中斷,但不指名是什麼錯誤(如果不是在偵錯程式下啟動,則遇到問題只會崩潰而無任何反饋)。

頁堆有兩種型別,正常頁堆(normal page heap)完全頁堆(full-page heap)

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

因為每次分配的記憶體都需要以這種形式布局,對於小片記憶體分配,即使分配1個位元組,也要分配乙個記憶體頁和乙個保留頁,這就需要大量記憶體。所以在使用完全頁堆前確保虛擬記憶體呢能滿足這樣的分配需求。

正常頁堆:類似於crt除錯記憶體分配函式,通過分配少量的填充資訊,在釋放記憶體塊時檢查填充區域,來檢測記憶體是否被破壞。此方法的優點是極大的減少了記憶體耗用量,缺點是只能在釋放記憶體塊時檢測,不方便跟蹤出錯**的位置。

我一般使用gflags,功能比較全,包含在windbg偵錯程式安裝包內。使用gflags配置頁堆選項的例子:

列出當前啟動了頁堆選項的程式列表

c:\windows\system32>d:\debugtools\debugging_tools_for_windows\gflags.exe /p

配置正常頁堆

配置完全頁堆

取消頁堆設定

一些特殊選項

/unaligned

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

/backwads

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

只要是最終(直接或間接的)調入到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 。

錯誤正常頁堆

整頁堆堆控制代碼無效

立即發現

立即發現

堆塊指標無效

立即發現

立即發現

多執行緒不同步訪問堆

立即發現

立即發現

假設重新分配返回相同位址

90% 在實際釋放後發現

90% 立即發現

記憶體塊重複釋放

90% 立即發現

90% 立即發現

訪問已釋放的記憶體塊

90% 在實際釋放後發現

90% 立即發現

訪問塊結尾之後的內容

在釋放後發現

立即發現

訪問塊開始之前的內容

在釋放後發現

立即發現(特殊標誌)

參考微軟幫助:how to use pageheap.exe in windows xp, windows 2000, and windows server 2003

盡早發現堆破壞

chromium程式在執行起始位置有 base enableterminationonheapcorruption void enableterminationonheapcorruption 以上做法會讓開發人員盡早的發現堆破壞。在unmanaged exe中盡早呼叫此函式,dll中無需呼叫此函式...

小記 Linux下的堆破壞

今天遇見乙個超鬱悶的bug,現象非常奇怪 delete pointer 刪除乙個指標的時候程式發生core dump,或者在此處掛死。從原理上分析,new出來的一塊記憶體,會在記憶體的頭或者尾處加上乙個標記,說明塊記憶體的大小。我猜想,一定是指標越界,導致了開始或者末尾的這塊標記被破壞,所以dele...

單例模式的破壞及任何防止被破壞

常用的單例模式有懶漢式 餓漢式兩種情況。實際的應用場景也是很常見,好比如資料庫連線池的設計,還有windows的task manager 任務管理器 等。所謂單例模式就是,某乙個類只能有乙個例項,實現的核心就是將類的建構函式私有化,只能由該類建立物件,其他物件就不能呼叫該類的建構函式,即不能建立物件...