探悉Windows 2000/XP Pool分配流程(http://webcrazy.yeah.net)

來源:互聯網
上載者:User
 探悉Windows 2000/XP Pool分配流程
            WebCrazy(http://webcrazy.yeah.net)

    對於Driver編寫者,最之煩瑣的莫過於各種記憶體緩衝區的使用(談到緩衝區,你可能還會想到諸如MDL等概念,其實MDL只是對StartVa指定的Pool的Page Frame Number進行組織而已)。在使用者態對於小塊零星的記憶體使用量牽涉到Heap,Windows 2000/XP在核心態提供了同樣的一個機制用於這部分核心態模組的記憶體需求,區別於使用者態,我們將這稱之為Pool(記憶體池)。撇去核心模組與使用者模組一些性質,如Pool不能直接由使用者模組訪問,Heap是進程相關,而Pool是系統相關等的區別,Pool與Heap在組織管理上有些異曲同工之處,本文著重對Pool的組織進行一些初淺的分析。

    我們知道,Windows總的來說Pool分為兩類:非分頁池(NonPagedPool)與可分頁池(PagedPool)。根據是否Aligned,是否MustSuccess等,又區分出部分類型。由ntddk.h中的POOL_TYPE定義,POOL_TYPE是一個enum定義,對於屬於NonPagedPool的其總是一個偶數(如NonPagedPool為0),而PagedPool則是一個奇數(如PagedPool是1)。通常我們分配一塊記憶體地區由核心常式ExAllocatePool(WithTag)來完成,她們接受一個POOL_TYPE與一個分配大小的的參數,當我們從Pool中分配大於PAGE_SIZE(更準確的應該說大於PAGE_SIZE-0x10,在x86中PAGE_SIZE為4K)的空間時,分配的結果總是頁對齊的(位於頁邊界),而如果小於PAGE_SIZE時(更準確的應該說小於PAGE_SIZE-0x10),分配總是在某一頁中並且總是8位元組對齊的,這一點對於我們以下的敘述是非常關鍵的。

    Windows針對非分頁池與分頁池各保留了不同的虛擬記憶體地區用於Pool的分配。核心變數MmPagedPoolStart與MmPagedPoolEnd指定了分頁池的範圍,通常這是一個從0xE1000000開始的。而非分頁池的空間由兩塊地區組成,常規的地區從MmNonPagedPoolStart開始,另外含一塊Expansion地區,由MmNonPagedPoolExpansionStart與MmNonPagedPoolEnd指定,對於Driver開發人員,我們一般很容易的根據這些區段判斷非分頁池與分頁池的。

    對於分頁池與非分頁池,Windows分別由一個POOL_DESCRIPTOR定義來組織(實際上Windows通常還會有另一些POOL_DESCRIPTOR,比如用於會話空間Session的等等,本文不加以討論),其定義如下:

   +0x000 PoolType         : _POOL_TYPE
   +0x004 PoolIndex        : Uint4B
   +0x008 RunningAllocs    : Uint4B
   +0x00c RunningDeAllocs  : Uint4B
   +0x010 TotalPages       : Uint4B
   +0x014 TotalBigPages    : Uint4B
   +0x018 Threshold        : Uint4B
   +0x01c LockAddress      : Ptr32 Void
   +0x020 PendingFrees     : Ptr32 Void
   +0x024 PendingFreeDepth : Int4B
   +0x028 ListHeads        : [512] _LIST_ENTRY

    而對於非分頁池與分頁池的兩個POOL_DESCRIPTOR,由系統變數PoolVector數組組織,我們可以使用PoolVector[POOL_TYPE&1]得到相應的POOL_DESCRIPTOR。請注意POOL_DESCRIPTOR的末尾有一個512長度的雙向鏈表。對於小於1頁的Pool,因為我們分配的Pool都是8位元組對齊的,而分配的結果總是在1頁中,所以這裡使用了一個長為512的雙向鏈表的數組,每個鏈表分別將系統中已經分配的頁的零星大小分別為8至4096的空閑地區組織成雙向鏈表。對於1頁中連續的空閑地區,系統總是儘可能的置入最大的鏈表中去,譬如對於有16位元組的地區總是置入ListHeads[1]中,而不會是插入兩個節點到ListHead[0]中。有了這樣一個鏈表,系統就很容易的對Pool分配進行管理了。而對於大於PAGE_SIZE的情況,系統將可能牽涉到重新分配System PTE,這將在我討論完小於4K位元組空間分配後,再加以敘述。

    在討論這些小塊Pool之前,我們必須先瞭解一下Lookaside,Lookaside與通常的Pool的區別是,她僅僅用於分配固定大小的的記憶體池。ExInitializeNPagedLookasideList與ExInitializePagedLookasideList用於初始化Lookaside,相關的從LookasideList中分配記憶體池的常式請參考DDK文檔。由於不考慮Spinlock同步且分配固定大小的空間使Lookaside相對比通常的Pool分配來得快得多。我們可以這樣能理解,通常我們使用ExInitializeNPagedLookasideList初始化Lookaside時,可以提供ALLOCATE_FUNCTION與FREE_FUNCTION參數用於分配大塊記憶體地區(系統預設使用ExAllocatePool與ExFreePool),然後從中進行零星分配。鑒於速度上的考慮,Windows執行體從8至256位元組每隔8位元組為NonPagedPool與PagedPool各建立一個Lookaside,位於KPRCB中(我通過分析ExAllocatePoolWithTag,在Windows XP Build 2600中他們分別位於位移0x598與0x698中),對於AllocatePool小於(0x20個8位元組,即256位元組),執行體函數ExAllocatePool直接從這些Lookaside中分配。至於Lookaside的Depth,記憶體管理器會定時使用KiAdjustLookasideDepth進行調整。

    現在我們考慮對於0x100(256位元組)與0x1000(4K,PAGE_SIZE)之間的Pool分配,假設我們分配0xB18個位元組,實際上系統將分配0xB20個位元組,多出的8個位元組是POOL_HEADER結構,用於對Pool的管理,POOL_HEADER的結構如下:

   +0x000 PreviousSize     : Pos 0, 9 Bits
   +0x000 PoolIndex        : Pos 9, 7 Bits
   +0x002 BlockSize        : Pos 0, 9 Bits
   +0x002 PoolType         : Pos 9, 7 Bits
   +0x000 Ulong1           : Uint4B
   +0x004 ProcessBilled    : Ptr32 _EPROCESS
   +0x004 PoolTag          : Uint4B
   +0x004 AllocatorBackTraceIndex : Uint2B
   +0x006 PoolTagHash      : Uint2B

    對於這一範圍的Pool,系統將根據POOL_TYPE,由PoolVector找到相應的POOL_DESCRIPTOR,我們很容易的算出系統將尋找PoolDescriptor.ListHeads[0x164]的雙向鏈表,因為0xB20/8為0x164。如果這一鏈表不為空白,系統將得到這一鏈表的一個節點,代表一個大小為0xB20的位元組空閑Pool,如果這是一個空鏈表,系統將定位下一個即0x165之後的雙向鏈表,直到找到一個大小在0xB28與0x1000之間的空閑塊。這時分配以後餘下的空間,系統將插入相應大小的LIST_ENTRY中(這裡要考慮片段合并),等待下一次的分配。當然如果這之間仍找不到符合相應條件的空間,系統將使用MiAllocatePoolPages分配整頁記憶體池使用。

    如果執行到MiAllocatePoolPages的話,其首先判斷分配的是分頁池或是非分頁池,如果是非分頁池,系統將首先尋找MmNonPagedPoolFreeListHead,與PoolDescriptor的ListHeads一樣,MmNonPagedPoolFreeListHead同樣也是一個雙向鏈表數組,其元素個數為4,分別代表1至4頁的非分頁池的空閑列表(4頁以上的空閑空間也存於第四個數組中,這就需要額外的判斷,這兒不加描述)。這是由系統記憶體管理器維護的鏈表,MiAllocatePoolPages將首先從此處得到空閑列表,如果找到的話,然後修改PFN資料庫。如果沒有找到,則需要重新Reserve System PTE,這點我會在後面的討論中繼續談到。而對於分頁池則是另外一種情況,她牽涉到系統的另外一個重要的結構:MM_PAGED_POOL_INFO,定義如下:

   +0x000 PagedPoolAllocationMap : Ptr32 _RTL_BITMAP
   +0x004 EndOfPagedPoolBitmap : Ptr32 _RTL_BITMAP
   +0x008 PagedPoolLargeSessionAllocationMap : Ptr32 _RTL_BITMAP
   +0x00c FirstPteForPagedPool : Ptr32 _MMPTE
   +0x010 LastPteForPagedPool : Ptr32 _MMPTE
   +0x014 NextPdeForPagedPoolExpansion : Ptr32 _MMPTE
   +0x018 PagedPoolHint    : Uint4B
   +0x01c PagedPoolCommit  : Uint4B
   +0x020 AllocatedPagedPool : Uint4B

   FirstPteForPagedPool是PagedPool虛擬位址的第一個PTE位置,通常是MmPagedPoolStart(0xE1000000)的PTE位置,也即在(0xE1000000>>a)&0x3ffffc-0x40000000=0xc0384000(具體演算法請參閱《小議Windows NT/2000分頁機制》),我們很容易通過核心變數MmPagedPoolInfo指向的MM_PAGED_POOL_INFO來驗證這一點。同樣的對於LastPteForPagedPool也是很容易通過MmPagedPoolEnd得到值。MiAllocatePoolPages就是通過PagedPoolAllocationMap等幾個RTL_BITMAP來分配頁的。RTL_BITMAP我在《淺議Windows 2000/XP Pagefile組織管理》中詳細介紹過,我也說過使用RtlFindSetBitsAndClear與RtlSetBits等相關操作函數來尋找相應的空閑位元,同樣的MiAllocatePoolPages也使用這一方法。
    
    接下來我們談談大於4K的情況,對於分配大於PAGE_SIZE(0x1000位元組,實際上大於0xFF0位元組,POOL_BLOCK_HEADER佔用16個位元組)的POOL,將調用MiAllocatePoolPages,則與上面的敘述一致。對於分頁池,系統在初始化階段因為已經對PTE進行了初始化,指向pagefile.sys,雖然因為pagefile.sys的大小可以自動擴充,但這也只涉及到擴充後MMPTE_SOFTWARE的初始化(指向分頁檔的PTE,詳見《淺議Windows 2000/XP Pagefile組織管理》),餘下的即只是RTL_BITMAP的位操作了,存取這些頁面出現的FAULT操作,則是int e(x86中)的任務了。所以底下我只對非分頁池進行重點的說明。

  對於非分頁池,能直接從MmNonPagedPoolFreeListHead得到頁面的情況我們已經討論過了,但如果我們不能從上面提及的MmNonPagedPoolFreeListHead中得到頁面時,系統必須調用MiReserveSystemPtes來申請System PTE,繼而調用MiReserveAlignedSystemPtes,保留了System PTE(頁數從分配大小中得到)以後,我們還必須調用MiChargeCommitmentCantExpand之類的進行實體記憶體的分配,然後才涉及到對PFN資料庫的操作。

    MiReserveAlignedSystemPtes用於保留System PTE,這一步驟其實就是分配系統虛擬位址的過程。他通過MmFirstFreeSystemPte,尋找指定分頁池的空閑虛擬位址(比如說是0xfb2b6000),得到這一地址相對於MmSystemPteBase的PTE(在Windows XP中MmSystemPteBase的值就為0xC0000000,這樣得到的結果就是0xC03ECAD8),根據這個PTE地址儲存的空閑頁面數(不知道為什麼,Microsoft將這一資料存於此),從虛擬位址的尾部分配虛擬位址。注意這裡是尾部地址,這樣也就不用更新MmFirstFreeSystemPte了,PTE位置存放的空閑頁面數只需減少相應的頁數,而不用更換位置。

    得到虛擬位址後,我們接下來必須分配實際的物理地址來滿足這次調用。這通常是由MiChargeCommitmentCantExpand來完成的,必要時他會調用MiRemoveAnyPage,然後填充由剛保留下的System PTE,完成這樣的一次分配工作。

    實際上介紹到此,我已經基本上將ExAllocatePoolWithTag(ExallocatePool只是傳遞一個Tag為'None'的ExAllocatePoolWithTag的調用)解釋了大部分。至於Pool的釋放,即ExFreePool的流程,有了這些知識後,也就不難對其進行分析了。ExAllocatePoolWithTag這個執行體常式相對其他常式複雜的多,對於POOL_TYPE提供的很多如Align或是MustSuccess等因素都要於以考慮。另外由於調試上如Driver Verifier或是Poolmon之類的需要,他也要考慮Pool Track(ExpInsertPoolTracker),還有Special Pool等等。我只是將這些基本流程說個大概,但這也許就可能會出現很多不好理解的地方,牽涉到先前我寫過的很多關於PFN Database,PTE等等很多知識,更何況我對這部分的內容的掌握程度也不至於可以到向大家說清楚的地步,本文就算是拋磚引玉吧。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.