[Windows] 内存

背景

在没有引入虚拟内存概念之前,程序的寻址范围是有限的, 比如32位系统的固定寻址范围是 4GB,如果没有虚拟内存,每打开一个进程都要分配 4GB 的物理内存,那么所有的内存资源就会被很快消耗完,如果此时再有新的进程被创建,就只能进入等待状态。

另外由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的。

引入虚拟内存后,每个进程被创建后都会被分配一个4GB的虚拟内存, 通俗点讲就是每个进程都认为自己拥有4G的空间,但实际在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存。并且进程认为它被分配的4GB的内存是一块连续的内存,而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。

虚拟地址和物理地址的转换

● 虚拟地址指的是 ip 寄存器的值。

● cs寄存器里保存的一个选择子,通过选择子可以到 GDT 或 LDT 表里得到一个基址,然后加上 ip 的值,得到线性地址。

● 线性地址分成页目录,页表,偏移三个部分,线性地址再通过页目录,页表信息得到物理地址。

虚拟地址的组成
对于 32位系统,虚拟地址由三部分组成:

位置 内容
高10位 二级页表号
中间10位 页号
低12位 页内偏移

虚拟地址->线性地址->物理地址 (详细过程)

由于 Windows 不分段,每个段的基地址都是0x0,长度为4GB,虚拟地址->线性地址->物理地址 = 虚拟地址->物理地址。简单来说,Windows 的线性地址就刚好是物理地址


在这里插入图片描述

  • CR3寄存器中记录的当前进程页表的物理地址找到总页表
  • 根据虚拟地址中的页表号,以页表号为索引,找到总页表中对应的PDE
  • 根据PDE,找到对应的二级页表
  • 根据虚拟地址中的页号为索引,找到二级页表中的对应PTE
  • 根据这个PTE记录的映射关系,找到这个虚拟页面对应的物理页面
  • 加上虚拟地址中的页内偏移部分,加上这个偏移值,就得出最后的物理地址。

看一个例子:

扫描二维码关注公众号,回复: 11894743 查看本文章
//这条指令的内部原理(没考虑二级缓冲情况)
mov DWORD PTR[0x00000004]100 
{
    
    
	  va=0x00000004;					// 页表号=0,页号=0,页内偏移=4
      总页表=CR3;  						// 本进程的总页表的物理地址固定保存在cr3寄存器中
      PDE=总页表[va.页表号];  			// PDE为对应的二级页表描述符
      二级页表=PDE.PageAddr;  			// 得出本二级页表的地址
      PTE=二级页表[va.页号];   			// 得出到该虚拟地址所在页面的PTE映射描述符
      If(PTE空白)  						// PTE为空表示该虚拟页面尚未建立映射
         触发0x0e号页面访问异常(具体为缺页异常)

      If(PTE.bPresent==false) 					// PTE的这个字段表示该虚拟页面当前是否映射到了物理内存
         触发0x0e号页面访问异常(具体为缺页异常)

      If(CR0.wp==1  &&  PTE.Writable==false) 	// 已开启页面写保护功能,就检查这个页面是否可写
         触发0x0e号页面访问异常(具体为页面访问保护越权异常)

      物理地址pa =cs.base + PTE.PageAddr + va.页内偏移  					//得出对应的物理地址
      将得到的pa放到地址总线上,100放在数据总线上,经由FSB->北桥->内存总线->内存条 写入内存
}

PTE是二级页表中的表项,记录了对应虚拟页面的映射情况,由于每次访问内存都要先访问一次PTE获取该虚拟页面对应的物理页面,导致效率很低,因此引入了二级缓存技术,用来保存那些频繁访问的PTE,这样,cpu每次去查物理页面时,就先尝试在二级缓冲中查找对应的PTE,如果找不到,再才去访问内存中的PTE,效率大大提高。


内存在内核中的结构

 // 地址空间描述符
Struct  MADDRESS_SPACE 
{
    
    
   MEMORY_AREA*  MemoryRoot;						// 本地址空间的已分配区段表(一个AVL树的根)
   VOID*  LowestAddress;							// 本地址空间的最低地址(用户空间是0,内核空间是0x80000000)
   EPROCESS* Process;								// 本地址空间的所属进程
   USHORT* PageTableRefCountTable; 					// 一个表,表中每个元素记录了本地址空间中各个二级页表中的PTE个数,一旦某个二级页表中的PTE个数减到了0,就自动释放该二级页面表本身,体现为稀疏数组特征
   ULONG PageTableRefCountTableSize;				// 上面那个表的大小
}

