《Windows用戶態程序高效排錯》第二章主要介紹用戶態調試相關的知識和工具。本文主要講了PageHeap,調試Heap問題的工具.
2.4.2 PageHeap,調試Heap問題的工具
幸運的是,Heap Manager的確提供了主動檢查錯誤的功能。只需要在注冊表里面做對應的修改,操作系統就會根據設置來改變Heap Manager的行為。Pageheap是用來配置該注冊表的工具。關於heap的詳細信息和原理請參考:
How to use Pageheap.exe in Windows XP and Windows 2000 |
Pageheap,Gflag和后面介紹的Application Verifier工具一樣,都是方便修改對應注冊表的工具。如果不使用這兩個工具,直接修改注冊表也可以達到一樣的效果。3個工具里面Application Verifier是目前的主流,Gflag是老牌。除了heap問題外,這兩個工具還可以修改其他的調試選項,后面都有說明。Pageheap.exe工具主要針對heap問題,使用起來簡單方便。目前gflag.exe包含在調試器的安裝包中,Application Verifier可以單獨下載安裝。如果調試安裝包中沒有包含pageheap.exe,可以從這里下載:
http://www.heijoy.com/debugdoc/pageheap.zip |
簡單例子的多種情況
看幾個簡單的但是卻很有意義的例子:
用release模式編譯運行下面的代碼:
char *p=(char*)malloc(1024); |
這里往分配的空間多寫一個字節。但是在release模式下運行,程序不會崩潰。
假設上面的代碼編譯成mytest.exe,用下面的方法可以對mytest.exe激活pageheap:
C:\Debuggers\pageheap>pageheap /enable mytest.exe /full |
直接運行pageheap可以查看當前pageheap的激活狀態:
C:\Debuggers\pageheap>pageheap |
(直接雙擊運行程序和在Windbg中用調試模式運行程序,觀察到的崩潰有差別。在Windbg中運行,pageheap會首先觸發break point異常,同時pageheap還會在調試器中輸出額外的調試信息方便調試。)
上面的例子說明了pageheap能夠讓錯誤盡快暴露出來。接下來我們稍微修改一下代碼:
char *p=(char*)malloc(1023); |
根據我的測試,分配1023字節的情況下,哪怕激活pageheap,也不會崩潰。你能說明原因嗎?如果看不出來,可以檢查一下每次malloc返回的地址的數值,注意對這個數值在二進制上敏感一點,然后結合Heap Manager和pageheap的原理思考一下,看看有沒有發現。
對於上面兩種代碼,如果用debug模式編譯,激活pageheap,程序會崩潰嗎?根據我的測試,無論是否激活pageheap,debug模式都不會崩潰的。你能想到原因嗎?
再來看下面一段代碼:
char *p=(char*)malloc(1023); |
如果沒有激活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的頻繁使用導致的內存分片,可以參考下面的解釋:
Heap fragmentation is often caused by one of the following two reasons |
為了更好地理解上面的解釋,考慮這樣的情況。假設開發人員設計了一個數據結構來描述一首歌曲,數據結構分成兩部分,第一部分是歌曲的名字、作者和其他相關的描述性信息,第二部分是歌曲的二進制內容。顯然第一部分比第二部分小得多。假設第一部分長度1KB,第二部分399KB。每處理一首歌需要調用兩次內存分配函數,分別分配數據結構第一部分和第二部分需要的空間。
假設每次處理完成后,只釋放了數據結構的第二部分,忘記釋放第一部分,這樣每處理一次,就會留下1個1KB的數據塊沒有釋放。程序長時間運行后,留下的1KB數據塊就會很多,雖然HeapManager的薄計信息中可能記錄了有很多399KB的數據塊可以分配,但是如果要申請500KB的內存,就會因為找不到連續的內存塊而失敗。對於內存碎片的調試,可以參考最后的案例討論。在Windows 2000上,可以用下面的方法來緩解問題:
The Windows XP Low Fragmentation Heap Algorithm |
關於 CLR上內存碎片的討論和圖文詳解,請參考:
.NET Memory usage - A restaurant analogy
|
