建立記憶體映射,就是把檔案内容映射到程序虛拟空間,這個可以通過閱讀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;