內(nèi)存與進(jìn)程管理器
==========================
But I fear tomorrow I'll be crying,
Yes I fear tomorrow I'll be crying.
King
Crimson'69 -Epitaph
關(guān)于Windows NT內(nèi)存管理器的高層次信息已經(jīng)夠多的了,所以這里不會再講什么FLAT模型、虛擬內(nèi)存之類的東西。這里我們只講具體的底層的東西。我假定大家都了解>i386的體系結(jié)構(gòu)。
目錄
==========
00.內(nèi)核進(jìn)程線程結(jié)構(gòu)體
01.頁表
02.Hyper Space
03.System PTE'S
04.Frame data base (MmPfnDatabase)
05.Working Set
06.向pagefile換頁
07.page fault的處理
08.從內(nèi)存管理器角度看進(jìn)程的創(chuàng)建
09.上下文切換
0a.某些未公開的內(nèi)存管理器函數(shù)
0b.結(jié)語
附錄
0c.某些未公開的系統(tǒng)調(diào)用
0d.附注及代碼分析草稿
00.內(nèi)核進(jìn)程線程結(jié)構(gòu)體
===================================
Windows
NT中的每一個進(jìn)程都是EPROCESS結(jié)構(gòu)體。此結(jié)構(gòu)體中除了進(jìn)程的屬性之外還引用了其它一些與實(shí)現(xiàn)進(jìn)程緊密相關(guān)的結(jié)構(gòu)體。例如,每個進(jìn)程都有一個或幾
個線程,線程在系統(tǒng)中就是ETHREAD結(jié)構(gòu)體。我來簡要描述一下存在于這個結(jié)構(gòu)體中的主要的信息,這些信息都是由對內(nèi)核函數(shù)的研究而得知的。首先,結(jié)構(gòu)
體中有KPROCESS結(jié)構(gòu)體,這個結(jié)構(gòu)體中又有指向這些進(jìn)程的內(nèi)核線程(KTHREAD)鏈表的指針(分配地址空間),基優(yōu)先級,在內(nèi)核模式或是用戶模
式執(zhí)行進(jìn)程的線程的時(shí)間,處理器affinity(掩碼,定義了哪個處理器能執(zhí)行進(jìn)程的線程),時(shí)間片值。在ETHREAD結(jié)構(gòu)體中還存在著這樣的信息:
進(jìn)程ID、父進(jìn)程ID、進(jìn)程映象名、section指針。quota定義了所能使用的分頁和非分頁池的極限值。VAD
(virtual address descriptors)樹定義了用戶地址空間內(nèi)存區(qū)的狀況。關(guān)于Working
Set的信息定義了在給定時(shí)間內(nèi)有那些物理頁是屬于進(jìn)程的。同時(shí)還有l(wèi)imit與statistics。ACCESS
TOKEN描述了當(dāng)前進(jìn)程的安全屬性。句柄表描述了進(jìn)程打開的對象的句柄。該表允許不在每一次訪問對象時(shí)檢查訪問權(quán)限。在EPROCESS結(jié)構(gòu)體中還有指
向PEB的指針。
ETHREAD結(jié)構(gòu)體還包含有創(chuàng)建時(shí)間和退出時(shí)間、進(jìn)程ID和指向EPROCESS的指針,啟動地址,I/O請求鏈表和KTHREAD結(jié)構(gòu)體。在
KTHREAD中包含有以下信息:內(nèi)核模式和用戶模式線程的創(chuàng)建時(shí)間,指向內(nèi)核堆?;泛晚旤c(diǎn)的指針、指向服務(wù)表的指針、基優(yōu)先級與當(dāng)前優(yōu)先級、指向
APC的指針和指向TEB的指針。KTHREAD中包含有許多其它的數(shù)據(jù),通過觀察這些數(shù)據(jù)可以分析出KTHREAD的結(jié)構(gòu)。
01.頁表
==================
通常操作系統(tǒng)使用頁表來進(jìn)行內(nèi)存操作。在Windows
NT中,每一個進(jìn)程都有自己私有的頁表(進(jìn)程的所有線程共享此頁表)。相應(yīng)的,在進(jìn)程切換時(shí)會發(fā)生頁表的切換。為了加快對頁表的訪問,硬件中有一個
translation lookaside buffer(TLB)。在Windows
NT中實(shí)現(xiàn)了兩級的轉(zhuǎn)換機(jī)制。在386+處理器上將虛擬地址轉(zhuǎn)換為物理地址過程(不考慮分段)如下:
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)屬于當(dāng)前用戶進(jìn)程,而高2G
(0x80000000-0xffffffff)屬于內(nèi)核。在上下文切換時(shí),要更新CR3寄存器的值,結(jié)果就更換了用戶地址空間,這樣就達(dá)到了進(jìn)程間相互
隔絕的效果。
注:在Windows NT中,從第4版起,除4Kb的頁之外同時(shí)還使用了4Mb的頁(Pentium及更高)來映射內(nèi)核代碼。但是在Windows NT中沒有實(shí)際對可變長的頁提供支持。
PTE和PDE的格式實(shí)際上是一樣的。
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 - 存在位。此位如果未設(shè)置,則在地址轉(zhuǎn)換時(shí)會產(chǎn)生異常。一般說來,在一些情況下NT內(nèi)核會使用未設(shè)置此位的PTE。
例如,如果向pagefile換出頁,保留這些位可以說明其在頁面文件中的位置和pagefile號。
U/S - 是否能從user模式訪問頁。正是借助于此位提供了對內(nèi)核空間的保護(hù)(通常為高2G)。
RW - 是否能寫入
NT使用的為OS設(shè)計(jì)者分配的空閑位
---------------------------------------------------------------------------
PPT - proto pte
TRN - transition pte
當(dāng)P位未設(shè)置時(shí),第5到第9位即派上用場(用于page fault處理)。它們叫做Protection Mask,樣子如下:
--------------------------------------------------------------------------------------
* MiCreatePagingFileMap
9 8 7 6 5
---------
| | | | |
| | | | +- Write Copy
| | | +--- Execute
| | +----- Write
| +------- NO CACHE
+--------- Guard
GUARD | NOCACHE組合就是NO ACCESS
* MmGetPhysicalAddress
函數(shù)很短,但能從中獲得很多信息。在虛地址0xc0000000 - 0xc03fffff上映射有進(jìn)程的頁表。并且,映射的機(jī)制非常精巧。在
Directory
Table(以下稱DT)有1100000000b個表項(xiàng)(對應(yīng)于地址0xc000..-0xc03ff..)指向自己,也就是說對于這些地址DT用作了
頁表(Page Table)!如果我們使用,比如說,地址(為方便起見使用二進(jìn)制)
1100000000.0000000101.0000001001.00b
---------- ---------- --------------
0xc0... 頁表選擇 頁表內(nèi)偏移
頁目錄
通過頁表101b的1001b號,我們得到了PTE。但這還沒完——DT本身映射在地址0xc0300000-0xc0300ffc上。在MmSystemPteBase中有值0xc0300000。為什么這樣——看個例子就知道了:
1100000000.1100000000.0000001001.00b
---------- ---------- --------------
0xc0... 0xc0... 頁目錄偏移
頁目錄 頁表-
頁目錄
選擇
最后,在c0300c00包含著用于目錄本身的PDE。這個PDE的基地址的值保存在MmSystemPageDirectory中。同時(shí)系統(tǒng)為映射物理頁MmSystemPageDirectory保留了一個PTE,這就是MmSystemPagePtes。
這樣做能簡化尋址操作。例如,如果有PTE的地址,則PTE描述的頁的地址就等于PTE<<10。反過來:PTE=(Addr>>10)+0xc0000000。
除此之外,在內(nèi)核中存在著全局變量MmKseg2Frame =
0x20000。該變量指示在從0x80000000開始的哪個地址區(qū)域直接映射到了物理內(nèi)存,也就是說,此時(shí)虛擬地址0x80000000 -
0x9fffffff映射到了物理地址00000000-1f000000。
還有幾個有意思的地方。從c0000000開始有個0x1000*0x200=0x200000=2M的描述地址的表(0-7fffffff)。描述這些
頁的PDE位于地址c0300000-0xc03007fc。對于i486,在地址c0200000-c027fffc應(yīng)該是描述80000000到
a0000000的512MB的表,但對于Pentium在區(qū)域0xc0300800-0xc03009fc是4MB的PDE,其描述了從0
到1fc00000的步長為00400000的4M的物理頁,也就是說選擇了4M的頁。對應(yīng)于這些PDE的虛地址為80000000,
9fffffff。
這樣我們就得到了頁表的分布:
范圍 c0000000 - c01ffffc 用于00000000-7fffffff的頁表
范圍 c0200000 - c027ffff "吃掉" 4M地址頁的地址
范圍 c0280000 - c02ffffc 包含用于a0000000 - bfffffff的頁
范圍 c0300000 - c0300ffc PD 本身 (描述范圍c0000000 - c03fffff)
范圍 c0301000 - c03013fc c0400000 - c04fffff HyperSpace (更準(zhǔn)確的說, 是1/4的hyper space)
范圍 c0301400 - c03fffff 包含用于c050000 - ffffffff的頁
注:在0xc0301000-0xc0301ffc包含有描述hyper
space的頁表。這是內(nèi)核的地址空間,且對于不同的進(jìn)程映射的內(nèi)容是不同的(另一方面,內(nèi)核空間又總是在每個用戶進(jìn)程的上下文中)。這是進(jìn)程私有的區(qū)
域。例如,working set就位于hyper space中。頁表的前256個PTE(hyper
space的前1/4)為內(nèi)核保留,而且在需要快速向frame中映射虛擬地址時(shí)使用。
我給出一個向區(qū)域0xc0200000-0xc027f000中一個地址進(jìn)行映射的例子。
1100000000.1000000000.000000000000 = 0xc0200000
1) 解析出 PDE #1100000000 (4k 頁) 并選出 PageDirectory
2) 在 Directory 中選出 PTE #1000000000 (c0300800)
這是個 4MB 的 PDE - 但這里忽略位長度,
因?yàn)?PDE 用作了 PTE. 結(jié)果 c0200000 - c0200fff 被映射為
80000000-80000fff
c0201000 映射到下面的 - 80400000- 80400fff.
等等直到 c027f000 - 9fc00000
PTE, 位于c0200000到c027fffc - 描述了80000000 - 9ffffc00 (512m)
02.Hyper Space
==============
HyperSpace是內(nèi)核空間中的一塊區(qū)域 (4mb),
不同的進(jìn)程映射內(nèi)容不同。對于轉(zhuǎn)換,4MB足夠放下頁表完整的一頁。這個表位于地址0xc0301000 -
0xc0301ffc(PDE的第0個表項(xiàng)位于0xc0300c04)。在內(nèi)部,為向HyperSpace區(qū)域中映射物理頁(當(dāng)需要快速為某個frame
組織虛擬地址時(shí))要使用函數(shù):
DWORD MiMapPageInHyperSpace(DWORD BaseAddr,OUT PDWORD Irql);
它返回HyperSpace中的虛擬地址,這個虛擬地址被映射到所要的物理頁上。這個函數(shù)是如何工作的,工作的時(shí)候用到了什么?
在內(nèi)核中有這樣的變量:
MmFirstReservedMappingPte=0xc0301000
MmLastReservedMappingPte=0xc03013fc
這兩個變量描述了255個pte,這些pte描述了區(qū)域:
0xc0400000-0xc04fffff (1/4 HyperSpace)
在MmFirstReservedMappingPte處是一個pte,其中的基址扮演了計(jì)數(shù)器的角色(從0到255)(當(dāng)然,pte是無效的,p位無
效)。為所需地址添加pte時(shí)要依賴計(jì)數(shù)器當(dāng)前的值……并且計(jì)數(shù)器使用了下開口堆棧的原理,從ff開始。一般來說,頁表中的pte用作信息上的目的并不是
唯一的情況。
03.System PTE'S
===============
在內(nèi)核中有一塊這樣的內(nèi)存——系統(tǒng)pte。什么是系統(tǒng)pte,以及內(nèi)核如何使用系統(tǒng)pte?
*見函數(shù) MiReserveSystemPtes(...)
系統(tǒng)為空閑PTE維護(hù)了某些結(jié)構(gòu)體。首先為了快速滿足密集請求(當(dāng)內(nèi)核需要pte映射某些物理頁時(shí))系統(tǒng)中有個Sytem Ptes
Pool。而且pool中有pte
blocks(blocks表示請求是以block為單位來滿足的,一個block中有一些pte,1、2、4、8和16個pte)。
系統(tǒng)中有以下這些表:
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被組織成了鏈表(當(dāng)然,pte是位于頁表中,也就是說鏈表結(jié)構(gòu)體位于頁表中,這是真的)。鏈表的元素:
typedef struct _FREE_SYSTEM_PTES_BLOCK{
/*pte0*/ SYSPTE_REF
NextRef;
// 指向后面的block
/*pte1*/ DWORD
FlushUnkn; //
在Flush時(shí)使用
/*pte2*/ DWORD ArrayOfNulls[ANY_SIZE_ARRAY]; // 空閑 PTE
}FREE_SYSTEM_PTES_BLOCK PFREE_SYSTEM_PTES_BLOCK;
用作指向后面元素指針的PTE的地址可如此獲得:VA=(NextRef>>10)+MmSystemPteBase
(低10位永遠(yuǎn)為0,相應(yīng)的p位也為0)。鏈表最后一個元素NextRef域的值為0xfffff000 (-1)
。相應(yīng)的,鏈表有5個(block大小分別為1,2,4,8和16個pte)。
*見函數(shù) MiReserveSystemPtes2(...) / MiInitializeSystemPtes
除pool外還有一個undocumented的空閑系統(tǒng)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有可能進(jìn)入System Ptes
Pool。若在請求MiReserveSystemPtes(...)時(shí)pte的數(shù)目大于16,則同時(shí)pte從0號鏈表分配。也就是說,0號鏈表與
pool有關(guān)聯(lián),而1號則沒有。
為了使工作的結(jié)果不與TLB相矛盾,系統(tǒng)要么使用重載cr3,要么使用命令invlpg?!案呒墶焙瘮?shù)
MiFlushPteList(PTE_LIST* PteList, BOOLEAN bFlushCounter, DWORD PteValue);
進(jìn)行以下工作:
初始化PTE并調(diào)用invlpg(匯編指令)。
typedef struct PTE_LIST{
DWORD Counter; // max equ 15
PVOID PtePointersInTable[15];
PVOID PteMappingAddresses[15];
};
如果Counter大于15,則調(diào)用KeFlushCurrentTb(只是重載CR3),并且如果設(shè)置了bFlushCounter,則向MmFlushCounter加0x1000。
04.Page Frame Number Data Base (MmPfnDatabase)
======================================
內(nèi)核將有關(guān)物理頁的信息保存在pfn數(shù)據(jù)庫中(MmPfnDatabase)。本質(zhì)上講,這只是個0x18字節(jié)長的結(jié)構(gòu)體塊。每一個結(jié)構(gòu)體對應(yīng)一個物理頁
(順序排列,所以元素常被稱為Pfn - page frame
number)。結(jié)構(gòu)體的數(shù)量對應(yīng)于系統(tǒng)中4KB頁的數(shù)量(或者說是內(nèi)核可見的頁的數(shù)量,需要的話可以在boot.ini中使用相應(yīng)的選項(xiàng)來為NT內(nèi)核做
出這塊“壞”頁區(qū))。通常,結(jié)構(gòu)體形式如下:
typedef struct _PfnDatabaseEntry
{
union {
DWORD NextRef; // 0x0 如果frame在鏈表中,則這個就是frame的號
// 最后的一個為 -1
DWORD Misc; // 同時(shí)另外一項(xiàng)信息, 依賴于上下文
// 見偽代碼 (通常 TmpPfn->0...)
// 通常這里有 *KTHREAD, *KPROCESS,
// *PAGESUPPORT_BLOCK...
};
PPTE PtePpte; // 0x4 指向 pte 或 ppte
union { // 0x8
DWORD
PrevRef; // 前面的frame或 (-1, 第一個)
DWORD ShareCounter; // Share 計(jì)數(shù)器
};
WORD Flags; // 0xc 見下面
WORD RefCounter; // 0xe 引用計(jì)數(shù)
DWORD Trans; // 0x10 ?? 見下面. 用于 pagefile
DWORD ContFrame;//ContainingFrame; // 14
}PfnDatabaseEntry;
/*
Flags (名字取自windbg !pfn的結(jié)果)
掩碼 位 名字 值
----- ---- --- --------
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的內(nèi)容位于PageFile中的時(shí)候或是frame的內(nèi)容位于與這個Page File PTE對應(yīng)的其它映象文件中的時(shí)候。
我給出未設(shè)置P位的PTE的例子(這種PTE不由平臺體系結(jié)構(gòu)確定,而由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
可能所有這些現(xiàn)在還不很易懂,但是看完下面就能明白了。當(dāng)然,這個結(jié)構(gòu)體是未公開的。顯然,結(jié)構(gòu)體能夠組織成鏈表。frame由以下結(jié)構(gòu)體支持:
struct _MmPageLocationList{
PPfnListHeader ZeroedPageListhead; //&MmZeroedPageListhead
PPfnListHeader FreePageListHead; //&MmFreePageListHead
PPfnListHeader StandbyPageListHead; //&MmStandbyPageListHead
PPfnListHeader ModifiedPageListHead; //&MmModifiedPageListHead
PPfnListHeader ModifiedNoWritePageListHead;//&MmModifiedNoWritePageListHead
PPfnListHeader BadPageListHead; //&MmBadPageListHead
}MmPageLocationList;
這其中包含了6個鏈表。各域的名字很好的說明了它們的用處。frame的狀態(tài)與這些鏈表密切關(guān)聯(lián)。下面列舉了frame的狀態(tài):
+---------------+----------------------------------------------------+------+
|狀態(tài)
|描述
|
鏈表 |
+---------------+----------------------------------------------------+------+
|Zero
|清零的可用空閑頁
| 0
|
|Free
|可用空閑頁
| 1
|
|Standby |不可用但可輕易恢復(fù)的頁
| 2
|
|Modified
|要換出的dirty頁
| 3 |
|ModifiedNoWrite|不換出的dirty頁
| 4 |
|Bad
|不可用的頁(有錯誤)
| 5
|
|Active
|活動頁,至少映射一個虛擬地址
| -
|
+---------------+----------------------------------------------------+------+
frame可能處在6個鏈表中的某一個,也可能不在這些鏈表中(狀態(tài)為Active)。如果頁屬于某個進(jìn)程,則這個頁就被記錄在Working Set中(見后面)。同時(shí),如果frame由內(nèi)存管理器自己使用,則一般可以不考慮這些frame的位置。
每個鏈表的表頭都是下面這個樣子:
typedef struct _PfnListHeader{
DWORD Counter; // 鏈表中frame的數(shù)目
DWORD LogNum; // 鏈表號.0 - zeroed, 1- Free etc...
DWORD FirstFn; // MmPfnDatabase中的第一個frame號
DWORD LastFn; // --//--- 最后一個.
}PfnListHeader PPfnListHeader;
除此之外,可以用“color”(就是cache)來尋址空閑frame(zeroed或是free)。如果看一下附錄中的偽代碼就容易理解了。我給出兩個結(jié)構(gòu)體:
struct {
ColorHashItem* Zeroed; //(-1) нет
ColorHashItem* Free;
}MmFreePagesByColor;
typedef struct _ColorHashItem{
DWORD FrameNum;
PfnDatabaseEntry* Pfn;
} ColorHashItem;
有一套函數(shù)使用color來處理frame(處理cache)。例如,MiRemovePageByColor(FrameNum, Color);
看一下這些函數(shù)及其參數(shù)返回值的名稱和函數(shù)的反匯編代碼,很容易猜到相應(yīng)的內(nèi)容,所以這里就不描述了,在說一句,這些函數(shù)都是未導(dǎo)出的。在使用color
的時(shí)候,要考慮color掩碼,最后選擇color。
Windows
NT符合C2安全等級,所以應(yīng)該在為進(jìn)程分配頁的時(shí)候應(yīng)將頁清零。我們來看一下將frame清零的系統(tǒng)進(jìn)程的線程。最后,在
Phase1Initialization()中所作的是調(diào)用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; // 清標(biāo)志
KeLowerIrql(OldIrql);
}
// 永不退出
}
// 函數(shù)只是將frame映射到定義的地址上
// 以使其可被清零
DWORD MiMapPageToZeroInHyperSpace(FrameNum)
{
if(FrameNum<MmKseg2Frame)return ((FrameNum+0x80000)<<12); // 落入直接映射區(qū)域
TmpPte=0xc0301404;
TmpVa=0xc0501000;
*TmpPte=0;
invlpg((void*)TmpVa); // asm instruction in fact
*TmpPte=FrameNum<<12|ValidPtePte;
return TmpVa; // always 0xc0501000;
}
在何時(shí)MmZeroingPageEvent被激活?這發(fā)生在向空閑頁鏈表中添加frame的時(shí)候:
MiInsertPageInList()
{
.....
if(MmFreePageListHead.Count>=MmMinimumFreePagesToZero&&
!MmZeroingPageThreadActive)
{
MmZeroingPageThreadActive=1;
KeSetEvent(&MmZeroingPageEvent,0,0);
}
....
}
注:內(nèi)核并不總是依賴這個線程,有時(shí)會遇到這樣的代碼,它獲取一個空閑頁,用過后自己將其清零。
05.Working Set
==============
Working Set——工作集,是屬于當(dāng)前進(jìn)程的物理頁集。內(nèi)存管理器使用一定的機(jī)制跟蹤進(jìn)程的工作集。working
set有兩個限額:maximum working set和minimum working
set。這是工作集的最大值和最小值。內(nèi)存管理器以這兩個值為依據(jù)來維護(hù)進(jìn)程的工作集(工作集大小不小于最小值,不大于最大值)。在定義條件的時(shí)候,工作
集被裁減,這時(shí)工作集的frame落入空閑鏈表。內(nèi)核工作集是結(jié)構(gòu)體的總和。
在進(jìn)程結(jié)構(gòu)體的偏移0xc8(NT4.0)有以下結(jié)構(gòu)體。
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的擴(kuò)展命令用到VM。這里重要的是,跟蹤page fault的數(shù)量(PageFaultCount),MaximumWorkingSet和MinimumWorkingSet,管理器以它們?yōu)榛A(chǔ)來支持工作集。
注:實(shí)際上,PageFaultCount并非是嚴(yán)格的計(jì)數(shù)。這個計(jì)數(shù)在MiLocateAndReserveWsle函數(shù)中被擴(kuò)大,因?yàn)檫@個函數(shù)不只在page fault時(shí)被調(diào)用,在某些其它情況下也會被調(diào)用(真的,很少見)。
下面這個結(jié)構(gòu)體描述了包含工作集頁的表。
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的倍數(shù))
#define WSLE_DONOTPUTINHASH 0x400 // 不放在cache中
#define WSLE_PRESENT 0x1 // 非空元素
#define WSLE_INTERNALUSE 0x2 // 被內(nèi)存管理器使用的frame
// 未設(shè)置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函數(shù)很簡單。內(nèi)部函數(shù)的偽代碼:
//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;
//}
我們來看一下典型的進(jìn)程working set。WorkingSetList位于地址MmWorkingSetList
(0xc0502000)。這是hyper
space的區(qū)域,所以在進(jìn)程切換時(shí),要更新這些虛地址,這樣,每個進(jìn)程都有自己的工作集結(jié)構(gòu)體。在地址MmWsle
(0xc0502690)上是Wsle動態(tài)表的起始地址。表的結(jié)尾的地址總是0x1000的倍數(shù),也就是說表可以結(jié)束在地址0xc0503000、
0xc0504000等等上(這是為了簡化對Wsle表大小的操作)。Cache(如果有)位于一個偏移上,Wsle不會向這個偏移增長。我們來詳細(xì)看一
下這個表:
// 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。同時(shí)這里還有頁目
錄frame,HyperSpace和某些其它的頁,這些頁是內(nèi)存管理器所需要的,不能從工作集中移出(標(biāo)志W(wǎng)SLE_INTERNALUSE)。之后,
我們還能看到兩種對Pfn frame域偏移0使用的變體。對于頁目錄frame,這是指向進(jìn)程的指針,對于通常的屬于工作集的頁,這是在表內(nèi)的索引。
在WorkingSetList和Wsle表的起始地址之間還有不大的0x660字節(jié)的空閑空間。關(guān)于如何分配這些空間的信息是沒有的,但是很快在
WorkingSetList開始有用于用戶空間(通常為低2GB)的page fault
counter,也就是說如果,譬如說,索引0x100的元素有值3,則表示從3開始(如果不考慮可能的溢出)page
fault用于范圍[0x40000000-0x403fffff]的頁。
工作集的限額在內(nèi)核模式下可以通過導(dǎo)出的未公開函數(shù)來修改:
NTOSKRNL MmAdjustWorkingSetSize(
DWORD MinimumWorkingSet OPTIONAL, // if both == -1
DWORD MaximumWorkingSet OPTIONAL, // empty working set
PVM Vm OPTIONAL);
為處理WorkingSet,管理器使用了許多內(nèi)部函數(shù),了解了這些函數(shù)就能明白其工作的原理。
06.向pagefile換頁
========================================
frame可以是空閑的——當(dāng)RefCounter等于0且位于一個鏈表中時(shí)。frame可以屬于工作集。在缺少空閑frame時(shí)或是在達(dá)到treshhold時(shí),就會發(fā)生frame的換出。這方面的高層次函數(shù)是有的。這里的任務(wù)是用偽代碼來證實(shí)。
在NT中有最多16個pagefile。pagefile的創(chuàng)建發(fā)生于模塊SMSS.EXE。這時(shí)打開文件及其句柄向
PsInitialSystemProcess進(jìn)程的句柄表拷貝。我給出創(chuàng)建pagefile的未公開系統(tǒng)函數(shù)的原型(如果不從核心調(diào)用的話就必須有創(chuàng)建
這種文件的權(quán)限)。
NTSTATUS NTAPI NtCreatePagingFile(
PUNICODE_STRING FileName,
PLARGE_INTEGER MinLen, // 高位雙字應(yīng)為0
PLARGE_INTEGER MaxLen, // minlen應(yīng)大于1M
DWORD Reserved // 忽略
);
每個pagefile都有一個PAGING_FILE結(jié)構(gòu)體。
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];
在內(nèi)存子系統(tǒng)啟動時(shí)(MmInitSystem(...))會啟動線程MiModifiedPageWriter,該線程進(jìn)行以下工作:初始化
MiPaging和
MiMappedFileHeader,在非換出域中創(chuàng)建并初始化MmMappedFileMdl,建立優(yōu)先級
LOW_REALTIME_PRIORITY+1,等待KEVENT,初始化MmMappedPageWriterEvent和
MmMappedPageWriterList鏈表,啟動MiMappedPageWriter線程,啟動函數(shù)
MiModifiedPageWriterWorker。
在任務(wù)MiModifiedPageWriterWorker中會等待事件MmModifiedPageWriterEvent,處理鏈表
MmModifiedNoWritePageList和MmModifiedPageList并準(zhǔn)備實(shí)現(xiàn)向映象文件或pagefile的頁換出(調(diào)用
MiGatherMappedPages或是MiGatherPagefilePages)。
在MiGatherPagefilePages中使用IoAsynchronousPageWrite(
)函數(shù)進(jìn)行frame的換出。而且不是一個frame,而是一簇(頁數(shù)目總和為MmModifiedWriteClasterSize)。向
pagefile換出頁是由PAGING_FILE結(jié)構(gòu)體中的PagefileMap來跟蹤的。
研究函數(shù)的偽代碼在appendix.txt中。這里描述偽代碼沒有什么意義——都很簡單。
07.page fault的處理
==============================
對于轉(zhuǎn)向?qū)agefault的研究,我們現(xiàn)在有了所有必須的信息了。轉(zhuǎn)換線性地址時(shí),當(dāng)線性地址(分頁機(jī)制打開)的所用的PDE/PTE的P
(present)位無效或是違反了保護(hù)規(guī)則,在+i386處理器里會產(chǎn)生異常14。這時(shí),在堆棧中有錯誤代號,包含有以下信息:用戶/內(nèi)核錯誤位(異常
發(fā)生在ring3還是ring0?),讀寫錯誤位(試圖讀還是寫?),頁存在位。除此之外,在CR2寄存器中存有產(chǎn)生異常的32位線性地址。內(nèi)核中處理
14號中斷的是_KiTrap0E。
當(dāng)要轉(zhuǎn)換的頁沒有相應(yīng)的物理頁時(shí),內(nèi)存管理器執(zhí)行確定好的工作來“修正”。這些是由異常處理函數(shù)調(diào)用高層函數(shù)MmAccessFault
(Wr,Addr,P);來完成的。在對偽代碼的進(jìn)行分析之前,想一下在什么樣的情況下會發(fā)生page fault是很有用的。
最顯然的就是訪問錯誤,這時(shí)ring3的代碼試圖寫入PTE/PDE中未設(shè)置U位的頁或是寫入了只讀的頁(PTE/PDE中未設(shè)置W位)。再有,頁可以被
換出到頁面文件中,對應(yīng)于這些頁的PTE中未設(shè)置P位,但有信息指示在哪個頁面文件中尋找frame,以及frame的偏移。還有一個類似的情況——
frame屬于映象文件。除此之外,所轉(zhuǎn)換的頁可能只屬于已分配的內(nèi)存區(qū)(使用NtAllocateMemory),也可能轉(zhuǎn)換的是原先沒轉(zhuǎn)換過的頁,這
中情況下,VMM分配清零過的frame(這是C2的要求)。最后,異常還可能是由寫copy on
write頁和轉(zhuǎn)換共享內(nèi)存引發(fā)。以上只列出了主要的情況。
處理的結(jié)果通常是向當(dāng)前進(jìn)程的Working Set中添加相應(yīng)的frame。
異常的每一種情況都相應(yīng)有一個內(nèi)部的結(jié)構(gòu)體與之相關(guān)聯(lián),VMM就處理這些結(jié)構(gòu)體。這些結(jié)構(gòu)體十分復(fù)雜,要對它們進(jìn)行完整的描述的話,需要反匯編大量的函
數(shù)。目前還沒有大部分結(jié)構(gòu)體的完整信息,但對于理解異常處理程序來說并不要求知道這些。我來大致描述一下VAD和PPTE的概念,研究異常處理程序的偽代
碼要用到。
VAD
操作虛擬地址需要用到VAD (Virtual Address
Descriptor)。我們熟知的(有一個幾乎與之同名的Win32函數(shù)調(diào)用這個函數(shù))未公開函數(shù)NtAllocateVirtualMemory
(ring0下是ZwAllocateVirtualMemory)操作這些結(jié)構(gòu)體。
每一個VAD都描述了虛地址空間中的區(qū)域,實(shí)際上,除了區(qū)域的起止地址外還有保護(hù)信息(見ZwAllocateVirualMemory函數(shù)的參數(shù))。而
同時(shí)還有其它一些特殊的信息(目前除了首部之外還沒有VAD的完整信息)。VAD結(jié)構(gòu)體只對用戶地址(低2GB)有意義,使用這些結(jié)構(gòu)體VMM可以捕獲到
發(fā)生異常的區(qū)域。VAD的結(jié)構(gòu)是一個平衡二叉樹(有內(nèi)部函數(shù)負(fù)責(zé)修整此樹),這是為查找而進(jìn)行的優(yōu)化。在VAD中有兩個指向后面元素——左右子樹——的指
針。樹的根位于EPROCESS結(jié)構(gòu)體的VadRoot域(NT
4.0下是偏移0x170)。當(dāng)然,每一個進(jìn)程都有自己的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是又一級的線性地址轉(zhuǎn)換并用于共享內(nèi)存。假設(shè)有個文件映射到了幾個(3個)進(jìn)程的地址空間。PPTE表包含有PPTE,這些PPTE描述了加載到內(nèi)
存的文件的物理頁。某些PPTE可以有P位(其位置與含義與PTE/PDE的相同),而某些則沒有,沒有P位的有信息用來決定是從頁來加載frame還是
從映象文件來加載文件。所有三個進(jìn)程的文件都映射在不同的地址上,對應(yīng)于這些頁的PTE的P位未設(shè)置,并且包含有文件頁的PPTE的引用。這樣,在轉(zhuǎn)換映
射到文件的線性地址的時(shí)候,在一號進(jìn)程中發(fā)生異常14,VMM找到PTE,得到對PPTE的引用,現(xiàn)在可以直接“修正”相應(yīng)的PTE,以使其指向?qū)儆谖募?
的frame,這時(shí)必需從文件中加載frame。我給出未設(shè)置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)
參數(shù)的意義很明顯:寫入標(biāo)志,發(fā)生異常的地址和頁存在位。對于確定異常的原因,這些信息就足夠了。根據(jù)Addr是屬于內(nèi)核地址空間還是用戶地址空間,處理
程序從兩個執(zhí)行分支中選擇一個。第一種情況下的處理程序較為簡單,跟蹤ACCESS VIOLATION或是收回在Working
Set中的頁(MiDispatchFault)。若是用戶空間的地址情況就就更為復(fù)雜一些。首先,如果PDE不在內(nèi)存中則執(zhí)行用于PDE的異常處理程
序。然后,出現(xiàn)了一個分支。第一個分支——頁存在。這表示要么是ACCESS VIOLATION,要么就是對copy on
write的處理。第二個分支——處理清零頁請求、ACCESS VIOLATION、頁邊界(GUARD)(堆棧增長)以及必須的對working
set中頁的回收。有趣的是,在大量發(fā)生page fault的時(shí)候,系統(tǒng)會增大working
set的大小。在零PTE的情況下,為確定狀況,處理程序不得不使用VAD樹來確定試圖訪問區(qū)域的屬性。這些都是MiAccessCheck的工作,這個
函數(shù)返回訪問的狀態(tài)。
一般情況下,異常處理程序的主要奠基工作是由MiDispatchFault函數(shù)執(zhí)行的。它能更精確的確定狀況并決定下一步的工作。
輪到MiDispatchFault了,它主要是基于一些更低級的函數(shù):MiResolveTransitionFault、
MiResolveDemandZeroFault、MiResolveDemandZeroFault、MiResolveProtoPteFault
和MiResolvePageFileFault。從這些函數(shù)的名字可以明顯看出,這個函數(shù)用于確定更為具體的情況:狀態(tài)為'transition'(可
能會很快回收入Working
Set)的頁應(yīng)該是空白的frame,PTE指向PPTE并且frame換出到相應(yīng)的頁面文件中。在與頁面文件有關(guān)的和某些與PPTE有關(guān)的情況下,接著
可能需要從文件中讀取frame,此時(shí)函數(shù)返回值為0xc0033333,表示必須從文件中讀取頁。這在MiDispatchFault中是靠
IoPageRead進(jìn)行的。我們來更仔細(xì)的研究一下所提到的函數(shù)。我們從MiResolveDemandZeroFault開始。
如果看一下這個函數(shù)的偽代碼,則可以輕易的明白它的工作邏輯。請求zero
frame并且進(jìn)程得到這個frame。這時(shí)執(zhí)行函數(shù)MiRemoveZeroPage或是MiRemoveAnyPage。第一個函數(shù)從zero頁的鏈
表中取一頁。如果未能成功,則通過第二個函數(shù)選擇任何一頁。這樣的話,該頁就由MiZeroPhysicalPage來清零。最終,在
MiAddValidPageToWorkingSet中,該清零的頁被添加到工作集中(恰好,這個事實(shí)證明在分配內(nèi)存時(shí)進(jìn)程不能取得對未處理頁的訪
問)?,F(xiàn)在我們來研究一下更為復(fù)雜的情況——頁位于頁面文件中。
前面的偽代碼需要一個結(jié)構(gòu)體。在準(zhǔn)備從文件中讀取頁的時(shí)候,會填充PAGE_SUPPORT_BLOCK結(jié)構(gòu)體。之后,對所有即將參與到操作中來的PFN
進(jìn)行以下操作:設(shè)置read in
progress標(biāo)志并在Misc域中寫入PAGE_SUPPORT_BLOCK的地址(函數(shù)
MiInitializeReadInProgressPfn)。最后,函數(shù)返回magic number
0xc0033333,表示隨后要在IoPageRead調(diào)用中使用此結(jié)構(gòu)體(恰巧,IoPageRead被導(dǎo)出了,但是未公開的。從其偽碼中可以很容易
地得到其原型)。
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有關(guān)的鏈表
}PAGE_SUPPORT_BLOCK *PAGE_SUPPORT_BLOCK;
struct _MmInPageSupportList{
LIST_ENTRY PageSupportList;
DWORD Count;
}MmInPageSupportList;
函數(shù)MiResolvePageFileFault本身非常簡單,除了填充相應(yīng)的結(jié)構(gòu)體并返回0xc0033333之外什么也不干。剩下的就是執(zhí)行MiDispatchFault。這很合乎情理,如果還記得復(fù)用代碼的原則的話。
還有一個不太復(fù)雜的函數(shù)MiResolveTransitionFault。對于狀態(tài)為transition的frame還需要再多說幾句。從這個狀態(tài)中frame可以很快地返回到進(jìn)程的Working Set中。
于是,剩下了最后一種情況——PROTO
PTE。這種情況的處理函數(shù)也不太復(fù)雜,而且支撐其的基礎(chǔ)我們已經(jīng)講過了。實(shí)際上還有一個函數(shù)與這種情況有關(guān),這就是
MiCompleteProtoPteFault,從MiDispatchFault中調(diào)用。要想理解這些函數(shù)的工作就去看一下偽代碼。
07. section 對象
================
NT 中的section對象就是一塊內(nèi)存,這塊內(nèi)存由一個進(jìn)程獨(dú)有或幾個進(jìn)程共享。在Win32子系統(tǒng)中section就是文件映射(file mapping object)。我們來看一下section對象到底是什么。
section是NT下非常常用的對象,執(zhí)行系統(tǒng)使用section來將可執(zhí)行映象加載到內(nèi)存中并用其來管理cache。section同時(shí)也用在向進(jìn)程
地址空間中映射文件。這時(shí)訪問文件就像訪問內(nèi)存。section對象,就像其它的對象一樣,是由對象管理器創(chuàng)建的。高層次的信息告訴我們,對象的body
中包含著以下類型的信息:section的最大值,保護(hù)屬性,其它屬性。什么是section的最大可訪問值,這不說也知道。保護(hù)屬性是用于
section頁的屬性。其它section屬性有表示是文件section還是為空值(映射入頁面文件)的標(biāo)志,以及section是否是base的。
base的section以相同的虛擬地址映射入所有進(jìn)程的地址空間。
為了得到此對象結(jié)構(gòu)的真實(shí)信息,我反匯編了一些用于section的內(nèi)存管理器函數(shù)。下面的信息可是在別的地方見不到的。我們先來看結(jié)構(gòu)體。
系統(tǒng)中的每一個文件都是對象(NTDDK.H中有描述)FILE_OBJECT。在這個結(jié)構(gòu)體中有SectionObjectPointer。NTDDK.H中同樣有它的結(jié)構(gòu)。
//
:
PSECTION_OBJECT_POINTERS SectionObjectPointer;
:
//
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject;
PVOID SharedCacheMap;
PVOID ImageSectionObject;
} SECTION_OBJECT_POINTERS;
在結(jié)構(gòu)體中有兩個指針——DataSectionObject 和
ImageSectionObject。NTDDK.H把它們寫成了PVOID,因?yàn)樗鼈円玫氖俏垂_的結(jié)構(gòu)體。DataSectionObject用
在將文件作為數(shù)據(jù)打開的時(shí)候。ImageSectionObject——此時(shí)當(dāng)作映象。這些指針的類型全都一樣,且可以稱之為
PCONTROL_AREA。所有下面這些結(jié)構(gòu)體都是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形成了一個鏈表,結(jié)構(gòu)體中包含著統(tǒng)計(jì)值和標(biāo)志。為了理解標(biāo)志所代表的信息,我給出它們的值(用于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的數(shù)目Subsections。每一個Subsection都描述了關(guān)于具體的文件映射
section的信息。例如,read-only, read-write,
copy-on-write等等的section。NT5.0的SUBSECTION結(jié)構(gòu)體:
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的指針,標(biāo)志,指向base Proto PTE的指針,Proto PTE的數(shù)目。StartingSector是4K block的編號,文件中的section起始于此。在標(biāo)志中還有額外的信息:
#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
我們來看剩下的最后一個結(jié)構(gòu)體SEGMENT,它描述了所有的映射和用于映射section的Proto PTE。SEGMENT的內(nèi)存是從paged pool中分配的。我給出SEGMENT結(jié)構(gòu)體(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;
正如我所料,結(jié)構(gòu)體包含著對CONTROL_AREA的引用,指向Proto
PTE的pool的指針和所有section的信息。有個東西需要解釋一下。結(jié)構(gòu)體的樣子依賴于是否支持PAE。PAE就是Physical
Address Extenion。從第5版開始,Windows
NT包含了支持PAE的內(nèi)核Ntkrnlpa.exe。總的來講,支持PAE就意味著在NT里可以使用的虛擬地址不是4GB而是64GB。在使用PAE時(shí)
的地址轉(zhuǎn)換又多了一級——所有的虛地址空間被分為4部分。在打開PAE時(shí)PTE和PDE的大小不是4B而是8B,這我們可以從SEGMENT結(jié)構(gòu)體中看
出?,F(xiàn)在還不需要進(jìn)一步詳細(xì)的講PAE,畢竟很少用到,所以我們就此打住。
描述section的所有結(jié)構(gòu)體都介紹過了,而section對象結(jié)構(gòu)體本身還沒有提到。從直觀上可以想到,它應(yīng)該會引用到SEGMENT或是
CONTROL_AREA,因?yàn)橛辛诉@兩個結(jié)構(gòu)體后就可以得到保存的所有信息。通過反匯編得到的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
我們看到,所得的結(jié)構(gòu)體完全符合現(xiàn)有的高層信息的描述。唯一可能有疑問的就是VAD_HEADER。它描述了base
section在地址空間中的位置。VAD_HEADER位于頂點(diǎn)為_MmSectionBasedRoot的VAD樹中。我們再次體會到,要理解操作系
統(tǒng)的工作原理,就要理解其內(nèi)部的結(jié)構(gòu)。為了有一個總體上的把握,下面給出了描述section的結(jié)構(gòu)體間互相聯(lián)系的一個圖。
SECTION_OBJECT->SEGMENT<->CONTROL_AREA->FILE_OBJECT->SECTION_OBJECT_POINTERS+
^ |
+--------------------------------------------+
08.從內(nèi)存管理器角度看進(jìn)程的創(chuàng)建
====================================================
前面我們從Win32角度介紹過進(jìn)程的創(chuàng)建,也講過內(nèi)存管理器和對象管理器的工作原理,以及section對象結(jié)構(gòu)體?,F(xiàn)在最有意思的當(dāng)然就是在進(jìn)程創(chuàng)建中將內(nèi)存管理器也考慮進(jìn)來。
進(jìn)程是用未公開的系統(tǒng)調(diào)用NtCreateProcess()創(chuàng)建的。下面給出其偽代碼:
/*****************************************************************/
/* -- 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是對另一個內(nèi)部函數(shù)PspCreateProcess的封裝。NtCreateProcess進(jìn)行的唯一工作就
是檢查Parent(父進(jìn)程句柄)。但是接下來我們看到,對于NT來說這并沒有什么意義,因?yàn)榭偟膩碚f,進(jìn)程的繼承性本身沒有特別的意義?,F(xiàn)在我們來看
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
);
我很快注意到,函數(shù)中的Parent參數(shù)可以接受值0,這就表明在NtCreateProcess中檢驗(yàn)此參數(shù)是為了限制用戶模式。函數(shù)的參數(shù)中有對
section、debug port和exception
port、父進(jìn)程的引用。通過調(diào)用ObReferenceObjectByHandle,可以得到指向這些對象的指針。實(shí)際上父進(jìn)程句柄通常傳遞的是-
1,這表示是當(dāng)前進(jìn)程。如果Parent等于0,則進(jìn)程的affinity就不從父進(jìn)程處取得,而是從系統(tǒng)變量中取得。
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;
}
優(yōu)先級總是為8。隨后,創(chuàng)建進(jìn)程對象。NT4.0下其大小為504字節(jié)。
// 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(見對象管理器的相關(guān)介紹)。
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;
}
之后,調(diào)用MmCreateProcessAddressSpace,創(chuàng)建地址上下文。參數(shù)是函數(shù)得到的指向進(jìn)程的指針、工作集的大小和指向結(jié)果結(jié)構(gòu)體的指針。這個結(jié)構(gòu)體形式如下:
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);
我們看到,函數(shù)向我們返回的是頁表的物理地址描述符(用于新地址空間的CR3的內(nèi)容),Hyper Space的頁地址和工作集的頁地址。在此之后是初始化進(jìn)程對象的某些域:
pProcess->MinimumWorkingSet=MinWorkingSet;
pProcess->MaximumWorkingSet=MaximumWorkingSet;
KeInitializeProcess(pProcess,Prior,AffinityMask,&CASResult,pProcess->
DefaultHardErrorProcessing&0x4);
pProcess->ForegroundQuantum=PspForegroundQuantum;
如果有父進(jìn)程且設(shè)置了標(biāo)志參數(shù),則會繼承父進(jìn)程的句柄表:
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執(zhí)行系統(tǒng)的靈活性,從表面上是看不出來的。如果在參數(shù)中有指定的section,則使用這個section來初始化進(jìn)程的地址空間,否則其工作就會像*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將進(jìn)程插入Active
Process鏈表,創(chuàng)建Peb和做其它輔助性的工作。我們不再贅述。最后,當(dāng)所有的工作都做完后,進(jìn)行安全子系統(tǒng)方面的工作。我們過去曾研究過安全子系
統(tǒng)(見對象管理器部分),所以這里只簡單的給出其偽代碼。只是我注意到,如果父進(jìn)程是system(句柄值等于
PspInitialSystemProcessHandle),則不對其安全性進(jìn)行檢驗(yàn)。
// 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);
最有意思的是函數(shù)KeInitializeProcess和MmCreateProcessAddressSpace。前一個函數(shù)除了初始化進(jìn)程對象的其它成員之外,還要初始化TSS中的IO位圖的偏移。
pProcess->IopmOffset=0x20ad; // IOMAP BASE!!!
//
You can patch kernel here and
//
got i/o port control ;)
偏移的選取是這樣的,它指向I/O位圖,這樣就能阻止進(jìn)程直接使用I/O端口。
在函數(shù)MmCreateProcessAddressSpace中進(jìn)行的是進(jìn)程地址空間的創(chuàng)建。我就不給出所有的偽代碼了,只簡要的寫寫主要的操作。它為
Hyper Space, Working Set和Page Directory選擇頁。反匯編后的代碼證實(shí)了,它們是從zero
frame鏈表中選出或是由MiZeroPhysicalPage函數(shù)來清零的。之后初始化新創(chuàng)建的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拷貝到內(nèi)核地址空間中去(其對所有的進(jìn)程不變,Hyper Space除外),而且是拷貝到不可換出的區(qū)域。同時(shí)這個空間是屬于系統(tǒng)cache的。
09.上下文切換
==========================
知道了ETHREAD、EPROCESS結(jié)構(gòu)體和內(nèi)存管理器的工作原理,就不難猜到上下文切換時(shí)會發(fā)生什么。Windows
NT的設(shè)計(jì)者使用線程,不關(guān)心共享的是誰的地址空間,也就是說有兩種可能:線程屬于當(dāng)前進(jìn)程——必需要切換到另一個線程(更新堆棧并更換GDT描述符),
而線程屬于另一個進(jìn)程,必需切換到那個進(jìn)程(重新加載CR3)。對此,為了證實(shí)我的推測,我反匯編了KeAttachProcess函數(shù)。這個函數(shù)是未公
開的,但所有已知的函數(shù)都用其來切換到另一進(jìn)程的地址空間。通過KeDetachProcess可以返回到當(dāng)前進(jìn)程。KeAttachProcess使用
下述內(nèi)部函數(shù):
KiAttachProcess - KeAttachProcess僅僅是對這個函數(shù)的封裝
KiSwapProcess - 更換地址空間。(本質(zhì)上就是重新加載CR3)
SwapContext - 更換上下文。一般不管地址空間的切換,只調(diào)整線程上下文。
KiSwapThred - 切換到鏈表中的下一個線程(SwapContext)調(diào)用
下面給出這些內(nèi)部函數(shù)的偽代碼。
-----------------------------------------------------------------------------
/************************ 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;
}
從這個函數(shù)可以得到以下結(jié)論。進(jìn)程可以處于以下狀態(tài)——0(準(zhǔn)備),1(Idle),2(Trans——切換)。這證實(shí)了高層次的信息。KiAttachProcess使用了另外兩個函數(shù)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;
}
切換進(jìn)程上下文。正如我所料,這個函數(shù)只是重新加載CR3寄存器,再加上一點(diǎn)相關(guān)的操作。例如,用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。如果線程屬于當(dāng)前進(jìn)程,則不進(jìn)行上下文切換。否則,進(jìn)行的操作和KiSwapProcess中的大致差不多。
為了一致,我給出KeDetachProcess的原型。
KeDetachProcess(EPROCESS *Process,Irql);
我們看到——這些函數(shù)的偽碼實(shí)際上完全描述出了操作系統(tǒng)的上下文切換??偟恼f來,代碼分析表明,理解OS的主要途徑就是要知道它的內(nèi)部結(jié)構(gòu)。
0a.某些未公開的內(nèi)存管理器函數(shù)
==========================================================
SP3的ntoskrnl.exe的內(nèi)存管理器導(dǎo)出了以下符號:
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
這里的符號'+'表示函數(shù)在DDK中有記載。我這里給出某些未公開函數(shù)的原型。
// 調(diào)整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; //標(biāo)準(zhǔn)的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的范圍標(biāo)記為更改并進(jìn)行相應(yīng)的操作
NTOSKRNL VOID MmSetAddressRangeModified(
PVOID StartAddress,
DWORD Length
);
// 在NtMapViewOfSection中調(diào)用
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 //棧頂?shù)牡刂?br>
);
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.結(jié)語
=============
就到這里吧。如果綜合的來看所有這些描述,對內(nèi)存管理器多少可以得到一些概念。遺憾的是,這些東西還遠(yuǎn)不能稱之為完整。內(nèi)存管理器,大概是最復(fù)雜和最重要
的內(nèi)核組件,對其要進(jìn)行完整的描述,我還得深挖不止十個八個的函數(shù)。但是主要的基本的東西我這里都寫到了。對于進(jìn)一步反匯編內(nèi)核來說,這些應(yīng)該是很有幫助
的吧,誰知道呢... ;)
Best Regards, Peter Kosy aka Gloomy.
Melancholy
Coding '2001.
mailto:gl00my@mail.ru
P.S. 我知道我的“大作”不可避免的會有錯誤。我將非常高興的聽取批評和建議。
附錄
0c.某些未公開的系統(tǒng)調(diào)用
==================================================
這里我描述了一些有用的Zw/Nt函數(shù),這些函數(shù)可以在USER模式下或是驅(qū)動程序中調(diào)用(Zw類的)。幾乎所有這些函數(shù)都來自于
Коберниченко的“Недокументированные возмождности Windows
NT”一書。再加上Working
Set結(jié)構(gòu)體的值,就可以描述用于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結(jié)構(gòu)體,也就不能寫出相應(yīng)的INFO結(jié)構(gòu)。
} 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.
否則開始線程間的協(xié)作。
選擇time out值(如果請求>=10頁,則為20秒),否則為-1秒。接著填充某個結(jié)構(gòu)體,大概是這個樣子:
typedef struct _REQUEST_FOR_COMMITED_MEMORY{
LIST_ENTRY ListEntry;
DWORD PagesToCommit;
DWORD Result;
KSEMAPHORE Semaphore;
}_REQUEST_FOR_COMMITED_MEMORY;
這個結(jié)構(gòu)體(或鏈表的元素)被插入到全局結(jié)構(gòu)體中的全局鏈表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,則再校驗(yàn)一次Limit并輸出OK(如果limit有問題——則所有都重新開始)。如果結(jié)果為0,MiCouseOverCommitPopup。如果發(fā)生了time out,分析如下:
如果ListOfReques.Flink==&ListOfReques.Flink,也就是說所有的請求都在隊(duì)列的尾部,則再一次等待信號量——并且已經(jīng)沒有time out了,因?yàn)椴皇俏覀兊膯栴};)
如果ListOfReques.Flink==&RequestForCommitedMemory.ListEntry,就是說隊(duì)列中的下一個是我們的請求(???)。則從隊(duì)列中收回請求,因?yàn)?br>
是從我們這里來的。
現(xiàn)在來看我們想看的幾個頁。如果>=10則MiCouseOverCommitPopup,否則MiChargeCommitmentCantExpand,之后輸出。
所有的操作都需要cli sti,同時(shí)使用FastMutex(進(jìn)程的10ch偏移),在進(jìn)程創(chuàng)建時(shí)調(diào)用這個函數(shù)不會進(jìn)行此操作。
現(xiàn)在,MiCouseOverCommitPopup(PagesNum,CommitTotalLimitDelta);又做些什么呢——如果我們想要
頁數(shù)大于128——則ExRaiseStatus(STATUS_COMMITMENT_LIMIT);
如果小于則IoRaiseInformationalHardError(STATUS_COMMITMENT_LIMIT,0,0);(這些函數(shù)都是公
開的)。如果成功調(diào)用最后一個函數(shù)——則累加statistic:
MiOverCommitCallCount++;
MmTotalCommitLimit+=CommitTotalLimitDelta;
MmExtendedCommit+=CommitTotalLimitDelta;
MmTotalCommittedPages+=PagesNum;
且不修正 MmPeakCommintment;
如果不成功但MiOverCommitCallCount==0,所有都等于statistic,否則ExRaiseStatus(STATUS_COMMITMENT_LIMIT);
輔助函數(shù):
DWORD NTOSKRNL RtlRandom(PDWORD Seed);
不奇怪,這個函數(shù)沒有公開。該函數(shù)使用一個128個DWORD的表。在操作之后被此表和Seed被修正??梢钥吹?,這給出了最大周期。
如果有兩個event
MmAvailablePagesEventHigh 和
MmAvailablePagesEventHigh.
MiSectionInitialization:
MmDereferenceSegmentHeader: это структура описанная выша с добавленным
spinlock сверху.
創(chuàng)建線程MiDereferenceSegmentThread
PsChargePoolQuota(PVOID Process,DWORD Type(NP/P),DWORD Charge);
[TO DO] -->> MmInfoCounters!!!! 使用相應(yīng)的NtQueryInfo...可以獲得非常多有用的信息,ПОСМОТРЕТЬ!!!