WindowsAPI查缺补漏-内存管理

碎碎念

x64下虚拟地址空间

空指针赋值分区,为0x00000000`00000000~0x00000000`0000FFFF,大小约为64KB。用于帮助开发人员捕捉对NULL指针的赋值,例如这样malloc失败时返回NULL,帮助开发人员调错:

1
2
LPINT pInt = (LPINT)malloc(sizeof(INT));
*pInt = 5;

用户模式分区,为0x00000000`00010000~0x00007FFF`FFFFFFFF,大小约为128TB。用于每个进程使用,动态链接库也装载到这里,需要内存管理单元MMU将虚地址映射为物理地址。因为这分区用不着这么大,操作系统选择不支持,如Windows Server 2016只支持24TB,Windows 10只支持8TB。

64KB禁入分区,为0x00007FFF`FFFF0000~0x00007FFF`FFFFFFFF,大小约为64KB。Windows系统保留,禁止访问。

内核模式分区,为0x00008000`00000000~0xFFFFFFFF`FFFFFFFF,大小约为16777208TB。操作系统代码, 如线程调度、内存管理、文件系统、网络支持、设备驱动等代码,应用程序访问时引发访问违例。因为这块地儿实在太大了,大部分不会被使用。

系统信息

GetSystemInfo

获取系统信息:

1
2
3
VOID GetSystemInfo(
LPSYSTEM_INFO lpSystemInfo
);

SYSTEM_INFO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture; //处理器体系结构
WORD wReserved;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
DWORD dwPageSize; //页面大小 4096字节
LPVOID lpMinimumApplicationAddress; //进程可用地址空间中最小内存地址
LPVOID lpMaximumApplicationAddress; //进程可用地址空间中最大内存地址
DWORD_PTR dwActiveProcessorMask; //哪些CPU处于活动状态
DWORD dwNumberOfProcessors; //逻辑CPU个数
DWORD dwProcessorType;
DWORD dwAllocationGranularity; //预定虚拟地址空间区域分配粒度
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO, *LPSYSTEM_INFO;

对于wProcessorArchitecture字段枚举值:

枚举值 含义
PROCESSOR_ARCHITECTURE_INTEL x64
PROCESSOR_ARCHITECTURE_AMD64 x64
PROCESSOR_ARCHITECTURE_IA64 IA-64
PROCESSOR_ARCHITECTURE_ARM ARM
PROCESSOR_ARCHITECTURE_ARM64 ARM64
PROCESSOR_ARCHITECTURE_UNKNOWN 未知

对于dwPageSize在x86或x64下都为0x00010000。

对于lpMinimumApplicationAddress和lpMaximumApplicationAddress分别为0x00010000和0x00007FFFFFFFFFFF。dwAllocationGranularity为0x00010000。

GlobalMemoryStatusEx

获取内存当前使用情况:

1
2
3
BOOL WINAPI GlobalMemoryStatusEx(
_Inout_ LPMEMORYSTATUSEX lpBuffer
);

例如:

1
2
3
4
5
MEMORYSTATUSEX ms = { 0 };
ms.dwLength = sizeof(MEMORYSTATUSEX);
GlobalMemoryStatusEx(&ms);
wsprintf(szBuf, TEXT("%d %I64d %I64d %I64d %I64d %I64d %I64d"), ms.dwMemoryLoad, ms.ullTotalPhys, ms.ullAvailPhys, ms.ullTotalPageFile, ms.ullAvailPageFile, ms.ullTotalVirtual, ms.ullAvailVirtual);
MessageBox(hwnd, szBuf, TEXT("xxx"), MB_OK);

MEMORYSTATUSEX

1
2
3
4
5
6
7
8
9
10
11
typedef struct _MEMORYSTATUSEX {
DWORD dwLength; //该结构大小
DWORD dwMemoryLoad; //已用物理内存百分比
DWORDLONG ullTotalPhys; //物理内存总量 单位字节 比真实物理内存总量少280MB用于非页面缓冲池
DWORDLONG ullAvailPhys; //当前可用物理内存总量 单位字节
DWORDLONG ullTotalPageFile; //最大内存总量 等于内存物理总量+页面交换文件大小
DWORDLONG ullAvailPageFile; //当前可用内存总量
DWORDLONG ullTotalVirtual; //进程虚拟地址空间中用户模式分区总大小 单位字节
DWORDLONG ullAvailVirtual; //进程虚拟地址空间中当前可用的用户模式分区大小 单位字节
DWORDLONG ullAvailExtendedVirtual;
} MEMORYSTATUSEX, * LPMEMORYSTATUSEX;

