第13章 Windows内存体系结构

2023-04-25,,

13.1 Windows的虚拟地址空间安排

13.1.1虚拟地址空间的分区(即虚拟地址空间布局)

进程的地址空间划分

分区

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

(1)空指针赋值分区

  ①为帮助程序员捕获对空指针的赋值,当线程试图读取或写入这一分区的内存地址,就会引发访问违规

  ②没有任何办法可以让我们分配到位于这一地址区间的虚拟内存。

(2)用户模式分区

  ①进程地址空间的驻地。对于应用程序来说,大部分数据都保存在这一分区。

  ②32位下,默认为2GB大小。打开/3GB开关时,可扩大到3GB空间,但同时内核空间缩小为1GB)

【x86 Windows下获得更大的用户模式分区】——修改Windows启动配置数据(Boot Configuration Data,BCD)

  ①运行BCDEdit.exe

  ②bcdedit /set IncreaseUserVa 3072,就可以为进程保留3GB用户模式地址空间,IncreaseUserVa可接受的最小值为2048,即默认的2GB。取消的话:bcdedit /deletevalue IncreaseUserVa。

  ③为了让应用程序可以访问2GB以上的地址空间(特别地,早期的应用程序是不允许这样做的)。在链接时,可以打开/LARGEADDRESSAWARE链接开关。

【在64位Windows下得到2GB用户模式分区】将32位应用程序移植到64位环境下

  ①因大量使用32位指针开发程序,仅重新编译程序会导致指针截断错误和不正确的内存访问。但可以让应用程序在地址空间沙箱(Address space sandbox)中运行,这也是默认的情况,系统能够保证高33位都为0的64地址截断为32位,这样进程可用的地址空间就被限制在最底部的2GB中。

  ②当运行64位应用程序时,默认下系统会保留用户模式地址空间中在2GB以下(即最底部的2GB),这就是所谓的地址空间沙箱。这空间对于大多数的应用程序来说是足够的。

  ③为了让64位应用程序能够访问整个用户地址空间,必须指定/LARGEADDRESSAWARE链接器开关来链接应用程序。

(3)内核模式分区

  操作系统代码的驻地。与线程调度、内存管理 、文件系统支持、网络支持以及设备驱动程序相关的代码都载入到这个分区中。该分区中的所有代码和数据都为所有进程共有,但这些代码和数据都是被保护起来的,如果试图在这分区的某个内存地址读取或写入数据时,会引发访问违规。

13.1.2 Windows内存安排(时间上的安排)

(1)每个应用程序都有自己的4GB寻址空间。该空间可存放操作系统、系统DLL和用户DLL代码,它们之中有各种函数供应用程序调用。再除去其他的一些空间,余下的是应用程序的代码、数据和可以分配的地址空间。

(2)不同应用程序的线性地址空间是隔离的。虽然它们在物理内存中同时存在,但在某个程序所属的时间片中,其他应用程序的代码和数据没有被映射到可寻址的线性地址中,所以是不可访问的。从编程的角度看,程序可供使用的4GB的寻址空间,而且这个空间是“私有的”

(3)DLL程序没有自己的“私有”的空间。它们总是被映射到其他应用程序的地址空间中,当做其他应用程序的一部分运行。原因很简单,如果它不和其他程序同属一个地址空间,应用程序就不能调用它。

(4)操作系统和系统DLL的代码需要供每个应用程序调用,所以在所有的时间片中都必须被映射;

(5)用户程序只在自己所属的时间片内被映射。用户DLL则有选择地被映射。如程序B和C都调用了xxx.dll,那么物理内存中xxx.dll(注意在内存中已经存在了!)的代码在图中的时间片2和n中被映射,其他时间片就不需要被映射。(当然物理内存中只需要一份xxx.dll的代码)。

13.2 地址空间中的区域

(1)预定地址空间中的一块区域(预订:VirtualAlloc、释放:VirtualFree)

  ①起始地址:分配粒度(一般是64K)的整数倍。(注意:分配粒度与CPU平台有关,同时系统自己预订的区域的起始地址不一定非得是64KB的整数倍,如系统为进程环境块(PEB)和线程环境块(TEB)预定的区域地址就可能不是64KB的整数倍,但区域的大小仍是系统页面大小的整数倍。应用程序自己预订的区域,)

  ②预定空间的区域的大小:系统页面大小的整数倍(x86和x64的页面大小为4KB,I64系统使用的页面大小为8KB)

