第12章 内存(第二部分)
# 第12章 内存(第二部分)
第8章介绍了与内存相关的标准原生API(native APIs)。本章将重点讲解节内核对象(Section kernel objects),这类对象能够实现文件到内存的映射,以及进程间的内存共享。此外,还将描述NtDll.dll提供的不太为人熟知的内存管理对象——内存区域(memory zones)和后备列表(lookaside lists)。
本章内容包括:
- 节(Sections)
- 内存区域(Memory Zones)
- 后备列表(Lookaside Lists)
# 12.1 节
节内核对象支持将文件映射到内存,同时允许多个进程共享该内存。节所使用的文件可以是页面文件(page file)——也就是说,内存由页面文件提供支持,这意味着一旦节对象被销毁,该内存中的数据也会丢失。
| Windows API中将节对象称为“内存映射文件(Memory Mapped Files)”。 |
|---|
“页面文件(the page file)”这一术语看似表示只有一个页面文件,但实际上Windows最多支持16个页面文件。一个节可以使用任意页面文件的空间。
本节(无双关之意)将介绍与节对象相关的原生API。其中部分API在Windows驱动程序工具包(Windows Driver Kit,WDK)中有间接文档说明,我们会明确指出这一点。
# 12.1.1 创建节
节对象可以选择是否命名,与其他可命名对象类型(如互斥体(mutex)、信号量(semaphore)、事件(event))类似,既可以创建命名或未命名的节对象,也可以基于已存在的命名对象打开节对象。创建或打开节对象的简便函数是NtCreateSection:
NTSTATUS NtCreateSection(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PLARGE_INTEGER MaximumSize,
_In_ ULONG SectionPageProtection,
_In_ ULONG AllocationAttributes,
_In_opt_ HANDLE FileHandle);
2
3
4
5
6
7
8
该函数在WDK中有文档说明(对应ZwCreateSection)。Windows API中的CreateFileMapping(Numa)函数会调用NtCreateSection。
SectionHandle是函数执行成功后返回的句柄。DesiredAccess是请求的访问权限,创建新节时通常指定为SECTION_ALL_ACCESS。其他节访问权限位在WinNt.h中定义(包括SECTION_MAP_READ、SECTION_MAP_WRITE、SECTION_MAP_EXECUTE、SECTION_QUERY和SECTION_EXTEND_SIZE)。
ObjectAttributes是常规的OBJECT_ATTRIBUTES结构,若指定该参数,可以为节设置名称。如果节已存在,NtCreateSection会执行失败,除非OBJECT_ATTRIBUTES中包含OBJ_OPENIF标志——在此情况下,只要调用者拥有请求的访问权限,函数就会返回一个句柄。MaximumSize指定最大内存大小:如果内存由页面文件提供支持,该参数直接指定最大内存大小;如果由文件提供支持,该参数则用于设置文件大小。SectionPageProtection是页面保护常量(page protection constants)之一,映射文件用于只读访问时通常指定为PAGE_READONLY,其他情况常指定为PAGE_READWRITE。
AllocationAttributes指定CreateFileMapping文档中说明的可选标志。若指定为0,则会被解释为SEC_COMMIT,这是一个合理的默认值,表示后续任何映射都会预先提交视图(commit the view upfront)。最后,FileHandle是可选的后备文件句柄,若为NULL,则表示没有显式的后备文件,将使用页面文件。
Windows 10 1809及更高版本支持扩展函数NtCreateSectionEx:
NTSTATUS NtCreateSectionEx(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PLARGE_INTEGER MaximumSize,
_In_ ULONG SectionPageProtection,
_In_ ULONG AllocationAttributes,
_In_opt_ HANDLE FileHandle,
_Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParamCount);
2
3
4
5
6
7
8
9
10
Windows API中的CreateFileMapping2函数会调用NtCreateSectionEx。
NtCreateSectionEx对NtCreateSection进行了泛化,通过MEM_EXTENDED_PARAMETER结构数组提供可扩展的自定义选项,该结构数组在第8章介绍NtAllocateVirtualMemoryEx时也曾遇到过,更多相关信息可参考第8章。
如果要通过名称查找节对象,可以使用NtOpenSection函数。如果找不到具有所需访问权限的对象,该函数会执行失败:
NTSTATUS NtOpenSection(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
2
3
4
与常规情况一致,名称必须在OBJECT_ATTRIBUTES结构中指定。
# 12.1.2 映射节
获取节句柄后,下一步是使用该句柄将节所表示的内存(全部或部分)映射到进程地址空间。这一功能由NtMapViewOfSection(Ex)函数实现:
NTSTATUS NtMapViewOfSection(
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_In_ ULONG_PTR ZeroBits,
_In_ SIZE_T CommitSize,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ SECTION_INHERIT InheritDisposition,
_In_ ULONG AllocationType,
_In_ ULONG Win32Protect);
NTSTATUS NtMapViewOfSectionEx( // Windows 10 1803及以上版本
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Inout_ PVOID *BaseAddress,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ ULONG AllocationType,
_In_ ULONG PageProtection,
_Inout_updates_opt_(ExtParamCount) PMEM_EXTENDED_PARAMETER ExtendedParameters,
_In_ ULONG ExtParamCount);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Windows API中的MapViewOfFile/Ex/Numa函数会调用NtMapViewOfSection。MapViewOfFile2和MapViewOfFileNuma2函数同样会调用NtMapViewOfSection。MapViewOfFile3函数则会调用NtMapViewOfSectionEx。
SectionHandle是表示映射的节句柄。ProcessHandle是要将内存映射到的目标进程句柄。通常情况下,目标进程是调用进程(可通过NtCurrentProcess获取),但也可以是其他进程——此时该句柄必须具有PROCESS_VM_OPERATION访问权限。
BaseAddress是输入/输出参数,指定目标进程中用于映射的地址。若指定为NULL,系统会在进程地址空间中查找合适的地址范围;否则,将使用指定的地址(向下取整到页面边界),如果该地址范围不可用,映射会执行失败。ZeroBits指定结果地址中必须为零的高位比特数(仅当*BaseAddress为NULL时有效)。通常指定为0,表示调用者没有特殊限制。
CommitSize是为该视图初始提交的内存大小(向上取整到页面边界),仅对页面文件支持的节有意义。通常指定为0,意味着内存会“按需提交”(即访问时才提交)。
SectionOffset是映射的起始偏移量(向下取整到页面边界)。若传入NULL,映射将从节的起始位置(偏移量0)开始。ViewSize是输入/输出参数,指定要映射的大小,函数返回时会给出实际映射的大小。若指定为0,视图将从SectionOffset映射到节的末尾;否则,大小会向上取整到页面边界。
InheritDisposition指定视图是否应被所有子进程继承(ViewShare表示继承,ViewUnmap表示不继承)。AllocationFlags通常指定为0,但也可以包含MEM_LARGE_PAGES、MEM_TOP_DOWN和MEM_RESERVE等标志,更多详细信息可参考VirtualAlloc的文档说明。
最后,Win32Protect是映射视图请求的页面保护权限(例如PAGE_READONLY、PAGE_READWRITE),该权限必须与节创建时的初始保护标志兼容。NtMapViewOfSection调用成功后,*BaseAddress中会返回映射后的地址,通过该地址可访问对应的内存。
使用NtMapViewOfSectionEx时,借助之前遇到的MEM_EXTENDED_PARAMETER数组可以实现更多自定义功能,更多信息可参考MapViewOfFile2的文档说明。
当视图不再需要时,应调用NtUnmapViewOfSection(Ex)函数解除映射:
NTSTATUS NtUnmapViewOfSection(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress);
NTSTATUS NtUnmapViewOfSectionEx(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ ULONG Flags);
2
3
4
5
6
7
8
函数的必填参数是进程句柄和NtMapViewOfSection(Ex)返回的基地址。NtUnmapViewOfSection函数会调用NtUnmapViewOfSectionEx,并将Flags参数设置为0。其他可用标志包括MEM_UNMAP_WITH_TRANSIENT_BOOST(值为1)和MEM_PRESERVE_PLACEHOLDER(值为2):其中MEM_UNMAP_WITH_TRANSIENT_BOOST表示由于调用者预计很快会重新映射这些页面,应对要解除映射的页面进行临时页面优先级提升;MEM_PRESERVE_PLACEHOLDER表示将页面恢复到“占位符”状态。更多信息可参考MapViewOfFile2的文档说明。
Windows API中的NtUnmapViewOfFile2函数会调用NtUnmapViewOfSectionEx。
# 12.1.3 示例:简单共享
以下示例项目使用前面介绍的主要节API,创建了一个简单的双进程“聊天”程序——两个进程通过共享内存传递字符串。
完整代码位于SharedMem项目中。
我们将使用一个事件对象(event object)来同步共享内存的读写操作:一个进程写入数据完成后,另一个进程读取数据,之后两者切换角色。
首先需要创建一个命名的节对象,以便于共享。该名称应在会话的命名空间(session’s namespace)中可见,因此需要先获取当前会话的基目录,代码如下:
#include <string>
#include <format>
std::wstring GetBaseDirectory()
{
ULONG session;
if ( !NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(),
ProcessSessionInformation, &session, sizeof(session), nullptr)))
return L"" ;
return std::format(L"\\Sessions\\{}\\BaseNamedObjects\\ " , session);
}
2
3
4
5
6
7
8
9
10
11
12
调用NtQueryInformationProcess函数并指定ProcessSessionInformation参数,可获取会话ID(session ID),进而构建完整的路径字符串。也可以直接使用会话0的命名空间(\BaseNamedObjects),这在某些场景下可能更为合适。需要注意的是,在会话0的命名空间中创建节对象通常需要管理员权限。
上述函数使用了C++ 20的std::format函数来格式化名称,也可以使用swprintf_s等类似函数实现相同效果。
接下来,初始化OBJECT_ATTRIBUTES结构,为节对象设置自定义名称:
int main()
{
auto baseDir = GetBaseDirectory();
if (baseDir.empty())
return 1;
HANDLE hSection;
OBJECT_ATTRIBUTES secAttr;
UNICODE_STRING secName;
auto fullSecName = baseDir + L"MySharedMem" ;
RtlInitUnicodeString(&secName, fullSecName .c_str());
InitializeObjectAttributes(&secAttr, &secName, OBJ_OPENIF, nullptr , nullptr);
2
3
4
5
6
7
8
9
10
11
12
注意OBJ_OPENIF标志的作用:如果节名称已存在,则打开对应的句柄,而非执行失败。
现在可以创建/打开一个大小为8KB(可设置为任意大小)的节对象:
LARGE_INTEGER size;
size.QuadPart = 1 << 13; // 8KB
auto status = NtCreateSection(&hSection, SECTION_ALL_ACCESS,
&secAttr, &size, PAGE_READWRITE, SEC_COMMIT, nullptr);
if ( !NT_SUCCESS(status))
{
printf("Failed to create/open section (0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
9
10
如果在调用NtCreateSection之前该对象已存在,则当前进程将以读取者身份启动,之后会等待通知:
bool wait = false;
if (status == STATUS_OBJECT_NAME_EXISTS)
{
//
// 该对象已由其他进程创建
//
wait = true;
}
2
3
4
5
6
7
8
接下来,将节对象映射到当前进程的地址空间。由于节对象很小(仅8KB),我们将映射整个节:
PVOID address = nullptr;
SIZE_T viewSize = 0;
status = NtMapViewOfSection(hSection, NtCurrentProcess(), &address, 0 , 0 , nullptr , &viewSize, ViewUnmap, 0, PAGE_READWRITE);
if ( !NT_SUCCESS(status))
{
printf("Failed to map section (0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
然后,创建一个用于同步的事件对象,名称为“MySharedMemDataReady”:
OBJECT_ATTRIBUTES evtAttr;
UNICODE_STRING evtName;
auto fullEvtName = baseDir + L"MySharedMemDataReady" ;
RtlInitUnicodeString(&evtName, fullEvtName .c_str());
InitializeObjectAttributes(&evtAttr, &evtName, OBJ_OPENIF, nullptr , nullptr);
HANDLE hEvent;
status = NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &evtAttr, SynchronizationEvent, FALSE);
if ( !NT_SUCCESS(status))
{
printf("Failed to create/open event (0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
9
10
11
12
至此,已准备就绪,可以通过读写共享内存交换信息:
char text[128];
for(;;)
{
if (wait)
{
printf("Waiting for data . . .\n " );
NtWaitForSingleObject(hEvent, FALSE, nullptr);
printf("%s\n " , (PCSTR)address);
}
else
{
printf("> ");
gets_s(text);
strcpy_s((PSTR)address, sizeof(text), text);
NtSetEvent(hEvent, nullptr);
if (strcmp(text, "quit") == 0)
break;
}
wait = !wait;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
处于等待状态的进程会调用NtWaitForSingleObject函数,等待事件被触发(signaled),之后通过printf输出共享内存中的内容。其他情况下,程序会读取用户输入的文本并写入共享内存,随后触发事件(KeSetEvent)。
当用户输入“quit”时,循环退出,此时可以执行清理操作:
NtUnmapViewOfSection(NtCurrentProcess(), address);
NtClose(hEvent);
NtClose(hSection);
2
3
以下是完整的main函数,方便参考:
int main()
{
auto baseDir = GetBaseDirectory();
if (baseDir.empty())
return 1;
HANDLE hSection;
OBJECT_ATTRIBUTES secAttr;
UNICODE_STRING secName;
auto fullSecName = baseDir + L"MySharedMem" ;
RtlInitUnicodeString(&secName, fullSecName .c_str());
InitializeObjectAttributes(&secAttr, &secName,
OBJ_OPENIF, nullptr , nullptr);
LARGE_INTEGER size;
size.QuadPart = 1 << 13; // 8KB
auto status = NtCreateSection(&hSection, SECTION_ALL_ACCESS,
&secAttr, &size, PAGE_READWRITE, SEC_COMMIT, nullptr);
if ( !NT_SUCCESS(status))
{
printf("Failed to create/open section (0x%X)\n " , status);
return status;
}
bool wait = false;
if (status == STATUS_OBJECT_NAME_EXISTS)
{
wait = true;
}
PVOID address = nullptr;
SIZE_T viewSize = 0;
status = NtMapViewOfSection(hSection, NtCurrentProcess(), &address, 0 , 0 , nullptr , &viewSize, ViewUnmap, 0, PAGE_READWRITE);
if ( !NT_SUCCESS(status))
{
printf("Failed to map section (0x%X)\n " , status);
return status;
}
OBJECT_ATTRIBUTES evtAttr;
UNICODE_STRING evtName;
auto fullEvtName = baseDir + L"MySharedMemDataReady" ;
RtlInitUnicodeString(&evtName, fullEvtName .c_str());
InitializeObjectAttributes(&evtAttr, &evtName,
OBJ_OPENIF, nullptr , nullptr);
HANDLE hEvent;
status = NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &evtAttr, SynchronizationEvent, FALSE);
if ( !NT_SUCCESS(status))
{
printf("Failed to create/open event (0x%X)\n " , status);
return status;
}
char text[128];
for(;;)
{
if (wait)
{
printf("Waiting for data . . .\n " );
NtWaitForSingleObject(hEvent, FALSE, nullptr);
printf("%s\n " , (PCSTR)address);
}
else
{
printf("> ");
gets_s(text);
strcpy_s((PSTR)address, sizeof(text), text);
NtSetEvent(hEvent, nullptr);
if (strcmp(text, "quit") == 0)
break;
}
wait = !wait;
}
NtUnmapViewOfSection(NtCurrentProcess(), address);
NtClose(hEvent);
NtClose(hSection);
return 0;
}
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
74
75
76
77
78
79
80
81
82
83
# 12.1.4 查询节信息
NtQuerySection函数用于获取现有节对象的相关信息:
typedef enum _SECTION_INFORMATION_CLASS
{
SectionBasicInformation,
SectionImageInformation,
SectionRelocationInformation,
SectionOriginalBaseInformation,
SectionInternalImageInformation,
MaxSectionInfoClass
} SECTION_INFORMATION_CLASS;
NTSTATUS NtQuerySection(
_In_ HANDLE SectionHandle,
_In_ SECTION_INFORMATION_CLASS SectionInformationClass,
_Out_writes_bytes_ (SectionInformationLength) PVOID SectionInformation,
_In_ SIZE_T SectionInformationLength,
_Out_opt_ PSIZE_T ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
要使该API正常工作,SectionHandle必须具有SECTION_QUERY访问权限。
当信息类别(SectionInformationClass)指定为SectionBasicInformation时,需要传入SECTION_BASIC_INFORMATION结构:
typedef struct _SECTION__BASIC__INFORMATION
{
PVOID BaseAddress;
ULONG AllocationAttributes;
LARGE_INTEGER MaximumSize;
} SECTION_BASIC_INFORMATION, *PSECTION_BASIC_INFORMATION;
2
3
4
5
6
BaseAddress是创建节时指定SEC_BASED分配属性的情况下所使用的基地址(即所有共享该节的进程中映射的地址)。AllocationAttributes是创建节时使用的标志(例如SEC_COMMIT、SEC_BASED)。最后,MaximumSize是该节可映射的最大内存大小。
当信息类别指定为SectionImageInformation时,函数返回SECTION_IMAGE_INFORMATION结构:
typedef struct _SECTION_IMAGE_INFORMATION
{
PVOID TransferAddress;
ULONG ZeroBits;
SIZE_T MaximumStackSize;
SIZE_T CommittedStackSize;
ULONG SubSystemType;
union
{
struct
{
USHORT SubSystemMinorVersion;
USHORT SubSystemMajorVersion;
} DUMMYSTRUCTNAME;
ULONG SubSystemVersion;
};
union
{
struct
{
USHORT MajorOperatingSystemVersion;
USHORT MinorOperatingSystemVersion;
};
ULONG OperatingSystemVersion;
};
USHORT ImageCharacteristics;
USHORT DllCharacteristics;
USHORT Machine;
BOOLEAN ImageContainsCode;
union
{
UCHAR ImageFlags;
struct
{
UCHAR ComPlusNativeReady : 1;
UCHAR ComPlusILOnly : 1;
UCHAR ImageDynamicallyRelocated : 1;
UCHAR ImageMappedFlat : 1;
UCHAR BaseBelow4gb : 1;
UCHAR ComPlusPrefer32bit : 1;
UCHAR Reserved : 2;
};
};
ULONG LoaderFlags;
ULONG ImageFileSize;
ULONG CheckSum;
} SECTION_IMAGE_INFORMATION, *PSECTION_IMAGE_INFORMATION;
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
该信息类别仅对以镜像文件(指定SEC_IMAGE属性)形式映射文件的节有效。
以下示例展示了如何映射镜像文件并读取其信息(省略了错误处理):
// 打开文件
HANDLE hFile;
OBJECT_ATTRIBUTES fileAttr;
UNICODE_STRING fileName;
RtlInitUnicodeString(&fileName, L"\\SystemRoot\\System32\\kernelbase.dll");
InitializeObjectAttributes(&fileAttr, &fileName, 0 , nullptr , nullptr);
IO_STATUS_BLOCK ioStatus;
NtOpenFile(&hFile, FILE_READ_ACCESS, &fileAttr, &ioStatus, FILE_SHARE_READ, 0);
// 基于hFile创建节
NtCreateSection(&hSection, SECTION_ALL_ACCESS,
nullptr , nullptr, PAGE_READONLY, SEC_IMAGE, hFile);
SECTION_IMAGE_INFORMATION sii;
NtQuerySection(hSection, SectionImageInformation, &sii, sizeof(sii), nullptr);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SectionInternalImageInformation信息类别对应一个扩展结构:
typedef struct _SECTION_INTERNAL_IMAGE_INFORMATION
{
SECTION_IMAGE_INFORMATION SectionInformation;
union
{
ULONG ExtendedFlags;
struct
{
ULONG ImageExportSuppressionEnabled: 1;
ULONG ImageCetShadowStacksReady: 1;
ULONG ImageXfgEnabled: 1;
ULONG ImageCetShadowStacksStrictMode: 1;
ULONG ImageCetSetContextIpValidationRelaxedMode: 1;
ULONG ImageCetDynamicApisAllowInProc: 1;
ULONG ImageCetDowngradeReserved1 : 1;
ULONG ImageCetDowngradeReserved2 : 1;
ULONG Reserved: 24;
};
};
} SECTION_INTERNAL_IMAGE_INFORMATION, *PSECTION_INTERNAL_IMAGE_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
遗憾的是,SectionInternalImageInformation无法正常使用——该信息仅在内核内部可用,外部无法访问。
最后两个信息类别(SectionRelocationInformation和SectionOriginalBaseInformation)同样仅适用于镜像文件。SectionRelocationInformation返回镜像文件的加载地址(SIZE_T类型),而SectionOriginalBaseInformation返回镜像文件的原始加载地址(PVOID类型)。
# 12.1.5 其他节相关API
NtExtendSection函数仅可扩展文件支持的节(非镜像映射形式)的大小:
NTSTATUS NtExtendSection(
_In_ HANDLE SectionHandle,
_Inout_ PLARGE_INTEGER NewSectionSize);
2
3
节句柄必须具有SECTION_EXTEND_SIZE访问权限。如果传入的大小小于当前节的大小,该调用将被忽略。
最后,NtAreMappedFilesTheSame函数用于检查两个指定地址是否映射了同一个文件:
NTSTATUS NtAreMappedFilesTheSame(
_In_ PVOID File1MappedAsAnImage,
_In_ PVOID File2MappedAsFile);
2
3
第一个地址必须位于以镜像形式映射文件的节中,不一定是节的起始地址。第二个地址必须位于映射文件(可以是镜像形式或非镜像形式)的节中。如果两个地址指向同一个文件,函数返回STATUS_SUCCESS;否则返回STATUS_NOT_SAME_DEVICE。
# 12.2 内存区域
内存区域(Memory Zone)是本地(当前进程)对象,用于管理已提交的内存缓冲区。可以在该缓冲区中分配内存,但无法显式释放单个分配的内存块——只能释放整个内存区域。内存区域是后备列表(详见下一节)的实现基础,但自身也具有独立的实用价值。
通过RtlCreateMemoryZone函数创建新的内存区域:
NTSTATUS RtlCreateMemoryZone(
_Out_ PVOID *MemoryZone,
_In_ SIZE_T InitialSize,
_Reserved_ ULONG Flags); // 目前无定义的标志
2
3
4
MemoryZone是返回的不透明指针(opaque pointer),指向分配的内存区域对象,该对象包含初始缓冲区大小(InitialSize字节)和管理结构本身。函数会分配(提交)InitialSize字节的内存,向上取整到下一个页面边界(同时考虑管理结构的大小)。
管理结构RTL_MEMORY_ZONE的定义可在PHNT库中找到,但为了应对未来Windows版本中可能的结构变更,最好将其视为不透明结构,避免直接操作内部成员。
创建内存区域后,可通过RtlAllocateMemoryZone函数分配内存:
NTSTATUS RtlAllocateMemoryZone(
_In_ PVOID MemoryZone,
_In_ SIZE_T BlockSize,
_Out_ PVOID *Block);
2
3
4
BlockSize是请求分配的字节数。函数执行成功后,*Block中会返回分配的内存块指针。如果新增的分配请求超出当前内存区域的大小,分配将失败。此时,调用者可以先使用RtlExtendMemoryZone函数扩展内存区域的大小,再重新尝试分配:
NTSTATUS RtlExtendMemoryZone(
_In_ PVOID MemoryZone,
_In_ SIZE_T Increment);
2
3
Increment是要添加到内存区域的字节数。
截至撰写本文时,PHNT头文件中未提供RtlExtendMemoryZone的声明。如需使用该函数,只需添加上述声明即可。
当内存区域不再需要时,可以通过以下函数销毁它:
NTSTATUS RtlDestroyMemoryZone(_In_ _Post_invalid_ PVOID MemoryZone);
该调用会释放内存区域分配的所有内存。如果需要重复使用内存区域,可以在不销毁它的情况下重置其内容(清空内存):
NTSTATUS RtlResetMemoryZone(_In_ PVOID MemoryZone);
目前,RtlResetMemoryZone似乎未包含在Visual Studio提供的Ntdll.lib导入库中。除非自行创建导入库,否则需要动态绑定该API,例如:
auto const pRtlResetMemoryZone = (decltype(RtlResetMemoryZone)*)GetProcAddress(
GetModuleHandle(L"ntdll"), "RtlResetMemoryZone");
2
最后,可以通过RtlLockMemoryZone函数将内存区域锁定到物理内存中,之后可通过RtlUnlockMemoryZone函数解锁。每次锁定/解锁操作会递增/递减内部的锁定计数:
NTSTATUS RtlLockMemoryZone(_In_ PVOID MemoryZone);
NTSTATUS RtlUnlockMemoryZone(_In_ PVOID MemoryZone);
2
# 12.2.1 示例:内存区域
以下示例展示了内存区域的使用。首先创建一个4KB大小的内存区域(可设置为任意大小):
int main()
{
PVOID zone;
auto status = RtlCreateMemoryZone(&zone, 1 << 12 , 0);
if ( !NT_SUCCESS(status))
{
printf("Failed to create memory zone (0x%X)\n " , status);
return status;
}
2
3
4
5
6
7
8
9
接下来进行多次内存分配,如果某次分配失败,则扩展内存区域并重新尝试:
PCHAR buffers[100]{};
for (int i = 0; i < _ARRAYSIZE(buffers); i++)
{
PVOID buffer;
status = RtlAllocateMemoryZone(zone, 128 + i * 2 , &buffer);
if ( !NT_SUCCESS(status))
{
printf("Failed to allocate block %d (0x%X) . Extending . . .\n " ,
i, status);
RtlExtendMemoryZone(zone, 256);
i-- ;
continue;
}
else
{
buffers[i] = (PCHAR)buffer;
sprintf_s(buffers[i], 128 , "Data stored in block %d" , i);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
现在可以遍历所有分配的内存块并访问其内容:
for (int i = 0; i < _ARRAYSIZE(buffers); i++)
{
if (buffers[i])
{
printf("%3d: %s\n " , i, buffers[i]);
}
}
2
3
4
5
6
7
最后销毁内存区域:
RtlDestroyMemoryZone(zone);
return 0;
}
2
3
完整源代码位于MemZones项目中。
# 12.3 后备列表
后备列表(Lookaside Lists,或称内存块后备列表Memory Block Lookaside Lists)是一类内存管理对象,通过不真正释放内存、仅将其标记为可用的方式,实现快速分配和释放。它们在底层使用内存区域,但与内存区域不同的是,后备列表支持“释放”单个分配的内存块。后备列表最适合用于固定大小的内存分配,但更一般地说,它支持指定最小和最大块大小的范围。
通过RtlCreateMemoryBlockLookaside函数创建后备列表:
NTSTATUS RtlCreateMemoryBlockLookaside(
_Out_ PVOID *MemoryBlockLookaside,
_Reserved_ ULONG Flags, // 目前无定义的标志
_In_ ULONG InitialSize,
_In_ ULONG MinimumBlockSize,
_In_ ULONG MaximumBlockSize);
2
3
4
5
6
后备列表管理对象通过第一个参数返回。InitialSize是预先为后备列表分配(提交)的内存大小。MinimumBlockSize和MaximumBlockSize指定了使用该后备列表进行有效分配的块大小范围。这些值会分别向下(最小值)和向上(最大值)取整到最接近的2的幂。
创建后备列表后,可通过RtlAllocateMemoryBlockLookaside函数分配内存块:
NTSTATUS RtlAllocateMemoryBlockLookaside(
_In_ PVOID MemoryBlockLookaside,
_In_ ULONG BlockSize,
_Out_ PVOID *Block);
2
3
4
BlockSize是要分配的内存块字节数,必须在创建后备列表对象时指定的范围内。分配成功后,*Block中会返回内存块指针。如果后备列表已满(实际上是内部内存区域已满),分配将失败,此时需要扩展后备列表。
要释放内存块(将其标记为可用),调用RtlFreeMemoryBlockLookaside函数:
NTSTATUS RtlFreeMemoryBlockLookaside(
_In_ PVOID MemoryBlockLookaside,
_In_ PVOID Block);
2
3
Block是RtlAllocateMemoryBlockLookaside函数返回的内存块指针。该内存块会被放回可用块池(块根据其大小分组管理)。
如果需要的内存超过初始请求的大小,则需要扩展后备列表:
NTSTATUS RtlExtendMemoryBlockLookaside(
_In_ PVOID MemoryBlockLookaside,
_In_ ULONG Increment);
2
3
Increment是要扩展后备列表的字节数。
与内存区域类似,后备列表也支持锁定/解锁操作,或重置操作(有效清除所有分配的内存块):
NTSTATUS RtlLockMemoryBlockLookaside(_In_ PVOID MemoryBlockLookaside);
NTSTATUS RtlUnlockMemoryBlockLookaside(_In_ PVOID MemoryBlockLookaside);
NTSTATUS RtlResetMemoryBlockLookaside(_In_ PVOID MemoryBlockLookaside);
2
3
这些函数是对底层使用的内存区域对应操作的轻量级封装。
最后,可以通过以下函数彻底销毁后备列表:
NTSTATUS RtlDestroyMemoryBlockLookaside(_In_ PVOID MemoryBlockLookaside);
# 12.4 总结
本章介绍了各类与内存管理相关的API。节是内核对象,而内存区域和后备列表是由NtDll.dll内部实现的对象类型——它们并非内核对象,但能为应用程序提供实用的内存管理服务。