GetProcessMemoryInfo

获取指定进程的内存使用情况。一个进程地址空间中被保存在物理内存中的那些页面称为它的工作集,即进程的虚拟地址空间中当前驻留在物理内存中的页面集。

1
2
3
4
5
BOOL WINAPI GetProcessMemoryInfo(
_In_ HANDLE hProcess, //进程句柄
_Out_ PROCESS_MEMORY_COUNTERS ppsmemCounters, //返回信息
_In_ DWORD cb //ppsmemCounter参数结构大小
)

PROCESS_MEMORY_COUNTERS

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _PROCESS_MEMORY_COUNTERS {
DWORD cb; //该结构大小
DWORD PageFaultCount; //页面错误数量
SIZE_T PeakWorkingSetSize; //峰值工作集大小 单位字节
SIZE_T WorkingSetSize; //当前工作集大小 单位字节
SIZE_T QuotaPeakPagedPoolUsage; //峰值分页池使用情况 单位字节
SIZE_T QuotaPagedPoolUsage; //当前分页池使用情况 单位字节
SIZE_T QuotaPeakNonPagedPoolUsage; //峰值非分页池使用情况 单位字节
SIZE_T QuotaNonPagedPoolUsage; //当前非分页池使用情况 单位字节
SIZE_T PagefileUsage; //进程提交的内存总量 单位字节
SIZE_T PeakPagefileUsage; //进程提交的内存总量峰值 单位字节
} PROCESS_MEMORY_COUNTERS;

虚拟地址空间

进程虚拟地址空间可以处于以下状态之一:

预定/保留状态:预定一块虚拟地址空间区域供未来使用,相当于占用。

已提交状态:映射物理地址,只有在提交后才可以被访问。

空闲状态:未预定未提交,进程无法访问,读写导致访问违例或异常。

VirtualAlloc

在调用进程虚拟地址空间预定、提交或预定提交一块地址空间内存区域,并自动初始化为0:

1
2
3
4
5
6
LPVOID WINAPI VirtualAlloc(
_In_opt_ LPVOID lpAddress, //要分配空间区域的起始地址 NULL自动分配闲置区域
_In_ SIZE_T dwSize, //要分配的空间区域大小 单位字节
_In_ DWORD flAllocationType, //内存分配类型
_In_ DWORD flProtect //内存保护类型
) //成功返回分配空间区域起始地址 失败NULL

举个例子:

1
LPVOID lp = VirtualAlloc((LPVOID)(500 * 1024 * 1024 + 8192), 7 * 1024, MEM_RESERVE, PAGE_READWRITE);

对于例子中的lpAddress参数,系统自动会转化为向下取整后分配粒度的整数倍,这里分配粒度为64KB。所以上述例子返回的分配空间基地址为500MB处。

对于例子中的dwSize参数,系统自动会转化为能够完全覆盖500MB~500MB+8192+7*1024空间区域的页面大小。

以上规则适用于下面各函数。

若lpAddress指定地址不合法或没有闲置区域,或闲置区域不够则返回NULL。

对于flAllocationType指定内存分配的类型:

枚举值 含义
MEM_RESERVE 预定保留一块虚拟地址空间区域供将来使用,被释放前其他内存分配函数无法使用该空间。
MEM_COMMIT 提交已预定的虚拟地址空间区域,只有提交后才可以被访问。
MEM_TOP_DOWN 预定一块区域并打算使用很久,则通知系统分配尽可能高的内存地址,避免引起内存碎片。使用时lpAddress为NULL。

flProtect指定要分配的空间区域内存保护属性:

枚举值 含义
PAGE_NOACCESS 禁止已提交页面所有访问权限
PAGE_READONLY 已提交的页面可以读取
PAGE_READWRITE 已提交的页面可以读写
PAGE_EXECUTE 已提交的页面可以执行
PAGE_EXECUTE_READ 已提交的页面可以读取执行
PAGE_EXECUTE_READWRITE 已提交的页面可以读写执行

