天天看點

windows程式員進階系列:《軟體調試》之Win32堆的調試支援

    Win32堆的調試支援

為了幫助程式員及時發現堆中的問題,堆管理器提供了以下功能來輔助調試。

1:堆尾檢查(Heap Tail Check) HTC,在堆尾添加額外的标記資訊,用于檢測堆塊是否溢出。

2:釋放檢查(Heap Free Check)在釋放堆塊時進行檢查,防止釋放同一個堆塊。

3:參數檢查,對傳遞給堆的各種參數進行更多的檢查。

4:調用時驗證(Heap Validate On Call)HVC,每次調用堆函數時都對整個堆進行驗證和檢查。

5:堆塊标記(Heap Tagging)為堆塊增加附加标記,以記錄堆塊的使用情況。

6:使用者态棧回溯(User Mode Stack Trace)UST,将每次調用堆函數的函數調用資訊記錄到一個資料庫中。

7:專門用于調試的頁堆(Debug  Page Heap)DHP堆,頁堆比較常用,且需要專門開啟,我們會專門對其進行介紹。

建立堆時,堆會根據目前程序的全局标志來決定是否 啟用堆的調試功能。作業系統在加載一個程序時會在系統資料庫HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Image File Execution Option表鍵下尋找以該程式命名的子鍵。如果存在該子鍵則讀取下面的GlobalFlag鍵值。

可以使用gflags.exe來編輯系統的全局标志或某個檔案的全局标志。

如果在調試器運作一個程式,但系統資料庫中并沒有設定GlobalFlag鍵值 ,那麼作業系統的加載器會預設将全局标志置為0x70,也就是啟用htc、hfc和hpc三項調試功能。

如果系統資料庫中設定了GlobalFlag鍵值,則使用系統資料庫中的設定,不再預設提供其他調試選項。

如果是附加到一個已經運作的程式上,則它的全局标志就是系統資料庫中的值。如果系統資料庫中不存在全局标志,則為0。

gflags.exe

gflags.exe被稱為全局标志編輯器是,windows調試工具集的一部分。該程式是用于對各個全局标志選項的集中式配置工具。它有gui和控制台兩種模式。

GUI模式開啟調試選項

運作gflag.exe,打開gui模式:

可以看到程式分為三個标簽頁:system Registry、Kernel flags 、Image File。

标簽頁System Registry用于設定針對整個系統的選項。設定之後需要重新開機系統才能生效。

Image File用于設定針對單個程序的配置。設定之後重新開機程序後才會生效。

Kernel Flags用于設定隻對核心産生影響的選項。

在gflags中包含了針對作業系統的各個方面的配置資訊,這些資訊是被儲存在系統資料庫相應的位置上。

單擊Image File标簽頁,在Image:(Tab to Refresh)編輯框内輸入要設定的程序名稱。如calc.exe。設定完成後點選Tab進行重新整理,重新整理後下面的各個控件變為可用狀态。在需要設定的調試選項後點勾,确認後即可。

控制台方式

在cmd視窗輸入一下指令來開啟相應的調試功能。

開啟堆尾檢查:gflags  /i calc.exe +htc

開啟釋放檢查:gflags /i calc.exe +hfc

開啟調用時驗證:gflags /I calc.exe +hvc

開啟參數檢查: gflags /I calc.exe +hpc

開啟使用者态棧回溯:gflags /I calc.exe +ust

開啟頁堆:gflags /p /enable  程式名 /full

或者gflags /I 程式名 +hpa

需要關閉時隻需要将+變為-即可。

在windbg中輸入!gflag開檢視開啟的調試選項。

注意,一旦調試之前設定頁堆系統資料庫相應位置便不再為0,預設便不會開啟hfc、hpc和htc。這一點要特别注意,調試以前要通過!gflag指令檢視到底開啟了何種調試選項。

因為在調試器運作一個程式且系統資料庫中沒有設定GlobalFlag鍵值 時,作業系統會啟用htc、hfc和hpc三項調試功能,且它們原理非常簡單,是以我們此處将主要精力放在經常使用,且需要手動開啟的頁堆上。

頁堆DPH

利用堆尾檢查可以在釋放堆塊時或在下次配置設定時檢查到堆結構的破壞。但是這些檢查都是滞後的,我們很難知道堆是何時發生的破壞。·   今天我們介紹的頁堆(Debug Page Heap DPH)可以解決這個問題。啟用DPH後堆管理器會在堆塊後增加用于檢測溢出的棧欄頁,一旦使用者資料溢出觸及棧欄頁将立即引發異常,進而讓我們在第一個時間知道堆破壞。

前面介紹的win32堆使用使用者資料區前面的_HEAP_ENTRY結構來描述堆塊,一旦使用者資料超出配置設定的空間将會覆寫堆塊後面的資料。有可能覆寫下一個堆塊的_HEAP_ENTRY結構或是空閑堆塊的_HEAP_FREE_ENTRY結構導緻堆被破壞。為了防止堆塊的管理資訊被覆寫後使堆發生不可恢複的破壞。頁堆管理器除了在堆塊使用者區前面存儲堆塊管理資訊外,還會将這些管理器資訊存儲在節點池内。

