前言
几年前就已经看过wrk中关于内存管理和缓存管理的实现,由于当时对内核调试尚不熟悉,因此仅仅停留在代码层面。现在结合windbg操作,希望能有新的收获。毛德操的<windows内核情景分析>对理解windows内核确有裨益,但是,ReactOS对内存管理和缓存管理部分的实现与wrk相去甚远(ReactOS内存管理更接近于Linux内存管理),因此这些的代码应以wrk为准。
正文
<windows内核原理与实现>提到一个系统PTE区域的概念,开始时不理解这个区域的作用,查了一下wrk的源码总算有所收获并记录于此。
使用和归还系统PTE通过MiReserveSystemPtes/MiReleaseSystemPtes来实现。搜索源码中对MiReserveSystemPtes函数的引用即可大概了解系统PTE区域的作用:
//请求物理页
PageFrameNumber = MiGetPageForHeader (TRUE);
...
BasePte = MiReserveSystemPtes (1, SystemPteSpace); //1)
ASSERT (BasePte->u.Hard.Valid == 0); //1.5) ASSERT语句说明系统PTE区域中所有的PTE都是无效PTE
Base = MiGetVirtualAddressMappedByPte (BasePte); //2)
//映射物理页
TempPte = ValidKernelPte; //3)
TempPte.u.Hard.PageFrameNumber = PageFrameNumber;
MI_WRITE_VALID_PTE (BasePte, TempPte);
...
DosHeader = (PIMAGE_DOS_HEADER) Base;
这段代码摘自MiCreateImageFileMap,用于创建文件映射。代码中首先获得可用的物理页,然后准备将文件内容映射到物理页上。但是windows运行在保护模式下,CPU发出的内存读写请求全是基于虚拟地址,因此,要将刚才获得的物理页面映射到一个虚拟页面上。系统PTE区域提供了这种牵线搭桥的功能:首先,MiReserveSystemPtes从系统PTE区域中获得一个无效PTE(注释1)处);其次,从无效PTE反向获取其对应的虚拟页面的基址Base(注释2),对于未开启PAE的32位系统,就是将PTE地址左移10bit(页面自映射)。最后,将物理页面的页帧号写到刚刚获得的无效PTE中(见注释3)),这也是建立页面映射的过程。之后CPU读写Base的操作就等于从物理页面上读写数据。
从上面的过程可以看出,系统PTE区域的作用就是向内核其他组件提供临时的PTE,而PTE和虚拟页面往往是密不可分的(左移/右移就可实现相互转换),进一步说,系统PTE区域的作用是为系统提供虚拟页面。
了解了系统PTE区域的作用后,再来看看系统PTE区域的管理方式。可能你会觉得系统PTE区域是以PTE数组的形式管理区域中(当然这个区域位于页表中)的PTE;其实不然,windows以单链表的形式管理系统PTE区域。链表头由全局变量MmFirstFreeSystemPte 指向,并用全局变量MmSystemPtesStart/MmSystemPtesEnd指定系统PTE区域的起止范围:
kd> x nt!MmSystemPtesStart
8055bde8 nt!MmSystemPtesStart = <no type information>
kd> x nt!MmSystemPtesEnd
8055bde0 nt!MmSystemPtesEnd = <no type information>
kd> dd 8055bde8 L1 ;系统PTE区域的开始位置位于0xc03b5000
8055bde8 c03b5000
kd> dd 8055bde0 L1 ;系统PTE区域的结束位置位于0xc03dfe34
8055bde0 c03dfe34
kd> x nt!MmFirstFree* ;搜索空闲系统PTE的链表头符号
805609c0 nt!MmFirstFreeSystemPte = <no type information>
8055bffc nt!MmFirstFreeSystemCache = <no type information>
kd> dd 805609c0 L1 ;空闲链表头位于0x805609c0,它指向的首个空闲系统PTE的值为0xed400000
805609c0 ed400000
windbg指出的系统PTE起止值的确位于页表区域中,但是nt!MmFirstFreeSystemPte指出的首个空闲系统PTE的值却明显在页表范围之外。哪里出错了?我核对<windows内核原理与实现>发现里面提到nt!MmFirstFreeSystemPte保存的是空闲系统PTE的内存地址。看来书和实际情况有出路了...
回到wrk的源码,我发现了这样的代码:
摘自:MiReserveAlignedSystemPtes
...
PointerPte = &MmFirstFreeSystemPte[SystemPtePoolType];
Previous = PointerPte;
//MmSystemPteBase: _MMPTE:0xc0000000
PointerPte = MmSystemPteBase + PointerPte->u.List.NextEntry; //<---------------------1)
while (TRUE) {
PointerFollowingPte = PointerPte + 1;
//对于有多个pte的块,第二个pte的nextentry记录了这个块中
//pte的数量
SizeInSet = (ULONG_PTR) PointerFollowingPte->u.List.NextEntry;
if (NumberOfPtes < SizeInSet) {
//
// Get the PTEs from this set and reduce the size of the
// set. Note that the size of the current set cannot be 1.
//
if ((SizeInSet - NumberOfPtes) == 1) {
//
// Collapse to the single PTE format.
//
PointerPte->u.List.OneEntry = 1;
}
else {
//
// Get the required PTEs from the end of the set.
//
PointerFollowingPte->u.List.NextEntry = SizeInSet - NumberOfPtes;
}
MmTotalFreeSystemPtes[SystemPtePoolType] -= NumberOfPtes;
这段代码摘自MiReserveAlignedSystemPtes,代码的意图是:每次调用MiReserveSystemPtes时(暂时先这么解释,对于系统PTE中间还有一步,后面会提到),会从空闲链表nt!MmFirstFreeSystemPte的末尾找出一段连续的系统PTE(即一段连续的虚拟页面),并将它的地址返回给调用者。大家注意到注释1)处,wrk的源码从空闲链表中获得系统PTE后,并没有急吼吼的将PTE的地址返回给调用者,而是做了一段简单的运算:将获得的系统PTE的值的高20bit与系统页表的基址求和,它们的结果才是页表中空闲的系统PTE的地址。wrk的源码没有很明显的体现这个过程,但是这段代码的反汇编代码清晰的向我们展示了这个过程:
8054bf76 57 push edi
8054bf77 8b7d0c mov edi,dword ptr [ebp+0Ch]
...
8054bf9b 8d87c0095680 lea eax,nt!MmFirstFreeSystemPte (805609c0)[edi] ;edi=0
8054bfa1 8b30 mov esi,dword ptr [eax] ;kd> dd MmFirstFreeSystemPte L1
805609c0 ed400000
kd> r eax
eax=805609c0
kd> r esi
esi=ed400000
8054bfa3 c1ee0c shr esi,0Ch ;kd> r esi取高20位,与-1比较,判断是空闲链表最后一个节点
esi=000ed400
8054bfa6 81feffff0f00 cmp esi,0FFFFFh
...
8054bfaf 8945f4 mov dword ptr [ebp-0Ch],eax ;PMMPTE Previous=&MmFirstFreeSystemPte[0];
nt!MiReserveAlignedSystemPtes+0x55:
8054bfbf 8b154c095680 mov edx,dword ptr [nt!MmSystemPteBase (8056094c)]
kd> t
nt!MiReserveAlignedSystemPtes+0x5b:
8054bfc5 8d34b2 lea esi,[edx+esi*4] ;sizeof(MMPTE)==4,在页表中取PTE
kd> r edx
edx=c0000000 ;页表的基址
kd> r esi
esi=c03b5000
kd> dd c03b5000 L1
c03b5000 fffff000
反汇编代码再结合源码,我们可以知道空闲链表中存放的是目标PTE的地址相对于页表基址的偏移。这是获取值的方式,wrk中必定有这样设置值的方式,仔细找找还真能找到:
MmFirstFreeSystemPte[SystemPtePoolType].u.Long = 0;
MmFirstFreeSystemPte[SystemPtePoolType].u.List.NextEntry =
StartingPte - MmSystemPteBase;//设置首个系统空闲PTE相对于页表基址的偏移
这段代码摘自nt!MiInitializeSystemPtes,初始化系统PTE区域。
利用这个结论,我们可以手工遍历出整个链表:
kd> dt nt!*MMPTE*
ntoskrnl!_MMPTE
ntoskrnl!_MMPTE_HARDWARE
ntoskrnl!_MMPTE_PROTOTYPE
ntoskrnl!_MMPTE_SOFTWARE
ntoskrnl!_MMPTE_TRANSITION
ntoskrnl!_MMPTE_SUBSECTION
ntoskrnl!_MMPTE_LIST <-这是系统PTE的类型
kd> x nt!MmFirstFree*
805609c0 nt!MmFirstFreeSystemPte = <no type information>
kd> dd 805609c0 L1 ;获得空闲系统PTE链表头
805609c0 ed400000 ;得到的是空闲PTE相对于页表基址的偏移
kd> ?? 0xc0000000+4*(0xed400000>>0x0c) ;计算节点1
unsigned int 0xc03b5000
kd> dt ntoskrnl!_MMPTE_LIST 0xc03b5000
+0x000 Valid : 0y0
+0x000 OneEntry : 0y0
+0x000 filler0 : 0y00000000 (0)
+0x000 Prototype : 0y0
+0x000 filler1 : 0y0
+0x000 NextEntry : 0y11101110100100010101 (0xee915) ;下一个节点的偏移
kd> ?? 0xc0000000+4*(0xee915) ;计算节点2
unsigned int 0xc03ba454
kd> dt ntoskrnl!_MMPTE_LIST 0xc03ba454
+0x000 Valid : 0y0
+0x000 OneEntry : 0y0
+0x000 filler0 : 0y00000000 (0)
+0x000 Prototype : 0y0
+0x000 filler1 : 0y0
+0x000 NextEntry : 0y11101110101010101101 (0xeeaad) ;下一个节点的偏移
kd> ?? 0xc0000000+4*(0xeeaad) ;节点3
unsigned int 0xc03baab4
kd> dt ntoskrnl!_MMPTE_LIST 0xc03baab4
+0x000 Valid : 0y0
+0x000 OneEntry : 0y0
+0x000 filler0 : 0y00000000 (0)
+0x000 Prototype : 0y0
+0x000 filler1 : 0y0
+0x000 NextEntry : 0y11110111010100101110 (0xf752e)
kd> ?? 0xc0000000+4*(0xf752e) ;节点4
unsigned int 0xc03dd4b8
kd> dt ntoskrnl!_MMPTE_LIST 0xc03dd4b8
+0x000 Valid : 0y0
+0x000 OneEntry : 0y0
+0x000 filler0 : 0y00000000 (0)
+0x000 Prototype : 0y0
+0x000 filler1 : 0y0
+0x000 NextEntry : 0y11110111010111010010 (0xf75d2)
kd> ?? 0xc0000000+4*(0xf75d2) ;节点5
unsigned int 0xc03dd748
kd> dt ntoskrnl!_MMPTE_LIST 0xc03dd748
+0x000 Valid : 0y0
+0x000 OneEntry : 0y0
+0x000 filler0 : 0y00000000 (0)
+0x000 Prototype : 0y0
+0x000 filler1 : 0y0
+0x000 NextEntry : 0y11111111111111111111 (0xfffff) ;下一个节点的偏移为-1,已经到了链表的结尾
kd> dd 0xc03dd748 L2
c03dd748 fffff000 00019000
上面的计算过程展示了整个系统空闲PTE链表。
如果你细心观察,会发现最后一个空闲PTE值和MmSystemPtesEnd的值不同。是不是遍历MmFirstFreeSystemPte时出错了?我觉得应该没错,只是有一部分节点存放在其他地方,这部分内容我放在另一篇博文中解释。