32位Windows系統中,進程在用戶態可用的地址空間範圍是低2G(x64下是低8192G)。隨着進程不斷的申請和釋放內存,這個2G的地址空間,有的地址範圍是保留狀態(reserved),有的地址範圍是提交狀態(映射到了物理頁面,committed),有的地址範圍是空閒的。Windows採用平衡二叉樹把這些離散的地址範圍管理起來。
常見的平衡二叉樹有紅黑樹和AVL樹兩種,其中紅黑樹應用更廣,C#/Java/C++STL等若干數據結構內部都是用紅黑樹實現的,然而Windows這次選擇了AVL樹。根據 《數據結構與算法C語言描述》,AVL樹的最大高度是1.44 * log(N+2) - 1.328,紅黑樹的最大高度是2.00* log(N+1)。與紅黑樹相比,AVL樹的插入刪除操作更慢一些,但是查詢操作更快。想必對進程地址空間的查詢操作更頻繁一些,所以AVL得以入選。
AVL樹的節點結構是
typedef struct _MMADDRESS_NODE {
ULONG_PTR StartingVpn; // 起始虛擬地址
ULONG_PTR EndingVpn; // 終止虛擬地址
struct _MMADDRESS_NODE *Parent;
struct _MMADDRESS_NODE *LeftChild;
struct _MMADDRESS_NODE *RightChild;
} MMADDRESS_NODE, *PMMADDRESS_NODE;
AVL樹的根節點保存在進程內核對象_EProcess中。_EProcess的結構沒有出現在文檔中,但是我們可以通過windbg獲取。在Windows 2003中,用windbg獲取如下輸出:
kd> dt _EProcess
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x078 ProcessLock : _EX_PUSH_LOCK
+0x080 CreateTime : _LARGE_INTEGER
+0x088 ExitTime : _LARGE_INTEGER
……
+0x24c PriorityClass : UChar
+0x250 VadRoot : _MM_AVL_TABLE
+0x270 Cookie : Uint4B
上圖中偏移量爲0x250處的VadRoot字段保存了AVL輸根節點所在的地址。因此,在驅動程序中,通過以下代碼可以獲取當前進程的AVL樹的根節點地址。
PMMADDRESS_NODE ZsaGetVmRoot(){
char * pEProcess = (char*)PsGetCurrentProcess();
char * avlRoot = pEProcess + 0x250;
char * p_MM_AVL_TABLE = avlRoot;
return (PMMADDRESS_NODE) p_MM_AVL_TABLE;
}
既然獲得了根地址,則可以對二叉樹進行遍歷,打印出整個數據結構。以下是某個測試進程在進行了1024*1024次new分配後,AVL樹的內容。可以看到,樹基本是平衡的。
0,0
├─────N
└─────280,2b3
├─────150,24f
│ ├─────130,134
│ │ ├─────20,20
│ │ │ ├─────10,10
│ │ │ └─────30,12f
│ │ └─────140,140
│ └─────260,275
│ ├─────250,25f
│ └─────N
└─────10200,10372
├─────400,502
│ ├─────310,315
│ │ ├─────2c0,300
│ │ └─────370,37f
│ │ ├─────320,360
│ │ └─────380,382
│ └─────c10,140f
│ ├─────610,80f
│ │ ├─────510,60f
│ │ └─────810,c0f
│ └─────2410,440f
│ ├─────1410,240f
│ └─────4410,840f
└─────7c930,7c9ff
├─────10540,1853f
│ ├─────10480,10536
│ └─────7c800,7c92a
│ ├─────18540,2853f
│ └─────N
└─────7ffdd,7ffdd
├─────7ffa0,7ffd2
│ ├─────7f6f0,7f7ef
│ └─────N
└─────7ffde,7ffde