內存與進程管理器
==========================
But I fear tomorrow I'll be crying,
Yes I fear tomorrow I'll be crying.
King
Crimson'69 -Epitaph
關于Windows NT內存管理器的高層次信息已經夠多的了,所以這里不會再講什么FLAT模型、虛擬內存之類的東西。這里我們只講具體的底層的東西。我假定大家都了解>i386的體系結構。
目錄
==========
00.內核進程線程結構體
01.頁表
02.Hyper Space
03.System PTE'S
04.Frame data base (MmPfnDatabase)
05.Working Set
06.向pagefile換頁
07.page fault的處理
08.從內存管理器角度看進程的創建
09.上下文切換
0a.某些未公開的內存管理器函數
0b.結語
附錄
0c.某些未公開的系統調用
0d.附注及代碼分析草稿
00.內核進程線程結構體
===================================
Windows
NT中的每一個進程都是EPROCESS結構體。此結構體中除了進程的屬性之外還引用了其它一些與實現進程緊密相關的結構體。例如,每個進程都有一個或幾
個線程,線程在系統中就是ETHREAD結構體。我來簡要描述一下存在于這個結構體中的主要的信息,這些信息都是由對內核函數的研究而得知的。首先,結構
體中有KPROCESS結構體,這個結構體中又有指向這些進程的內核線程(KTHREAD)鏈表的指針(分配地址空間),基優先級,在內核模式或是用戶模
式執行進程的線程的時間,處理器affinity(掩碼,定義了哪個處理器能執行進程的線程),時間片值。在ETHREAD結構體中還存在著這樣的信息:
進程ID、父進程ID、進程映象名、section指針。quota定義了所能使用的分頁和非分頁池的極限值。VAD
(virtual address descriptors)樹定義了用戶地址空間內存區的狀況。關于Working
Set的信息定義了在給定時間內有那些物理頁是屬于進程的。同時還有limit與statistics。ACCESS
TOKEN描述了當前進程的安全屬性。句柄表描述了進程打開的對象的句柄。該表允許不在每一次訪問對象時檢查訪問權限。在EPROCESS結構體中還有指
向PEB的指針。
ETHREAD結構體還包含有創建時間和退出時間、進程ID和指向EPROCESS的指針,啟動地址,I/O請求鏈表和KTHREAD結構體。在
KTHREAD中包含有以下信息:內核模式和用戶模式線程的創建時間,指向內核堆棧基址和頂點的指針、指向服務表的指針、基優先級與當前優先級、指向
APC的指針和指向TEB的指針。KTHREAD中包含有許多其它的數據,通過觀察這些數據可以分析出KTHREAD的結構。
01.頁表
==================
通常操作系統使用頁表來進行內存操作。在Windows
NT中,每一個進程都有自己私有的頁表(進程的所有線程共享此頁表)。相應的,在進程切換時會發生頁表的切換。為了加快對頁表的訪問,硬件中有一個
translation lookaside buffer(TLB)。在Windows
NT中實現了兩級的轉換機制。在386+處理器上將虛擬地址轉換為物理地址過程(不考慮分段)如下:
Virtual Address
+-------------------+-------------------+-----------------------+
|3 3 2 2 2 2 2 2 2 2|2 2 1 1 1 1 1 1 1 1|1
1 |
|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2 1 0|
+-------------------+-------------------+-----------------------+
| Directory index | Page Table index
| Offset in page |
+-+-----------------+----+--------------+-----+-----------------+
| | |
| | |
| Page Directory (4Kb)| Page Table (4Kb) | Frame(4Kb)
| +-------------+
| +-------------+ | +-------------+
| |
0 |
| |
0 |
| |
|
| +-------------+
| +-------------+
| |
|
| |
1 |
| |
1 |
| |
|
| +-------------+
| +-------------+
| |
|
| |
|
+->| PTE +-+
| |
|
| +-------------+ +-------------+
| | | ----------- |
+->| PDE +-+ |
| | +->| byte |
+-------------+
| +-------------+
| | ----------- |
|
|
| |
|
| |
|
+-------------+
| +-------------+
| |
|
|
|
| |
|
| |
|
... | ...
| |
|
| 1023 |
| | 1023
|
| |
|
CR3->+-------------+ +----->+-------------+ +--->+-------------+
Windows NT
4.0使用平面尋址。NT的地址空間為4G。這4G地址空間中,低2G(地址0-0x7fffffff)屬于當前用戶進程,而高2G
(0x80000000-0xffffffff)屬于內核。在上下文切換時,要更新CR3寄存器的值,結果就更換了用戶地址空間,這樣就達到了進程間相互
隔絕的效果。
注:在Windows NT中,從第4版起,除4Kb的頁之外同時還使用了4Mb的頁(Pentium及更高)來映射內核代碼。但是在Windows NT中沒有實際對可變長的頁提供支持。
PTE和PDE的格式實際上是一樣的。
PTE
+---------------+---------------+---------------+---------------+
|3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1
1 |
|
|1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
+---------------------------------------+-----------------------+
|
|T P C U R D A P P U R P|
| Base address 20
bits
|R P W C W S
W |
|
|N T D
T |
+---------------------------------------+-----------------------+
一些重要的位在i386+下的定義如下:
---------------------------------------------------------------------------
P - 存在位。此位如果未設置,則在地址轉換時會產生異常。一般說來,在一些情況下NT內核會使用未設置此位的PTE。
例如,如果向pagefile換出頁,保留這些位可以說明其在頁面文件中的位置和pagefile號。
U/S - 是否能從user模式訪問頁。正是借助于此位提供了對內核空間的保護(通常為高2G)。
RW - 是否能寫入
NT使用的為OS設計者分配的空閑位
---------------------------------------------------------------------------
PPT - proto pte
TRN - transition pte
當P位未設置時,第5到第9位即派上用場(用于page fault處理)。它們叫做Protection Mask,樣子如下:
--------------------------------------------------------------------------------------
* MiCreatePagingFileMap
9 8 7 6 5
---------
| | | | |
| | | | +- Write Copy
| | | +--- Execute
| | +----- Write
| +------- NO CACHE
+--------- Guard
GUARD | NOCACHE組合就是NO ACCESS
* MmGetPhysicalAddress
函數很短,但能從中獲得很多信息。在虛地址0xc0000000 - 0xc03fffff上映射有進程的頁表。并且,映射的機制非常精巧。在
Directory
Table(以下稱DT)有1100000000b個表項(對應于地址0xc000..-0xc03ff..)指向自己,也就是說對于這些地址DT用作了
頁表(Page Table)!如果我們使用,比如說,地址(為方便起見使用二進制)
1100000000.0000000101.0000001001.00b
---------- ---------- --------------
0xc0... 頁表選擇 頁表內偏移
頁目錄
通過頁表101b的1001b號,我們得到了PTE。但這還沒完——DT本身映射在地址0xc0300000-0xc0300ffc上。在MmSystemPteBase中有值0xc0300000。為什么這樣——看個例子就知道了:
1100000000.1100000000.0000001001.00b
---------- ---------- --------------
0xc0... 0xc0... 頁目錄偏移
頁目錄 頁表-
頁目錄
選擇
最后,在c0300c00包含著用于目錄本身的PDE。這個PDE的基地址的值保存在MmSystemPageDirectory中。同時系統為映射物理頁MmSystemPageDirectory保留了一個PTE,這就是MmSystemPagePtes。
這樣做能簡化尋址操作。例如,如果有PTE的地址,則PTE描述的頁的地址就等于PTE<<10。反過來:PTE=(Addr>>10)+0xc0000000。
除此之外,在內核中存在著全局變量MmKseg2Frame =
0x20000。該變量指示在從0x80000000開始的哪個地址區域直接映射到了物理內存,也就是說,此時虛擬地址0x80000000 -
0x9fffffff映射到了物理地址00000000-1f000000。
還有幾個有意思的地方。從c0000000開始有個0x1000*0x200=0x200000=2M的描述地址的表(0-7fffffff)。描述這些
頁的PDE位于地址c0300000-0xc03007fc。對于i486,在地址c0200000-c027fffc應該是描述80000000到
a0000000的512MB的表,但對于Pentium在區域0xc0300800-0xc03009fc是4MB的PDE,其描述了從0
到1fc00000的步長為00400000的4M的物理頁,也就是說選擇了4M的頁。對應于這些PDE的虛地址為80000000,
9fffffff。
這樣我們就得到了頁表的分布:
范圍 c0000000 - c01ffffc 用于00000000-7fffffff的頁表
范圍 c0200000 - c027ffff "吃掉" 4M地址頁的地址
范圍 c0280000 - c02ffffc 包含用于a0000000 - bfffffff的頁
范圍 c0300000 - c0300ffc PD 本身 (描述范圍c0000000 - c03fffff)
范圍 c0301000 - c03013fc c0400000 - c04fffff HyperSpace (更準確的說, 是1/4的hyper space)
范圍 c0301400 - c03fffff 包含用于c050000 - ffffffff的頁
注:在0xc0301000-0xc0301ffc包含有描述hyper
space的頁表。這是內核的地址空間,且對于不同的進程映射的內容是不同的(另一方面,內核空間又總是在每個用戶進程的上下文中)。這是進程私有的區
域。例如,working set就位于hyper space中。頁表的前256個PTE(hyper
space的前1/4)為內核保留,而且在需要快速向frame中映射虛擬地址時使用。
我給出一個向區域0xc0200000-0xc027f000中一個地址進行映射的例子。
1100000000.1000000000.000000000000 = 0xc0200000
1) 解析出 PDE #1100000000 (4k 頁) 并選出 PageDirectory
2) 在 Directory 中選出 PTE #1000000000 (c0300800)
這是個 4MB 的 PDE - 但這里忽略位長度,
因為 PDE 用作了 PTE. 結果 c0200000 - c0200fff 被映射為
80000000-80000fff
c0201000 映射到下面的 - 80400000- 80400fff.
等等直到 c027f000 - 9fc00000
PTE, 位于c0200000到c027fffc - 描述了80000000 - 9ffffc00 (512m)
02.Hyper Space
==============
HyperSpace是內核空間中的一塊區域 (4mb),
不同的進程映射內容不同。對于轉換,4MB足夠放下頁表完整的一頁。這個表位于地址0xc0301000 -
0xc0301ffc(PDE的第0個表項位于0xc0300c04)。在內部,為向HyperSpace區域中映射物理頁(當需要快速為某個frame
組織虛擬地址時)要使用函數:
DWORD MiMapPageInHyperSpace(DWORD BaseAddr,OUT PDWORD Irql);
它返回HyperSpace中的虛擬地址,這個虛擬地址被映射到所要的物理頁上。這個函數是如何工作的,工作的時候用到了什么?
在內核中有這樣的變量:
MmFirstReservedMappingPte=0xc0301000
MmLastReservedMappingPte=0xc03013fc
這兩個變量描述了255個pte,這些pte描述了區域:
0xc0400000-0xc04fffff (1/4 HyperSpace)
在MmFirstReservedMappingPte處是一個pte,其中的基址扮演了計數器的角色(從0到255)(當然,pte是無效的,p位無
效)。為所需地址添加pte時要依賴計數器當前的值……并且計數器使用了下開口堆棧的原理,從ff開始。一般來說,頁表中的pte用作信息上的目的并不是
唯一的情況。
03.System PTE'S
===============
在內核中有一塊這樣的內存——系統pte。什么是系統pte,以及內核如何使用系統pte?
*見函數 MiReserveSystemPtes(...)
系統為空閑PTE維護了某些結構體。首先為了快速滿足密集請求(當內核需要pte映射某些物理頁時)系統中有個Sytem Ptes
Pool。而且pool中有pte
blocks(blocks表示請求是以block為單位來滿足的,一個block中有一些pte,1、2、4、8和16個pte)。
系統中有以下這些表:
BYTE MmSysPteTables[16]={0,0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4};
DWORD MmSysPteIndex[5]={1,2,4,8,16};
DWORD MmFreeSysPteListBySize[5];
PPTE MmLastSysPteListBySize[5];
DWORD MmSysPteListBySizeCount[5];
DWORD MmSysPteMinimumFree[5]={100,50,30,20,20}
PVOID MmSystemPteBase;// 0xc0200000
在pool中的空閑PTE被組織成了鏈表(當然,pte是位于頁表中,也就是說鏈表結構體位于頁表中,這是真的)。鏈表的元素:
typedef struct _FREE_SYSTEM_PTES_BLOCK{
/*pte0*/ SYSPTE_REF
NextRef;
// 指向后面的block
/*pte1*/ DWORD
FlushUnkn; //
在Flush時使用
/*pte2*/ DWORD ArrayOfNulls[ANY_SIZE_ARRAY]; // 空閑 PTE
}FREE_SYSTEM_PTES_BLOCK PFREE_SYSTEM_PTES_BLOCK;
用作指向后面元素指針的PTE的地址可如此獲得:VA=(NextRef>>10)+MmSystemPteBase
(低10位永遠為0,相應的p位也為0)。鏈表最后一個元素NextRef域的值為0xfffff000 (-1)
。相應的,鏈表有5個(block大小分別為1,2,4,8和16個pte)。
*見函數 MiReserveSystemPtes2(...) / MiInitializeSystemPtes
除pool外還有一個undocumented的空閑系統pte鏈表。
PPTE MmSystemPtesStart[2];
PPTE MmSystemPtesEnd[2];
SYSPTE_REF MmFirstFreeSystemPte[2];
DWORD MmTotalFreeSystemPtes[2];
在兩個鏈表中有兩個引用。鏈表的元素:
typedef struct _FREE_SYSTEM_PTES{
SYSPTE_REF Next; // #define ONLY_ONE_PTE_FLAG 2, last = 0xfffff000
DWORD NumOfFreePtes;
}FREE_SYSTEM_PTES PFREE_SYSTEM_PTES;
而且,1號鏈表原則上沒有組織。0號鏈表(MiReleaseSystemPtes)用于釋放的pte。pte有可能進入System Ptes
Pool。若在請求MiReserveSystemPtes(...)時pte的數目大于16,則同時pte從0號鏈表分配。也就是說,0號鏈表與
pool有關聯,而1號則沒有。
為了使工作的結果不與TLB相矛盾,系統要么使用重載cr3,要么使用命令invlpg。“高級”函數
MiFlushPteList(PTE_LIST* PteList, BOOLEAN bFlushCounter, DWORD PteValue);
進行以下工作:
初始化PTE并調用invlpg(匯編指令)。
typedef struct PTE_LIST{
DWORD Counter; // max equ 15
PVOID PtePointersInTable[15];
PVOID PteMappingAddresses[15];
};
如果Counter大于15,則調用KeFlushCurrentTb(只是重載CR3),并且如果設置了bFlushCounter,則向MmFlushCounter加0x1000。
04.Page Frame Number Data Base (MmPfnDatabase)
======================================
內核將有關物理頁的信息保存在pfn數據庫中(MmPfnDatabase)。本質上講,這只是個0x18字節長的結構體塊。每一個結構體對應一個物理頁
(順序排列,所以元素常被稱為Pfn - page frame
number)。結構體的數量對應于系統中4KB頁的數量(或者說是內核可見的頁的數量,需要的話可以在boot.ini中使用相應的選項來為NT內核做
出這塊“壞”頁區)。通常,結構體形式如下:
typedef struct _PfnDatabaseEntry
{
union {
DWORD NextRef; // 0x0 如果frame在鏈表中,則這個就是frame的號
// 最后的一個為 -1
DWORD Misc; // 同時另外一項信息, 依賴于上下文
// 見偽代碼 (通常 TmpPfn->0...)
// 通常這里有 *KTHREAD, *KPROCESS,
// *PAGESUPPORT_BLOCK...
};
PPTE PtePpte; // 0x4 指向 pte 或 ppte
union { // 0x8
DWORD
PrevRef; // 前面的frame或 (-1, 第一個)
DWORD ShareCounter; // Share 計數器
};
WORD Flags; // 0xc 見下面
WORD RefCounter; // 0xe 引用計數
DWORD Trans; // 0x10 ?? 見下面. 用于 pagefile
DWORD ContFrame;//ContainingFrame; // 14
}PfnDatabaseEntry;
/*
Flags (名字取自windbg !pfn的結果)
掩碼 位 名字 值
----- ---- --- --------
0001 0 M Modifyied
0002 1 R Read In Progress
0004 2 W WriteInProgress
0008 3 P Shared
0070 [4:6] Color Color (In fact Always null for x86)
0080 7 X Parity Error
0700 [8:10] State 0- Zeroed
/List 1- Free
2-
StandBy
3-
Modified
4-
ModifiedNoWrite
5-
BadPage
6-
Active
7- Trans
0800 11 E InPageError
Trans域的值用在frame的內容位于PageFile中的時候或是frame的內容位于與這個Page File PTE對應的其它映象文件中的時候。
我給出未設置P位的PTE的例子(這種PTE不由平臺體系結構確定,而由OS確定)。
* 取自 @MiReleasePageFileSpace (Trans)
Page File PTE
+---------------------------------------+-+-+---------+-------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0|
+---------------------------------------+-+-+---------+-------+-+
|
offset |T|P|Protect.
|page |0|
|
|R|P|mask |file | |
|
|N|T|
|Num | |
+---------------------------------------+-+-+---------+-------+-+
Transition PTE
+---------------------------------------+-+-+---------+-------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0|
+---------------------------------------+-+-+---------+-------+-+
|
PFN
|T|P|Protect. |C W O W|0|
|
|R|P|mask |D T | |
|
|N|T|
| | |
+---------------------------------------+-+-+---------+-------+-+
W - write
O - owner
WT - write throuth
CD - cache disable
可能所有這些現在還不很易懂,但是看完下面就能明白了。當然,這個結構體是未公開的。顯然,結構體能夠組織成鏈表。frame由以下結構體支持:
struct _MmPageLocationList{
PPfnListHeader ZeroedPageListhead; //&MmZeroedPageListhead
PPfnListHeader FreePageListHead; //&MmFreePageListHead
PPfnListHeader StandbyPageListHead; //&MmStandbyPageListHead
PPfnListHeader ModifiedPageListHead; //&MmModifiedPageListHead
PPfnListHeader ModifiedNoWritePageListHead;//&MmModifiedNoWritePageListHead
PPfnListHeader BadPageListHead; //&MmBadPageListHead
}MmPageLocationList;
這其中包含了6個鏈表。各域的名字很好的說明了它們的用處。frame的狀態與這些鏈表密切關聯。下面列舉了frame的狀態:
+---------------+----------------------------------------------------+------+
|狀態
|描述
|
鏈表 |
+---------------+----------------------------------------------------+------+
|Zero
|清零的可用空閑頁
| 0
|
|Free
|可用空閑頁
| 1
|
|Standby |不可用但可輕易恢復的頁
| 2
|
|Modified
|要換出的dirty頁
| 3 |
|ModifiedNoWrite|不換出的dirty頁
| 4 |
|Bad
|不可用的頁(有錯誤)
| 5
|
|Active
|活動頁,至少映射一個虛擬地址
| -
|
+---------------+----------------------------------------------------+------+
frame可能處在6個鏈表中的某一個,也可能不在這些鏈表中(狀態為Active)。如果頁屬于某個進程,則這個頁就被記錄在Working Set中(見后面)。同時,如果frame由內存管理器自己使用,則一般可以不考慮這些frame的位置。
每個鏈表的表頭都是下面這個樣子:
typedef struct _PfnListHeader{
DWORD Counter; // 鏈表中frame的數目
DWORD LogNum; // 鏈表號.0 - zeroed, 1- Free etc...
DWORD FirstFn; // MmPfnDatabase中的第一個frame號
DWORD LastFn; // --//--- 最后一個.
}PfnListHeader PPfnListHeader;
除此之外,可以用“color”(就是cache)來尋址空閑frame(zeroed或是free)。如果看一下附錄中的偽代碼就容易理解了。我給出兩個結構體:
struct {
ColorHashItem* Zeroed; //(-1) нет
ColorHashItem* Free;
}MmFreePagesByColor;
typedef struct _ColorHashItem{
DWORD FrameNum;
PfnDatabaseEntry* Pfn;
} ColorHashItem;
有一套函數使用color來處理frame(處理cache)。例如,MiRemovePageByColor(FrameNum, Color);
看一下這些函數及其參數返回值的名稱和函數的反匯編代碼,很容易猜到相應的內容,所以這里就不描述了,在說一句,這些函數都是未導出的。在使用color
的時候,要考慮color掩碼,最后選擇color。
Windows
NT符合C2安全等級,所以應該在為進程分配頁的時候應將頁清零。我們來看一下將frame清零的系統進程的線程。最后,在
Phase1Initialization()中所作的是調用MmZeroPageThread。不難猜到——線程將空閑頁清零并將其移動到zeroed
頁的鏈表中。
MmZeroPageThread
{
//
//.... 沒意思的東西我們略過 ;)
//
while(1)
{
KeWaitForSingleObject(MmZeroingPageEvent,8,0,0,0); // 等待事件
while(!KeTryToAcquireSpinLock(MmPfnLock,&OldIrql)); // 獲取 PfnDatabase
while(MmFreePageListHead.Count){
MiRemoveAnyPage(MmFreePageListHead.FirstFn&MmSecondaryColorMask);
// 從空閑鏈表中取出頁
Va=MiMapPageToZeroInHyperSpace(MmFreePageListHead.FirstFn);
KeLowerIrql(OldIrql);
memset(Va,0,0x1000); // clear page
while(!KeTryToAcquireSpinLock(MmPfnLock,&OldIrql);
MiInsertPageInList(&MmZeroedpageListHead,FrameNum);
// 將已清零的頁插入Zero鏈表
}
MmZeroingPageThreadActive=0; // 清標志
KeLowerIrql(OldIrql);
}
// 永不退出
}
// 函數只是將frame映射到定義的地址上
// 以使其可被清零
DWORD MiMapPageToZeroInHyperSpace(FrameNum)
{
if(FrameNum<MmKseg2Frame)return ((FrameNum+0x80000)<<12); // 落入直接映射區域
TmpPte=0xc0301404;
TmpVa=0xc0501000;
*TmpPte=0;
invlpg((void*)TmpVa); // asm instruction in fact
*TmpPte=FrameNum<<12|ValidPtePte;
return TmpVa; // always 0xc0501000;
}
在何時MmZeroingPageEvent被激活?這發生在向空閑頁鏈表中添加frame的時候:
MiInsertPageInList()
{
.....
if(MmFreePageListHead.Count>=MmMinimumFreePagesToZero&&
!MmZeroingPageThreadActive)
{
MmZeroingPageThreadActive=1;
KeSetEvent(&MmZeroingPageEvent,0,0);
}
....
}
注:內核并不總是依賴這個線程,有時會遇到這樣的代碼,它獲取一個空閑頁,用過后自己將其清零。
05.Working Set
==============
Working Set——工作集,是屬于當前進程的物理頁集。內存管理器使用一定的機制跟蹤進程的工作集。working
set有兩個限額:maximum working set和minimum working
set。這是工作集的最大值和最小值。內存管理器以這兩個值為依據來維護進程的工作集(工作集大小不小于最小值,不大于最大值)。在定義條件的時候,工作
集被裁減,這時工作集的frame落入空閑鏈表。內核工作集是結構體的總和。
在進程結構體的偏移0xc8(NT4.0)有以下結構體。
typedef struct _VM{
/* C8*/ LARGE_INTEGER
UpdateTime; //0
/* D0*/ DWORD
Pages;
//8 called so, by S-Ice authors
/* D4*/ DWORD
PageFaultCount
//0c faults;
// in
fact number of MiLocateAndReserveWsle calls
/* D8*/ DWORD
PeakWorkingSetSize; //10
all
/* DC*/ DWORD
WorkingSetSize; //14 in
/* E0*/
DWORD MinimumWorkingSet; //18
pages, not in
/* E4*/
DWORD MaximumWorkingSet; //1c
bytes
/* E8*/ PWS_LIST
WorkingSetList;
//20 data table
/* EC*/ LIST_ENTRY WorkingSetExpansion; //24 expansion
/* F4*/ BYTE fl0; //
Operation??? //2c
BYTE fl1; // always
2???
//2d
BYTE fl2; // reserved??? always 0 //2e
BYTE fl3;
//
//2f
}VM *PVM;
WinDbg !procfields的擴展命令用到VM。這里重要的是,跟蹤page fault的數量(PageFaultCount),MaximumWorkingSet和MinimumWorkingSet,管理器以它們為基礎來支持工作集。
注:實際上,PageFaultCount并非是嚴格的計數。這個計數在MiLocateAndReserveWsle函數中被擴大,因為這個函數不只在page fault時被調用,在某些其它情況下也會被調用(真的,很少見)。
下面這個結構體描述了包含工作集頁的表。
typedef struct _WS_LIST{
DWORD Quota; //0
??? i'm not shure....
DWORD FirstFreeWsle; //
4 start of indexed list of free items
DWORD FirstDynamic;
// 8 Num of working set wsle entries in the start
// FirstDynamic
DWORD LastWsleIndex; //
c above - only empty items
DWORD NextSlot;
// 10 in fact always == FirstDynamic
// NextSlot
PWSLE Wsle;
// 14 pointer to table with Wsle
DWORD Reserved1
// 18 ???
DWORD NumOfWsleItems; // 1c Num of items in Wsle table
// (last initialized)
DWORD NumOfWsleInserted; // 20 of Wsle items inserted (WsleInsert/
// WsleRemove)
PWSHASH_ITEM
HashPtr; //
24 pinter to hash, now we can get index of
// Wsle item by address. Present only if
// NumOfWsleItems>0x180
DWORD HashSize;
// 28 hash size
DWORD Reserved2; //
2c ???
}WS_LIST *PWS_LIST;
typedef struct _WSLE{ // 工作集表的元素
DWORD PageAddress;
}WSLE *PWSLE;
// PageAddress 本身是工作集頁的虛地址
// 低12位用作頁屬性(虛地址總是4K的倍數)
#define WSLE_DONOTPUTINHASH 0x400 // 不放在cache中
#define WSLE_PRESENT 0x1 // 非空元素
#define WSLE_INTERNALUSE 0x2 // 被內存管理器使用的frame
// 未設置WSLE_PRESENT的空閑WSLE本身是下一個空閑WSLE的索引。這樣,空閑的WSLE就組織成了鏈表。最后一個空閑WSLE表示為-1。
#define EMPTY_WSLE (next_emty_wsle_index) (next_emty_wsle_index<<4)
#define LAST_EMPTY_WSLE 0xfffffff0
typedef struct _WSHASH_ITEM{
DWORD PageAddress; //Value
DWORD WsleIndex; //index in Wsle table
}WSHASH_ITEM *PWSHASH_ITEM;
//cache函數很簡單。內部函數的偽代碼:
//MiLookupWsleHashIndex(Value,WorkingSetList)
//{
//Val=value&0xfffff000;
//TmpPtr=WorkingSetList->HashPtr;
//Mod=(Val>>0xa)%(WorkingSetList->HashSize-1);
//if(*(TmpPtr+Mod*8)==Val)return Mod;
//while(*(TmpPtr+Mod*8)!=Val)){
// Mod++;
// if(WorkingSetList->HashSize>Mod)continue;
// Mod=0;
// if(fl)KeBugCheckEx(0x1a,0x41884,Val,Value,WorkingSetList);
// fl=1;
// }
//return Mod;
//}
我們來看一下典型的進程working set。WorkingSetList位于地址MmWorkingSetList
(0xc0502000)。這是hyper
space的區域,所以在進程切換時,要更新這些虛地址,這樣,每個進程都有自己的工作集結構體。在地址MmWsle
(0xc0502690)上是Wsle動態表的起始地址。表的結尾的地址總是0x1000的倍數,也就是說表可以結束在地址0xc0503000、
0xc0504000等等上(這是為了簡化對Wsle表大小的操作)。Cache(如果有)位于一個偏移上,Wsle不會向這個偏移增長。我們來詳細看一
下這個表:
// WsList-0xc0502000---
// ....
// -------0xc0502030----
// pde 00 fault counter
// pde 01 fault counter
// pde 02 fault counter
//
//
+-Wsle==0xc0502690---
+--Pde/pte +-----Pfn[0]------
// |0 c0300000|403 Page Directory |c0300c00 pde |pProcess
// |4 c0301000|403 Hyper Space |c0300c04 pte |1
// |8 MmWorkingSetList(c0502000)|403 |c0301408 pte |2
// |c MmWorkingSetList+0x1000 | 403 |. |3
// |10 MmWorkingSetList+0x2000 | 403 |. .
// | ....
// |FirstDynamic*4 FrameN
//
|....
|. .
//
.
// |LastWsleIndex*4 FrameM
//
+--------
+------ +-------
// | free items
// ....
// | 0xfffffff0
// +-------------------
// Cache
// ....
這里有個有意思的地方,在表的起始部分有FirstDynamic的頁,用于建立Wsle,WorkingSetList和cache。同時這里還有頁目
錄frame,HyperSpace和某些其它的頁,這些頁是內存管理器所需要的,不能從工作集中移出(標志WSLE_INTERNALUSE)。之后,
我們還能看到兩種對Pfn frame域偏移0使用的變體。對于頁目錄frame,這是指向進程的指針,對于通常的屬于工作集的頁,這是在表內的索引。
在WorkingSetList和Wsle表的起始地址之間還有不大的0x660字節的空閑空間。關于如何分配這些空間的信息是沒有的,但是很快在
WorkingSetList開始有用于用戶空間(通常為低2GB)的page fault
counter,也就是說如果,譬如說,索引0x100的元素有值3,則表示從3開始(如果不考慮可能的溢出)page
fault用于范圍[0x40000000-0x403fffff]的頁。
工作集的限額在內核模式下可以通過導出的未公開函數來修改:
NTOSKRNL MmAdjustWorkingSetSize(
DWORD MinimumWorkingSet OPTIONAL, // if both == -1
DWORD MaximumWorkingSet OPTIONAL, // empty working set
PVM Vm OPTIONAL);
為處理WorkingSet,管理器使用了許多內部函數,了解了這些函數就能明白其工作的原理。
06.向pagefile換頁
========================================
frame可以是空閑的——當RefCounter等于0且位于一個鏈表中時。frame可以屬于工作集。在缺少空閑frame時或是在達到treshhold時,就會發生frame的換出。這方面的高層次函數是有的。這里的任務是用偽代碼來證實。
在NT中有最多16個pagefile。pagefile的創建發生于模塊SMSS.EXE。這時打開文件及其句柄向
PsInitialSystemProcess進程的句柄表拷貝。我給出創建pagefile的未公開系統函數的原型(如果不從核心調用的話就必須有創建
這種文件的權限)。
NTSTATUS NTAPI NtCreatePagingFile(
PUNICODE_STRING FileName,
PLARGE_INTEGER MinLen, // 高位雙字應為0
PLARGE_INTEGER MaxLen, // minlen應大于1M
DWORD Reserved // 忽略
);
每個pagefile都有一個PAGING_FILE結構體。
typedef struct _PAGING_FILE{
DWORD MinPagesNumber; //0
DWORD MaxPagesNumber; //4
DWORD MaxPagesForFlushing; //8 (換出頁的最大值)
DWORD
FreePages;
//c(Free pages in PageFile)
DWORD UsedPages; //10 忙著的頁
DWORD MaxUsedPages; //14
DWORD CurFlushingPosition; //18 -???
DWORD Reserved1; //1c
PPAGEFILE_MDL Mdl1; // 20 0x61 - empty ???
PPAGEFILE_MDL Mdl2; // 24 0x61 - empty ???
PRTL_BITMAP PagefileMap; // 28 0 - 空閑, 1 - 包含換出頁
PFILE_OBJECT FileObject; //2c
DWORD NumberOfPageFile; //30
UNICODE_STRING FileName; //34
DWORD
Lock; //3d
}PAGING_FILE *PPAGING_FILE;
DWORD MmNumberOfActiveMdlEntries;
DWORD MmNumberOfPagingFiles;
#define MAX_NUM_OF_PAGE_FILES 16
PPAGING_FILE MmPagingFile[MAX_NUM_OF_PAGE_FILES];
在內存子系統啟動時(MmInitSystem(...))會啟動線程MiModifiedPageWriter,該線程進行以下工作:初始化
MiPaging和
MiMappedFileHeader,在非換出域中創建并初始化MmMappedFileMdl,建立優先級
LOW_REALTIME_PRIORITY+1,等待KEVENT,初始化MmMappedPageWriterEvent和
MmMappedPageWriterList鏈表,啟動MiMappedPageWriter線程,啟動函數
MiModifiedPageWriterWorker。
在任務MiModifiedPageWriterWorker中會等待事件MmModifiedPageWriterEvent,處理鏈表
MmModifiedNoWritePageList和MmModifiedPageList并準備實現向映象文件或pagefile的頁換出(調用
MiGatherMappedPages或是MiGatherPagefilePages)。
在MiGatherPagefilePages中使用IoAsynchronousPageWrite(
)函數進行frame的換出。而且不是一個frame,而是一簇(頁數目總和為MmModifiedWriteClasterSize)。向
pagefile換出頁是由PAGING_FILE結構體中的PagefileMap來跟蹤的。
研究函數的偽代碼在appendix.txt中。這里描述偽代碼沒有什么意義——都很簡單。
07.page fault的處理
==============================
對于轉向對pagefault的研究,我們現在有了所有必須的信息了。轉換線性地址時,當線性地址(分頁機制打開)的所用的PDE/PTE的P
(present)位無效或是違反了保護規則,在+i386處理器里會產生異常14。這時,在堆棧中有錯誤代號,包含有以下信息:用戶/內核錯誤位(異常
發生在ring3還是ring0?),讀寫錯誤位(試圖讀還是寫?),頁存在位。除此之外,在CR2寄存器中存有產生異常的32位線性地址。內核中處理
14號中斷的是_KiTrap0E。
當要轉換的頁沒有相應的物理頁時,內存管理器執行確定好的工作來“修正”。這些是由異常處理函數調用高層函數MmAccessFault
(Wr,Addr,P);來完成的。在對偽代碼的進行分析之前,想一下在什么樣的情況下會發生page fault是很有用的。
最顯然的就是訪問錯誤,這時ring3的代碼試圖寫入PTE/PDE中未設置U位的頁或是寫入了只讀的頁(PTE/PDE中未設置W位)。再有,頁可以被
換出到頁面文件中,對應于這些頁的PTE中未設置P位,但有信息指示在哪個頁面文件中尋找frame,以及frame的偏移。還有一個類似的情況——
frame屬于映象文件。除此之外,所轉換的頁可能只屬于已分配的內存區(使用NtAllocateMemory),也可能轉換的是原先沒轉換過的頁,這
中情況下,VMM分配清零過的frame(這是C2的要求)。最后,異常還可能是由寫copy on
write頁和轉換共享內存引發。以上只列出了主要的情況。
處理的結果通常是向當前進程的Working Set中添加相應的frame。
異常的每一種情況都相應有一個內部的結構體與之相關聯,VMM就處理這些結構體。這些結構體十分復雜,要對它們進行完整的描述的話,需要反匯編大量的函
數。目前還沒有大部分結構體的完整信息,但對于理解異常處理程序來說并不要求知道這些。我來大致描述一下VAD和PPTE的概念,研究異常處理程序的偽代
碼要用到。
VAD
操作虛擬地址需要用到VAD (Virtual Address
Descriptor)。我們熟知的(有一個幾乎與之同名的Win32函數調用這個函數)未公開函數NtAllocateVirtualMemory
(ring0下是ZwAllocateVirtualMemory)操作這些結構體。
每一個VAD都描述了虛地址空間中的區域,實際上,除了區域的起止地址外還有保護信息(見ZwAllocateVirualMemory函數的參數)。而
同時還有其它一些特殊的信息(目前除了首部之外還沒有VAD的完整信息)。VAD結構體只對用戶地址(低2GB)有意義,使用這些結構體VMM可以捕獲到
發生異常的區域。VAD的結構是一個平衡二叉樹(有內部函數負責修整此樹),這是為查找而進行的優化。在VAD中有兩個指向后面元素——左右子樹——的指
針。樹的根位于EPROCESS結構體的VadRoot域(NT
4.0下是偏移0x170)。當然,每一個進程都有自己的VAD樹。VAD的首部形式如下:
typedef struct vad_header {
void *StartingAddress;
void *EndingAddress;
struct vad *ParentLink;
struct vad *LeftLink;
struct vad *RightLink;
ULONG Flags;
}VAD_HEADER, *PVAD;
PPTE
Prototype
Pte是又一級的線性地址轉換并用于共享內存。假設有個文件映射到了幾個(3個)進程的地址空間。PPTE表包含有PPTE,這些PPTE描述了加載到內
存的文件的物理頁。某些PPTE可以有P位(其位置與含義與PTE/PDE的相同),而某些則沒有,沒有P位的有信息用來決定是從頁來加載frame還是
從映象文件來加載文件。所有三個進程的文件都映射在不同的地址上,對應于這些頁的PTE的P位未設置,并且包含有文件頁的PPTE的引用。這樣,在轉換映
射到文件的線性地址的時候,在一號進程中發生異常14,VMM找到PTE,得到對PPTE的引用,現在可以直接“修正”相應的PTE,以使其指向屬于文件
的frame,這時必需從文件中加載frame。我給出未設置P位PTE的格式,在頁表中其指向原型PTE。
PTE points to PPTE
+-----------------------------------------+-+---+-------------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1|1|0 0|0 0 0 0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1|0|9 8|7 6 5 4 3 2 1|0|
+-----------------------------------------+-+---+-------------+-+
| Address
[7:27] |1|Un
| Address |0|
|
| |use| [0:6] | |
|
|
|d |
| |
+-----------------------------------------+-+---+-------------+-+
*MmAccessFault
我們開始來研究一下MmAccessFault的偽代碼。其原型:
NTSTATUS MmAccessFault (BOOL Wr,DWORD Addr, BOOL P)
參數的意義很明顯:寫入標志,發生異常的地址和頁存在位。對于確定異常的原因,這些信息就足夠了。根據Addr是屬于內核地址空間還是用戶地址空間,處理
程序從兩個執行分支中選擇一個。第一種情況下的處理程序較為簡單,跟蹤ACCESS VIOLATION或是收回在Working
Set中的頁(MiDispatchFault)。若是用戶空間的地址情況就就更為復雜一些。首先,如果PDE不在內存中則執行用于PDE的異常處理程
序。然后,出現了一個分支。第一個分支——頁存在。這表示要么是ACCESS VIOLATION,要么就是對copy on
write的處理。第二個分支——處理清零頁請求、ACCESS VIOLATION、頁邊界(GUARD)(堆棧增長)以及必須的對working
set中頁的回收。有趣的是,在大量發生page fault的時候,系統會增大working
set的大小。在零PTE的情況下,為確定狀況,處理程序不得不使用VAD樹來確定試圖訪問區域的屬性。這些都是MiAccessCheck的工作,這個
函數返回訪問的狀態。
一般情況下,異常處理程序的主要奠基工作是由MiDispatchFault函數執行的。它能更精確的確定狀況并決定下一步的工作。
輪到MiDispatchFault了,它主要是基于一些更低級的函數:MiResolveTransitionFault、
MiResolveDemandZeroFault、MiResolveDemandZeroFault、MiResolveProtoPteFault
和MiResolvePageFileFault。從這些函數的名字可以明顯看出,這個函數用于確定更為具體的情況:狀態為'transition'(可
能會很快回收入Working
Set)的頁應該是空白的frame,PTE指向PPTE并且frame換出到相應的頁面文件中。在與頁面文件有關的和某些與PPTE有關的情況下,接著
可能需要從文件中讀取frame,此時函數返回值為0xc0033333,表示必須從文件中讀取頁。這在MiDispatchFault中是靠
IoPageRead進行的。我們來更仔細的研究一下所提到的函數。我們從MiResolveDemandZeroFault開始。
如果看一下這個函數的偽代碼,則可以輕易的明白它的工作邏輯。請求zero
frame并且進程得到這個frame。這時執行函數MiRemoveZeroPage或是MiRemoveAnyPage。第一個函數從zero頁的鏈
表中取一頁。如果未能成功,則通過第二個函數選擇任何一頁。這樣的話,該頁就由MiZeroPhysicalPage來清零。最終,在
MiAddValidPageToWorkingSet中,該清零的頁被添加到工作集中(恰好,這個事實證明在分配內存時進程不能取得對未處理頁的訪
問)。現在我們來研究一下更為復雜的情況——頁位于頁面文件中。
前面的偽代碼需要一個結構體。在準備從文件中讀取頁的時候,會填充PAGE_SUPPORT_BLOCK結構體。之后,對所有即將參與到操作中來的PFN
進行以下操作:設置read in
progress標志并在Misc域中寫入PAGE_SUPPORT_BLOCK的地址(函數
MiInitializeReadInProgressPfn)。最后,函數返回magic number
0xc0033333,表示隨后要在IoPageRead調用中使用此結構體(恰巧,IoPageRead被導出了,但是未公開的。從其偽碼中可以很容易
地得到其原型)。
typedef struct _PAGE_SUPPORT_BLOCK{ // size: 0x98
DISPATCHER_HEADER DispHeader; // 0 FastMutex
IO_STATUS_BLOCK IoStatusBlock; // 0x10
LARGE_INTEGER AddrInPageFile; // 0x18 (file offset)
DWORD
RefCounter; //
0x20 (0|1) ???
KTHREAD
Thread; //
0x24
PFILE_OBJECT FileObject; // 0x28
DWORD
AddrPte; //
0x2c
PPFN
pPfn; //
0x30
MDL
Mdl;
// 0x34
DWORD MdlFrameBuffer[0x10]; // 0x50
LIST_ENTRY
PageSupportList; // 0x90
與MmInPageSupportList有關的鏈表
}PAGE_SUPPORT_BLOCK *PAGE_SUPPORT_BLOCK;
struct _MmInPageSupportList{
LIST_ENTRY PageSupportList;
DWORD Count;
}MmInPageSupportList;
函數MiResolvePageFileFault本身非常簡單,除了填充相應的結構體并返回0xc0033333之外什么也不干。剩下的就是執行MiDispatchFault。這很合乎情理,如果還記得復用代碼的原則的話。
還有一個不太復雜的函數MiResolveTransitionFault。對于狀態為transition的frame還需要再多說幾句。從這個狀態中frame可以很快地返回到進程的Working Set中。
于是,剩下了最后一種情況——PROTO
PTE。這種情況的處理函數也不太復雜,而且支撐其的基礎我們已經講過了。實際上還有一個函數與這種情況有關,這就是
MiCompleteProtoPteFault,從MiDispatchFault中調用。要想理解這些函數的工作就去看一下偽代碼。
07. section 對象
================
NT 中的section對象就是一塊內存,這塊內存由一個進程獨有或幾個進程共享。在Win32子系統中section就是文件映射(file mapping object)。我們來看一下section對象到底是什么。
section是NT下非常常用的對象,執行系統使用section來將可執行映象加載到內存中并用其來管理cache。section同時也用在向進程
地址空間中映射文件。這時訪問文件就像訪問內存。section對象,就像其它的對象一樣,是由對象管理器創建的。高層次的信息告訴我們,對象的body
中包含著以下類型的信息:section的最大值,保護屬性,其它屬性。什么是section的最大可訪問值,這不說也知道。保護屬性是用于
section頁的屬性。其它section屬性有表示是文件section還是為空值(映射入頁面文件)的標志,以及section是否是base的。
base的section以相同的虛擬地址映射入所有進程的地址空間。
為了得到此對象結構的真實信息,我反匯編了一些用于section的內存管理器函數。下面的信息可是在別的地方見不到的。我們先來看結構體。
系統中的每一個文件都是對象(NTDDK.H中有描述)FILE_OBJECT。在這個結構體中有SectionObjectPointer。NTDDK.H中同樣有它的結構。
//
:
PSECTION_OBJECT_POINTERS SectionObjectPointer;
:
//
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
在結構體中有兩個指針——DataSectionObject 和
ImageSectionObject。NTDDK.H把它們寫成了PVOID,因為它們引用的是未公開的結構體。DataSectionObject用
在將文件作為數據打開的時候。ImageSectionObject——此時當作映象。這些指針的類型全都一樣,且可以稱之為
PCONTROL_AREA。所有下面這些結構體都是Windows 2K的,較之NT 4.0的有些變化。
typedef struct _CONTROL_AREA { // for NT 5.0, size = 0x38
PSEGMENT pSegment; //00
PCONTROL_AREA Flink;
//04
PCONTROL_AREA Blink;
//08
DWORD SectionRef; //0c
DWORD PfnRef;
//10
DWORD MappedViews;
//14
WORD Subsections;
//18
WORD FlushCount; //1a
DWORD UserRef;
//1c
DWORD Flags;
//20
PFILE_OBJECT
FileObject; //24
DWORD Unknown;
//28
WORD ModWriteCount;
//2c
WORD SystemViews;
//2e
DWORD
PagedPoolUsage; //30
DWORD
NonPagedPoolUsage; //34
}
CONTROL_AREA, *PCONTROL_AREA;
我們可以看到,CONTROL_AREA形成了一個鏈表,結構體中包含著統計值和標志。為了理解標志所代表的信息,我給出它們的值(用于NT5.0
/******************** nt5.0 ******************/
#define BeingDeleted 0x1
#define BeingCreated 0x2
#define BeingPurged 0x4
#define NoModifiedWriting 0x8
#define
FailAllIo 0x10
#define
Image 0x20
#define
Based 0x40
#define
File
0x80
#define
Networked 0x100
#define
NoCache 0x200
#define PhysicalMemory 0x400
#define CopyOnWrite 0x800
#define
Reserve 0x1000
#define
Commit
0x2000
#define FloppyMedia 0x4000
#define
WasPurged 0x8000
#define UserReference 0x10000
#define GlobalMemory 0x20000
#define DeleteOnClose 0x40000
#define FilePointerNull 0x80000
#define DebugSymbolsLoaded 0x100000
#define SetMappedFileIoComplete 0x200000
#define CollidedFlush 0x400000
#define
NoChange
0x800000
#define HadUserReference 0x1000000
#define ImageMappedInSystemSpace 0x2000000
緊隨CONTROL_AREA之后的是Subsection的數目Subsections。每一個Subsection都描述了關于具體的文件映射
section的信息。例如,read-only, read-write,
copy-on-write等等的section。NT5.0的SUBSECTION結構體:
typedef struct _SUBSECTION { // size=0x20 nt5.0
// +0x10 if GlobalOnlyPerSession
PCONTROL_AREA ControlArea; //38, 00
DWORD
Flags; //3c, 04
DWORD
StartingSector;//40, 08
DWORD
NumberOfSectors; //44, 0c
PVOID
BasePte; //48, 10 pointer to start
pte
DWORD
UnusedPtes; //4c, 14
DWORD
PtesInSubsect; //50, 18
PSUBSECTION
pNext; //54, 1c
}SUBSECTION, *PSUBSECTION;
在subsection中有指向CONTROL_AREA的指針,標志,指向base Proto PTE的指針,Proto PTE的數目。StartingSector是4K block的編號,文件中的section起始于此。在標志中還有額外的信息:
#define SS_PROTECTION_MASK 0x1f0
#define SS_SECTOR_OFFSET_MASK 0xfff00000 // (low 12 bits)
#define SS_STARTING_SECTOR_HIGH_MASK 0x000ffc00 // (nt5 only) (in pages)
//other 5 bit(s)
#define ReadOnly 1
#define ReadWrite 2
#define CopyOnWrite 4
#define GlobalMemory 8
#define LargePages 0x200
我們來看剩下的最后一個結構體SEGMENT,它描述了所有的映射和用于映射section的Proto PTE。SEGMENT的內存是從paged pool中分配的。我給出SEGMENT結構體(NT 5.0)
typedef struct _SEGMENT {
PCONTROL_AREA ControlArea; //00
DWORD
BaseAddr; //04
DWORD
TotalPtes; //08
DWORD
NonExtendedPtes;//0c
LARGE_INTEGER SizeOfsegemnt; //10
DWORD ImageCommit;
//18
DWORD ImageInfo;
//1c
DWORD ImageBase;
//20
DWORD Commited; //24
PTE PteTemplate;
//28 or 64 bits if pae enabled
DWORD
BasedAddr; //2c
DWORD
BaseAddrPae; //30 if PAE enabled
DWORD
ProtoPtes; //34
DWORD
ProtoPtesPae;
//38 if PAE enabled
}SEGMENT,*PSEGMENT;
正如我所料,結構體包含著對CONTROL_AREA的引用,指向Proto
PTE的pool的指針和所有section的信息。有個東西需要解釋一下。結構體的樣子依賴于是否支持PAE。PAE就是Physical
Address Extenion。從第5版開始,Windows
NT包含了支持PAE的內核Ntkrnlpa.exe。總的來講,支持PAE就意味著在NT里可以使用的虛擬地址不是4GB而是64GB。在使用PAE時
的地址轉換又多了一級——所有的虛地址空間被分為4部分。在打開PAE時PTE和PDE的大小不是4B而是8B,這我們可以從SEGMENT結構體中看
出。現在還不需要進一步詳細的講PAE,畢竟很少用到,所以我們就此打住。
描述section的所有結構體都介紹過了,而section對象結構體本身還沒有提到。從直觀上可以想到,它應該會引用到SEGMENT或是
CONTROL_AREA,因為有了這兩個結構體后就可以得到保存的所有信息。通過反匯編得到的section對象的body為以下形式:
typedef struct _SECTION_OBJECT { // size 0x28
VAD_HEADER VadHeader; // 0
PSEGMENT pSegment;
//0x14 Segment
LARGE_INTEGER SectionSize; //0x18
DWORD
ControlFlags; //0x20
DWORD
PgProtection; //0x24
} SECTION_OBJECT, *SECTION_OBJECT;
#define PageFile 0x10000
#define MappingFile 0x8000000
#define Based 0x40
#define Unknown 0x800000 // not sure, in fact it's AllocAttrib&0x400000
我們看到,所得的結構體完全符合現有的高層信息的描述。唯一可能有疑問的就是VAD_HEADER。它描述了base
section在地址空間中的位置。VAD_HEADER位于頂點為_MmSectionBasedRoot的VAD樹中。我們再次體會到,要理解操作系
統的工作原理,就要理解其內部的結構。為了有一個總體上的把握,下面給出了描述section的結構體間互相聯系的一個圖。
SECTION_OBJECT->SEGMENT<->CONTROL_AREA->FILE_OBJECT->SECTION_OBJECT_POINTERS+
^ |
+--------------------------------------------+
08.從內存管理器角度看進程的創建
====================================================
前面我們從Win32角度介紹過進程的創建,也講過內存管理器和對象管理器的工作原理,以及section對象結構體。現在最有意思的當然就是在進程創建中將內存管理器也考慮進來。
進程是用未公開的系統調用NtCreateProcess()創建的。下面給出其偽代碼:
/*****************************************************************/
/* -- Here it is, just wrapper -- */
NtCreateProcess(
OUT Handle,
IN ACCESS_MASK Access,
IN POBJECT_ATTRIBUTES ObjectAttrib,
IN HANDLE Parent,
IN BOOLEAN InheritHandles,
IN HANDLE SectionHandle,
IN HANDLE DebugPort,
IN HANDLE ExceptionPort
)
{
if(Parent)
{
ret=PspCreateProcess(Handle,
Access,
ObjectAttrib,
Parent,
InheritHandles,
SectionHandle,
DebugPort,
ExceptionPort);
}
else ret=STATUS_INVALID_PARAMETER;
return ret;
}
我們看到,NtCreateProcess是對另一個內部函數PspCreateProcess的封裝。NtCreateProcess進行的唯一工作就
是檢查Parent(父進程句柄)。但是接下來我們看到,對于NT來說這并沒有什么意義,因為總的來說,進程的繼承性本身沒有特別的意義。現在我們來看
PspCreateProcess()。
PspCreateProcess(
OUT PHANDLE Handle,
IN ACCESS_MASK Access,
IN POBJECT_ATTRIBUTES ObjectAttrib,
IN HANDLE Parent,
IN BOOLEAN InheritHandles,
IN HANDLE SectionHandle,
IN HANDLE DebugPort,
IN HANDLE ExceptionPort
);
我很快注意到,函數中的Parent參數可以接受值0,這就表明在NtCreateProcess中檢驗此參數是為了限制用戶模式。函數的參數中有對
section、debug port和exception
port、父進程的引用。通過調用ObReferenceObjectByHandle,可以得到指向這些對象的指針。實際上父進程句柄通常傳遞的是-
1,這表示是當前進程。如果Parent等于0,則進程的affinity就不從父進程處取得,而是從系統變量中取得。
if(Parent)
{ //Get pointer to father's body
ObReferenceObjectByHandle(Parent,0x80,PsProcessType,PrevMode,&pFather,0);
AffinityMask=pFather->Affinity; // on witch processors will be executed
Prior=8;
}
else {
pFather=0;
AffinityMask=KeActiveProcessors;
Prior=8;
}
優先級總是為8。隨后,創建進程對象。NT4.0下其大小為504字節。
// size of process body - 504 bytes
// creating process object... (type object PsProcessType)
ObCreateObject(PrevMode,PsProcessType,ObjectAttrib,PrevMode,0,504,&pProcess);
// clear body
memset(pProcess,0,504);
初始化某些域和Quota Block(見對象管理器的相關介紹)。
pProcess->CreateProcessReported=0;
pProcess->DebugPort=pDebugPort;
pProcess->ExceptPort=pExceptPort;
// Inherit Quota Block, if pFather==NULL, PspDefaultQuotaBlock
PspInheritQuota(pProcess,pFather);
if(pFather){
pProcess->DefaultHardErrorMode=pFather->DefaultHardErrorMode;
pProcess->InheritedFromUniqueProcessId=pFather->UniqueProcessId;
}
else {
pProcess->InheritedFromUniqueProcessId=0;
pProcess->DefaultHardErrorMode=1;
}
之后,調用MmCreateProcessAddressSpace,創建地址上下文。參數是函數得到的指向進程的指針、工作集的大小和指向結果結構體的指針。這個結構體形式如下:
struct PROCESS_ADDRESS_SPACE_RESULT{
dword Dt; // dict. table phys. addr.
dword HypSpace; // hyp space page phys. addr.
dword WorkingSet; // working set page phys. addr.
}CASResult;
MmCreateProcessAddressSpace(PsMinimumWorkingSet,pProcess,&CASResult);
我們看到,函數向我們返回的是頁表的物理地址描述符(用于新地址空間的CR3的內容),Hyper Space的頁地址和工作集的頁地址。在此之后是初始化進程對象的某些域:
pProcess->MinimumWorkingSet=MinWorkingSet;
pProcess->MaximumWorkingSet=MaximumWorkingSet;
KeInitializeProcess(pProcess,Prior,AffinityMask,&CASResult,pProcess->
DefaultHardErrorProcessing&0x4);
pProcess->ForegroundQuantum=PspForegroundQuantum;
如果有父進程且設置了標志參數,則會繼承父進程的句柄表:
if(pFather) // if there is father and inherithandle, so, inherit handle db
{
pFather2=0;
if(bInheritHandle)pFather2=pFather;
ObInitProcess(pFather2,pProcess); // see info about ObjectManager
}
下面的東西比較有意思,證明了NT執行系統的靈活性,從表面上是看不出來的。如果在參數中有指定的section,則使用這個section來初始化進程的地址空間,否則其工作就會像*UNIX中的fork()。
if(pSection)
{
MmInitializeProcessAddressSpace(pProcess,0,pSection);
ObDereferenceObject(pSection);
res=ObInitProcess2(pProcess); //work with unknown byte +0x22 in process
if(res>=0)PspMapSystemDll(pProcess,0);
Flag=1; //Created addr space
}
else { // if there is futher, but no section, so, do operation like fork()
if(pFatherProcess){
if(PsInitialSystemProcess==pFather){
MmRes=MmInitializeProcessAddressSpace(pProcess,0,0);
}
else {
pProcess->SectionBaseAddress=pFather->SectionBaseAddress;
MmRes=MmInitializeProcessAddressSpace(pProcess,pFather,0);
Flag=1;
//created addr space
}
}
}
接下來是使用PsActiveProcessHead將進程插入Active
Process鏈表,創建Peb和做其它輔助性的工作。我們不再贅述。最后,當所有的工作都做完后,進行安全子系統方面的工作。我們過去曾研究過安全子系
統(見對象管理器部分),所以這里只簡單的給出其偽代碼。只是我注意到,如果父進程是system(句柄值等于
PspInitialSystemProcessHandle),則不對其安全性進行檢驗。
// finally, security operations
if(pFather&&PspInitialSystemProcessHandle!=Father)
{
ObGetObjectSecurity(pProcess,&SecurityDescriptor,&MemoryAllocated);
pToken=PsReferencePrimaryToken(pProcess);
AccessRes=SeAccessCheck(SecurityDescriptor,&SecurityContext,
0,0x2000000,
0,0,&PsProcessToken->GenericMapping,
PrevMode,pProcess->GrantedAccess,
&AccessStatus);
ObDereferenceObject(pToken);
ObReleaseObjectSecuryty(SecurityDescriptor,MemoryAllocated);
if(!AccessRes)pProcess->GrantedAccess=0;
pProcess->GrantedAccess|=0x6fb;
}
else{
pProcess->GrantedAccess=0x1f0fff;
}
if(SeDetailedAuditing)SeAuditProcessCreation(pProcess,pFather);
最有意思的是函數KeInitializeProcess和MmCreateProcessAddressSpace。前一個函數除了初始化進程對象的其它成員之外,還要初始化TSS中的IO位圖的偏移。
pProcess->IopmOffset=0x20ad; // IOMAP BASE!!!
//
You can patch kernel here and
//
got i/o port control ;)
偏移的選取是這樣的,它指向I/O位圖,這樣就能阻止進程直接使用I/O端口。
在函數MmCreateProcessAddressSpace中進行的是進程地址空間的創建。我就不給出所有的偽代碼了,只簡要的寫寫主要的操作。它為
Hyper Space, Working Set和Page Directory選擇頁。反匯編后的代碼證實了,它們是從zero
frame鏈表中選出或是由MiZeroPhysicalPage函數來清零的。之后初始化新創建的Page Directory。
pProcess->WorkingSetPage=Frame3; // WorkingSetPage
(MmPfnDatabase+0x18*Frame)->Pte=0xc0300000;
ValidPde_U=ValidPdePde&0xeff^Frame2; // HyperSpace
/**************IMPORTANT!!!!!!!!!!!!!!************************/
/* 重要!
這里初始化PD
*/
/*************************************************************/
Va=MiMapPageInHyperSpace(Frame,&LastIrql);
// no we got Va of our new Page Directory
// Fill some fields
*(Va+0xc04)=ValidPde_U; // HyperSpace
ValidPde_U=ValidPde_U&0xfff^PhysAddr; // DT
*(Va+0xc00)=ValidPde_U; // self-pde
// copy from current process, kernel address mapping
memcpy(
(MmVirtualBias+0x80000000)>>0x14+Va, // it's like that we found,
//
what MmVirtualBias is it ;)
(MmVirtualBias+0x80000000)>>0x14+0xc0300000,
0x80 // 32 pdes -> 4Mb*32=128Mb
);
memcpy( // copy pdes, corresponding to NonPagedArea
MmNonPagedSystemStart>>0x14+Va,
MmNonPagedSystemStart>>0x14+0xc0300000,
(0xc0300ffc-MmNonPagedSystemStart>>0x14+0xc0300000)&0xfffffffc+4);
memcpy(Va+0xc0c, // cache, forgot about it now, it's another story ;)
0xc0300c0c,
(MmSystemCacheEnd>>0x14)-0xc0c+4
);
也就是將PDE拷貝到內核地址空間中去(其對所有的進程不變,Hyper Space除外),而且是拷貝到不可換出的區域。同時這個空間是屬于系統cache的。
09.上下文切換
==========================
知道了ETHREAD、EPROCESS結構體和內存管理器的工作原理,就不難猜到上下文切換時會發生什么。Windows
NT的設計者使用線程,不關心共享的是誰的地址空間,也就是說有兩種可能:線程屬于當前進程——必需要切換到另一個線程(更新堆棧并更換GDT描述符),
而線程屬于另一個進程,必需切換到那個進程(重新加載CR3)。對此,為了證實我的推測,我反匯編了KeAttachProcess函數。這個函數是未公
開的,但所有已知的函數都用其來切換到另一進程的地址空間。通過KeDetachProcess可以返回到當前進程。KeAttachProcess使用
下述內部函數:
KiAttachProcess - KeAttachProcess僅僅是對這個函數的封裝
KiSwapProcess - 更換地址空間。(本質上就是重新加載CR3)
SwapContext - 更換上下文。一般不管地址空間的切換,只調整線程上下文。
KiSwapThred - 切換到鏈表中的下一個線程(SwapContext)調用
下面給出這些內部函數的偽代碼。
-----------------------------------------------------------------------------
/************************ KeAttachProcess ***************************/
// just wrapper
//
KeAttachProcess(EPROCESS *Process)
{
KiAttachProcess(Process,KeRaiseIrqlToSynchLevel);
}
/************************ KiAttachProcess ***************************/
KiAttachProcess(EPROCESS *Process,Irql){
//CurThread=fs:124h
//CurProcess=CurThread->ApcState.Process;
if(CurProcess!=Process){
if(CurProcess->ApcStateIndex || KPCR->DpcRoutineActive)KeBugCheckEx...
}
//if we already in process's context
if(CurProcess==Process){KiUnlockDispatcherDatabase(Irql);return;}
Process->StackCount++;
KiMoveApcState(&CurThread->ApcState,&CurThread->SavedApcState);
// init lists
CurThread->ApcState.ApcListHead[0].Blink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[0].Flink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[1].Blink=&CurThread->ApcState.ApcListHead[1];
CurThread->ApcState.ApcListHead[1].Flink=&CurThread->ApcState.ApcListHead[1];;
//fill curtheads's fields
CurThread->ApcState.Process=Process;
CurThread->ApcState.KernelApcInProgress=0;
CurThread->ApcState.KernelApcPending=0;
CurThread->ApcState.UserApcPending=0;
CurThread->ApcState.ApcStatePointer.SavedApcState=&CurThread->SavedApcState;
CurThread->ApcState.ApcStatePointer.ApcState=&CurThread->ApcState;
CurThread->ApcStateIndex=1;
//if process ready, just swap it...
if(!Process->State)//state==0, ready
{
KiSwapProcess(Process,CurThread->SavedApcState.Process);
KiUnlockDispatcherDatabase(Irql);
return;
}
CurThread->State=1; //ready?
CurThread->ProcessReadyQueue=1;
//put Process in Thread's waitlist
CurThread->WaitListEntry.Flink=&Process->ReadyListHead.Flink;
CurThread->WaitListEntry.Blink=Process->ReadyListHead.Blink;
Process->ReadyListHead.Flink->Flink=&CurThread->WaitListEntry.Flink;
Process->ReadyListHead.Blink=&CurThread->WaitListEntry.Flink;
// else, move process to swap list and wait
if(Process->State==1){//idle?
Process->State=2; //trans
Process->SwapListEntry.Flink=&KiProcessInSwapListHead.Flink;
Process->SwapListEntry.Blink=KiProcessInSwapListHead.Blink;
KiProcessInSwapListHead.Blink=&Process->SwapListEntry.Flink;
KiSwapEvent.Header.SignalState=1;
if(KiSwapEvent.Header.WaitListHead.Flink!=&KiSwapEvent.Header.WaitListHead.
Flink)
KiWaitTest(&KiSwapEvent,0xa);
//fastcall
}
CurThread->WaitIrql=Irql;
KiSwapThread();
return;
}
從這個函數可以得到以下結論。進程可以處于以下狀態——0(準備),1(Idle),2(Trans——切換)。這證實了高層次的信息。KiAttachProcess使用了另外兩個函數KiSwapProcess和KiSwapThread。
/************************* KiSwapProcess ****************************/
KiSwapProcess(EPROCESS* NewProcess, EPROCESS* OldProcess)
{
// just reload cr3 and small work with TSS
// TSS=KPCR->TSS;
// xor eax,eax
// mov gs,ax
TSS->CR3=NewProcess->DirectoryTableBase;//0x1c
// mov cr3,NewProcess->DirectoryTableBase
TSS->IopmOffset=NewProcess->IopmOffset;//0x66
if(WORD(NewProcess->LdtDescriptor)==0){lldt 0x00; return;//}
//GDT=KPCR->GDT;
(QWORD)GDT->0x48=(QWORD)NewProcess->LdtDescriptor;
(QWORD)GDT->0x108=(QWORD)NewProcess->Int21Descriptor;
lldt 0x48;
return;
}
切換進程上下文。正如我所料,這個函數只是重新加載CR3寄存器,再加上一點相關的操作。例如,用IopmOffset域的值建立TSS中的I/O位圖的偏移。還必需將選擇子的值加載到ldt(只用于VDM session)。
/************************* SwapContext ******************************/
SwapContext(NextThread,CurThread,WaitIrql)
{
NextThread.State=ThreadStateRunning; //2
KPCR.DebugActive=NextThread.DebugActive;
cli();
//Save Stack
CurThread.KernelStack=esp;
//Set stack
KPCR.StackLimit=NextThread.StackLimit;
KPCR.StackBase=NextThread.InitialStack;
tmp=NextThread.InitialStack-0x70;
newcr0=cr0&0xfffffff1|NextThread.NpxState|*(tmp+0x6c);
if(newcr0!=cr0)reloadcr0();
if(!*(tmp-0x1c)&0x20000)tmp-=0x10;
TSS=KPCB.TSS;
TSS->ESP0=tmp;
//set pTeb
KPCB.Self=NextThread.pTeb;
esp=NextThread.KernelStack;
sti();
//correct GDT
GDT=KPCB.GDT;
WORD(GDT->0x3a)=NextThread.pTeb;
BYTE(GDT->0x3c)=NextThread.pTeb>>16;
BYTE(GDT->0x3f)=NextThread.pTeb>>24;
//if we must swap processes, do it (like KiSwapProcess)
if(CurThread.ApcState.Process!=NextThread.ApcState.Process)
{
//******** like KiSwapProcess
}
NextThread->ContextSwitches++;
KPCB->KeContextSwitches++;
if(!NextThread->ApcState.KernelApcPending)return 0;
//popf;
// jnz HalRequestSoftwareInterrupt// return 0
return 1;
}
切換堆棧,修正GDT,以使FS寄存器指向TEB。如果線程屬于當前進程,則不進行上下文切換。否則,進行的操作和KiSwapProcess中的大致差不多。
為了一致,我給出KeDetachProcess的原型。
KeDetachProcess(EPROCESS *Process,Irql);
我們看到——這些函數的偽碼實際上完全描述出了操作系統的上下文切換。總的說來,代碼分析表明,理解OS的主要途徑就是要知道它的內部結構。
0a.某些未公開的內存管理器函數
==========================================================
SP3的ntoskrnl.exe的內存管理器導出了以下符號:
467 1D0 00051080 MmAdjustWorkingSetSize
468 1D1 0001EDFA+MmAllocateContiguousMemory
469 1D2 00051A14+MmAllocateNonCachedMemory
470 1D3 0001EAE8+MmBuildMdlForNonPagedPool
471 1D4 000206BC MmCanFileBeTruncated
472 1D5 0001EF5A+MmCreateMdl
473 1D6 0002095C MmCreateSection
474 1D7 00021224 MmDbgTranslatePhysicalAddress
475 1D8 000224AC MmDisableModifiedWriteOfSection
476 1D9 000230C8 MmFlushImageSection
477 1DA 0001FA9C MmForceSectionClosed
478 1DB 0001EEA0+MmFreeContiguousMemory
479 1DC 00051AFE+MmFreeNonCachedMemory
480 1DD 0001EEAC+MmGetPhysicalAddress
481 1DE 00024028 MmGrowKernelStack
482 1DF 0004E144 MmHighestUserAddress
483 1E0 0002645A+MmIsAddressValid
484 1E1 00026CD8+MmIsNonPagedSystemAddressValid
485 1E2 0001F5D8 MmIsRecursiveIoFault
486 1E3 00026D56+MmIsThisAnNtAsSystem
487 1E4 000766C8+MmLockPagableDataSection
488 1E5 000766C8 MmLockPagableImageSection
489 1E6 0001F160+MmLockPagableSectionByHandle
490 1E7 0001ED18+MmMapIoSpace
491 1E8 0001EB74+MmMapLockedPages
492 1E9 0001F5F6 MmMapMemoryDumpMdl
493 1EA 00076A14 MmMapVideoDisplay
494 1EB 0005206C MmMapViewInSystemSpace
495 1EC 00079B0E MmMapViewOfSection
496 1ED 0007A7EE+MmPageEntireDriver
497 1EE 0001E758+MmProbeAndLockPages
498 1EF 00026D50+MmQuerySystemSize
499 1F0 00052A8A+MmResetDriverPaging
500 1F1 0004E0A4 MmSectionObjectType
501 1F2 00079D28 MmSecureVirtualMemory
502 1F3 0001EFCE MmSetAddressRangeModified
503 1F4 0007684E MmSetBankedSection
504 1F5 0001EF2C+MmSizeOfMdl
505 1F6 0004E0A0 MmSystemRangeStart
506 1F7 0001F516+MmUnlockPagableImageSection
507 1F8 0001EA16+MmUnlockPages
508 1F9 0007669A+MmUnmapIoSpace
509 1FA 0001ECA8+MmUnmapLockedPages
510 1FB 00076A2E MmUnmapVideoDisplay
511 1FC 00052284 MmUnmapViewInSystemSpace
512 1FD 0007AFE4 MmUnmapViewOfSection
513 1FE 0007A00A MmUnsecureVirtualMemory
514 1FF 0004DDCC MmUserProbeAddress
這里的符號'+'表示函數在DDK中有記載。我這里給出某些未公開函數的原型。
// 調整working set的大小.
NTOSKRNL NTSTATUS MmAdjustWorkingSetSize(
DWORD MinimumWorkingSet OPTIONAL, // if both == -1
DWORD MaximumWorkingSet OPTIONAL, // empty working set
PVM Vm OPTIONAL);
//can file be truncated???
NTOSKRNL BOOLEAN MmCanFileBeTruncated(
PSECTION_OBJECT_POINTERS SectionPointer, // see FILE_OBJECT
PLARGE_INTEGER NewFileSize
);
// create section. NtCreateSection call this function...
NTOSKRNL NTSTATUS MmCreateSection (
OUT
PVOID
*SectionObject,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN PLARGE_INTEGER MaximumSize,
IN
ULONG SectionPageProtection,//PAGE_XXXX
IN
ULONG AllocationAttributes,//SEC_XXX
IN
HANDLE
FileHandle OPTIONAL,
IN PFILE_OBJECT File OPTIONAL
);
typedef enum _MMFLUSH_TYPE {
MmFlushForDelete,
MmFlushForWrite
} MMFLUSH_TYPE;
NTOSKRNL BOOLEAN MmFlushImageSection (
IN PSECTION_OBJECT_POINTERS SectionObjectPointer,
IN
MMFLUSH_TYPE
FlushType
);
NTOSKRNL DWORD MmHighestUserAddress; // 一般為0x7ffeffff
NTOSKRNL BOOLEAN MmIsRecursiveIoFault();
//其代碼
#define _MmIsRecursiveIoFault() ( \
(PsGetCurrentThread()->DisablePageFaultClustering) | \
(PsGetCurrentThread()->ForwardClusterOnly) \
)
NTOSKRNL POBJECT_TYPE MmSectionObjectType; //標準的Section對象
NTOSKRNL DWORD MmSystemRangeStart; //一般為0x80000000
NTOSKRNL DWORD MmUserProbeAddress; //一般為0x7fff0000
NTOSKRNL PVOID MmMapVideoDisplay( // для i386 враппер в MmMapIoSpace
IN PHYSICAL_ADDRESS PhysicalAddress,
IN ULONG NumberOfBytes,
IN BOOLEAN CacheEnable
);
NTOSKRNL VOID MmUnmapVideoDisplay ( // для i386 враппер в MmUnmapIoSpace
IN PVOID BaseAddress,
IN ULONG NumberOfBytes
);
// 將frame的范圍標記為更改并進行相應的操作
NTOSKRNL VOID MmSetAddressRangeModified(
PVOID StartAddress,
DWORD Length
);
// 在NtMapViewOfSection中調用
typedef enum _SECTION_INHERIT {
ViewShare=1;
ViewUnmap=2;
}SECTION_INHERIT;
NTOSKRNL NTSTATUS MmMapViewOfSection(
PVOID pSection,
PEPROCESS pProcess,
OUT PVOID *BaseAddress,
DWORD ZeroBits,
DWORD CommitSize,
OUT PLARGE_INTEGER SectionOffset OPTIONAL,
OUT PDWORD ViewSize,
SECTION_INHERIT InheritDisposition,
DWORD AllocationType,
DWORD ProtectionType
);
NTOSKRNL NTSTATUS MmUnmapViewOfSection(
PEPROCESS Process,
PVOID Address
);
PVOID MmLockPagableImageSection(
PVOID AddressWithinImageSection // same entry as MmLockPagableDataSection
);
// 減少StackLimit(堆棧增長)
NTSTATUS MmGrowKernelStack(
PVOID CurESP //棧頂的地址
);
I talk to the wind
My
words are all carried away
I talk to the wind
The
wind does not hear
The wind cannot hear.
King
Crimson'69 -I Talk to the Wind
0b.結語
=============
就到這里吧。如果綜合的來看所有這些描述,對內存管理器多少可以得到一些概念。遺憾的是,這些東西還遠不能稱之為完整。內存管理器,大概是最復雜和最重要
的內核組件,對其要進行完整的描述,我還得深挖不止十個八個的函數。但是主要的基本的東西我這里都寫到了。對于進一步反匯編內核來說,這些應該是很有幫助
的吧,誰知道呢... ;)
Best Regards, Peter Kosy aka Gloomy.
Melancholy
Coding '2001.
mailto:gl00my@mail.ru
P.S. 我知道我的“大作”不可避免的會有錯誤。我將非常高興的聽取批評和建議。
附錄
0c.某些未公開的系統調用
==================================================
這里我描述了一些有用的Zw/Nt函數,這些函數可以在USER模式下或是驅動程序中調用(Zw類的)。幾乎所有這些函數都來自于
Коберниченко的“Недокументированные возмождности Windows
NT”一書。再加上Working
Set結構體的值,就可以描述用于NtQueryVirtualMemory的MEMORY_WORKING_SET_INFORMATION。
NTSYSAPI NTSTATUS NTAPI NtAllocateVirtualMemory(
HANDLE Process,
OUT
PVOID *BaseAddr,
DWORD
ZeroBits,
OUT
PDWORD RegionSize,
DWORD
AllocationType,// MEM_RESERVE|MEM_COMMIT|MEM_TOP_DOWN
DWORD
Protect); // PAGE_XXXX...
NTSYSAPI NTSTATUS NTAPI NtFreeVirtualMemory(
HANDLE Process,
OUT
PVOID* BaseAddr,
OUT
PULONG RegionSize,
DWORD
FreeType //MEM_DECOMMIT|MEM_RELEASE
);
NTSYSAPI NTSTATUS NTAPI NtCreateSection(
OUT PHANDLE Section,
ACCESS_MASK DesirdAccess, //SECTION_MAP_XXX...
POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
PLARGE_IBTEGER MaximumSize OPTIONAL,
DWORD SectionPageProtection, //PAGE_...
DWORD AllocationAttributes, //SEC_XXX
HANDLE FileHandle OPTIONAL // NULL - pagefile
);
typedef enum _SECTION_INHERIT {
ViewShare=1;
ViewUnmap=2;
}SECTION_INHERIT;
NTSYSAPI NTSTATUS NTAPI NtMapViewOfSection(
HANDLE Section,
HANDLE Process,
OUT PVOID *BaseAddress,
DWORD ZeroBits,
DWORD CommitSize,
OUT PLARGE_INTEGER SectionOffset OPTIONAL,
OUT PDWORD ViewSize,
SECTION_INHERIT InheritDisposition,
DWORD AllocationType, //MEM_TOP_DOWN,MEM_LARGE_BAGE,MEM_AUTO_ALIGN=0x40000000
DWORD ProtectionType // PAGE_...
);
#define UNLOCK_TYPE_NON_PRIVILEGED 0x00000001L
#define UNLOCK_TYPE_PRIVILEGED 0x00000002L
NTSYSAPI NTSTATUS NTAPI NtLockVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID *RegionAddress,
IN OUT PULONG RegionSize,
IN ULONG UnlockTypeRequired
);
NTSYSAPI NTSTATUS NTAPI NtUnlockVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID *RegionAddress,
IN OUT PULONG RegionSize,
IN ULONG UnlockTypeRequiested
);
NTSYSAPI NTSTATUS NTAPI NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID StartAddress,
OUT PVOID Buffer,
IN ULONG BytesToRead,
OUT PULONG BytesReaded OPTIONAL
);
NTSYSAPI NTSTATUS NTAPI NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID StartAddress,
IN PVOID Buffer,
IN ULONG BytesToWrite,
OUT PULONG BytesWritten OPTIONAL
);
NTSYSAPI NTSTATUS NTAPI NtProtectVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID *RegionAddress,
IN OUT PULONG RegionSize,
IN ULONG DesiredProtection,
OUT PULONG OldProtection
);
NTSYSAPI NTSTATUS NTAPI NtFlushVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID* StartAddress,
IN PULONG BytesToFlush,
OUT PIO_STATUS_BLOCK StatusBlock
);
typedef enum _MEMORYINFOCLASS {
MemoryBasicInformation,
MemoryWorkingSetInformation,
// 還有class 2 - 這是VAD中的信息, 我目前還不完全了解VAD結構體,也就不能寫出相應的INFO結構。
} MEMORYINFOCLASS;
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
ULONG RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
#define WSFRAMEINFO_SHARED_FRAME 0x100
#define WSFRAMEINFO_INTERNAL_USE 0x4
#define WSFRAMEINFO_UNKNOWN 0x3
typedef struct _MEMORY_WORKING_SET_INFORMATION {
ULONG SizeOfWorkingSet;
DWORD WsEntries[ANYSIZE_ARRAY]; // is Page VA | WSFRAMEINFO...
} MEMORY_ENTRY_INFORMATION, *PMEMORY_ENTRY_INFORMATION;
NTSYSAPI NTSTATUS NTAPI NtQueryVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID RegionAddress,
IN MEMORYINFOCLASS MemoryInformationClass,
IN PVOID VirtualMemoryInfo,
IN ULONG Length,
OUT PULONG ActualLength OPTIONAL
);
0d.附注及代碼分析草稿
==========================================
**** К MmCreateProcessAddressSpace ... ****
=============================================
__fastcall MiTotalCommitLimit(PVOID pProcess, DWORD NumOfPages); // edx:ecx
有statistic
dd MmTotalCommitLimit
dd MmTotalCommitedPages
如果NumOfPages+MmTotalCommitedPages不超過Limit - 一切OK,并只是簡單的修正statistic.
否則開始線程間的協作。
選擇time out值(如果請求>=10頁,則為20秒),否則為-1秒。接著填充某個結構體,大概是這個樣子:
typedef struct _REQUEST_FOR_COMMITED_MEMORY{
LIST_ENTRY ListEntry;
DWORD PagesToCommit;
DWORD Result;
KSEMAPHORE Semaphore;
}_REQUEST_FOR_COMMITED_MEMORY;
這個結構體(或鏈表的元素)被插入到全局結構體中的全局鏈表ListOfRequest:
[Pre List Item]<->[Our List Item]<->[ListOfRequest]
typedef struct _COMMIT_MEMORY_REQUEST_LIST{
KSEMAPHORE CommitMemorySemaphore;
LIST_ENTRY ListOfRequest;
}COMMIT_MEMORY_REQUEST_LIST;
之后對CommitMemorySemaphore使用KeReleaseSemaphore并等待REQUEST_FOR_COMMITED_MEMORY中帶有time out的信號量。
如果未超出time out并因此Result不為0,則再校驗一次Limit并輸出OK(如果limit有問題——則所有都重新開始)。如果結果為0,MiCouseOverCommitPopup。如果發生了time out,分析如下:
如果ListOfReques.Flink==&ListOfReques.Flink,也就是說所有的請求都在隊列的尾部,則再一次等待信號量——并且已經沒有time out了,因為不是我們的問題;)
如果ListOfReques.Flink==&RequestForCommitedMemory.ListEntry,就是說隊列中的下一個是我們的請求(???)。則從隊列中收回請求,因為
是從我們這里來的。
現在來看我們想看的幾個頁。如果>=10則MiCouseOverCommitPopup,否則MiChargeCommitmentCantExpand,之后輸出。
所有的操作都需要cli sti,同時使用FastMutex(進程的10ch偏移),在進程創建時調用這個函數不會進行此操作。
現在,MiCouseOverCommitPopup(PagesNum,CommitTotalLimitDelta);又做些什么呢——如果我們想要
頁數大于128——則ExRaiseStatus(STATUS_COMMITMENT_LIMIT);
如果小于則IoRaiseInformationalHardError(STATUS_COMMITMENT_LIMIT,0,0);(這些函數都是公
開的)。如果成功調用最后一個函數——則累加statistic:
MiOverCommitCallCount++;
MmTotalCommitLimit+=CommitTotalLimitDelta;
MmExtendedCommit+=CommitTotalLimitDelta;
MmTotalCommittedPages+=PagesNum;
且不修正 MmPeakCommintment;
如果不成功但MiOverCommitCallCount==0,所有都等于statistic,否則ExRaiseStatus(STATUS_COMMITMENT_LIMIT);
輔助函數:
DWORD NTOSKRNL RtlRandom(PDWORD Seed);
不奇怪,這個函數沒有公開。該函數使用一個128個DWORD的表。在操作之后被此表和Seed被修正。可以看到,這給出了最大周期。
如果有兩個event
MmAvailablePagesEventHigh 和
MmAvailablePagesEventHigh.
MiSectionInitialization:
MmDereferenceSegmentHeader: это структура описанная выша с добавленным
spinlock сверху.
創建線程MiDereferenceSegmentThread
PsChargePoolQuota(PVOID Process,DWORD Type(NP/P),DWORD Charge);
[TO DO] -->> MmInfoCounters!!!! 使用相應的NtQueryInfo...可以獲得非常多有用的信息,ПОСМОТРЕТЬ!!!