第13章 Windows内存体系结构
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内存体系结构的更多相关文章
- Windowsw核心编程 第13章 Windows内存结构
第1 3章 Wi n d o w s的内存结构 13.1 进程的虚拟地址空间 每个进程都被赋予它自己的虚拟地址空间.对于 3 2位进程来说,这个地址空间是4 G B,因为3 2位指针可以拥有从0 x ...
- 读书笔记——Windows核心编程(13)Windows内存体系结构
对于32位进程(0x0000 0000~0xFFFF FFFF),有4GB的地址空间. 每个进程都有自己专有的地址空间,当进程的各个线程运行时,它们只能访问属于该进程的内存. 这4GB其实是虚拟地址空 ...
- windows内存体系结构 内存查询,读,写(附录源码)
“进程内存管理器”这个程序实现的最基本功能也就是对内存的读写,之前的两篇文章也就是做的一个铺垫,介绍了内核模式切换和IoDeviceControl函数进行的应用程序与驱动程序通信的问题.接下来就进入正 ...
- 《Windows核心编程系列》十二谈谈Windows内存体系结构
Windows内存体系结构 理解Windows内存体系结构是每一个励志成为优秀的Windows程序员所必须的. 进程虚拟地址空间 每个进程都有自己的虚拟地址空间.对于32位操作系统来说,它的地址空间是 ...
- Windows核心编程:第13章 内存体系结构
Github https://github.com/gongluck/Windows-Core-Program.git //第13章 内存体系结构.cpp: 定义应用程序的入口点. // #inclu ...
- C# 6 与 .NET Core 1.0 高级编程 - 39 章 Windows 服务(上)
译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 39 章 Windows 服务(上)),不对的地方欢迎指出与交流. 章节出自<Professional C ...
- Hadoop专业解决方案-第13章 Hadoop的发展趋势
一.前言: 非常感谢Hadoop专业解决方案群:313702010,兄弟们的大力支持,在此说一声辛苦了,经过两周的努力,已经有啦初步的成果,目前第13章 Hadoop的发展趋势小组已经翻译完成,在此对 ...
- 【Android】13.0 第13章 创建和访问SQLite数据库—本章示例主界面
分类:C#.Android.VS2015: 创建日期:2016-02-26 一.简介 Android 内置了三种数据存取方式:SQLite数据库.文件.SharedPreferences. 这一章我们 ...
- 前言和第一章.NET的体系结构
前言 COM:组件对象模型(Component Object Model COM)源自对象链接和嵌入(Object Linking and Embedding )OLE. DCOM:(Distribu ...
随机推荐
- 硅谷新闻4--解决页签手指按下从左到右滑动的bug
有一种方法可以阻止父层的View截获touch事件,就是调用 getParent().requestDisallowInterceptTouchEvent(true);方法.一旦底层View收到tou ...
- mybatis/callablestatement调用存储过程mysql connector产生不必要的元数据查询
INFO | jvm 1 | 2016/08/25 15:17:01 | 16-08-25 15:17:01 DEBUG pool-1-thread-371dao.ITaskDao.callProce ...
- 【.net程序破解】实战之标志位破解绕过注册法
今天有时间玩了下一个不错的软件Advanced System Cleaner,可惜要注册 于是想办法给破解了,这是跟之前不同的地方,属于.NET破解教程: 软件地址 - http://www.crsk ...
- JavaScript的作用域和闭包
首发于:https://mingjiezhang.github.io/ 闭包和作用域有着千丝万缕的联系. js的作用域 具体的作用域我就不展开叙述了.其中很重要的两点就是:js的作用域链机制和函数词法 ...
- 在 SharePoint 2013 中配置 Office Web Apps
原文发布于 2012 年 7 月 23 日(星期一) 如您所知或您即将知道,SharePoint 2013 中的 Office Web Apps 不再是 SharePoint 场中的服务应用程序.相反 ...
- TCP/IP协议握手过程详解
1,建立连接 在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接,如图1所示. (1)第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SE ...
- 二叉查找树(binary search tree)详解
二叉查找树(Binary Search Tree),也称二叉排序树(binary sorted tree),是指一棵空树或者具有下列性质的二叉树: 若任意节点的左子树不空,则左子树上所有结点的值均小于 ...
- Spring(八)SSH整合简述
一.Spring与Struts2的整合 1.1.整合步骤 1.2.配置web.xml 1.3.配置spring配置文件applicationContext.xml 1.4.配置struts配置文件 1 ...
- IOS 网络浅析-(三 NSURLConnection代理)
对于现在的iOS开发,用法简单,最古老最经典最直接的NSURLConnection的作用不是很大,但是作为一名ios开发者,我们应该拥有一颗热爱学习的心,下面通过代码的实现简单介绍一下NSURLCon ...
- 快速与MySQL交互,使用XMAPP打开MySQL数据库,并用shell进行与MySQL交互<Window 10>
1.如果想要通过XAMPP shell登录MySQL,还需要下载安装好XAMPP. 2.双击打开xampp-control.exe,会出现以下界面,点击开启Apache和MySQL,这样我们就开启服务 ...