Loading... # Windows核心编程-学习笔记8 ## Windows内存架构 ### 进程的虚拟地址空间 每个进程都有自己的虚拟地址空间。对32位进程,地址空间大小为4GB,64位大小为16EB 由于每个进程都有私有的地址空间,当进程中的各个线程运行时,它们只能访问属于该进程的内存。线程既看不到属于其他进程的内存,也无法访问它们。 ### 虚拟地址空间的分区 每个进程的虚拟地址空间被划分为许多分区(partition)。由于地址空间的分区依赖于OS底层实现,所以会随Windows内核的不同而略有变化 **空指针赋值分区**: 0\~0xFFFF闭区间,如果进程中的线程试图读取或写入位于这一分区的内存地址,会引发访问违例。例如: ~~~c++ int* pnSomeInteger = (int*)malloc(sizeof(int)); *pnSomeInteger = 5; ~~~ malloc可能返回 NULL(例如内存不足),此时解引空指针就会触发访问违例。正确安全做法是先检查malloc返回值是否为NULL **用户模式分区**: 这一分区大小取决于CPU架构。一个进程无法通过指针来读取、写入或以其他任何方式访问驻留在这一分区中其他进程的数据。进程的大部分数据都保留在这一分区。由于每个进程都有自己的数据分区,所以一个应用程序破坏另一个应用程序的可能性非常小,使整个系统更加健壮 在Windows中,每个exe、dll模块都在这个区域加载。每个进程都可能将DLL加载到这一分区的不同地址。系统还在这一分区映射该进程可以访问的所有内存映射文件。 **内核模式分区**: 这一分区是OS代码所在的地方。与线程调度、内存管理、文件系统支持、网络支持以及设备驱动程序相关的代码都加载到这一分区。驻留在该分区的一切均由所有进程共享。虽然这一分区就在每个进程中用户模式分区的上方,该分区中的所有代码和数据都被完全保护起来。应用程序试图读取或写入位于这一分区的内存地址会引发访问违例。 ### 地址空间中的区域 系统创建一个进程并赋予它地址空间时,可用地址空间中的大部分都是闲置的或尚未分配的。为了使用这部分地址空间,必须调用VirtualAlloc来分配其中的区域。分配区域的操作被称为预定。 应用程序预定地址空间的一个区域时,系统会确保 1. 区域的起始地址在分配粒度(allocation granularity)的边界上。分配粒度随不同的CPU平台而不同。 2. 区域大小是系统页面大小的整数倍。页面(page)是一个内存单位,系统通过它来管理内存。和分配粒度类似,页面大小会因不同CPU而变化,x86和x64系统页面大小为4KB,IA-64系统页面大小为8KB。所以假如x86或x64系统中,预定一个10KB地址空间,系统会预定12KB区域 当程序不再需要访问预定的地址空间区域时,应释放该区域。这个过程称为释放地址空间区域,通常调用VirtualFree函数完成 ### 为区域调拨物理存储 预定的地址空间区域要想使用,必须分配物理存储,并将该存储映射到预定的区域。这个过程称为调拨物理存储。物理存储始终以页面为单位来调拨。我们调用VirtualAlloc函数将物理存储调拨给所预定的区域 为区域调拨物理存储时,并不需要为整个区域都调拨物理存储。程序不再需要访问预定区域中已调拨的物理存储时,应将其释放。这个过程称为撤销物理存储,通过调用VirtualFree完成 ### 物理存储和分页文件 如今的OS使磁盘空间看起来像内存一样,一种称为分页文件的磁盘文件包含可供任何进程使用的虚拟内存。当线程试图访问存储中的一个字节时,CPU必须知道该字节是在RAM中还是在磁盘上 一个线程试图访问所属进程的地址空间中的一个数据块时,有两种情况: 1. 线程要访问的数据就在RAM中。CPU会先将数据的虚拟内存地址映射到内存的物理地址,接着就可以访问内存中的数据 2. 线程要访问的数据不在RAM中,而是在分页文件的某处。这次不成功的访问称为页面错误(page fault)。发生这个错误时,CPU会通知OS,OS在RAM中找到一个闲置页面。如果找不到,OS必须先释放一个。如果待释放的页面没有修改过,OS可直接释放该页面。但如果系统需要释放一个修改过的页面,必须先将页面从RAM中复制到分页文件,接着OS对它内部的表项进行更新,以指出该数据的虚拟内存地址现已映射到了RAM中对应的物理内存地址。这时,CPU会重试那条引发页面错误的指令。但这一次CPU能将虚拟内存地址映射到物理RAM地址,并成功访问数据块 **不在分页文件中维护的物理存储**: 并不是每运行一个程序,系统就必须为该进程的代码和数据预定地址空间区域,为这些区域调拨物理存储,再将硬盘上的程序文件中的代码和数据复制到分页文件中已调拨的物理存储中。 相反,用户执行一个应用程序时,系统会打开应用程序的exe文件,确定应用程序的代码和数据大小,然后系统预定一个地址空间,注明与该区域关联的物理存储就是exe文件本身。这样一来不但能快速加载应用程序,分页文件还可以保持在合理的大小。 将程序位于磁盘上的文件映像用作一个地址空间区域的物理存储时,这个文件映像就是所谓的内存映射文件。 ### 页面保护属性 可为每个已分配的物理存储页指定不同的页面保护属性 | 保护属性 | 描述 | | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | | PAGE_NOACCESS | 试图读取页面、写入页面或执行页面中的代码将引发访问违例 | | PAGE_READONLY | 试图写入页面或执行页面中的代码将引发访问违例 | | PAGE_READWRITE | 试图执行页面中的代码将引发访问违例 | | PAGE_EXECUTE | 试图读取页面或写入页面将引发访问违例 | | PAGE_EXECUTE_READ | 试图写入页面将引发访问违例 | | PAGE_EXECUTE_READWRITE | 对页面执行任何操作都不会引发访问违例 | | PAGE_WRITECOPY | 试图执行页面中的代码将引发访问违例。试图写入页面将使系统为进程单独创建一份该页面的私有副本(以分页文件为后备存储) | | PAGE_EXECUTE_WRITECOPY | 对页面执行任何操作都不会引发访问违例。试图写入页面将使系统为进程单独创建一份该页面的私有副本(以分页文件为后备存储) | Windows数据执行保护(Data Execution Protection,DEP)提供了针对写入内存区域恶意代码的防护。如果启用了DEP,OS只为那些真正需要执行代码的额内存区域使用PAGE_EXECUTE\_*保护属性。其他保护属性(如PAGE_READWRITE)用于只存放数据的内存区域 **写时复制**: 上表最后两个属性是为了节省RAM的使用和分页文件的空间。Windows支持一种允许两个或更多进程共享同一个存储块的机制,提升了系统的性能,同时要求所有实例将存储视为只读或只执行 为避免混乱,OS会为共享存储块分配写时复制保护。一个exe或dll被映射到地址空间时,系统会计算有多少页面可写,然后从分页文件中分配存储空间来容纳可写页面。除非模块的可写页面被实际写入,否则不会用到分页文件存储。 进程中的一个线程试图向共享块写入时: 1. 系统在RAM中找到一个闲置页面(都有对应的分页空间文件) 2. 系统将线程想要修改的页面内容复制到第一步找到的闲置页,系统会为该闲置页分配PAGE_READWRITE或PAGE_EXECUTE_READWRITE,原始页的保护和数据不会发生任何修改 3. 系统更新进程的页面表,这样访问的虚拟地址就对应到RAM中的一个新页 此外,使用VirtualAlloc函数预定地址空间或调拨物理存储时,不能传递PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY,否则会导致调用失败,此时GetLastError返回ERROR_INVALID_PARAMETER。这两个属性是OS在映射exe和dll是用的 **特殊访问保护属性标志**: * PAGE_NOCACHE:禁止对已调拨页面进行缓存 * PAGE_WRITECOMBINE:对单个设备的多次写操作可以组合在一起,提高性能 * PAGE_GUARD:使应用程序在页面中有任何字节被写入时得到通知 内存区域类型 * Free:该区域的虚拟地址没有任何后备存储。该地址空间尚未预定,应用程序既可以从显示的基地址开始预定区域,也可以从闲置区域中的任何地方开始预定 * Private:该区域的虚拟地址由系统的分页文件提供后备存储 * Image:该区域的虚拟地址最初由映像文件(exe或DLL)提供后备存储,但之后就不一定了。例如,如果模块映像中的一个全局变量被写入,那么“写时复制”机制会改用分页文件作为特定页面的后备存储 * Mapped:区域的虚拟地址最初由内存映射文件提供后备存储,但之后就不一定了。例如,数据文件可能使用“写时复制”保护来映射。向文件的任何写入都会造成特定页面由分页文件提供原始存储。 为节省磁盘空间,链接器会尽量压缩生成的PE文件。但是当Windows将PE文件映射到进程的虚拟地址空间时,每个section必须从它自己的页面边界开始,而且大小必须是系统页面大小的整数倍。这意味着PE文件通常需要比文件大小更多的虚拟地址空间。 ### 数据对齐的重要性 只有在访问正确对齐的数据时,CPU执行效率最高。将数据的地址模数据的大小,结果为0则数据为对齐的。例如,一个WORD值的起始地址应该能被2整除,一个DWORD值的起始地址应该能被4整除 如果CPU要访问的数据没有对齐: 1. CPU引发异常 2. CPU通过多次访问已对齐的内存,取得完整的错位数据 最后修改:2026 年 01 月 09 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