VirtualFree

解除提交或释放调用进程虚拟地址空间中页面区域:

1
2
3
4
5
BOOL WINAPI VirtualFree(
_In_ LPVOID lpAddress, //要释放的空间区域起始地址
_In_ SIZE_T dwSize, //大小 单位字节 通常0
_In_ DWORD dwFreeType //释放操作类型
)

对于dwFreeType参数二选一:

枚举值 含义
MEM_DECOMMIT 解除提交已提交的空间区域
MEM_RELEASE 释放空间区域

VirtualAllocEx/VirtualFreeEx

在另一个进程虚拟地址中分配、释放内存,略。

VirtualProtect

更改已提交页区域保护属性:

1
2
3
4
5
6
BOOL WINAPI VirtualProtect(
_In_ LPVOID lpAddress, //起始地址
_In_ SIZE_T dwSize, //大小 单位字节
_In_ DWORD flNewProtect, //新保护属性
_Out_ PDWORD lpfloOldProtect //返回原页面保护属性
)

更改其他进程页面保护属性用VirtualProtectEx函数。

VirtualQuery

查询调用进程虚拟地址空间一篇页面区域信息:

1
2
3
4
5
SIZE_T WINAPI VirtualQuery(
_In_opt_ LPCVOID, //要查询的页面区域起始地址
_Out_ PMEMORY_BASIC_INFORMATION lpBuffer, //返回信息
_In_ SIZE_t dwLength //返回结构大小
);

MEMORY_BASIC_INFORMATION

1
2
3
4
5
6
7
8
9
10
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; //页面区域基地址 lpAddress向下取整到下一页边界
PVOID AllocationBase; //空间区域基地址 即lpAddress指定的
ULONG AllocationProtect; //最开始分配空间时指定的保护属性
USHORT PartitionId;
SIZE_T RegionSize; //页面区域大小 BaseAddress为起始地址 单位字节
ULONG State; //页面状态
ULONG Protect; //页面内存保护属性
ULONG Type; //页面类型
} MEMORY_BASIC_INFORMATION, * PMEMORY_BASIC_INFORMATION;

堆管理

堆管理是对虚拟地址空间操作的高级封装。堆分为默认堆和私有堆,默认堆在进程开始时即默认分配1MB堆。

HeapCreate

创建一个私有堆:

1
2
3
4
5
HANDLE WINAPI HeapCreate(
_In_ DWORD flOptions, //分配选项
_In_ SIZE_T dwInitialSize, //提交的初始内存大小 单位字节 0表示初始提交1页
_In_ SIZE_T dwMaximumSize //预定的内存空间大小 单位字节 0不限制
) //失败返回NULL

对于flOptions有:

枚举值 含义
HEAP_CREATE_ENABLE_EXECUTE 可执行
HEAP_GENERATE_EXCEPTIONS 分配失败则抛出异常
HEAP_NO_SERIALIZE 多个线程对同一个堆同时操作(危险!)

HeapDestroy

销毁堆:

1
2
3
BOOL WINAPI HeapDestroy(
_In_ HANDLE hHeap //要销毁的堆句柄
)

HeapAlloc

从堆中分配一块内存:

1
2
3
4
5
LPVOID WINAPI HeapAlloc(
_In_ HANDLE hHeap, //堆句柄
_In_ DWORD dwFlags, //堆分配选项
_In_ SIZE_T dwBytes //要分配的内存块大小 单位字节
) //失败返回NULL

参数dwFlags为:

枚举值 含义
HEAP_ZERO_MEMORY 分配的清零
HEAP_GENERATE_EXCEPTIONS 分配失败抛出异常
HEAP_NO_SERIALIZE 不进行独占检查(危险!)

指定HEAP_GENERATE_EXCEPTIONS标志后抛出的异常代码:

枚举值 含义
STATUS_NO_MEMORY 缺少可用内存或堆损坏
STATUS_ACCESS_VIOLATION 堆损坏或不正确的函数参数

例如创建一个不限最大大小的私有堆,并从堆中分配1024字节内存:

