系列文章目录
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
相关:
前言
内存映射文件 与虚拟内存相似,内存映射文件允许开发人员预定一块地址空间区域并给区域调拨物理存储器。不同之处在于内存映射文件的物理存储器来自磁盘上已有的文件,而不 是来自系统的页交换文件。
一旦把文件映射到地址空间,我们就可以对它进行访问,就好像整个文件都已经在被载入内存一样。
页交换文件,简单讲就是系统用于做虚拟内存的一个磁盘文件
共享内存是一种特殊的文件映射
内存映射主要用于以下三种情况:
- 系统使用内存映射文件来载入并运行.exe和动态链接库(DLL)。这大量节省了页交换文件的空间以及应用程序启动的时间。
- 开发人员可以使用内存映射文件来访问磁盘上的数据文件。这使得我们可以避免直接对文件进行I/O操作和对文件内容进行缓存。
- 通过使用内存映射文件,我们可以在同一台机器的不同进程间共享数据。Windows的确提供了其它一些方法来在进程间传送数据,但这些方法都是通过内存映射文件来实现的。因此,如果在同一台机器的不同进程之间共享数据,内存映射文件时最高效的方法。
一、映射到内存的可执行文件和DLL
CreateProcess调用过程
当一个线程在调用CreateProcess的时候,系统会执行以下步骤:
- 系统会先确定CreateProcess所指定的可执行文件所在的位置。如果无法找到该.exe文件,那么系统将不会创建进程,这时CreateProcess会返回FALSE.
- 系统创建一个新的进程内核对象。
- 系统为新进程创建一个私有地址空间。
- 系统预定一块足够大的地址空间来容纳.exe。待预定的地址空间区域的具体位置已经在.exe文件中指定。 默认情况下,.exe文件的基地址是0x00400000(
OEP PE可执行程序入口点地址
)。但是,只需在构建应用程序的.exe文件时使用/BASE连接器开关,我们就可以给自己的应用程序指定一个不同的地址。 - 系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的.exe文件,而并非来自系统的页交换文件。
通过LoadLibrary从exe和dll的段地址 读取DLL列表
当系统把.exe文件映射到进程的地址空间之后,会访问.exe文件中一个段,这个段列出了一些DLL文件,它们包含该.exe文件调用到的函数。然后系统会调用LoadLibrary来载入每个DLL,如果哪个DLL需要用到其它DLL,那么系统同样会调用其它DLL,那么系统同样会调用 LoadLibrary来载入相应的DLL。
每当调用LoadLibrary来加载一个DLL时,系统将执行下列操作步骤,它们均类似上面的第4和第5个步骤:
- 系统会预定一块足够大的地址空间区域来容纳DLL文件。待预定的的地址空间区域的具体位置已经在DLL文件中指定。
按照默认设置, Microsoft的Visual C++建立的 D L L文件基地址是0 x10000000(这个地址可能不同于在 64位Windows 2000上运行的6 4位D L L的地址)但是,你可以在创建DLL文件时重载这个地址,方法是使用链接程序的 /BASE选项。所有与Windows一起发布的系统DLL都有不同的基地址,这样即使把它们载入到同一个地址空间,也不会发生重叠。
- 如果系统无法在DLL文件指定的基地址处预定区域,这可能是因为该区域已经被另一个DLL或.exe占用,也可能是区域不够大,这时系统会尝试在另一个地址 来为DLL预定地址空间区域。
- 如果DLL不包含重定位信息(当使用连接器的/FIXED开关来构建DLL),这意味着DLL必须被载入到指定的基地址,否则无法被载入。
- 如果对DLL执行重定位,重定位不仅需要占用页交换文件中额外的存储空间,而且会增加载入DLL所需的时间。
- 系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的DLL文件,而并非来自页交换文件。如果由于Windows不能将DLL载入到指定的基地址而必须重定位的话,那么系统还会另外进行标注,表明DLL中有一部分物理存储器映射到了页交换文件。
注意:如果由于某个原因系统无法映射 . e x e和所有必要的D L L文件,那么系统就会向用户显示一个消息框,并且释放进程的地址空间和进程对象。CreateProcess函数将向调用者返回 FALSE,调用者可以调用GetLastError函数,以便更好地了解为什么无法创建该进程。
把所有的.exe文件和DLL文件都映射到进程的地址空间之后,系统会开始执行.exe文件的启动代码。当完成对.exe文件的映射后,系统会负责所有的换页(paging)、缓存(buffering)、以及高速缓存(caching)操作。
例如,
如果.exe文件中的代码使它跳到一个尚未加载到内存的指令地址,那么就会出现一个错误。
系统能够发现这个错误,并且自动将这页代码从该文件的映像加载到一个 RAM页面。
然后,系统将这个RAM页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。
当然,这一切是应用程序看不见的。当进程中的线程每次试图访问尚未加载到RAM的代码或数据时,该进程就会重复执行
同一个可执行文件或DLL的多个实例不会共享静态数据
如果一个应用程序已经在运行,那么当我们为这个应用程序创建一个新的进程时,系统只不过是打开另一个内存映射试图(memory-mapped view),创建一个新的进程对象,并为主线程创建一个新的线程对象。这个新打开的内存映射视图隶属一个文件映射对象(file-mapping object),后者用来标识可执行文件的映像。系统同时给进程对象和线程对象分别制定新的进程ID和线程ID。通过使用内存映射文件,同一个应用程序的多个实例可以共享内存中的代码和数据。
如下图,它描述了如何把可执行程序的代码和数据载入到虚拟内存,并映射到地址空间中。可执行页面---->载入到虚拟内存—>映射到应用程序地址空间
假设应用第二个实例程序,这时系统不过是把包含应用程序的代码和数据映射到第二个实例的地址空间中,如下图所示:
注意:
实际上,文件的内容被分为段,代码在一个段中,而全局变量再另一个段中,段是对齐到页面大小的整数倍,应用程序可以通过调用GetSystemInfo来检测页面大小,在.exe或DLL文件中,代码段通常在数据段的前面。
如果应用程序的一个实例修改了数据页面中的一些全局变量,那么应用程序所有实例的内存都会被修改。由于这种类型的修改可能会导致灾难性的结果,因此必须避免。
操作系统通过内存管理系统的写时复制特性来防止这种情况的发生。当写入内存映射文件时,系统会截获此尝试,接着为应用程序分配一块新内存,然后复制 页面内容,最终的结果是其它实例不会受到影响。
下图描绘了当应用程序的第一个实例试图修改数据页面2虽的一个全局变量时,会产生的结果。
在同一个可执行文件或DLL的多个实例间共享静态数据
背景知识
每个exe文件和dll文件映象都有许多段组成。
比如:
- 编译器会将代码放在一个叫.text段中,
- 将已经初始化的数据放在.data段中。
- 将未初始化得数据放在.bss段中。
每个段都有一些与之关联的属性:
多个实例共享数据的方法
自定义自己的段
#pragma data_seg("sectionname")
例如我们可以用下面代码来合建一个名为Share的段,它只包含一个LONG的变量
#pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_set();
注意:
编译器只会将已经初始化的变量放入自己定义的段当中,如果上面代码中g_lInstanceCount 没有初始化,则不会放到我们指定的段之中。
但是Vc++ 编译器提供了一个allocate声明符,它允许我们将未经初始化的数据放到任何我们想要放入的段中。
多个实例共享数据的方法:
之所以将变量放在一个单独的段中,最常见的原因就是为了共享exe或dll多个实例中共享数据。
注意:
但是MricroSoft并不鼓励使用共享段:
1.有潜在安全漏洞
2.意味着一个应用程序中的错误可能影响到另一应用程序。