(2)将预订区域提交物理存储器

  ①提交时,可以只提交区域的一部分。如预订64KB空间大小,但可以只提交第2、第4两个页面(同样是调用VirtualAlloc函数,但传入的是MEM_COMMIT类型的参数)。

  ②撤消提交:VirtualFree,并传入MEM_DECOMMIT

13.3 物理存储器和页交换文件

(1)虚拟内存的实现:当应用程序调用VirtualAlloc函数将预订的空间区域提交物理存储器(物理内存或页交换文件)时,该空间实际上仍然不是从物理内存而是页交换文件中分配得到的,以后当访问该空间时,会因数据并不存在于物理内存而发生访问“页面错误”,从而引发操作系统利用异常处理机制将虚拟地址空间真正映射到对应的物理内存中,如下图所示。

(2)内存映射文件:把硬盘上的文件映像(如一个.exe或DLL文件)作为虚拟内存的一部分(注意是文件映射,而不是页交换文件)。当用户要执行一个可执行文件时,系统会打开应用程序对应的.exe文件并计算出应用程序的代码和数据的大小。然后预订一块地址空间,并注明与该区域相关的存储场所是.exe文件本身,而不是页交换文件。这样做可以将.exe的实际内容用作程序预订的地址空间区域,不仅载入程序速度快,而且可避免将为每个程序文件的代码和数据复制到页交换文件而造成页交换文件过于庞大和臃肿。

13.4 页面保护属性

保护属性

描述

PAGE_NOACCESS

不可访问。试图读取、写入或执行页面中的数据(代码)时将引发访问违规。

PAGE_READONLY

只读。试图写入页面或执行页面中的代码将引发访问违规

PAGE_READWRITE

读写属性。试图执行页面中的代码将引发访问违规 。

PAGE_EXECUTE

可执行属性。试图读取或写入页面将引发访问违规。

PAGE_EXECUTE_READ

可读、可执行。读图写入页面将引发访问违规。

PAGE_EXECUTE_READWRITE

可读可写可执行。对页面的任何操作都不会引发访问违规

PAGE_WRITECOPY

①写时复制。试图执行页面中的代码将引发访问违规。

②试图写入页面将使系统为进程单独创建一份该页面私有副本(以页交换文件为后备存储器)

PAGE_EXECUTE_WRITECOPY

对页面执行任何操作都不会引发访问违规。试图写入页面将使系统为进程单独创建一份该页面私有副本(以页交换文件为后备存储器)

★注意:如果Windows启用了数据执行保护(Data Execution Protection,DEP),当CPU试图执行某个页面中的代码,而该页面又没有PAGE_EXECUTE_*保护属性,那么CPU会抛出访问违规异常。(DEP开启方法:我的电脑→右键“属性”→高级系统设置→性能→设置→数据执行保护,选中“仅为基本Windows程序和服务启用DEP”)

13.4.1 写时复制

(1)写时复制属性的作用:节省内存和页交换文件的使用

Windows提供一种机制,允许两个或两个以上的进程共享一块存储器。如10个记事本进程正在运行,所有的进程会共享应用程序的代码页和数据页。当只读或执行时,这种共享存储页的方式极大地提高了性能。但当某个实例写入一个存储页时,就要求给共享的存储页指定写时复制属性,这样在映射地址空间时,系统会计算有多少可写页面,然后从页交换文件中分配空间来容纳这些可写页面,在程序真正写入的时候,就存储在页交换文件中。

(2)写入共享页面时,系统介入的操作

  ①系统在内存中找到一个空闲页面。注意,该空闲页的后备页面来自页交换文件。它是系统最初将模块映射到进程的地址空间时分配的。由于是第1次映射时就分配了所需的页交换文件空间。所以这步不可能失败。

  ②系统将要修改的页面内容复制到第1步找到的空闲页面,然后给这些空闲页面指定PAGE_READWRITE或PAGE_EXECUTE_READWRITE属性。(注意系统不会修改原始页面的保护属性和数据)

  ③然后系统更新进程的页面表,这样,原来的虚拟地址现在就对应到内存中一个新的页面了。以后进程就可以访问它自己的副本了。

(3)在预订地址空间或提交物理存储器时,不能使用PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY保护属性,否则VirtualAlloc会失败,GetLastError将返回ERROR_INVALID_PARAMETER。

13.4.2 一些特殊的访问保护属性标志

保护属性

描述

PAGE_NOCACHE

