天天看点

MmCreateSection/MmMapViewOfSection个人注释及理解(一)

    建立内存映射,就是把文件内容映射到进程虚拟空间,这个可以通过阅读MSDN达成共识。NtCreateSection是建立内存映射的第一步,而MmCreateSection是建立内存映射NtCreateSection的具体实现。前面 <windows内核中Section/Segment/ControlArea/Subsection/MMVAD之间的关系>一文提到了Section/Segment等对象之间的关系,而

MmCreateSection就是用来建立这些对象的关系。

    MmCreateSection代码过于繁杂,各对象之间的关系相互牵连,但是仍然能从MiCreateImageFileMap窥见他们之间的一缕关系。而MiCreateImageFileMap简直就是一个解析PE文件的过程,虽然较之于MiCreateDataFileMap解析数据文件更为复杂,但是MiCreateImageFileMap更能体现出Section/Segment/ControlArea/Subsection/MMVAD各对象之间的关系。因此本文将从MmCreateSection进入,步入到MiCreateImageFileMap然后退出。

    本质上讲,MmCreateSection只涉及到为ControlArea建立子内存区对象,然后为Segment对象中的原型pte阵列与将被映射的文件建立一对一的映射关系,以及创建Segment对象,但不包括建立虚拟内存,真正的建立虚拟内存要等到MmMapViewOfSection来完成,最终与物理内存建立关系则因为程序访问了MmMapViewOfSection建立的虚拟内存页,产生缺页异常,通过换页异常建立起页面映射。

    在整个内存映射中框架中,核心对象是ControlArea,为什么这么说?1.通过ControlArea可以找到被建立映射的文件,即FileObject对象;2.文件各部分被映射到进程空间的虚拟地址,即MMVAD(MMVAD本身就是管理进程虚拟地址空间的结构),这部分页面可以通过ControlArea搜索到;3.虚拟内存页终究要反映到物理页面上,但是物理页面可能被换入换出,为了管理这些物理页面win使用Segment对象的原型PTE阵列来完成这个使命,同样这个Segment对象页可以通过ControlArea中的指针搜索到。鉴于ControlArea的核心地位,MmCreateSection开始时创建创建ControlArea对象,不过这个应该算是一个占位符,真正创建和映像文件相关的ControlArea还是在MiCreateImageFileMap函数中。PE文件头记录了文件内的节区数,MiCreateImageFileMap创建ControlArea时,为每个节区包扩PE文件头创建一个子内存区对象。

代码开始处,简单的记录映像文件占用多少页面,以此为依据创建Segment对象及其中的原型pte阵列。映像文件对齐后的大小/PAGE_SIZE,即为将要创建的原型pte阵列中pte的数量:

ImageAlignment = NtHeader32->OptionalHeader.SectionAlignment;
        FileAlignment = NtHeader32->OptionalHeader.FileAlignment - 1;
        SizeOfImage = NtHeader32->OptionalHeader.SizeOfImage;
        LoaderFlags = NtHeader32->OptionalHeader.LoaderFlags;
        ImageBase = NtHeader32->OptionalHeader.ImageBase;
        SizeOfHeaders = NtHeader32->OptionalHeader.SizeOfHeaders;
...
        //文件占用页面的数量
        NumberOfPtes = BYTES_TO_PAGES (SizeOfImage);      

接下来计算PE文件中节的数量,内核会为每个PE节创建一个Subsection对象,然后从非分页池中创建ControlArea和与其内存上相邻的SubSection阵列:

