天天看點

PageHeap,調試Heap問題的工具

《Windows使用者态程式高效排錯》第二章主要介紹使用者态調試相關的知識和工具。本文主要講了PageHeap,調試Heap問題的工具.

2.4.2  PageHeap,調試Heap問題的工具

幸運的是,Heap Manager的确提供了主動檢查錯誤的功能。隻需要在系統資料庫裡面做對應的修改,作業系統就會根據設定來改變Heap Manager的行為。Pageheap是用來配置該系統資料庫的工具。關于heap的詳細資訊和原理請參考:

Pageheap,Gflag和後面介紹的Application Verifier工具一樣,都是友善修改對應系統資料庫的工具。如果不使用這兩個工具,直接修改系統資料庫也可以達到一樣的效果。3個工具裡面Application Verifier是目前的主流,Gflag是老牌。除了heap問題外,這兩個工具還可以修改其他的調試選項,後面都有說明。Pageheap.exe工具主要針對heap問題,使用起來簡單友善。目前gflag.exe包含在調試器的安裝包中,Application Verifier可以單獨下載下傳安裝。如果調試安裝包中沒有包含pageheap.exe,可以從這裡下載下傳:

簡單例子的多種情況

看幾個簡單的但是卻很有意義的例子:

用release模式編譯運作下面的代碼:

這裡往配置設定的空間多寫一個位元組。但是在release模式下運作,程式不會崩潰。

假設上面的代碼編譯成mytest.exe,用下面的方法可以對mytest.exe激活pageheap:

直接運作pageheap可以檢視目前pageheap的激活狀态:

當激活pageheap後,重新運作一次上面的代碼,程式就崩潰了。

(直接輕按兩下運作程式和在Windbg中用調試模式運作程式,觀察到的崩潰有差别。在Windbg中運作,pageheap會首先觸發break point異常,同時pageheap還會在調試器中輸出額外的調試資訊友善調試。)

上面的例子說明了pageheap能夠讓錯誤盡快暴露出來。接下來我們稍微修改一下代碼:

試試看,修改後的代碼還會導緻程式崩潰嗎?

根據我的測試,配置設定1023位元組的情況下,哪怕激活pageheap,也不會崩潰。你能說明原因嗎?如果看不出來,可以檢查一下每次malloc傳回的位址的數值,注意對這個數值在二進制上敏感一點,然後結合Heap Manager和pageheap的原理思考一下,看看有沒有發現。

對于上面兩種代碼,如果用debug模式編譯,激活pageheap,程式會崩潰嗎?根據我的測試,無論是否激活pageheap,debug模式都不會崩潰的。你能想到原因嗎?

再來看下面一段代碼:

這裡顯然有double free的問題。

如果沒有激活pageheap,分别在debug和release模式下運作,根據我的測試,debug模式下會崩潰,release模式下運作正常。

如果激活pageheap,同樣在debug/release模式下運作。根據我的測試,在兩種模式下都會崩潰。如果細心觀察,會發現兩種模式下,崩潰後彈出的提示各自不同。你能想到原因嗎?

如果有興趣,你還可以測試一下heap誤用的其他幾種情況,看看pageheap是不是都有幫助。

Heap上的記憶體洩漏和記憶體碎片

從上面的例子,可以很清楚地看到pageheap對于檢查這類問題的幫助。同時也可以看到,pageheap無法保證檢查出所有潛在問題,比如配置設定1023個位元組,但是寫1024個位元組這種情況。隻有了解pageheap的工作原理,同時對問題作認真的思考和測試後,才會了解其中的差别。

除了Heap使用不當導緻崩潰外,還有一類問題是記憶體洩漏。記憶體洩漏是指随着程式的運作,記憶體消耗越來越多,最後發生記憶體不足,或者整體性能下降。從代碼上看,這類問題是由于記憶體使用後沒有及時釋放導緻的。這裡的記憶體,可以是VirtualAlloc配置設定的,也有可能是HeapAllocate配置設定的。

這裡隻讨論Heap相關的記憶體洩漏。檢查記憶體洩漏是一個比較大的題目,第4章會作詳細讨論。

舉個例子,客戶開發一個cd刻錄程式。每次把盤片中所有内容寫入記憶體,然後開始刻錄。如果每次刻錄完成後都忘記去釋放配置設定的空間,那麼最多能夠刻3張CD。因為3張CD,每一張600MB,加在一起就是1.8GB,瀕臨2GB的上限。

另外還有一種跟記憶體洩漏相關的問題,是記憶體碎片(Fragmentation)。記憶體碎片是指記憶體被分割成很多的小塊,以至于很難找到連續的記憶體來滿足比較大的記憶體申請。導緻記憶體碎片常見原因有兩種,一種是加載了過多DLL,還有一種是小塊Heap的頻繁使用。

DLL分割記憶體空間最常見的情況是ASP.NET中的batch compilation沒有打開,導緻每一個ASP.NET頁面都會被編譯成一個單獨的DLL檔案。運作一段時間後,就可以看到幾千個DLL檔案加載到程序中。一個極端的例子是5000個DLL把2GB記憶體平均分成5000份,導緻每一份的大小在400KB左右(假設DLL本身隻占用1個位元組),于是無法申請大于400KB的記憶體,哪怕總的記憶體還是接近2GB。對于這種情況的檢查很簡單,列一下目前程序中所有加載起來的DLL就可以看出問題來。

對于小塊Heap的頻繁使用導緻的記憶體分片,可以參考下面的解釋:

為了更好地了解上面的解釋,考慮這樣的情況。假設開發人員設計了一個資料結構來描述一首歌曲,資料結構分成兩部分,第一部分是歌曲的名字、作者和其他相關的描述性資訊,第二部分是歌曲的二進制内容。顯然第一部分比第二部分小得多。假設第一部分長度1KB,第二部分399KB。每處理一首歌需要調用兩次記憶體配置設定函數,分别配置設定資料結構第一部分和第二部分需要的空間。

假設每次處理完成後,隻釋放了資料結構的第二部分,忘記釋放第一部分,這樣每處理一次,就會留下1個1KB的資料塊沒有釋放。程式長時間運作後,留下的1KB資料塊就會很多,雖然HeapManager的薄計資訊中可能記錄了有很多399KB的資料塊可以配置設定,但是如果要申請500KB的記憶體,就會因為找不到連續的記憶體塊而失敗。對于記憶體碎片的調試,可以參考最後的案例讨論。在Windows 2000上,可以用下面的方法來緩解問題:

關于 CLR上記憶體碎片的讨論和圖文詳解,請參考:

繼續閱讀