禁止对己提交的页面进行缓存。该标志的目的是为了让需要操控内存缓冲区的驱动程序开发人员使用。一般不建议用将这标志用于除此以外的其他用途。

PAGE_WRITECOMBINE

允许把单个设备的多次写操作组合在一起,以提高性能。也是给驱动程序开发人员用的。

PAGE_GUARD

使应用程序能够在页面中的任何一个字节被写入时得到通知。

13.5 实例分析

13.5.1 各区域分析

……

……

(1)基地址

  ①从0x0000 0000开始,到0x7FFE 0000+ FFFF结束。

  ②几乎所有的非空闲区域的基地址都是64KB的整数倍(这是由系统地址空间的分配粒度决定的)。如果不是64KB的整数倍,这意味着该区域是由操作系统以进程名义分配的。

(2)区域类型

类型

描述

Free(空闲)

区域的虚拟地址没有任何后备存储器。该地址空间尚未预订,应用程序可以从基地址开始预订,也可以从空闲区域内的任何地方开始预订区域

Private(私有)

区域的虚拟地址以系统的页交换文件为后备存储器

Image(映像)

一开始以映像文件(如exe或DLL)为后备存储器,但以后不一定以映像文件为后备存储器(如程序写入映像文件中一个全局变量,那么写时复制会改用页交换文件来作为后备存储器)(映射文件可理解为exe或dll文件)

Mapped(己映射)

一开始以内存映射文件为后备存储器,此后不一定以内存映像文件为后备存储器。(如内存映射文件可能会使用写时复制保护属性。任何写操作会使对应的页面改用页交换文件来作为后备存储器)

★注意:对于每个区域整体而言,该区域的类型是推测出来的(除空闲外),详细见13.5.2节《区域内部》的内容。

(3)区域预订的字节数

  ①始终是CPU页面大小的整数倍(对于x86为4字节,即4096的倍数)

  ②为了节省磁盘空间,链接器会尽可能对对PE文件进行压缩,所以磁盘上的文件大小与映射到内存所需要的字节数是有差异的。

(4)预订区域内部的块的数量(block)

  ①块是一些连续的页面,这些页面具有相同的保护属性,并以相同类型的物理存储器为后备存储器。对闲置页面来说,由于不可能将存储器拨给他们,该值始终为0。

  ②每个区域最大能容纳的块的数量为:区域大小/页面大小,即当每个页面都是一个不同的块时,这里块的数量最多。

(5)区域的保护属性:

  ①E=execute,R=read,W=Write,C=copy on write。如果区域没有显示任保护属性,表示该区域没有任何访问保护。闲置区域没有与之相关联的保护属性。

  ②PAGE_GAUARD和PAGE_ONCACHE标志对地址空间没有意义,这些标志只有当用于物理存储时才有意义。

  ③如果同时给区域和物理存储器指定了保护属性,那么以后者为准。(见区域内部一节的分析)

13.5.2 区域内部——以0x767F000所在区域为例(本例中用来装载User32.dll的区域)

基地址

类型

大小

块数

保护属性

描述

767F0000

767F0000

767F1000

7685A000

7685B000

7685C000

映像

映像

映像

映射

映像

映像

647168

4096

430080

4096

4096

40960

5

ERWC

-R—(只读)

ER—(可执行,可读)

-RW—(可读可写)

-RWC-

-R-—

C:\Windows\system32\USER32.dll

//提交了105个页面(430080/4096)

7688E0000

空闲

8192

(1)第1列显示的是具有相同状态和保护属性的一组页面的地址。如第1组只读,第2组可执行可读,第3组可读可写。

(2)第2列块的类型,即以何种类型的物理存储器为后备存储器。Private、Mapped、Image分别表示以页交换文件、内存映射文件和加载的Exe(或Dll)文件为后备存储器。但Free和Reserved表示该块没有后备物理存储器。

(3)第3列:块的大小。一个区域中所有的块都是连续的,不会存在任何的间隙。

(4)第4列:所预订区域内部中块的数量