NumberOfSubsections = FileHeader->NumberOfSections;

    if ((ImageAlignment >= PAGE_SIZE) || (CheckSplitPages == TRUE)) {

        //
        // Allocate a control area and a subsection for each section
        // header plus one for the image header which has no section.
        //
        //PE头独占一个子内存区,因此会多一个子内存区
        SubsectionsAllocated = NumberOfSubsections + 1;

        ControlArea = ExAllocatePoolWithTag (NonPagedPool,
                                             sizeof(CONTROL_AREA) +
                                                    (sizeof(SUBSECTION) *
                                                    SubsectionsAllocated),
                                            'iCmM');
RtlZeroMemory (ControlArea, sizeof(CONTROL_AREA) + sizeof(SUBSECTION));      

按文件将占用的pte数量创建Segment对象,从下面的代码大概就能猜到Segment对象中的原型pte阵列中的原型pte将和文件中的页面一一对应:

//pte包含在segment中
    //前面NumberOfPtes = BYTES_TO_PAGES (SizeOfImage);按映像文件的大小计算出将要用到多少pte(页面)
    //sizeof(MMPTE) * ((ULONG)NumberOfPtes--->段对象的原型pte阵列指向文件各页面的另一个辅证
    SizeOfSegment = sizeof(SEGMENT) + (sizeof(MMPTE) * ((ULONG)NumberOfPtes - 1)) +
                    sizeof(SECTION_IMAGE_INFORMATION);

    NewSegment = ExAllocatePoolWithTag (PagedPool | POOL_MM_ALLOCATION,
                                        SizeOfSegment,
                                        MMSECT);
    *Segment = NewSegment;

    RtlZeroMemory (NewSegment, sizeof(SEGMENT));      

前面已经提到了Segment与ControlArea的关系,ControlArea是整个内存映射的核心,下面的代码应证了这点:

NewSegment->ControlArea = ControlArea;

NewSegment->u2.ImageInformation->ImageCharacteristics =
                            FileHeader->Characteristics;
    NewSegment->u2.ImageInformation->Machine = FileHeader->Machine;
    NewSegment->u2.ImageInformation->LoaderFlags = LoaderFlags;

    ControlArea->Segment = NewSegment;
    ControlArea->NumberOfSectionReferences = 1;      

前面提起过,Segment中有原型pte阵列。同时,也说到,Segment对象中的原型pte阵列指向文件中相应的偏移处。潘爱民的书中提到过原型pte可以指向交换文件中页面,这些原型pte反应了这些页面在那个交换设备中以及在交换设备的哪段偏移处。这有点类似linux swap分区的实现,当pte有效位为0,该pte解释为无效pte,其剩余的31位用来在swap文件中查找swap页面。放到win内核中,Segment中的原型pte阵列也充当了这个角色。另外,对应映像文件,每个节对应一个Subsection对象。这子内存区对象干啥的?下面是我个人的理解:

假设,一个PE文件有2个节(包含PE头),每个节占10个页面,那么Segment->ThePtes (即原型pte阵列)中共20个原型pte。那么经过MiCreateImageFileMap函数的折腾,会创建两个子内存区对象,第一个子内存区对象代表PE文件的第一个节,第二个子内存区对象代表PE文件第二个节。第一个子内存区对象用Subsection->SubsectionBase = &Segment->ThePtes[0];

第二个子内存区对象用Subsection->SubsectionBase = &Segment->ThePtes[10];

这样看着,就可以用各个Subsection来管理Segment->ThePtes,并最终影响到被映射到内存中的各个节的页面了。具体的代码在MmMapViewOfSection中得以体现。

不过在MiCreateImageFileMap中先是初始化了PE头对应的子内存区对象:

//前面NewSegment->PrototypePte = &NewSegment->ThePtes[0];
    PointerPte = NewSegment->PrototypePte;
    Subsection->SubsectionBase = PointerPte;
    //设置一个模版pte,后面用它初始化segment中原型pte阵列
    //Temp.u.Long为首个子内存区对象的地址
    TempPte.u.Long = MiGetSubsectionAddressForPte (Subsection);
    TempPte.u.Soft.Prototype = 1;

    NewSegment->SegmentPteTemplate = TempPte;
    SectorOffset = 0;      

前面已经解释过NewSegment->ThePtes[0],ThePtes数组就是Segment中的原型pte数组。然后代码中建立一个无效pte模板,其原型位置位。这很明显的,MmCreateSection还没有建立虚拟内存,就算建立了虚拟内存也没有映射到物理页面,因此必然是无效pte。而且,这些页面目前还在文件中,得用原型pte去标注这些页面的位置:

Subsection->u.SubsectionFlags.Protection = MM_EXECUTE_WRITECOPY;

        //
        // Set all the PTEs to the execute-read-write protection.
        // The section will control access to these and the segment
        // must provide a method to allow other users to map the file
        // for various protections.
        //

        TempPte.u.Soft.Protection = MM_EXECUTE_WRITECOPY;
    //前面TempPte.u.Long = MiGetSubsectionAddressForPte (Subsection);
        NewSegment->SegmentPteTemplate = TempPte;

        TempPteDemandZero.u.Long = 0;
        TempPteDemandZero.u.Soft.Protection = MM_EXECUTE_WRITECOPY;
        SectorOffset = 0;

        for (i = 0; i < NumberOfPtes; i += 1) {

            //
            // Set prototype PTEs.
            //
      //步进的方式给Segment对象中原型pte阵列依次赋值
      /*
      如果在文件偏移内的页面,变为需要写时复制的原型pte;
      超过文件偏移的,全部变为求零页面pte,文件空洞啊
      */
            if (SectorOffset < EndOfFile.LowPart) {

                //
                // Data resides on the disk, refer to the control section.
                //

                MI_WRITE_INVALID_PTE (PointerPte, TempPte);

            }
            else {

                //
                // Data does not reside on the disk, use Demand zero pages.
                //

                MI_WRITE_INVALID_PTE (PointerPte, TempPteDemandZero);
            }

            SectorOffset += PAGE_SIZE;
            PointerPte += 1;
        }

        NewSegment->u1.ImageCommitment = NumberOfPtes;      

这段代码的for循环深深的出卖了Segment中原型pte阵列和磁盘文件的关系:每个pte对应磁盘文件上的一个页面的偏移,但是这里仍然没有建立Segment与文件的关联,可以看到pte阵列中的每个原型pte都是无效pte。

到这里,只是建立了PE头对应的原型pte阵列。后面是对PE文件中的各个节加以相同或类似的操作,最终使得原型pte阵列中剩下的原型pte指向磁盘上的文件页面。

NewControlArea->Segment = NewSegment;

        Subsection = (PSUBSECTION)(ControlArea + 1);
        NewSubsection = (PSUBSECTION)(NewControlArea + 1);
        NewSubsection->PtesInSubsection += AdditionalBasePtes;
    //跟上面的过程一样,设置每个PE节对应于Segment中相应偏移处的原型pte阵列
        for (i = 0; i < SubsectionsAllocated; i += 1) {

            //
            // Note: SubsectionsAllocated is always 1 (for wx86), so this loop
            // is executed only once.
            //

            NewSubsection->ControlArea = (PCONTROL_AREA) NewControlArea;

            NewSubsection->SubsectionBase = NewSegment->PrototypePte +
                    (Subsection->SubsectionBase - OldSegment->PrototypePte);

            NewPointerPte = NewSegment->PrototypePte;
            OldPointerPte = OldSegment->PrototypePte;

            TempPte.u.Long = MiGetSubsectionAddressForPte (NewSubsection);
            TempPte.u.Soft.Prototype = 1;
      //这些原型pte阵列全部设置为TempPte.u.Soft.Prototype = 1;
      //虚拟地址指向Subsection对象的起始地址
            for (j = 0; j < OldSegment->TotalNumberOfPtes+AdditionalBasePtes; j += 1) {

                if ((OldPointerPte->u.Soft.Prototype == 1) &&
                    (MiGetSubsectionAddress (OldPointerPte) == Subsection)) {
                    OriginalProtection = MI_GET_PROTECTION_FROM_SOFT_PTE (OldPointerPte);
                    TempPte.u.Soft.Protection = OriginalProtection;
                    MI_WRITE_INVALID_PTE (NewPointerPte, TempPte);
                }
                else if (i == 0) {

                    //
                    // Since the outer for loop is executed only once, there
                    // is no need for the i == 0 above, but it is safer to
                    // have it. If the code changes later and other sections
                    // are added, their PTEs will get initialized here as
                    // DemandZero and if they are not DemandZero, they will be
                    // overwritten in a later iteration of the outer loop.
                    // For now, this else if clause will be executed only
                    // for DemandZero PTEs.
                    //

                    OriginalProtection = MI_GET_PROTECTION_FROM_SOFT_PTE (OldPointerPte);
                    TempPteDemandZero.u.Long = 0;
                    TempPteDemandZero.u.Soft.Protection = OriginalProtection;
                    MI_WRITE_INVALID_PTE (NewPointerPte, TempPteDemandZero);
                }

                NewPointerPte += 1;

                //
                // Stop incrementing the OldPointerPte at the last entry
                // and use it for the additional base PTEs.
                //

                if (j < OldSegment->TotalNumberOfPtes - 1) {
                    OldPointerPte += 1;
                }
            }

            Subsection += 1;
            NewSubsection += 1;
        }      

在MiCreateImageFileMap的结尾处,除了处理各个Subsection和ControlArea的关系,还做了一些统计信息,比如各个Subsection中有多少pte等

MiCreateImageFileMap结束退到MmCreateSection,ControlArea和Segment两对象的关系已经成功建立,但是Section对象尚未建立,因此创建Section,并和MiCreateImageFileMap返回的Segment建立关系:

Section.Segment = NewSegment;
    Section.u.LongFlags = ControlArea->u.LongFlags;