0%

Windows核心编程 - 内存映射文件

最近在学习 Win32编程,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习Win32开发的童鞋们提供一份参考。

本系列博文均根据学习《Windows核心编程》一书总结而来;

运行环境:

  • 操作系统: Windows 10家庭版
  • 编译器:Visual Studio 2019

内存映射文件

与虚拟内存一样,内存映射文件用来保留一个地址空间的区域,并将物理内存提交给它,它们之间的差别是,物理内存来自于一个位于磁盘上的文件,而不是系统文件。一旦该文件被映射,就像整个文件加载内存一样;

内存映射文件可以用于三个不同的目的:

  • 系统使用内存映射文件,以便于加载和执行exe或dll文件。这可以节约页文件空间和系统执行时间;
  • 可以使用内存映射文件来访问磁盘上的文件数据。
  • 可以使用内存映射文件,使同一台计算上运行的多个线程可以共享数据。

内存映射的可执行文件和DLL

当系统调用CreateProcess函数时,系统将执行以下步骤:

  • 系统找到调用CreateProcess时设定的.exe文件;
  • 系统创建一个进程的内核对象
  • 系统为这个新进程创建一个私有地址空间
  • 系统保留一个足够大的地址空间区域,用于存放.exe文件。
  • 系统注意到支持已保留区域的物理内存是在磁盘上的.exe文件中,而不是在系统的页文件中。

当exe文件被映射到进程的地址空间中以后,系统将访问.exe文件的每一个部分,该文件列举了包含.exe文件中的代码要调用的函数的DLL文件。然后系统为每个DLL文件调用LoadLibrary函数。

当所有的exe和dll都被映射到进程的地址空间以后,系统就可以开始执行exe文件的启动代码。当exe被映射以后,系统将负责所有分页、缓冲和高速缓存的处理。

可执行文件或DLL多个实例不能共享数据

当为正在运行的应用程序创建新实例的时候,系统将打开用于标识可执行文件映像的文件影响对象和另一个内存映像试图,并创建一个新进程对象和一个新线程对象。

系统应用写拷贝这样的特性来防止这种行为,如果我们要尝试修改它的内存映射文件时,系统就会为我们建立一个页文件;

可执行文件或DLL中共享静态数据

每个exe或dll的映像都由许多个节组成。

每个节都拥有与其相关的一组属性。

我们可以借助工具来查看我们的一个EXE的节:

使用内存映射文件

若要使用内存应文件,必须执行以下步骤:

  • 创建或打开一个文件内核对象,该对象用于识别磁盘上你想用作内存映射的文件
  • 创建一个文件映射内核对象,告诉系统该文件大小和你想如何访问文件
  • 让系统将文件映射对象全部或一部分映射到你的进程地址空间中

创建或打开文件内核对象

若要创建或打开一个文件内核对象,总是要调用CreateFile函数:

1
2
3
4
5
6
7
HANDLE CreateFile(LPCTSTR lpFileName,			//文件名
DWORD dwDesiredAccess, //何种权限访问,可读可写
DWORD dwShareMode, //何种权限共享文件
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDispostion , //如何创建
DWORD dwFlagsAndAttributes, //文件属性
HANDLE hTemplateFile); //用于复制文件句柄

创建文件映射内核对象

调用CreateFileMapping函数,就可以将文件映像的物理内存高速系统;

1
2
3
4
5
6
HANDLE CreateFileMapping(HANDLE hFile, 		//文件句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, //安全描述符
DWORD flProtect, //保护属性
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //地位文件大小
LPCTSTR lpName ); //共享内存名称

将文件数据映射到进程地址空间

当创建了一个文件映射对象后,仍然必须让系统为文件保留一个地址空间区域,并将文件的数据作为映射到该区域的物理内存进行提交;

1
2
3
4
5
LPVOID MapViewOfFile( HANDLE hFileMappingObject, 	//文件映射对象句柄
DWORD dwDesiredAccess, //如何访问该数据
DWORD dwFileOffsetHigh, //高32位映射
DWORD dwFileOffsetLow, //低32位映射
DWORD dwNumberOfBytesToMap );//NULL

从进程地址空间撤销映射

当不再需要保留映射到进程地址空间中的文件数据时,可以调用以下函数释放:

1
BOOL UnmapViewOfFile( LPCVOID lpBaseAddress );		//区域基地址

为了提高效率,系统将文件的数据页面缓存,并在文件的映像操作的时候不立即更新;我们可以利用FlushViewOfFile

1
2
BOOL FlushViewOfFile( LPCVOID lpBaseAddress, 		//区域基地址
DWORD dwNumberOfBytesToFlush ); //刷新的字节数

设定内存映射文件的基地址

正如可以使用VirtualAlloc函数来确定对地址空间进行倒叙所用的起始地址一样;

