XpSp3(未开启PAE模式)内存管理之系统PTE区域 上

    前言

    几年前就已经看过wrk中关于内存管理和缓存管理的实现,由于当时对内核调试尚不熟悉,因此仅仅停留在代码层面。现在结合windbg操作,希望能有新的收获。毛德操的对理解windows内核确有裨益,但是,ReactOS对内存管理和缓存管理部分的实现与wrk相去甚远(ReactOS内存管理更接近于Linux内存管理),因此这些的代码应以wrk为准。

    正文

    提到一个系统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 = 
kd> x nt!MmSystemPtesEnd
8055bde0 nt!MmSystemPtesEnd = 
kd> dd 8055bde8 L1 ;系统PTE区域的开始位置位于0xc03b5000
8055bde8  c03b5000 
kd> dd 8055bde0 L1 ;系统PTE区域的结束位置位于0xc03dfe34
8055bde0  c03dfe34 
kd> x nt!MmFirstFree* ;搜索空闲系统PTE的链表头符号
805609c0 nt!MmFirstFreeSystemPte = 
8055bffc nt!MmFirstFreeSystemCache = 
kd> dd 805609c0 L1 ;空闲链表头位于0x805609c0,它指向的首个空闲系统PTE的值为0xed400000
805609c0  ed400000 
    windbg指出的系统PTE起止值的确位于页表区域中,但是nt!MmFirstFreeSystemPte指出的首个空闲系统PTE的值却明显在页表范围之外。哪里出错了?我核对发现里面提到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 = 
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时出错了?我觉得应该没错,只是有一部分节点存放在其他地方,这部分内容我放在另一篇博文中解释。




你可能感兴趣的:(win内核,win内核,内核,windows)