前言
Windows内存分为:
- 虚拟内存:虚拟内存表示逻辑地址,在物理内存并非真正存在的,但是跟物理内存有映射对应关系
- 物理内存:物理内存条上能找到实际地址的内存。
每个进程都有自己独立的虚拟内存,在32系统中,每个进程是4G的虚拟内存
而每个进程的虚拟内存只是预定的,而非实际提交的,不然系统这么多进程,系统不得需要4*n G 大小的内存条了
13.1 进程的虚拟地址空间
进程的虚拟地址空间
32位进程
地址范围:0x00000000 ~0xFFFFFFFF 任一值
地址空间大小:4GB
64位进程
地址范围:0x00000000·00000000 ~0xFFFFFFFF·FFFFFFFF 任一值
地址空间大小:16EB1
- 在windows中,正在运行的线程看不到属于操作系统操作本身的内存,这意味着它不能无意间访问到操作系统的数据
- 每个进程都有自己的私有的地址空间
- 同属于一个进程的线程们共享地址空间
- 虚拟地址空间有这么大,但不是物理存储器。仍然需要物理存储器分配或映射相应的地址空间,否则将会导致访问违规。
13.2 虚拟地址空间的分区
32位Windows内核和64位Windows内核的分区基本一致,唯一不同在于大小和分区的位置。
分区 | x86 32位 Windows | 3GB用户模式下的x86 32位Windows | X64 64位Windows | IA-64 64位Windows |
空指针赋值区 | 0x0000 0000 0x0000 FFFF | 0x0000·0000 0x0000·FFFF | 0x00000000·00000000 0x00000000·0000FFFF | 0x00000000·00000000 0x00000000·0000FFFF |
用户模式分区 | 0x0001·0000 0x7FFE·FFFF | 0x0001·0000 0xBFFE·FFFF | 0x00000000·00010000 0x000007FF·FFFEFFFF | 0x00000000·00010000 0x000006FB·FFFEFFFF |
64KB禁入分区 | 0x7FFF·0000 0x7FFF·FFFF | 0xBFFF·0000 0xBFFF·FFFF | 0x000007FF·FFFF0000 0x000007FF·FFFFFFFF | 0x000006FB·FFFF0000 0x000006FB·FFFFFFFF |
内核模式分区 | 0x8000·0000 0xFFFF·FFFF | 0xC000·0000 0xFFFF·FFFF | 0x00000800 ·00000000 0xFFFFFFFF·FFFFFFFF | 0x000006FC·00000000 0xFFFFFFFF·FFFFFFFF |
13.2.1 空指针赋值区
地址范围
0x00000000 ~ 0x0000FFFF
目的
目的:帮助程序员捕获对空指针的赋值,如果进程中的线程视图读取或写入位于这一分区内的内存地址,就会引发访问违规。
例如下面代码就没有执行错误检查。
int * pnSomeInteger = (int*) malloc(sizeof(int)); *pnSomeInteger = 5;
如果malloc无法分配足够的内存,那么它会返回NULL,但是代码没有检查这种可能,想当然认为分配一定会非常成功。
地址空间中空指针赋值区是禁止访问的,所以会引发内存访问违规并导致进程被终止。
13.2.2 用户模式分区
这一分区时进程地址空间的驻地,可用的地址空间和用户模式分区的大小取决于CPU体系。
CPU体系 | 用户空间可用地址区间 | 用户模式分区的大小 |
x86 普通 | 0x00010000 -> 0x7FFFFFFF | ~2G |
x86 /3GB | 0x00010000 -> 0xBFFEFFFF | ~3G |
x64 | 0x00000000·00010000 -> 0x000007FF·FFFEFFFF | ~8192G |
IA-64 | 0x00000000·00010000 -> 0x00006FB·FFFEFFFF | ~7152G |
32位下,默认为2GB大小。打开/3GB开关时,可扩大到3GB空间,但同时内核空间缩小为1GB)
exe/Dll 都载入到这一区域,每个进程都可能将这些dll载入到这一分区内的不同地址。系统同时会把该进程可以访问的所有内存映射文件映射到这一分区
1.在x86 Windows下得到更大的用户模式分区
修改Windows启动配置数据(Boot Configuration Data,BCD)
- 运行B.CDEdit.exe
- bcdedit /set IncreaseUserVa 3072,就可以为进程保留3GB用户模式地址空间,IncreaseUserVa可接受的最小值为2048,即默认的2GB。取消的话:bcdedit /deletevalue IncreaseUserVa。
- 为了让应用程序可以访问2GB以上的地址空间(特别地,早期的应用程序是不允许这样做的)。在链接时,可以打开/LARGEADDRESSAWARE链接开关。
2.在64位windows下得到2GB用户模式分区
- 因大量使用32位指针开发程序,仅重新编译程序会导致指针截断错误和不正确的内存访问。但可以让应用程序在地址空间沙箱(Address space sandbox)中运行,这也是默认的情况,系统能够保证高33位都为0的64地址截断为32位,这样进程可用的地址空间就被限制在最底部的2GB中。
- 当运行64位应用程序时,默认下系统会保留用户模式地址空间中在2GB以下(即最底部的2GB),这就是所谓的地址空间沙箱。这空间对于大多数的应用程序来说是足够的。
- 为了让64位应用程序能够访问整个用户地址空间,必须指定/LARGEADDRESSAWARE链接器开关来链接应用程序。
13.2.3 64KB禁入分区
- 64K禁入区的作用很明显是隔离了用户和内核空间;防止用户程序跨越到内核空间中。与内核交互会涉及到SSDT表,后续破解驱动保护部分会讲到。
- 因为分配粒度目前暂定 64KB 为了保持兼容。所以null和禁入区是最小64KB。分配粒度64KB是为了考虑以后cpu发展到64KB页面大小的时候可以和现在的程序兼容。
13.2.4 内核模式分区
- 操作系统代码的驻地。
- 与线程调度、内存管理、文件系统支持、网络支持以及设备驱动程序相关代码都载入到该分区。
- 应用程序试图读取和写入这一分区内存中的内存地址会引发访问违规。
13.3 地址空间中区域
预定地址空间中的一块区域
预订:为了使用可用地址空间,我们一般通过VirtualAlloc来分配其中的区域,分配区域的操作被称为 预定。
释放:VirtualFree
分配粒度:当前所有CPU平台都使用相同的分配粒度 大小为64KB
系统页面:
起始地址: 分配粒度(一般是64K)的整数倍。(分配粒度,会随不同CPU平台而有所不同,目前都是64KB,即系统会把分配请求取整到64KB的整数倍)
预定空间的区域的大小: 系统页面大小的整数倍(x86和x64的页面大小为4KB,I64系统使用的页面大小为8KB),即例如当应用程序预定一块大小为10KB的地址空间,系统自动取为页面大小整数倍,然后预定取整的大小区域。( x862/x643系统中会预定一块12KB,IA-64系统4会预定16KB.)
13.4 给区域调拨物理储存器
13.4.1 调拨/撤销 物理存储器
调拨物理存储器:为了使用所预定的地址空间区域,我们还必须分配物理存储器,并将存储器映射到所预定的区域。
撤销调拨物理存储器:当程序不在需要访问所预定区域中已调拨的物理存储器时,应该释放物理存储器。
13.5 物理存储器和页交换文件
13.5.1 页交换文件
页交换文件:磁盘上的文件一般被称为页交换文件,其中包含虚拟内存,可供任何进程使用。
注: 一般Windows页交换文件名为pagefile.sys
作用
页交换文件以一种透明的方式增大了应用程序的可用内存的总量,例如一台机器装备了1GB的内存,硬盘上还有1GB的页交换文件,那么应用程序会认为可用内存的总量为2GB
13.5.2 虚拟地址转为物理存储器地址
当一个线程试图访问所属进程地址空间中的一块数据时,可能出现两种情况:
- 访问数据就在内存中。在这时,CPU会先把虚拟内存地址映射到内存的物理地址,接下来就可以访问内存数据了。
- 访问数据不在内存中,而是在交换内存中的某处。这种情况下,这次不成功的访问称为页面错误, 具体情况如下图:
注:系统需要在内存和页交换文件之间复制页面的频率越高,硬盘颠簸得越厉害,系统运行得也越缓慢。
(颠簸是指操作系统把所有时间都花在页面文件和内存之间交换数据上,导致没有时间运行程序)
13.5.3 不在页交换文件中维护得物理存储器
内存映射文件:把硬盘上的文件映像(如一个.exe或DLL文件)作为虚拟内存的一部分(注意是文件映射,而不是页交换文件)。
当用户要执行一个可执行文件时,系统会打开应用程序对应的.exe文件并计算出应用程序的代码和数据的大小。然后预订一块地址空间,并注明与该区域相关的存储场所是.exe文件本身,而不是页交换文件。这样做可以将.exe的实际内容用作程序预订的地址空间区域,不仅载入程序速度快,而且可避免将为每个程序文件的代码和数据复制到页交换文件而造成页交换文件过于庞大和臃肿。
13.6 页面保护属性
我们可以给每个已分配物理存储页指定不同的页面保护属性。
保护属性 | 描述 |
PAGE_NOACCESS | 不可访问。试图读取、写入或执行页面中的数据(代码)时将引发访问违规。 |
PAGE_READONLY | 只读。试图写入页面或执行页面中的代码将引发访问违规 |
PAGE_READWRITE | 读写属性。试图执行页面中的代码将引发访问违规 。 |
PAGE_EXECUTE | 可执行属性。试图读取或写入页面将引发访问违规。 |
PAGE_EXECUTE_READ | 可读、可执行。读图写入页面将引发访问违规。 |
PAGE_EXECUTE_READWRITE | 可读可写可执行。对页面的任何操作都不会引发访问违规 |
PAGE_WRITECOPY | ①写时复制。试图执行页面中的代码将引发访问违规。②试图写入页面将使系统为进程单独创建一份该页面私有副本(以页交换文件为后备存储器) |
13.6.1 写时复制
写时复制属性以及作用
写时复制属性:PAGE_WRITECOPY
写时复制属性的作用:节省内存和页交换文件的使用
Windows提供一种机制,允许两个或两个以上的进程共享一块存储器。如10个记事本进程正在运行,所有的进程会共享应用程序的代码页和数据页。当只读或执行时,这种共享存储页的方式极大地提高了性能。但当某个实例写入一个存储页时,就要求给共享的存储页指定写时复制属性,这样在映射地址空间时,系统会计算有多少可写页面,然后从页交换文件中分配空间来容纳这些可写页面,在程序真正写入的时候,就存储在页交换文件中。
写入共享页面时,系统介入的操作
- 系统在内存中找到一个空闲页面。注意,该空闲页的后备页面来自页交换文件。它是系统最初将模块映射到进程的地址空间时分配的。由于是第1次映射时就分配了所需的页交换文件空间。所以这步不可能失败。
- 系统将要修改的页面内容复制到第1步找到的空闲页面,然后给这些空闲页面指定PAGE_READWRITE或PAGE_EXECUTE_READWRITE属性。(注意系统不会修改原始页面的保护属性和数据)
- 然后系统更新进程的页面表,这样,原来的虚拟地址现在就对应到内存中一个新的页面了。以后进程就可以访问它自己的副本了。
在预订地址空间或提交物理存储器时
在预订地址空间或提交物理存储器时,不能使用PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY保护属性,否则VirtualAlloc会失败,GetLastError将返回ERROR_INVALID_PARAMETER。
13.6.2 一些特殊得访问保护属性标志
保护属性 | 描述 |
PAGE_NOCACHE | 禁止对己提交的页面进行缓存。该标志的目的是为了让需要操控内存缓冲区的驱动程序开发人员使用。一般不建议用将这标志用于除此以外的其他用途。 |
PAGE_WRITECOMBINE | 允许把单个设备的多次写操作组合在一起,以提高性能。也是给驱动程序开发人员用的。 |
PAGE_GUARD | 使应用程序能够在页面中的任何一个字节被写入时得到通知。 |
13.7 实例分析
13.8 数据对齐的重要性
数据对齐:数据地址%数据大小 = 0时 数据是对齐的
数据没有对齐的两种情况
- CPU会引发一个异常
- CPU会通过多次访问已对齐的内存,来取得整个错位数据即未对齐数据。
x86CPU对错位数据的处理
- EFLAGS寄存器的AC标志位(AlignmentCheck)为0时,CPU自动执行必要的操作来访问错位数据)
- AC标志位为1时,如果试图访问错位数据,CPU会触发INT 17H中断。
(对于x86版本的Windows从来不变为AC标志位(即永远为0),因此x86处理器上运行应用程序,绝对不会发生数据错位的异常,
AMDx86-64CPU 对错位数据处理
会得到和x86的相同的结果,这是因为在默认情况下CPU处理了数据错位的错误。
IA-64CPU对错位数据处理
IA-64CPU处理器不能自己处理数据错误的错误,因此当访问错位数据时,会抛出一个EXECPTION_DATATYPE_MISALIGNMENT异常,我们通用SetErrorMode函数并传SEM_NOALIGNMENTFAULTEXCEPT 标志,让系统自动修正数据错位的错误。(注意传入这个标志会影响进程中所有的线程,而且这个错误模式会被进程的子进程继承)
编译器对错位数据的处理
- IA-64版本的VC/C++编译器支持__unaligned关键字
如DWORD dw = (__unaligned DWORD)pvDataBuffer; - x86版本的VC/C++编译器:不支持__nnaligned关键字,所以这个关键字在x86版本的编译器下会报错。
- 鉴于编译器对__unaligned有不同的支持,为代码的通用性,建议用UNALIGNED和UNLIGNED64宏来替换__unaligned。
总结
- exabytes,百亿亿字节 ↩︎
- x86:从1978年来的8086处理器开始,就已经出现了x86架构CPU,即32位处理器。 ↩︎
- x86-64:又简称为x64,最初开发为1999年AMD,为了扩充IA64。当时的x86-64架构诞生颇有时代意义,处理器的发展遇到了瓶颈,内存寻址空间由于受到32位CPU的限制而只能最大到约4G。于是就有了x86-64。后被INTEL所采用。 ↩︎
- ia- 64:其实ia64的历史早于x86-64x,最初由INTEL和惠普联合推出。由于ia-64不与32位兼容,所以没有受到重视。直到INTEL采用了 AMD的x86-64架构,才正式的批量生产。而后为了日益扩张的计算需求,INTEL重新将IA-64拿出来,发布了安腾系列服务器CPU。