windows程序員進階系列:《軟件調試》之Win32堆
win32堆及內部結構
Windows在創建一個新的進程時會為該進程創建第一個堆,被稱為進程的默認堆。默認堆的句柄會被保存在進程環境塊_PEB的ProcessHeap字段中。
要獲得_PEB的地址,可以通過$peb偽寄存器來獲得,dt _PEB @$peb。也可以通過.process獲得。
如上圖ProcessHeap字段即為進程默認堆。其上的HeapSegmentReserve是進程堆的預訂(默認為1MB)大小。HeapSegmentCommit是進程堆的初始提交大小。默認值為2個內存頁大小(x86內存頁為4KB)。
可以通過GetProcessHeap函數來取得當前進程的默認堆句柄:
[cpp] view plaincopy- HANDLE GetProcessHeap(void);
該函數僅僅是找到_PEB結構然后取出PEB結構中的ProcessHeaps字段的值。獲得進程默認堆的句柄后就可以調用HeapAlloc從默認堆上分配空間了 。
除了進程的默認堆以外,應用程序也可以調用HeapCreate函數創建自己的堆,這樣創建的堆被稱為私有堆。
[cpp] view plaincopy
- <span style="font-size:18px;">HANDLE HeapCreate(
- DWORD flOptions,
- SIZE_T dwInitialSize,
- SIZE_T dwMaximumSize
- );
- </span>
具體使用方法請參考MSDN。
進程的默認堆與私有堆沒有本質的區別。兩者都是通過調用RtlHeapCreate創建的。只是用途不同。
除了默認堆,進程的_PEB中還記錄了當前進程的所有堆句柄。NumberOfHeaps字段用來介紹堆的總數。ProcessHeaps是一個數組,用來記錄每個堆的句柄。
使用windbg加載calc.exe。并查看_PEB結構。
可以看到calc.exe共有13各堆,堆數組首地址位0x7c99ffe0。
打印出此地址的內容:
使用!heap –h命令可以打印出所有的堆
可以看到共有13個堆。每個堆的首地址與前面堆數組中顯示的內容相同。
調用HeapAlloc函數可以從win32堆中分配空間。
[cpp] view plaincopy- <span style="font-size:18px;">LPVOID HeapAlloc(
- HANDLE hHeap,
- DWORD dwFlags,
- SIZE_T dwBytes
- );
- </span>
具體使用方法,請參考MSDN。
如果分配成功該函數返回所分配空間的指針。還可以使用HeapReAlloc來改變從堆中分配內存的大小。
[cpp] view plaincopy- <span style="font-size:18px;">LPVOID HeapReAlloc(
- HANDLE hHeap,
- DWORD dwFlags,
- LPVOID lpMem,
- SIZE_T dwBytes
- );
- </span>
當不再需要使用堆中的空間時可以調用HeapFree釋放。
- <span style="font-size:18px;">BOOL HeapFree(
- HANDLE hHeap,
- DWORD dwFlags,
- LPVOID lpMem
- );
- </span>
調用HeapFree并不意味者對管理器會將這塊內存交還給內存管理器。這是因為應用程序還有可能會繼續申請空間,為了減少與對管理器交互的次數,堆管理器只有在下面兩個條件同時滿足時才會將其交還給內存管理器,這個過程被稱為解除提交。
第一個條件:本次釋放的堆塊大小超過了_PEB中的HeapDeCommitFreeBlockThreshold字段的值。
第二個條件:空閑空間的總大小超過了_PEB中的eapDeCommitTotalFreeThreshold字段的值。
可以看到當要釋放的堆塊大小超過4KB,并且堆上的空閑空間的總大小大于64KB時,堆管理器才會將空閑空間交還給內存管理器,即執行解除提交操作。
堆內部結構
接下來將介紹對內部結構,也是深入理解堆的重中之重。
從前面的_PEB中的堆數組我們可以知道,進程中可以存在多個堆。在每個堆內部又可以分為多個堆段。堆管理器在創建堆時創建的第一個段,我們將其稱為0號段。如果堆是可增長的,當一個段不能滿足要求時,堆管理器會繼續創建其他段。但最多可以有64個段。段內部又由堆塊構成。
每個堆使用_HEAP結構來描述。
_HEAP結構記錄該堆的屬性和資產情況。因此該結構也被稱為是堆的頭結構。調用HeapCreate函數返回的句柄便是此結構的地址。
VirtualMemoryThreshold為虛擬內存分配閾值,表示可以在段中分配的堆塊的最大有效(即應用程序可以實際使用的)值,該值為508kB。當應用程序從堆中分配的堆塊的最大大小大于該值的申請,堆管理器會直接從內存管理器中分配,并不會從從空閑鏈表申請。同時將此空間添加到VirtualAllocdBlocks結構所指向的鏈表中。
VirtualAllocdBlocks是一個鏈表的頭指針,該鏈表維護著所有大于VirtualMemoryThreshold直接從內存管理器申請的空間。
Segments是一個數組,它記錄著堆擁有的所有段。每個元素類型為_HEAP_SEGMENT結構。
LastSegmentIndex表示堆中最后一個段的序號,加1便是總段數。
FreeLists是一個雙向鏈表的頭指針,該鏈表記錄著所有空閑堆塊的地址。鏈表元素為FREE_LIST結構,
該鏈表為雙向鏈表,每個鏈表中都保存著一些空閑堆塊。各個鏈表項都指向_HEAP_FREE_ENTRY結構中的FreeList字段。
當應用程序申請新的空間時,堆管理器會首先遍歷這個鏈表,如果找到滿足需要的堆塊就分配出去。否則便要考慮建立新的堆塊或從內存管理器申請空間。在釋放時,當不滿足解除提交條件時,大多數情況下也是將要釋放的堆塊加入到該空閑鏈表中。
FrontEndHeap該字段為指針指向前端分配器。在本文的最后部分我專門介紹前端分配器。
堆段
每個段使用_HEAP_SEGMENT結構描述。
Entry字段是一個數組,存儲著該段所有的堆塊。由于每個堆塊使用_HEAP_ENTRY結構描述,因此該數組元素類型為_HEAP_ENTRY。
Heap字段維護該塊塊所屬的堆的_HEAP結構的首地址。
BaseAddress字段維護該段的基地址。
FirstEntry表示該段中第一個堆塊的地址。
堆塊
段內部又可以分為多個堆塊。堆塊使用 _HEAP_ENTYR結構來描述,該結構占8 Byte。_HEAP_ENTRY結構之后就是供應用程序使用的區域。調用HeapAlloc函數將返回HEAP_ENTRY之后的地址。此地址減去8Byte便可以得到_HEAP_ENTRY結構。
_HEAP_ENTRY的前兩個字節Size字段表示該堆塊的大小。其單位為8byte。表示每個堆塊的最大大小為2^16 *8 byte = 512KB。由于每個堆塊都需要8字節的_HEAP_ENTRY結構,因此每個堆塊能提供給應用程序的最大大小為512KB-8B = 0x7ffdefff。該值等于_HEAP結構的MaximumAllocationSize字段的值。
PreviousSize表示前一個堆塊的大小。
Flags字段代表堆塊的狀態。
可以通過!heap –a獲得。
UnusedBytes表示多分配的字節數。比如應用程序申請1020個字節,但堆管理器為了內存對齊分配了1024個字節。這4個字節就是多分配的值,此時Unused字段就為4。
SegmentIndex表示該堆塊所在的段在HEAP結構Segments數組的序號。若為0則表示該堆塊是從堆中0號段中分配的。
堆分配和釋放實例
使用以下代碼構建HeapTest.exe程序。
[cpp] view plaincopy
- <span style="font-size:18px;">int main(int argc, _TCHAR* argv[])
- {
- HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 1024*1024);
- void * p = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 1012);
- bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);
- return 0;
- }
- </span>
在HeapTest.exe中創建了一個私有堆,該堆大小為1MB。然后從該私有堆中分配1012Byte的空間,最后進行釋放。非常的簡單,僅僅是為了演示堆的創建、分配空間和釋放空間的過程。
由于進程在退出時會清理默認堆和所有其他堆,因此上面的代碼中并沒有銷毀堆的操作。讀者只要明白最后操作系統會執行所有的堆的清理工作即可。
在HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 1024*1024)處設置斷點。在windbg中載入HeapTest.exe。開始調試,程序停在該處。
觀察hHeap 的值為0x00420000。該值即為該私有堆_HEAP結構的地址。
dt _HEAP 0x00420000
SegmentReserve字段的值為0x100000 = 1MB。表示我們請求創建的堆的最大大小。SegmentCommit為0x2000,表示僅僅提交兩個頁面為8KB。LastSegmentIndex為0,表示該堆中只有1個段。
單步執行,代碼執行到void * p = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 1012);該語句表示從私有堆中分配1012Byte。
觀察p的值為0x00420650。該值表示返回給應用程序的有效用戶區地址。該地址減去8byte,得到_HEAP_ENTRY結構的首地址。
Size 為0x82,由于塊大小粒度為8byte,可以得到0x82 *8byte = 1040Byte。UnusedBytes 為28byte。 1040 - 28得到段塊的用戶有效長度為1012byte。SegmentIndex等于0,表示該斷塊處于第0號段。
Flags等于7,表示該堆塊處于占用狀態,且塊尾有額外的描述,且進行過填充(使用baadf00d填充)。
觀察0x00420650處得內存,發現的確進行過填充。
繼續單步執行到bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);該語句表示從堆中釋放p表示的空間。
執行此語句后觀察p處得內存:
發現除開頭的8個字節外,其余字節已經使用feee(類似free)填充過。這是堆對已經釋放的內存的填充。
繼續觀察該堆塊
大小從剛才的0x82變為0x137,即0x137 *8 = 2488Byte。為什么釋放后該堆塊的大小范圍變大了呢?這是因為堆管理器將剛才釋放的堆塊與其后的空閑區域合并成了一個大的堆塊。這樣可以防止堆碎片的發生。堆塊標志變為0x14,表示該堆塊為空閑,進行過填充且是這個段中的最后一個塊。
對于已經釋放的堆塊,堆管理器定義了_HEAP_ FREE_ENTRY結構來描述。該結構的前八個字節與_HEAP_ENTRY結構完全相同。但增加了8個字節來存儲空閑鏈表的頭節點。
使用上面的地址觀察_HEAP_FREE_ENTRY結構。
_HEAP_FREE_ENTRY字段比_HEAP_ENTRY結構多了一個FreeList字段,用來存儲空閑鏈表的一個鏈表項。多個鏈表項構成一個空閑鏈表。
由于空閑了鏈表為雙向鏈表,Flink和Blink字段分別指向前一個和后一個空閑的堆塊(指向_HEAP_FREE_ENTRY的_FreeList字段)。
由于該鏈表僅僅只有一個空閑堆塊,因此上述_LIST_ENTRY的Flink和Blink 字段均指向空閑鏈表的頭結點。
在前面介紹的_HEAP結構中包含了一個Free_list數組,該數組有128個元素,用來存儲各個空閑鏈表的表頭。空閑鏈表的元素即為_HEAP_FREE_LIST類型。
不知大家注意到沒有,上面在觀察釋放后的堆空間時,前8個字節并不是feeefeee,而是有具體的數值。這8個字節就是_HEAP_FREE_ENTRY的FreeList字段的內容。
偏移16Byte后便可觀察到釋放后的實際數據。
上面的分析過程很繁瑣,其實使用windbg的一個命令就可以詳細顯示堆的各種信息。
該命為!heap addr –hf
下面將上面HeapCreate返回的私有堆的句柄作為!heap的參數。
可以發現上面的很多字段與前面的分析是相同的。
前端分配器
現在是可以介紹前端分配器的時候了。
前面在介紹_HEAP結構的時候時候遇到一個FrontEndHeap字段,該字段指向前端分配器。與前端分配器對應的是后端分配器。
前端分配器維護固定大小的自由列表。當從堆中分配內存時,堆管理器會首先從前端分配器中查找符合條件的堆塊。如果失敗則會從后端分配器分配。
可以將前端分配器比作一個”快表”,它的存在就是為了加快分配速度。
Windows有兩種類型的前端分配器:
旁視列表(LAL)前端分配器和低碎片(LF)前端分配器。它們分別對應兩種不同的堆塊分配和回收策略。
可以調用HeapQueryInformation來查詢堆支持何種前端分配器
[cpp] view plaincopy
- <span style="font-size:18px;">BOOL HeapQueryInformation(
- HANDLE HeapHandle,
- HEAP_INFORMATION_CLASS HeapInformationClass,
- PVOID HeapInformation,
- SIZE_T HeapInformationLength,
- PSIZE_T ReturnLength
- );
- </span>
接下來我們使用下面的語句分別測試下heapTest.exe的私有堆和默認堆是支持何種前端分配器。
[cpp] view plaincopy
- <span style="font-size:18px;">int HeapInfo;
- int err;
- size_t len = 0;
- bRetVal = HeapQueryInformation( hHeap,
- HeapCompatibilityInformation,
- &HeapInfo,
- sizeof(HeapInfo),
- NULL);
- if(bRetVal)
- {
- std::cout<<"HeapInfo = "<<HeapInfo<<std::endl;
- }
- bRetVal = HeapQueryInformation(GetProcessHeap(),
- HeapCompatibilityInformation,
- &HeapInfo,
- sizeof(HeapInfo),
- NULL);
- if(bRetVal)
- {
- std::cout<<"HeapInfo = "<<HeapInfo<<std::endl;
- }
- </span>
在XP下運行得到:
在win7下運行得到:
查詢msdn可以知道:
私有堆默認為標準堆,不支持旁視列表。
默認進程堆在xp下默認開啟旁視列表前端分配器,在win7下默認開啟低碎片前端分配器。
HeapSetInformation用于設置堆的屬性
[cpp] view plaincopy- <span style="font-size:18px;">BOOL HeapSetInformation(
- HANDLE HeapHandle,
- HEAP_INFORMATION_CLASS HeapInformationClass,
- PVOID HeapInformation,
- SIZE_T HeapInformationLength
- );
- </span>
通過該函數我們可以設置指定堆支持何種前端分配器。
旁視列表前端分配器和低碎片前端分配器
旁視列表是一張表,包含128個項,每一項對應一個單項鏈表。每個單向鏈表中都包含一組固定大小的空閑堆塊。從16Byte開始遞增。由于每個堆塊都有_HEAP_ENTRY結構描述,為8Byte。如果應用程序請求24字節的空間,前端分配器將查找大小為32字節的空閑堆塊。由于每個鏈表的空閑塊從16字節開始遞增,每個堆塊需要8字節的管理結構,因此最小可以返回給應用程序的空間為16 – 8 = 8Byte。
旁視列表沒有使用索引為0的項,堆塊大小為16的鏈表的索引為1。每個索引表示一組空閑的堆塊,堆塊的大小是前一個索引中堆塊的大小加8字節。最后一個索引為127,包含大于1024字節的空閑堆塊。
當程序釋放一塊內存時堆管理器會將該塊內存標記為空閑,根據該塊的大小并放入到相應索引指向的鏈表中旁視列表中。當下一個請求內存空間時,堆管理器會先從前端分配器檢查是否存在滿足條件的堆塊,如果存在則將此堆塊返回給應用程序。否則將請求轉發到后端分配器。
低碎片前端分配器
顧名思義,低碎片前端分配器在使用過程中不會產生大量的堆碎片。它將可用空間分為128個桶位,編號1-128。每個桶位大小依次遞增:第一個桶位大小8byte,128號桶位16384byte。當需要從低碎片前端分配器上分配器空間時,堆管理器會將滿足要求的最小的桶分配出去。
如果應用請求7個字節,則將第一號桶分配出去。如果1號桶已經被分配出去則分配2號桶,依次遞推。低碎片前端分配器為不同區域設置了不同的分配粒度。如1-32號桶的分配粒度為8byte,這意味著這些桶的分配粒度為8,不足8byte的分配也會被分配給8byte。
下圖列出了各個桶的分配粒度。
后端分配器
后端分配器包含一個空閑列表數組。數組中的每一項是一個空閑鏈表。該數組共有128項,也就說包含128個空閑鏈表。與旁視列表類似,每個項也都包含了固定大小的堆塊。每個空閑鏈表中的堆塊大小都比前一個空閑鏈表的堆塊長度多8個字節。每個空閑鏈表的元素類型為_HEAP_FREE_ENTRY結構,該結構為16字節。索引為1的數組項沒有使用。因為堆塊的最小值為16字節。索引為0 的空閑鏈表包含的空閑塊的大小最小為1016,一直到0x7fff0字節。
對于大于0x7fff0的內存分配請求將轉發到虛擬分配鏈表中。如果虛擬分配鏈表中存在則分配,否則直接從內存管理器中分配,并添加到虛擬分配鏈表中。
為了提高搜索效率,每個鏈表中的項是按堆塊大小升序排列。
如果堆管理器無法找到一個堆塊滿足請求的大小,堆管理器將進行塊分割。首先堆管理器會找大一塊比請求空間更大的塊,然后將其對半分割成兩個相同大小的堆塊以滿足分配請求。如果對半分割后滿足分配請求,堆管理器會將其中一塊標記為占用狀態并返回給應用程序。將另一個堆塊放入到與該堆塊大小相等的空閑鏈表中。
在釋放時堆管理器會判斷這個堆塊的左右是否有相鄰的堆塊也是空閑的。如果有則將它們合并成為一個更大的堆塊,從當前空閑鏈表將它們移除并加入到長度等于新堆塊長度的空閑鏈表中。堆塊合并的開銷是很大的,但是它能夠避免所謂的堆碎片。堆合并將小的空閑的堆塊合并成大的堆塊,避免由于堆中大量存在小的堆塊,而無法分配更大的堆塊的情況。
內存分配步驟:
1:堆管理器查看前端分配器是否存在滿足條件的堆塊。如果存在將返回給調用者。否則進入步驟2。
2:堆管理器繼續查看后端分配器。
a:如果找到剛好合適的堆塊,將此堆塊標記為占用狀態從空閑鏈表移除并返還給調用者。
b:如果沒有找到,堆管理器將會將更大的堆塊分割為兩個更小的堆塊。將其中一塊標記為占用狀態并從空閑鏈表移除。另一塊則添加到新的空閑鏈表中。最初的大堆塊將從空閑鏈表中移除。
3:如果空閑列表不能滿足要求,堆管理器將會提交更多的內存,并將該塊內存返回給調用者。
內存釋放過程
1:首先檢查前端分配器能否處理該空閑塊。如果前端分配器沒有處理,則交由后端分配器。
2:堆管理器判斷該空閑塊的左右是否存在空閑堆塊,如存在會將這些空閑堆塊合并成更大的堆塊,合并步驟如下:
a:將相鄰的空閑塊從空閑鏈表移除。
b:將新的大堆快添加到空閑列表。
c:將新的大堆快設置為空閑。
3:如果不能進行合并操作,該空閑塊將被移入空閑列表。
雖然某些堆塊沒有被應用程序使用,但是在后端分配器看來這些堆塊仍然是占用狀態。這是因為所有在前端分配器中的堆塊,在后端分配器的眼里均為占用狀態。
到此對win32堆的內部結構討論完畢。
from:http://blog.csdn.net/ithzhang/article/details/12711431
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成