// 区段描述符
Struct  MEMORY_AREA    
{
    
    
   Void* StartingAddress; 					// 开始地址,普通区段对齐64KB,其它类型区段对齐4KB
   Void* EndAddress;						// 结尾地址,EndAddress – StartingAddress就是该区段的大小
   MEMORY_AREA*  Parent;					// AVL树中的父节点
   MEMORY_AREA*  LeftChild;					// 左边的子节点
   MEMORY_AREA*  RightChild;				// 右边的子节点
   ULONG type;								// 本区段的类型(普通型区段、视图型区段、缓冲型区段等)
   		/*
			MEMORY_AREA_VIRTUAL_MEMORY:		//普通型区段,由VirtuAlloc应用层用户分配的区段都是普通区段
			MEMORY_AREA_SECTION_VIEW:			//视图型区段,用于文件映射、共享内存
			MEMORY_AREA_CACHE_SEGMENT:			//用于文件缓冲的区段(一个簇大小)
			MEMORY_AREA_PAGED_POOL:				//内核分页池中的区段
			MEMORY_AREA_KERNEL_STACK:			//用于内核栈中的区段
			MEMORY_AREA_PEB_OR_TEB:				//用于PEB、TEB的区段
			MEMORY_AREA_MDL_MAPPING:			//内核中专用于建立MDL映射的区段
			MEMORY_AREA_CONTINUOUS_MEMORY:		//对应的物理页面也连续的区段
			MEMORY_AREA_IO_MAPPING:			//内核空间中用于映射外设内存(如显存)的区段
			MEMORY_AREA_SHARED_DATA:			//内核空间中用于与用户空间共享的区段
		*/
   ULONG protect;							// 本区段的保护权限,可读、可写、可执行的组合
   ULONG flags;								// 当初分配本区段时的分配标志
   BOOLEAN DeleteInProgress;				// 本区段是否标记为了 "已删除"
   ULONG PageOpCount;

	  Union
	{
    
    
		// 专用于视图型区段
	    Struct
	    {
    
    
	       // 凡是含有ROS字样的函数与结构体都表示是ReactOS与Windows中不同的实现细节
	       ROS_SECTION_OBJECT*  section; 
	       ULONG ViewOffest;					// 指本视图型区段在所在Segment内部的偏移
	       MM_SECTION_SEGMENT* Segment;			// 所属Segment
	       BOOLEAN WriteCopyView;				// 本视图区段是不是一个写复制区段     
	    }SectionData;
		LIST_ENTRY  RegionListHead;				// 本区段内部的所有Region区块,放在一个链表中
	}Data;
}

// 区块描述符
Struct  MM_REGION  
{
    
    
   ULONG type;						// 指本区块的分配类型(预定型分配、提交型分配),又叫映射状态(已映射、尚未映射)
   ULONG protect;					// 本区块的访问保护权限,可读、可写、可执行的组合
   ULONG length;					// 区块长度,对齐页面大小(4KB)
   LIST_ENTRY RegionListEntry;		// 用来挂入所在区段的区块链表
}

// 物理页面描述
Struct  PHYSICAL_PAGE  
{
    
    
   Type ;							// 该物理页面的空闲占用状态(1表示空闲,2表示已占用,3表示分给了BIOS)
   Consumer;						// 该物理页面的消费用途(用户/内核分页池/内核非分页池/文件缓冲 四种)
   Zero;							// 标志本页面是否已清0
   ListEntry;						// 用来挂入那7个链表之一
   ReferenceCount;					// 引用计数,一旦减到0,页面就变为空闲状态,进入空闲链表
   SWAPENTRY  SavedSwapEntry;		// 对应的来源页文件,用于置换,一般为空 
   LockCount;						// 本物理页面的锁定计数(物理页面可锁定在内存中,不许置换到外存)
   MapCount;						// 同一个物理页面可以映射到N个进程的N个虚拟页面
   MM_RMAP_ENTRY*  RmapListHead;	// 本物理页面映射给的那些虚拟页面,组成的链表  
}

内存相关 API



虚拟页面与物理页面之间的映射

一个物理页面可以映射到N个进程的N个虚拟页面中,但一个虚拟页面同一时刻只能映射到一个物理页面。

每个虚拟页面又分四种映射状态:
1.映射着某个物理页面(已分配且已映射)
2.映射着某个磁盘页文件中的某个页面(已分配且已映射)
3.没映射到任何物理存储介质(对应的PTE=0),但是可能被预定了(已分配,但尚未映射)
4.裸奔(尚未分配,以上情况都不满足)

同一时刻,只有最频繁访问的那些虚拟页面映射着物理页面(否则物理内存早就用完了),这些虚拟页面被保存在一个工作集中,当访问工作集以外的虚拟页面时就会产生缺页异常,此时缺页异常处理函数会分配一个物理页面并将这个虚拟页面映射到这个物理页面中。如果该虚拟页面映射着了某个物理页面,但是读写访问权限不匹配,触发 0x0e 越权异常