1
2
3
4
5
6
7
LPVOID MapViewOfFileEx(
HANDLE hFileMappingObject, // 文件映射对象的句柄
DWORD dwDesiredAccess, // 访问模式
DWORD dwFileOffsetHigh, // 文件偏移的高32位
DWORD dwFileOffsetLow, // 文件偏移的低32位
SIZE_T dwNumberOfBytesToMap, // 映射视图的大小
LPVOID lpBaseAddress // 指定映射视图的其实内存地址

demo01:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//进程01创建文件映射对象
#include <Windows.h>
#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
// 打开文件
HANDLE hFile = CreateFile
(
TEXT("gushi.txt"), // 文件名:要打开的文件
GENERIC_READ, // 访问权限:只读
FILE_SHARE_READ, // 共享对象:只读
NULL, // 安全属性:NULL
OPEN_EXISTING, // 对文件操作:打开
FILE_ATTRIBUTE_NORMAL, // 文件属性标志:正常
NULL // 必须为NULL
);

// 判断文件是否成功打开
if (hFile == INVALID_HANDLE_VALUE)
{
// 获取错误信息
cout << "打开文件失败:" << GetLastError() << endl;
return -1;
}

// 创建文件映射对象(物理页与文件相连)
HANDLE hMapFile = CreateFileMapping
(
hFile, // 需要创建映射的文件
NULL, // 安全属性:NULL
PAGE_READONLY, // 访问类型:只读
0, // 默认为0
0, // 默认为0
TEXT("文件映射") /* 如果有不同的进程访问,
则取个独一的名字,否则为NULL */
);

if (hMapFile == NULL)
{
cout << "创建文件映射失败:" << GetLastError() << endl;
return -2;
}

// 链接物理页 (进程与文件视图进行链接)
// 返回的是文件在内存中的地址
LPVOID lpDw = MapViewOfFile
(
hMapFile, // 文件映射对象映射到地址空间
FILE_MAP_READ, // 访问类型:只读
0, // 默认为0
0, // 默认为0
0 // 默认为0
);

if (lpDw == NULL)
{
cout << "链接物理页失败:" << GetLastError() << endl;
return -3;
}

printf("%ls", lpDw);
getchar();
// 此处要加上断点,否则内存将释放,进程B就访问不到此内存
UnmapViewOfFile(lpDw); // 关闭链接
CloseHandle(hFile); // 关闭句柄
CloseHandle(hMapFile);
return 0;
}


demo02:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//进程02打开文件映射对象
#include <Windows.h>
#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
// 打开一个命名的文件映射对象
HANDLE hFile = OpenFileMapping
(
FILE_MAP_READ, // 访问模式:只读
FALSE, // 继承标志:FALSE
TEXT("文件映射") // 文件映射对象id
);

// 判断是否打开成功
if (hFile == NULL)
{
cout << "打开文件映射对象失败:" << GetLastError() << endl;
return -1;
}

LPVOID lpDw = MapViewOfFile(hFile, FILE_MAP_READ, 0, 0, 0);

printf("%ls\n",(LPCSTR)lpDw);

UnmapViewOfFile(lpDw);
CloseHandle(hFile);

return 0;
}

共享内存

共享内存的申请与释放

共享内存

通过VirtualAlloc或VirtualAllocEx申请的内存我们称为私有内存,它的物理页是独占的,只有在进程内部可以访问,但是我们可以通过Mapping的方式来映射内存,达到两个进程共享一个物理页;

Mapping

1
2
3
4
5
6
7
8
9
CreateFileMapping(
INVALID_HANDLE_VALUE,//为INVALID_HANDLE_VALUE时,不与文件关联
NULL, //安全描述符
PAGE_READWRITE, //保护模式(读写的权限)
0, //32位通常为空8777777
3...=---------------""""""""????????????????????????????????????
BUFSIZ, //物理页的大小
MapFileName //指定文件映射对象的名字
);
1
2
3
4
5
6
7
g_lpBuff = (LPTSTR)MapViewOfFile(
g_hMapFile, //句柄
FILE_MAP_ALL_ACCESS,//虚拟内存读写权限
0, //32位用不上
0, //从哪开始映射
BUFSIZ //物理页多大它就多大
);

demo01:·

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <Windows.h>
#include <stdio.h>
#include <tchar.h>

#define MapFileName "共享内存"
#define BUF_SIZE 0x1000
HANDLE g_hMapFile;
LPTSTR g_lpBuff;

int main()
{
getchar();
HANDLE g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFSIZ, L"MapFileName");

//将物理页与线性地址进行映射
LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
DWORD dwError = GetLastError();
*(PDWORD)g_lpBuff = 0x12345678; //向共享内存中写入数据
printf("%p", g_lpBuff);
getchar();
UnmapViewOfFile(g_lpBuff);
CloseHandle(g_hMapFile);
getchar();
return 0;
}

demo02:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "windows.h"
#include <tchar.h>
#include <stdio.h>

#define MapFileName "共享内存"
#define BUF_SIZE 0x1000
HANDLE g_hMapFile;
LPTSTR g_lpBuff;

int main(int argc, char* argv[])
{

g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUF_SIZE, L"MapFileName");
//将物理页与线性地址进行映射,g_lpBuff就是虚拟内存地址
g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);

printf("进程B:%x", *(PDWORD)g_lpBuff);
//关闭映射
UnmapViewOfFile(g_lpBuff);
//关闭句柄
CloseHandle(g_hMapFile);
getchar();
return 0;
}