(5)第5列:块的页保护属性:一个块的保护属性会优先于所属区域的保护属性。(注意:PAGE_GUARD、PAGE_NOCACHE、PAGE_WRITECOMBINE保护属性只能用于块(即物理存储器,不能用于区域)。(注意:区域可以理解为预订的地址空间,块可以理解为在这个预订的地址空间中进一步细分出来的更小的一片地址空间)。

13.6 数据对齐的重要性

(1)数据对齐:将数据的地址 % 数据大小 = 0时的数据是对齐的

(2)x86CPU对错位数据的处理

  ①EFLAGS寄存器的AC标志位(AlignmentCheck)为0时,CPU自动执行必要的操作来访问错位数据)

  ②AC标志位为1时,如果试图访问错位数据,CPU会触发INT 17H中断。(对于x86版本的Windows从来不变为AC标志位(即永远为0),因此x86处理器上运行应用程序,绝对不会发生数据错位的异常,但IA-64CPU处理器不能自己处理数据错误的错误,因此当访问错位数据时,会抛出一个EXECPTION_DATATYPE_MISALIGNMENT异常,我们通用SetErrorMode函数并传为SEM_NOALIGNMENTFAULTEXCEPT标志,让系统自动修正数据错位的错误。(注意传入这个标志会影响进程中所有的线程,而且这个错误模式会被进程的子进程继承)

(3)编译器对错位数据的处理

  ①IA-64版本的VC/C++编译器支持__unaligned关键字

    如DWORD dw = *(__unaligned DWORD*)pvDataBuffer;

  ②x86版本的VC/C++编译器:不支持__nnaligned关键字,所以这个关键字在x86版本的编译器下会报错。

  ③鉴于编译器对__unaligned有不同的支持,为代码的通用性,建议用UNALIGNED和UNLIGNED64宏来替换__unaligned。

#if defined(_M_MRX000) || defined(_M_ALPHA) || defined(_M_PPC) ||defined(_M_IA64) || defined(_M_AMD64)
#define ALIGNMENT_MACHINE
#define UNALIGNED __unaligned
#if defined(_WIN64)
#define UNALIGNED64 __unaligned
#else
#define UNALIGNED64
#endif
#else
#undef ALIGNMENT_MACHINE
#define UNALIGNED
#define UNALIGNED64
#endif

【AlignOf程序】内存对齐演示程序

#include <windows.h>
#include <tchar.h>
#include <locale.h> //在MSVC中,一般使用#progma pack来指定内存对齐:
#pragma pack(show) //以警告信息的形式显示当前字节对齐的值(在编译输出框显示)
//默认的8字节对齐
struct BYTE1{
char ch1;
int i1;
}; #pragma pack(push)
#pragma pack(1)
#pragma pack(show)
struct BYTE2{
char ch2;
int i2;
};
#pragma pack(pop) //微软的__declspec(align(#)),其#的内容可以是预编译宏,但不能是编译期数值
struct __declspec(align()) BYTE3{
char ch3;
int i3;
}; VOID AlignTest(PVOID pvDataBuffer){ char *pc = (PCHAR)pvDataBuffer;
pc++; //指向第2个字节 //未对齐方式访问:将第2-5个字节当成DWORD来看待,此时内存没对齐,
//因为DWORD的起始地址而是4的倍数
DWORD dwUnAligned = *(DWORD*)(pc);
_tprintf(_T("dwUnAligned=0x%08X\n"), dwUnAligned); //用对齐方式访问,效率更高
DWORD dwAligned = *(UNALIGNED DWORD*)pc;//*(DWORD*)pc;
_tprintf(_T("dwAligned =0x%08X\n"), dwAligned);
} int _tmain(){ char c[] = { , , , , , , , , , };
AlignTest((PVOID)c); //内存对齐
size_t sz1 = sizeof(BYTE1);
size_t sz2 = sizeof(BYTE2);
size_t sz3 = sizeof(BYTE2);
_tprintf(_T("sizeof(BYTE1)==%d\n"), sz1);
_tprintf(_T("sizeof(BYTE2)==%d\n"), sz2);
_tprintf(_T("sizeof(BYTE3)==%d\n"), sz3); //MSVC使用__alignof获得结构体中最大成员变量的对齐大小,即结构体的对齐大小
sz1 = __alignof(BYTE1); //最大成员为i1,对齐大小应为4
sz2 = __alignof(BYTE2); //最大成员为ch2,对齐大小应为1
sz3 = __alignof(BYTE3); //最大成员为i3,对齐大小应为4
_tprintf(_T("__alignof(BYTE1)==%d\n"), sz1);
_tprintf(_T("__alignof(BYTE2)==%d\n"), sz2);
_tprintf(_T("__alignof(BYTE3)==%d\n"), sz3); return ;
}

第13章 Windows内存体系结构的相关教程结束。

《第13章 Windows内存体系结构.doc》

下载本文的Word格式文档,以方便收藏与打印。