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;