关于 PDE 和 PTE:
PDE:页目录表基地址,存放在 CR3 寄存器中。
PTD:页表基地址,存放在页目录表中。


内存映射文件与共享物理内存

内存映射文件就是把磁盘上的文件当做物理内存使用。这样,要读写文件时,不用再原始地调用ReadFile,WriteFile函数读写文件。可以直接把文件映射到虚拟内存,然后直接读写虚拟内存即可对文件进行读写。

除此之外,两个进程也可以共享物理内存,只要把同一个物理页面映射到这两个进程的地址空间即可,物理内存共享也是靠内存映射文件机制实现的,只不过映射的不是普通磁盘文件,而是页文件。

struct  ROS_SECTION_OBJECT
{
    
    
   CSHORT  type;					// 类型
   CSHORT  size;					// 实际长度 
   ULONG  protect;					// 的保护权限
   ULONGLONG  MaxSize;				// 最大长度
   ULONG  AllocationAttributes;		// section的文件类型
   FILE_OBJECT*  FileObject;		// 创建本section的那个文件对象(文件句柄)
   Union
   {
    
    
      MM_SECTION_SEGMENT*  Segment;				// 数据文件section中的唯一segment
      MM_IMAGE_SECTION_OBJECT*  ImageSegments;	// 镜像文件中的Segment数组
   }; 
};

struct  MM_SECTION_SEGMENT
{
    
    
   LONG   FileOffset;				// foa
   ULONG  VirtualAddress;			// 其实是相对虚拟地址偏移rva,不是va
   ULONG  RawLength;				// 本节在文件中的原始实际长度
   ULONG  Length;					// 本节对齐后的长度(一般指对齐4KB)
   ULONG  protect;					// 可读、可写、可执行这些保护属性
   ULONG  ReferenceCount;
   SECTION_PAGE_DIRECTORY PageDir;	// 本节内部的页表,后文有详说
   ULONG  Flags;
   ULONG  Characteristics;
   BOOL   WriteCopy;				// 本节是否写复制,pe文件中一般为TRUE,数据文件中一般为FALSE
}

struct  MM_IMAGE_SECTION_OBJECT
{
    
    
    ULONG_PTR ImageBase;
    ULONG_PTR StackReserve;
    ULONG_PTR StackCommit;
    ULONG_PTR EntryPoint;
    USHORT Subsystem;
    USHORT ImageCharacteristics;
    USHORT MinorSubsystemVersion;
    USHORT MajorSubsystemVersion;
    USHORT Machine;
    BOOLEAN Executable;
    ULONG NrSegments;				// 本ImageSection中的segment个数,也即‘节’个数
    ULONG ImageSize;
    PMM_SECTION_SEGMENT Segments;	// 本ImageSection中的segment数组
};


struct  PE_SEGMENT
{
    
    
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME=8]; 	//8个字节的节名 如".text"    ".rdata"     ".data"    
  DWORD  VirtualSize;						//该节未对齐前的原始数据大小 
  DWORD  VirtualAddress; 					//该节的RVA
  DWORD  SizeOfRawData;  					//该节的FAS也即文件对齐大小,一般指对齐512B后的大小
  DWORD  PointerToRawData; 					//该节的FOA,即文件偏移
  DWORD  PointerToRelocations; 				//专用于obj文件
  DWORD  PointerToLinenumbers; 				//用于调试
  WORD   NumberOfRelocations;   			//专用于obj文件
  WORD   NumberOfLinenumbers;   			//用于调试
  DWORD  Characteristics;  					//该节的属性(可读、可写、可执行、可共享、可丢弃、可分页等属性)
  	/*
		IMAGE_SCN_CNT_CODE    				该节中包含有代码 如.text
		IMAGE_SCN_CNT_INITIALIZED_DATA   	该节中包含有已初始化的数据 如.data
		IMAGE_SCN_CNT_UNINITIALIZED_DATA  	该节中包含有尚未初始化的数据,如.bss .data?
		IMAGE_SCN_MEM_DISCARDABLE 			该节加载到内存后是可抛弃的,如dll中的.reloc重定位节就是可以抛弃的
		IMAGE_SCN_MEM_NOT_CACHED  			节中数据不会经过缓冲
		IMAGE_SCN_MEM_NOT_PAGED   			该节不准交换到页文件中,sys文件中的节(除.page)都不可换出
		IMAGE_SCN_MEM_SHARED  				这个节可以被多个进程共享,如dll中的共享节。也即表示本节是否允许写复制。(默认允许)
		IMAGE_SCN_MEM_EXECUTE 				本节可执行
		IMAGE_SCN_MEM_READ  				本节可读
		IMAGE_SCN_MEM_WRITE 				本节可写
	*/
} IMAGE_SECTION_HEADER;

猜你喜欢

转载自blog.csdn.net/Simon798/article/details/108531368