typedef struct_SECTION {
MMADDRESS_NODE Address; //内存区的VAD节点,用于把所有特定类型的内存区对象组织成一颗平衡树(SEC_BASED)类型
PSEGMENT Segment; // 段对象
LARGE_INTEGER SizeOfSection;
union {
ULONG LongFlags;
MMSECTION_FLAGS Flags; //内存区的一组标志
} u;
MM_PROTECTION_MASK InitialPageProtection;//内存区中页面的保护属性
} SECTION,*PSECTION;
内存管理器是Windows 平台上两个或多个进程之间共享内存的常用方法,可以被映射到系统的页面文件或者其它文件中。
Windows 进程管理器通过内存区对象把一个可执行文件的映像映射和加载到内存中,然后创建新进程的地址空间;利用内存区对象将一个文件映射到进程地址空间在中,从而直接以操作内存的方式来访问文件,这既简化了对文件的读写操作,也提高了性能;Windows 缓存管理器利用内存区对象来访问缓存文件中的数据;常用的IPC 底层支持都是内存区对象。
内存区对象可以被一个进程打开,也可以被多个进程打开(内存区对象具有标准的Windows访问控制列表(ACL),当进程视图打开共享内存区对象,该ACL 会被检查),内存区对象并不一定等同于共享内存。
上层的封装不再描述,实际的内存区的创建由MmCreateSection完成:
NTSTATUS
MmCreateSection(
OUT PVOID *SectionObject,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributesOPTIONAL,
IN PLARGE_INTEGER InputMaximumSize,
IN ULONG SectionPageProtection,
IN ULONG AllocationAttributes,
IN HANDLE FileHandle OPTIONAL,
IN PFILE_OBJECT FileObject OPTIONAL
);
FileObject 仅用于缓存管理器,非空的话此次为系统缓存管理器的调用。
ObjectAttributes 指定了对象管理器所需要的信息,对象名,安全描述符等
DesiredAccess 指定了访问方式,SectionPageProtection指定了内存区中各个页面的保护标志。
AllocationAttributes 指示了内存区对象中内存段的分配方式:SEC_BASED 每个进程地址空间中相同的虚拟地址上分配;SEC_RESERVE 内存区所有的页面被保留;
SEC_COMMIT 所有的页面被提交
SEC_IMAGE FileHandle 参数指定了一个可执行映像文件
SEC_FILE FileHandle 指定映射文件
SEC_LARGE_PAGES 内存区使用大页面,且立即分配物理内存
typedef struct _FILE_OBJECT {
…
PSECTION_OBJECT_POINTERS SectionObjectPointer;
struct _FILE_OBJECT *RelatedFileObject;
…
UNICODE_STRING FileName;
LARGE_INTEGER CurrentByteOffset;
…
} FILE_OBJECT, *PFILE_OBJECT;
文件对象中有一个SectionObjectPointer指向控制区对象。
typedef struct _SECTION_OBJECT_POINTERS {
PVOID DataSectionObject; 用于数据文件
PVOID SharedCacheMap; 用于共享缓存集合
PVOID ImageSectionObject; 用于可执行映像文件 反指向 控制区域
} SECTION_OBJECT_POINTERS;
其包含三个指针,分别指向用于数据文件、可执行映像文件和缓存文件控制区对象。
内存区对象指向段对象
typedef struct _SECTION {
…
PSEGMENT Segment;
…
} SECTION, *PSECTION;
typedef struct _SEGMENT {
struct _CONTROL_AREA *ControlArea;
PVOIDSegmentBaseAddress;
ULONGTotalNumberOfPtes;
ULONGNonExtendedPtes;
UINT64SizeOfSegment;
SIZE_TImageCommitment;
PSECTION_IMAGE_INFORMATION ImageInformation;
PVOIDSystemImageBase;
SIZE_TNumberOfCommittedPages;
MMPTESegmentPteTemplate;
PVOIDBasedAddress;
PMMEXTEND_INFO ExtendInfo;
PMMPTEPrototypePte;
MMPTEThePtes[MM_PROTO_PTE_ALIGNMENT /PAGE_SIZE];
} SEGMENT, *PSEGMENT;
段对象是真正描述内存区数据的对象。段对象本身是在换页内存池中分配的,包含了段内页面数量以及字节数、用于描述此段内页面映射的原型PTE 阵列等信息,有一个指针指向一个控制区对象
typedef struct _CONTROL_AREA { // must be quadwordsized.
PSEGMENT Segment;
ULONG NumberOfSectionReferences;
…
ULONGNumberOfPfnReferences;
ULONGNumberOfMappedViews;
USHORTNumberOfSubsections;
…
union {
ULONGLongFlags;
MMSECTION_FLAGS Flags;
} u;
PFILE_OBJECT FilePointer;
PEVENT_COUNTER WaitingForDeletion;
USHORTModifiedWriteCount;
USHORTNumberOfSystemCacheViews;
SIZE_TPagedPoolUsage;
SIZE_TNonPagedPoolUsage;
} CONTROL_AREA, *PCONTROL_AREA;
控制区对象在非换页内存区中分配的,有一个指针指向段对象,控制区对象的职责是维护I/O操作所必须的信息,包含有一个文件对象指针成员。计数成员NumberOfMappedViews 反映了与之关联的内存区对象被映射了多少次。
与控制区对象同在一块内存中的是一个或者多个子内存区对象(SUBSECTION)
typedef struct _SUBSECTION { // Must start on quadword boundary and be quad sized
PCONTROL_AREA ControlArea;
union {
ULONGLongFlags;
MMSUBSECTION_FLAGS SubsectionFlags;
} u;
ULONGStartingSector;
ULONG NumberOfFullSectors; // (4GB-1) * 4K == 16TB-4K limit per subsection
PMMPTESubsectionBase;
ULONGUnusedPtes;
ULONGPtesInSubsection;
struct _SUBSECTION *NextSubsection;
} SUBSECTION, *PSUBSECTION;
这些子内存区结构描述了该文件的每个内存区的映射信息(只读、读写、写时复制等)。记录了从原型PTE到磁盘上逻辑扇区之间的关联关联信息,子内存区对象构成一个链表,且有一个指针指回到控制区对象。
下面这张图片展示结构体之间的关系
讲述完这些结构体之间的关系之后,我们来看上面介绍的三个创建段对象的函数MiCreatePagingFileMap,MiCreateImageFileMap,MiCreateDataFileMap ,这三个函数分别对应于三种内存区:页面文件支撑的内存区,可执行映像文件内存区以及数据文件映射内存区。后两者有对应的文件对象,可以在多个进程之间共享。如果多个内存区对象映射了同一个文件,它们引用同一个段对象。
在NtAllocateVirtualMemory中申请的MMVAD 是MMVAD_SHORT 类型的,它不包含ControlArea 成员,因为此方式申请的内存属于进程私有内存,并不需要共享。但是,凡是跟内存区对象关联的MMVAD 对象都有ControlArea 指针域,以便将两者关联起来。
对于页面文件支撑的内存区,或数据文件内存区,都是通过MiMapViewOfDataSection函数来映射的。MiMapViewOfDataSection 函数的任务是把内存区对象中描述的存储资源映射到指定进程的地址空间中。ControlArea 参数代表了内存区对象中段对象所指的控制区对象.
对于可执行映像文件的内存区对象,在MmCreateSection 函数中,其AllocationAtrributes 参数包含SEC_IMAGE 标志,且FileHandle 参数指向可执行文件的句柄。如果文件对象还没有控制区对象和段对象,MmCreateSection 调用MiInsertImageSectionObjects 函数把一个新建的控制区对象插入到映像文件对象中,并调用MiCreateImageFileMap 创建一个新的段对象。MiInsertImageSectionObjects把参数中指定的控制区对象设置到文件对象的内存区对象指针的ImageSectionObject成员中。MmCreateSection 函数中创建的控制区对象并非真正用于映射文件对象的控制区对象,而仅仅是个占位的控制区对象;真正能够理解映像文件因而可用于映射内存区对象的控制区是由MiCreateImageFileMap函数创建的,段对象被创建以后,MmCreateSection 将段对象所指向的控制区对象设置到文件对象的内存区对象指针的ImageSectionObject成员中,原来由MmCreateSection 创建的控制区对象被释放掉。
MiCreateImageFileMap读头信息--->按照PE 格式解析映像文件头信息-->在非换页内存池中创建一个控制区对象,在换页内存区创建段对象。控制区对象附带多个子内存区对象,子内存区对象的数量与映像文件中区域(SECTION)的数量相等;而段对象附带的原型PTE 阵列能描述整个映像文件大小。然后,MiCreateImageFileMap 利用文件头中的信息填充控制区和段对象中各个成员。段对象中的子内存区,正好对应于映像文件中的各个区域,它们有自己的保护属性、原型PTE、在磁盘上的位置等信息。
然后,经过MmMapViewOfSection 映射到一个进程地址中以后进程才可以访问映像文件中的内容。函数内部调用MiMapViewOfImageSection 函数:
函数任务是把映像文件中的各个区域映射到指定进程的虚拟地址空间中。其中ControlArea参数代表了内存区对象中段对象所指的控制区对象,Section代表了内存区对象。
最后我们看一下基于数据文件的内存区对象。MmCreateSection---->MiCreateDataFileMap创建段对象,MmCreateSection在调用该函数之前创建一个控制区对象,存放在文件对象的内存区对象指针的DataSectionObject成员中,MiCreateDataFileMap首先确定内存区对象的大小,如果文件的大小比参数中指定的最大尺寸要小,要求内存区的页面必须是可读写的,且文件大小可以被扩充到指定大小。
然后MiCreateDataFileMap在换页内存池中申请一个段对象(MAPPED_FILE_SEGMENT 类型),填充成员。这里的对象并不直接包含原型PTE阵列(以后,当相应的内存区对象被映射时,构造必要的原型PTE)。另外,控制区对象的子内存区也被从一个扩展到多个。MiCreateDataFileMap按照全局变量MmAllocationFragment 指定的大小把原文件打散成一组chunk。这里默认大小64KB、32KB 或 16KB,取决于系统中物理内存的数量。每个chunk 对应一个子内存区。
最后,MiCreateDataFileMap 初始化控制区对象和段对象,填充好控制区对象的子内存区链表。然后,返回。
数据文件内存区对象的映射通过 MiMapViewOfDataSection 函数来映射的。函数任务是:把内存区对象中描述的段映射到进程的虚拟地址空间中,如果虚拟地址范围尚未确定,尝试在进程的VAD 树中找到合适的地址方位,然后利用控制区中的信息建立内存区映射,其逻辑过程与页面支撑的内存区对象的映射过程相同。
系列文章均参考
深入理解windows操作系统
windows内核原理与实现