談一談Windows中的堆
本文轉載自微信公眾號「一個程序員的修煉之路」,作者河邊一枝柳。轉載本文請聯系一個程序員的修煉之路公眾號。
如果在Windows中編程應該了解一些Windows的內存管理,而堆(Heap)也屬于內存管理的一部分。這篇文章對你理解Windows內存分配的基本原理和調試堆內存問題或許會有所幫助。
Windows Heap概述
下圖參考<<Windows高級調試>>所畫,并做了一些小小的修改??梢钥闯鰜沓绦蛑袑Χ训闹苯硬僮髦饕腥N:
- 進程默認堆。每個進程啟動的時候系統會創建一個默認堆。比如LocalAlloc或者GlobalAlloc也是從進程默認堆上分配內存。你也可以使用GetProcessHeap獲取進程默認堆的句柄,然后根據用這個句柄去調用HeapAlloc達到在系統默認堆上分配內存的效果。
- C++編程中常用的是malloc和new去申請內存,這些由CRT庫提供方法。而根據查看在VS2010之前(包含),CRT庫會使用HeapCreate去創建一個堆,供CRT庫自己使用。在VS2015以后CRT庫的實現,并不會再去創建一個單獨的堆,而使用進程默認堆。 (VS2013的CRT源碼我并未查看,有興趣的可以看看VS2013默認的CRT庫采用的是進程默認堆還是新建的堆)。
- 自建堆。這個泛指程序通過HeapCreate去創建的堆,然后利用HeapAlloc等API去操作堆,比如申請空間。
那么堆管理器是通過調用虛擬管理器的一些方法進行堆管理的實現,比如VirtualAlloc之類的函數。同樣應用程序也可以直接使用VirtualAlloc之類的函數對內存進行使用。
說到這里不免有些生澀,我們就寫一個示例代碼來看看一個進程的堆情況。
- #include <windows.h>
- #include <iostream>
- #include <intsafe.h>
- using namespace std;
- const char* GetHeapTypeString(HANDLE pHandle)
- {
- ULONG ulHeapInfo;
- HeapQueryInformation(pHandle,
- HeapCompatibilityInformation,
- &ulHeapInfo,
- sizeof(ulHeapInfo),
- NULL);
- switch (ulHeapInfo)
- {
- case 0:
- return "Standard";
- case 1:
- return "Look Aside List";
- case 2:
- return "Low Fragmentation";
- }
- return "Unknow type";
- }
- void PrintAllHeaps()
- {
- DWORD dwNumHeap = GetProcessHeaps(0, NULL);
- if (dwNumHeap == 0)
- {
- cout << "No Heap!" << endl;
- return;
- }
- PHANDLE pHeaps;
- SIZE_T uBytes;
- HRESULT Result = SIZETMult(dwNumHeap, sizeof(*pHeaps), &uBytes);
- if (Result != S_OK) {
- return;
- }
- pHeaps = (PHANDLE)malloc(uBytes);
- dwNumHeap = GetProcessHeaps(dwNumHeap, pHeaps);
- cout << "Process has heaps: " << dwNumHeap << endl;
- for (int i = 0; i < dwNumHeap; ++i)
- {
- cout << "Heap Address: " << pHeaps[i]
- << ", Heap Type: " << GetHeapTypeString(pHeaps[i]) << endl;
- }
- return;
- }
- int main()
- {
- cout << "========================" << endl;
- PrintAllHeaps();
- cout << "========================" << endl;
- HANDLE hDefaultHeap = GetProcessHeap();
- cout << "Default Heap: " << hDefaultHeap
- << ", Heap Type: " << GetHeapTypeString(hDefaultHeap) << endl;
- return 0;
- }
這是一個在Win10上運行的64位程序輸出的結果: 這個進程我們并沒有在main中顯示的創建Heap,我們都知道進程在啟動的時候初始化會創建相關的資源,其中也包含了堆。這個進程共創建了四個堆??梢钥闯鰜淼谝粋€堆就是進程的默認堆,并且是采用的 Low Fragmentation的分配策略的堆。
堆的內存分配策略
堆主要有前端分配器和后端分配器,我所理解的前端分配器就是類似于緩存一樣,便于快速的查詢所需要的內存塊,當前端分配器搞不定的時候,就交給后端分配器。
前端分配器主要分為, 而Windows Vista之后進程默認堆均采用低碎片前端分配器。
- 旁視列表 (Look Aside List)
- 低碎片 (Low Fragmentation)
以下的場景均采用32位的程序進行的描述。
前端分配器之旁視列表
旁視列表 (Look Aside List, LAL)是一種老的前端分配器,在Windows XP中使用。
這是一個連續的數組大小為128,每個元素對應一個鏈表,因為其存儲的是整個Heap塊的大小,那就包含了用戶申請的大小+堆塊元數據,而這里元數據大小為8字節, 而最小分配粒度為8字節(32位程序),那么最小的堆塊的大小則為16個字節。從數據1~127,每個鏈表鎖存儲的堆塊大小按照8字節粒度增加。
那么當用戶申請一個比如10字節大小的的內存,則在LAL中查找的堆塊大小為18字節=10字節+元數據8字節,則在表中找到的剛好匹配的堆塊大小為24字節的節點,并將其從鏈表中刪除。
而當用戶釋放內存的時候,也會優先查看前端處理器是否處理,如果處理則將內存插入到相應的鏈表中。
前端分配器之低碎片
先說說內存碎片我這里簡要概述下: 如下圖所示假設一段大的連續的內存被分割為若干個8字節的內存塊,然后這個時候釋放了圖中綠色部分的內存塊,那么此時總共空出了40字節的內存,但想去申請一個16字節的內存塊,卻無法申請到一個連續的16字節內存塊,從而分配內存失敗,這就是內存碎片。
所謂的低碎片前端分配器,是將LAL類似的數組中的粒度重新進行了劃分:
數據Index | 堆塊遞增粒度 | 堆塊字節范圍 |
---|---|---|
0~31 | 8 | 8~256 |
32~47 | 16 | 272~512 |
… | … | … |
112-127 | 512 | 8704~16384 |
可以看到同樣的數組的大小,將其按照不同的粒度劃分,相比較LAL分配的大小粒度逐步增大,到了最后的112-127區間粒度已經增大到了512字節,最大支持的16384。粒度更大的分配有利于緩解內存碎片,提高內存的使用效率。Windows Vista之后進程默認堆均采用低碎片前端分配器。
后端分配器
其實講到前面這部分可能還有一些人云里霧里。那么我們的內存到底是怎么劃分出來的呢?這就是后端分配器要做的事情了??纯春蠖朔峙淦魇侨绾喂芾磉@些內存的。
先說說堆在內存中的展現形式,一個堆主要由若干個Segment(段)組成,每個Segment都是一段連續的空間,然后用雙向鏈表串起來。而一般情況下,一開始只有一個Segment,然后在這個Segment上申請空間,叫做Heap Entry(堆塊)。但是這個Segment可能會被用完,那就新開辟一個Segment,而且一般新的Segement大小是原先的2倍,如果內存不足則不斷的將申請空間減半。這里有個要注意的就是當劃分了一個新的Segment后比如其空間為1GBytes,那么其真實的使用的物理內存肯定不會是1GBytes,因為此時內存還沒有被應用程序申請,這個時候實際上這個Segment只是Reserve了這段虛擬地址空間,而當真正應用程序申請內存的時候,才會一小部分一小部分的Commit,這個時候才會用到真正的物理存儲空間。
而應用程序申請的內存在Segment上叫做Entry(塊),他們是連續的,可以看到一個塊一般具有:
- 前置的元數據: 這里主要存儲有當前塊的大小,前一個塊的大小,當前塊的狀態等。
- 用戶數據區: 這段內存才是用戶申請并且使用的內存。當然這塊數據可能比你申請的內存要大一些,因為32位下面最小的分配粒度是8字節。這也是為什么有時候程序有時候溢出了幾個字符,好像也沒有導致程序異常或者崩潰的原因。
- 后置的元數據: 這個一般用于調試所用。一般發布的時候不會占用這塊空間。
那么哪些塊是可以直接使用的呢?這就涉及到這些塊元數據中的狀態,可以表明這個塊是否被占用,如果是空閑狀態則可以使用。
后端分配器,不會傻傻的去遍歷所有的塊的狀態來決定是否可以分配吧?這個時候就用到了后端分配器的策略。
這個表有點類似于LAL, 只是注意看下這個index為0的多了一個list,從小到大排列,可變大小的從大于1016字節的小于524272字節的將在這個鏈表里面存儲。超過524272字節將直接通過VirtualAlloc之類的API直接獲取內存。
假設此時前端堆管理器需要尋找一個32字節的堆塊, 后端管理器將如何操作?
這個時候請求到了后端分配器,后端分配器假設也沒有在這個表中查找到32字節的空閑塊,那么將先查找64字節的空閑塊,如果找到,則將其從列表中移除,然后將其分割為兩個16字節的塊, 一個設置為占用狀態返回給應用程序,一個設置為空閑狀態插入響應的鏈表中。
那如果還沒有找到呢?那么這個時候堆管理器會從Segment中提交(Commit)更多的內存去使用,創建新的塊, 如果當前Segment空間也不夠了,那就創建新的Segement
有細心的同學可能說,那前端分配器和后端分配器差不多嗎,這里面有個很重要的就是,前端分配器鏈表中的塊是屬于占用狀態的, 而后端分配器鏈表中的塊是屬于空閑狀態的。
假設釋放內存,該如何操作?
首先要看前端分配器是否處理這個釋放的塊,比如加入到相應的鏈表中去,如果不處理,那么后端分配器將會查看相鄰的塊是否也是空閑的,如果是空閑狀態,將會采用塊合并成一個大的塊,并對相應的后端分配器鏈表進行操作。
當然了當你釋放的內存足夠多的時候,其實堆管理器也不會長期霸占著物理存儲器的空間,也會在適當的情況下調用Decommit操作來減少物理存儲器的使用。
Windbg查看進程中的堆
進程堆信息查看
進程堆的信息是放在PEB(進程環境塊)中,可以通過查看PEB相關的信息, 可以看到當前進程包含有3個堆,并且堆的數組地址為0x77756660
- 0:000> dt _PEB @$peb
- ......
- +0x088 NumberOfHeaps : 3
- ......
- +0x090 ProcessHeaps : 0x77756660 -> 0x00fa0000 Void
- ......
然后我們查看對應的三個堆的地址,分別為0xfa0000, 0x14b0000和0x2e10000, 而第一個一般為進程的默認堆00fa0000。
- 0:006> dd 0x77756660
- 77756660 00fa0000 014b0000 02e10000 00000000
- 77756670 00000000 00000000 00000000 00000000
- 77756680 00000000 00000000 00000000 00000000
- 77756690 00000000 00000000 00000000 00000000
- 777566a0 00000000 00000000 00000000 00000000
- 777566b0 00000000 00000000 00000000 00000000
- 777566c0 ffffffff ffffffff 00000000 00000000
- 777566d0 00000000 020007d0 00000000 00000000
其實上述步驟Windbg提供了一個方法可以直接查看概要信息了, 可以看到系統默認堆00fa0000為LFH堆,并且已經Reserve了空間為1128K, Commit的內存為552K。
- 0:000> !heap -s
- ......
- LFH Key : 0x8302caa1
- Termination on corruption : ENABLED
- Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
- (k) (k) (k) (k) length blocks cont. heap
- -----------------------------------------------------------------------------
- 00fa0000 00000002 1128 552 1020 178 21 1 1 0 LFH
- 014b0000 00001002 60 12 60 1 2 1 0 0
- 02e10000 00001002 1188 92 1080 4 4 2 0 0 LFH
- -----------------------------------------------------------------------------
可以通過dt _HEAP 00fa0000命令去查看進程默認堆的信息,也可以通過Windbg直接提供的命令去查看, 可以看到其分配空間的最小粒度(Granularity)為8字節。并且只有一個Segment.
- 0:006> !heap -a 00fa0000
- Index Address Name Debugging options enabled
- 1: 00fa0000
- Segment at 00fa0000 to 0109f000 (00089000 bytes committed)
- Flags: 00000002
- ForceFlags: 00000000
- Granularity: 8 bytes
- Segment Reserve: 00100000
- Segment Commit: 00002000
- DeCommit Block Thres: 00000800
- DeCommit Total Thres: 00002000
- Total Free Size: 0000597f
- Max. Allocation Size: 7ffdefff
- Lock Variable at: 00fa0248
- Next TagIndex: 0000
- Maximum TagIndex: 0000
- Tag Entries: 00000000
- PsuedoTag Entries: 00000000
- Virtual Alloc List: 00fa009c
- 03321000: 00100000 [commited 101000, unused 1000] - busy (b), tail fill
- Uncommitted ranges: 00fa008c
- 01029000: 00076000 (483328 bytes)
- FreeList[ 00 ] at 00fa00c0: 00ffcf40 . 00ff3290
- 00ff3288: 00208 . 00010 [100] - free
- 00fb1370: 00060 . 00010 [100] - free
- 00fb10a0: 00020 . 00010 [100] - free
- 00fa6c40: 00088 . 00010 [100] - free
- 00fa8e98: 00010 . 00010 [100] - free
- 00fafa78: 000d0 . 00018 [100] - free
- 00faea20: 00138 . 00018 [100] - free
- 00fafc38: 00030 . 00020 [100] - free
- 00ff4570: 00128 . 00028 [100] - free
- 00faeeb8: 00058 . 00028 [100] - free
- 00faf0c8: 00060 . 00028 [100] - free
- 00fad980: 00050 . 00028 [100] - free
- 00fb83f0: 00050 . 00040 [100] - free
- 00faed78: 00030 . 00080 [100] - free
- 00feebd8: 000e8 . 00080 [100] - free
- 00faeb80: 00050 . 000d0 [100] - free
- 00ff0398: 00148 . 000d8 [100] - free
- 00fafed0: 000b0 . 000f0 [100] - free
- 00fb8130: 00210 . 00270 [100] - free
- 00fef460: 00808 . 003c8 [100] - free
- 00ffcf38: 003c8 . 2c0a8 [100] - free
- Segment00 at 00fa0000:
- Flags: 00000000
- Base: 00fa0000
- First Entry: 00fa0498
- Last Entry: 0109f000
- Total Pages: 000000ff
- Total UnCommit: 00000076
- Largest UnCommit:00000000
- UnCommitted Ranges: (1)
- Heap entries for Segment00 in Heap 00fa0000
- address: psize . size flags state (requested size)
- 00fa0000: 00000 . 00498 [101] - busy (497)
- 00fa0498: 00498 . 00108 [101] - busy (100)
- 00fa05a0: 00108 . 000d8 [101] - busy (d0)
- ......
- 01029000: 00076000 - uncommitted bytes.
查看Segment
一般來說我們通過上述的命令已經可以基本查看到Segment在一個堆中的信息了。如果要針對一個Segment進行查看可以用如下方式:
- 0:006> dt _HEAP_SEGMENT 00fa0000
- ntdll!_HEAP_SEGMENT
- +0x000 Entry : _HEAP_ENTRY
- +0x008 SegmentSignature : 0xffeeffee
- +0x00c SegmentFlags : 2
- +0x010 SegmentListEntry : _LIST_ENTRY [ 0xfa00a4 - 0xfa00a4 ]
- +0x018 Heap : 0x00fa0000 _HEAP
- +0x01c BaseAddress : 0x00fa0000 Void
- +0x020 NumberOfPages : 0xff
- +0x024 FirstEntry : 0x00fa0498 _HEAP_ENTRY
- +0x028 LastValidEntry : 0x0109f000 _HEAP_ENTRY
- +0x02c NumberOfUnCommittedPages : 0x76
- +0x030 NumberOfUnCommittedRanges : 1
- +0x034 SegmentAllocatorBackTraceIndex : 0
- +0x036 Reserved : 0
- +0x038 UCRSegmentList : _LIST_ENTRY [ 0x1028ff0 - 0x1028ff0 ]
查看申請的內存地址
其實在調試過程中一般最關注的是變量的地址關聯的內容信息。比如說我寫了個程序其申請的內存變量地址為0x00fb5440, 申請的大小為5字節。
首先可以通過如下命令查找到地址所在的位置為堆:
- 0:000> !address 0x00fb5440
- Building memory map: 00000000
- Mapping file section regions...
- Mapping module regions...
- Mapping PEB regions...
- Mapping TEB and stack regions...
- Mapping heap regions...
- Mapping page heap regions...
- Mapping other regions...
- Mapping stack trace database regions...
- Mapping activation context regions...
- Usage: Heap
- Base Address: 00fa0000
- End Address: 01029000
- Region Size: 00089000 ( 548.000 kB)
- State: 00001000 MEM_COMMIT
- Protect: 00000004 PAGE_READWRITE
- Type: 00020000 MEM_PRIVATE
- Allocation Base: 00fa0000
- Allocation Protect: 00000004 PAGE_READWRITE
- More info: heap owning the address: !heap 0xfa0000
- More info: heap segment
- More info: heap entry containing the address: !heap -x 0xfb5440
然后可以通過如下命令查看當前申請內存的詳細堆塊信息, 其處于被占用狀態(busy)??梢钥吹狡涠褖K的大小為0x10, 我們實際申請的內存為5字節,那么0x10(Size) - 0xb (Unused) = 5, 可以看出來Unused是包含了_HEAP_ENTRY塊元數據的大小的。而我們實際用戶可用的內存是8字節 (最小分配粒度),比我們申請的5字節多了三個字節,這也是為什么程序有時候溢出了幾個字符,并沒有導致程序崩潰或者異常的原因。
- 0:000> !heap -x 0xfb5440
- Entry User Heap Segment Size PrevSize Unused Flags
- -----------------------------------------------------------------------------
- 00fb5438 00fb5440 00fa0000 00fad348 10 - b LFH;busy
那么我們也可以直接查看Entry的結構:
- 0:000> dt _HEAP_ENTRY 00fb5438
- ntdll!_HEAP_ENTRY
- +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
- +0x000 Size : 0xa026
- +0x002 Flags : 0xdc ''
- +0x003 SmallTagIndex : 0x83 ''
- +0x000 SubSegmentCode : 0x83dca026
- +0x004 PreviousSize : 0x1b00
- +0x006 SegmentOffset : 0 ''
- +0x006 LFHFlags : 0 ''
- +0x007 UnusedBytes : 0x8b ''
- +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
- +0x000 FunctionIndex : 0xa026
- +0x002 ContextValue : 0x83dc
- +0x000 InterceptorValue : 0x83dca026
- +0x004 UnusedBytesLength : 0x1b00
- +0x006 EntryOffset : 0 ''
- +0x007 ExtendedBlockSignature : 0x8b ''
- +0x000 Code1 : 0x83dca026
- +0x004 Code2 : 0x1b00
- +0x006 Code3 : 0 ''
- +0x007 Code4 : 0x8b ''
- +0x004 Code234 : 0x8b001b00
- +0x000 AgregateCode : 0x8b001b00`83dca026
如果細心的同學可以能會發現以下兩個問題:
- 結構中Size的值是0xa026和之前命令中看到的大小0x10不一樣,這個是因為Windows對這些元數據做了編碼,需要用堆中的一個編碼數據做異或操作才能得到真實的值。具體方法筆者試過,在這里不在贅述,可以在參考文章中獲取方法。
- Size是2字節描述,那么最大可以描述的大小應該為0xffff,但是之前不是說最大的塊可以是0x7FFF0 (524272字節), 應該不夠存儲啊?這個也和第一個問題有關聯,在通過上述方法計算出的Size之后還需要乘以8, 才是真正的數據大小。
Windows 自建堆的使用建議
在<
保護組件
先看看書中原話:
假如你的應用程序需要保護兩個組件,一個是節點結構的鏈接表,一個是 B R A N C H結構的二進制樹。你有兩個源代碼文件,一個是 L n k L s t . c p p,它包含負責處理N O D E鏈接表的各個函數,另一個文件是 B i n Tr e e . c p p,它包含負責處理分支的二進制樹的各個函數。
現在假設鏈接表代碼中有一個錯誤,它使節點 1后面的8個字節不
小心被改寫了,從而導致分支 3中的數據被破壞。當B i n Tr e e . c p p文件中的代碼后來試圖遍歷二進制樹時,它將無法進行這項操作,因為它的內存已經被破壞。當然,這使你認為二進制樹代碼中存在一個錯誤,而實際上錯誤是在鏈接表代碼中。由于不同類型的對象混合放在單個堆棧中,因此跟蹤和確定錯誤將變得非常困難。
我個人認為在一個應用的工程中,也許不需要做到上述那么精細的劃分。但是你想一想,在一個大型工程中,會混合多個模塊。比如你是做產品的,那么產品會集成其他部門甚至是外部第三方的組件,那么這些組件同時在同一個進程,使用同一個堆的時候,那么難免會出現,A模塊的內存溢出問題,導致了B模塊的數據處理異常,從而讓你追蹤問題異常復雜,更坑的是,很可能讓B模塊的團隊背鍋了。而這些是切實存在的。 這里的建議更適合于讓一些關鍵模塊使用自己的堆,從而降低自己內存使用不當,覆蓋了其他組件使用的內存,從而導致異常,讓問題的追蹤可以集中在出錯的模塊中。當然這也不是絕對的,因為進程的組件都在同一個地址空間內,內存破壞也存在一種跳躍式內存訪問破壞,但是大多數時候內存溢出是連續的上溢較多,這樣做確實可以提高這種問題追蹤的效率。
更有效的內存管理
這個主要強調是,將同種類型大小的對象放在一個堆中,盡量避免不同大小內存對象摻雜在一起導致的內存碎片問題,從而帶來的堆管理效率下降。同一種對象,則可以避免內存碎片問題。當然了這些只是提供了一種思想,至于你的工程是否有必要采用這樣的做法,由工程師自己來做決定。
進行本地訪問
先來看看原文的描述:
每當系統必須在 R A M與系統的頁文件之間進行 R A M頁面的交換時,系統的運行性能就會受到很大的影響。如果經常訪問局限于一個小范圍地址的內存,那么系統就不太可能需要在 R A M與磁盤之間進行頁面的交換。
所以,在設計應用程序的時候,如果有些數據將被同時訪問,那么最好把它們分配在互相靠近的位置上。讓我們回到鏈接表和二進制樹的例子上來,遍歷鏈接表與遍歷二進制樹之間并無什么關系。如果將所有的節點放在一起(放在一個堆棧中),就可以使這些節點位于相鄰的頁面上。實際上,若干個節點很可能恰好放入單個物理內存頁面上。遍歷鏈接表將不需要 C P U為了訪問每個節點而引用若干不同的內存頁面。
這個思想其實就是一種Cache思想,RAM與磁盤上的page.sys存儲器(磁盤上的虛擬內存)進行頁交換會帶來一些時間成本。舉個極限的例子,你的RAM只有一個頁,你有兩個對象A和B,A存放在Page1上,而B存放在Page2上,當你訪問A對象的時候,必然要把Page1的內容加載到RAM中,那么這個時候B對象所在Page2肯定就在page.sys中,當你又訪問B對象的時候,這個時候就得把Page2從page.sys中加載到RAM中替換掉Page1.
理解了頁切換帶來的性能開銷后,其實這一段的思想就是將最可能連續訪問的對象放在一個堆中,那么他們在一個頁面的可能性也更大,提高了效率。
減少線程同步的開銷
這一個很好理解,一般情況下創建的自建堆是支持多線程的,那么多線程的內存分配必然會帶來同步的時間消耗,但是對于有些工程來說,只有一個線程,那么對于這一個線程的程序,在調用HeapCreate的時候設置HEAP_NO_SERIALIZE, 則這個堆只支持單線程,從而提高內存申請的效率。
迅速釋放堆棧
這種思想第一提高了內存釋放的效率,第二是盡可能的降低了內存泄露。記得之前看過一篇文章介紹過Arena感覺比較類似,在一個生命周期內的內存是從Arena申請,然后這個聲明周期結束后,不是直接釋放各個對象,而是直接銷毀這個Arena,提高了釋放效率,并且降低了內存泄露的可能。那么使用自建堆的原理和Arena是類似的,比如在一個任務處理之前創建一個堆,在任務處理過程中所申請的內存在這個堆上申請,然后釋放的時候,直接銷毀這個堆即可。
那對于對象的申請,C++中可以重載new和delete等操作符,來實現自定義的內存分配,并且可以將這個先封裝成一個基類,在這個過程中需要創建的對象均繼承于這個基類,復用new和delete。
總結和參考
我本以為這些是已經掌握的知識,但是寫文章的時間也超過了我預想的時間,在實踐中也也發現了一些自己曾經錯誤的理解。如果文中還有不當的地方,也希望讀者給與指正。
參考
《Windows核心編程》
《Windows高級調試》
Windows Heap Chunk Header Parsing and Size Calculation: https://stackoverflow.com/questions/28483473/windows-heap-chunk-header-parsing-and-size-calculation
Understanding the Low Fragmentation Heap: http://www.illmatics.com/Understanding_the_LFH.pdf
WINDOWS 10SEGMENT HEAP INTERNALS: https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals-wp.pdf