啟用頁堆

開啟頁堆可以使用gflags.exe來實作。

Gui方式開啟DPH。

gflag.exe /I calc.exe +hpa

設定完成後,使用windbg打開程序進行調試。為了驗證是否開始DHP,可以執行!gflag /p指令:

也可以檢視全局變量ntdll!RtlpDebugPageHeap的值來驗證是否開啟。當該值為1時表示開啟dhp。

與普通堆相比頁堆有很大的不同,每個堆塊至少占用兩個記憶體頁。在存放使用者資料的第一個記憶體頁後面,堆管理器會額外多配置設定一個記憶體頁。這個記憶體頁是用來檢測溢出的,被稱為栅欄頁。栅欄頁的屬性為PAEG_NOACCESS,是以一旦使用者資料發生溢出觸及到栅欄頁便會引發異常,使調試人員第一時間發現問題,進而可以迅速定位到導緻溢出的代碼。

有人也許會有疑問,當堆塊非常小時,難道也是占用兩個記憶體頁麼?答案是肯定的。為了及時檢測溢出,堆塊被放到第一個記憶體頁的末尾緊鄰栅欄頁,是以第一個記憶體頁前面的大半部分有可能都是沒有被使用的。由于配置設定粒度為8byte,堆塊和栅欄頁之間可能會有填充字段。對于很小的堆塊也需要占用兩個記憶體頁,這是很耗費空間的。

測試頁堆在調試中的效果

     使用下面的代碼生成HeapTest.exe,并使用windbg調試。

HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 1024*1024);  

char * p = (char*)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 1012);  

char *q = p;  

for(int i = 0 ;i < 1048; i++)  

{  

  *q++ = 0;  

}  

bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);  

這段代碼首先建立一個私有堆然後從私有堆配置設定1012個位元組。但卻通路配置設定位址後的1048byte的位址,很明顯發生了堆溢出。

第一次我們不開啟任何選項,觀察全局标志:

發現預設開啟了htc、hfc、hpc。

按F5繼續運作,可以看到程式抛出異常而中斷。

第一行的調試資訊顯示在通路堆塊00420648時超過了它的大小3f4 = 1012Byte。而導緻了通路違規。

檢視堆棧調用:

可以發現是在對堆塊釋放時檢測到堆塊被破壞。顯然這種方法不能在第一時間中斷到出現為問題的地方。

第二次我們開啟開啟dph來觀察:

首先開啟dhp。gflags /i HeapTest.exe +hpa

   在windbg中重新開始HeapTest.exe。可以看到程式發生記憶體通路異常。

最後一條指令由于通路eax所代表的位址而導緻。

檢視局部變量的值:

可以看到i等于1016時發生了異常。細心的同學可能會發現,我們申請的空間隻有1012byte,為什麼通路到1016時才會導緻異常。這是因為在堆中的配置設定粒度是8byte,即配置設定的空間大小必須為8的倍數。是以此處填充了4個位元組。

啟用頁堆後我們在第一時間發現了堆溢出的問題,結合源碼分析便可很容易的找到導緻堆溢出的地方。

準頁堆

使用頁堆确實是非常友善的,但是美中不足的是頁堆要為每個堆塊都配置設定兩個記憶體頁且隻利用第一個記憶體頁得後半部分,使用率是非常低的。在調試需要使用大量記憶體的應用程式時有可能會導緻一些問題。為此引入了準頁堆。

準頁堆彌補了頁堆的不足,同時還具有頁堆的一些功能。準頁堆也被稱為正常頁堆,頁堆也被稱為完全頁堆。

準頁堆不再為每個堆塊配置設定栅欄頁,隻是在堆塊的前後添加一些類似于安全Cookie的附加标記。當釋放堆塊時,堆管理器會檢測這些标記的完好性。一旦檢測到這些标記被破壞,便會産生異常。這種機制與釋放時檢查hfc類似。

由于是在釋放時檢測,是以準頁堆并不具備頁堆第一時間便能檢測到堆破壞的優點。是以本文并不準備詳細介紹。

啟用準頁堆

準頁堆也需要手動開啟,開啟指令與頁堆很像:gflags /p /enable calc.exe

本文介紹了win32堆的調試支援,重點介紹了頁堆,其他調試功能調試器預設是開啟的,僅以非常小的篇幅介紹。對于一些非常複雜的堆破壞問題,使用其他方式很難發現問題。即使有錯誤報告,錯誤報告處往往距離問題發生地十萬八千裡。而使用頁堆卻能很好的解決這個問題。僅僅使用一個指令打開頁堆的調試功能便可以使困擾很久的問題迎刃而解,這也是本文之是以如此推崇頁堆的原因。

下一篇文章将會介紹CRT堆。

繼續閱讀