1
2
3
LPVOID lp = NULL;
hHeap = HeapCreate(0, 0, 0);
lp = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, 1024);

HeapReAlloc

调整内存块大小:

1
2
3
4
5
6
LPVOID WINAPI HeapReAlloc(
_In_ HANDLE hHeap, //堆的句柄
_In_ DWORD dwFlags, //堆分配选项
_In_ LPVOID lpMem, //要调整大小的内存块指针
_In_ SIZE_T dwBytes //要调整到的大小 单位字节
)

dwFlags参数选项:

枚举值 含义
HEAP_ZERO_MEMORY 当重新分配内存块比原来大,则超出部分初始化为0,原部分不影响
HEAP_REALLOC_IN_PLACE_ONLY 不移动内容块。不指定时可能会调整原内存块地址并移动原部分
HEAP_GENERATE_EXCEPTIONS
HEAP_NO_SERIALIZE

HeapFree

释放堆中分配的内存块:

1
2
3
4
5
BOOL WINAPI HeapFree(
_In_ HANDLE hHeap,
_In_ DWORD dwFlags, //HEAP_NO_SERIALIZE
_In_ LPVOID lpMem //要释放内存块的地址
)

dwFlags可以是HEAP_NO_SERIALIZE。

HeapLock/HeapUnlock

锁定/解锁堆,锁定时该线程暂时成为指定堆的所有者,其他线程需要对这个堆操作时只能等待。这俩函数不要使用,在其他堆操作函数中会被自动调用。

1
2
3
4
5
6
BOOL WINAPI HeapLock(
_In_ HANDLE hHeap
);
BOOL WINAPI HeapUnlock(
_In_ HANDLE hHeap
);

HeapSize

获取内存实际大小:

1
2
3
4
5
SIZE_T WINAPI HeapSize(
_In_ HANDLE hHeap, //内存所在堆句柄
_In_ DWORD dwFlags, //选项 0或HEAP_NO_SERIALIZE
_In_ LPCVOID lpMem //要获取大小的内存块的指针
) //成功返回大小 单位字节 失败返回SIZE_T-1

HeapValidate

验证整个堆或堆中某个内存块的完整性:

1
2
3
4
5
BOOL WINAPI HeapValidate(
_In_ HANDLE hHeap, //要验证堆的句柄
_In_ DWORD dwFlags, //选项 0或HEAP_NO_SERIALIZE
_In_opt_ LPCVOID lpMem //内存块 NULL为整个堆
)

GetProcessHeaps

获取调用进程的所有堆的句柄:

1
2
3
4
DWORD WINAPI GetProcessHeaps(
_In_ DWORD NumberOfHeaps, //数组元素个数
_Out_ PHANDLE ProcessHeaps //接收堆句柄数组
) //返回调用进程中堆个数 失败返回0

HeapWalk

枚举指定堆中内存块:

1
2
3
4
BOOL WINAPI HeapWalk(
_In_ HANDLE hHeap, //堆的句柄
_Inout_ LPROCESS_HEAP_ENTRY lpEntry
)

这个函数不详细讲。

内存管理杂项

CopyMemory/RtlCopyMemory

把一块内存从一个位置复制到另一个位置:

1
2
3
4
5
VOID CopyMemory(
_In_ PVOID Destination, //目标内存块起始地址
_In_ CONST PVOID Source, //要复制的内存块起始地址
_In_ SIZE_T Length //要复制的大小
)

RtlCopyMemoryCopyMemory就完全是memcpy

MoveMemory/RtlMoveMemory

把一块内存从一个位置移动到另一个位置:

1
2
3
4
5
VOID MoveMemory(
_In_ PVOID Destination, //目标内存块起始地址
_In_ CONST PVOID Source, //要移动的内存块起始地址
_In_ SIZE_T Length //要移动的大小
)

RtlMoveMemoryMoveMemory就完全是memmove

RtlEqualMemory

比较两个内存块指定字节是否相同:

1
2
3
4
5
BOOL RtlEqualMemory(
_In_ CONST PVOID Source1, //内存块1起始地址
_In_ CONST PVOID Source2, //内存块2起始地址
_In_ SIZE_T Length //要比较的字节数
) //相同TRUE 否则FALSE

这玩意儿就完全是memcmp