一. 关于内存管理
1.1 虚拟地址空间
每个进程的虚拟地址是私有的并且无法被其他进程访问(除非共享)。系统为每个进程维护一个页表用来将虚拟地址变为物理地址。
虚拟地址空间的大小受到不同的cpu架构,不同的windows操作系统和LargeAddressAware
与 4GT 设置影响。
1.1.1 32位 ——4GT设置
- 在 32 位系统中,虚拟地址是 4 GB(2³² = 4 GB):
默认情况:用户程序最多只能访问 2 GB 的虚拟地址
- 启用 4GT 后分布变成:
这样用户态程序能用更多虚拟内存(适合数据库、游戏服务器等),但内核空间只有 1 GB,系统缓存和驱动可用空间变少。
- 启用方式
Windows Vista 及更高版本
:bcdedit /set increaseuserva <值>
Windows Server 2003 及更早
:在 Boot.ini
中添加:
/3GB
或者更细调
/USERVA=2560
- 让程序识别 4GT ——
/LARGEADDRESSAWARE
编译时加:
link /LARGEADDRESSAWARE myapp.obj
-
没加此标志 → 即使系统启用了 4GT,程序仍只能看到 2 GB。
-
加了 → 在 /3GB 系统上可看到 3 GB,在 64 位系统上甚至可用 4 GB。
-
在 64 位 Windows 上运行的 32 位 LargeAddressAware 程序 → 可用 4 GB 虚拟地址。
问题 | 说明 |
---|---|
系统资源减少 | 内核空间从 2 GB → 1 GB,Paged Pool / NonPaged Pool 都缩小。重网络或驱动的程序性能可能下降。 |
地址连续性受限 | 2 GB 以上区域常被系统 DLL 占用,无法获得完整连续 3 GB 空间。 |
必须检测实际值 | 用 GlobalMemoryStatusEx() 看总虚拟空间,用 GetSystemInfo() 取最高可用地址;不要硬编码 0xC0000000 。 |
指针比较陷阱 | 不要用有符号比较:if (pointer > 40000000) 。当指针 > 2 GB 时最高位为 1 → 变负数。 |
指针高位用途失效 | 旧代码若用最高位区分“错误码/地址”(如 >= 0x80000000 判错)会出错。 |
分配地址偏低 | VirtualAlloc 默认从低地址开始分配;要测试高地址,可传 MEM_TOP_DOWN 或设置注册表:HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Memory Management AllocationPreference = 0x100000 |
- 优点:内存密集型程序(DB、图形渲染、大缓存)可显著受益。
缺点:系统缓存变小 → 文件 I/O 、网络堆栈可能变慢。
建议:先在测试环境启用 4GT,用性能计数器(PerfMon)观察分页池、系统缓存、工作集等指标,再决定是否长期启用。
- 其他
32位架构的物理地址空间大小还会受到 Physical Address Extension (PAE)的影响。可以让32位windows系统使用超过4GB物理内存大小。
1.1.2 PAE
todo
1.1.3 64位系统内存限制
虽然64位可以使用更大的内存,但是应用程序可以指定,让系统在分配内存时仅使用 2 GB 以下的虚拟地址。如果以下情况成立,这个功能对 64 位应用程序会有好处:
-
2 GB 的地址空间足够使用;
-
代码中有大量“指针被截断”的编译警告;
-
代码中指针与整数混用(比如把指针强制转成 int);
-
代码中有使用 32 位数据类型的多态操作。
所有指针仍然是 64 位的,只不过系统会确保程序的所有内存分配都位于 2 GB 以下,这样即使应用程序在运行中不小心截断指针(丢掉高 32 位),也不会丢失有意义的数据。指针可以被截断为 32 位值,然后在需要时通过或零扩展恢复为 64 位值。要启用这个“2GB 限制”特性,可以在链接阶段使用选项:/LARGEADDRESSAWARE:NO
不过,使用该选项时要非常小心。
如果你编译了一个带有此选项的 DLL,而该 DLL 被一个没有使用此选项的程序调用, 那么 DLL 可能会截断一个 64 位指针(高 32 位仍然有意义),从而导致程序崩溃,而且不会有任何警告。
1.2 工作集
工作集是进程虚拟地址空间中当前驻留在物理内存中的页面集合。工作集中只有可以分页的内存,不可分页的内存(Address Windowing Extension (AWE)或大页面分配)不在工作集中。
1.2.1 Address Windowing Extension (AWE)
todo(32位程序使用)
1.2.2 Large page
每个大页映射只使用cpu内的单个TLB项,大页通常比普通页(4KB)大1000倍。
-
使用大页支持的步骤
-
调用
AdjustTokenPrivileges
函数获取SeLockMemoryPrivilege
权限。(这个权限允许进程锁定物理内存页。) -
调用
GetLargePageMinimum
函数,获取系统支持的最小大页大小(例如 2MB 或 1GB)。 -
在调用
VirtualAlloc
时,加入MEM_LARGE_PAGES
标志。所分配的内存大小与起始地址必须是大页大小的倍数并对齐。 -
void* p = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE | MEM_LARGE_PAGES, PAGE_READWRITE);
系统会根据 GetLargePageMinimum() 的结果(如 2MB),分配多个连续的大页;这些页直接映射到物理内存;它们不会分页,不会换出到磁盘;释放时必须一次性释放整个块。
-
-
使用大页的注意事项
-
内存碎片问题:
系统运行时间长后,物理内存可能被碎片化。
因为每个大页必须来自一块连续的物理内存,
所以分配大页可能会失败,或显著影响性能。
建议:在程序启动时一次性分配所有大页内存,避免频繁申请。 -
特性 大页内存是非分页的,即永久驻留在物理内存中; 始终为读写权限; 属于进程的私有字节,但不在工作集中, 因为工作集定义中只包含可分页内存。
-
限制 大页分配不受作业限制。 大页内存必须一次性预留并提交,不能先 Reserve 再 Commit。 对于 Itanium 架构下的 WOW64 模式(即在 Itanium 上运行的 32 位程序),不支持该特性。 这类应用必须重新编译为原生 64 位程序。
-
权限要求 由于大页锁定了物理内存,系统为了安全,要求进程具有: “Lock Pages in Memory” 权限
(SeLockMemoryPrivilege)
否则调用VirtualAlloc(..., MEM_LARGE_PAGES, ...)
会失败。 管理员可以在组策略或secpol.msc
的用户权限分配中添加此权限。 -
对比
特性 普通页 (4KB) 大页 (2MB / 1GB) 页大小 4KB 通常 2MB 或 1GB 分配灵活度 高 必须对齐且连续 可分页 是 否 性能 普通 高(TLB 利用率更高) 适用场景 一般程序 数据库 / 缓存 / 内存池 / 科学计算 是否在工作集 是 否 权限要求 无 需要 SeLockMemoryPrivilege
-
1.2.3 页面错误
当进程访问不在工作集中的页时,会发生页面错误。当操作系统成功找到页时,会把它放入工作集(AWE和Large Page 不会发生段错误,因为他们是不可分页的,不会包含在工作集中)。 硬页错误(Hard Page Fault)是当操作系统需要从磁盘的后备存储(backing store)中读取页面到物理内存时发生的错误,后备存储可以是:系统的分页文件(pagefile.sys)或内存映射文件(memory-mapped file) 而软页错误(Soft Page Fault)是不需要访问磁盘,可以直接从内存中解决。
常见情况:
- 该页已在其他进程的工作集中;
- 该页处于过渡状态(transition state);
- 第一次访问一个被分配但尚未初始化的页(称为“需求零页错误(demand-zero fault)”)。
1.2.4 页面从工作集中移除的情况
以下情况会让页面被移出工作集(即不再常驻物理内存) :
- 进程调用:
SetProcessWorkingSetSize
SetProcessWorkingSetSizeEx
EmptyWorkingSet
来手动调整或清空工作集;
- 调用了
VirtualUnlock
解除内存锁定; - 调用了
UnmapViewOfFile
卸载内存映射文件; - 内存管理器为释放更多物理内存,主动裁剪(trim)工作集;
- 工作集达到上限,需要腾出空间以容纳新页。
1.2.5 页面共享与过渡页(Transition Page)
当一个物理页被多个进程共享:
- 把它从某个进程的工作集移除,不会影响其他进程;
- 当它从所有使用它的进程工作集中都被移除后,
- 该页会变为一个 过渡页(Transition Page)。 过渡页仍然缓存在RAM中,直到:
- 再次被访问 → 重新加入某个进程工作集;
- 或被回收 → 分配给其他用途。 如果过渡页是“脏页”(即被修改过,尚未写回磁盘), 在被重新利用前,系统会将其写回后备存储(pagefile 或文件)。 系统也可能在页面变为过渡页后立即异步写回这些“脏页”。
1.2.6 工作集的大小控制
每个进程都有:
- 最小工作集大小
- 最大工作集大小 这两个值会影响虚拟内存的分页行为。 你可以:
- 用
GetProcessMemoryInfo
获取当前工作集大小; - 用
GetProcessWorkingSetSizeEx
/SetProcessWorkingSetSizeEx
获取或修改最小与最大值。
1.3 页面状态
状态 | 描述 |
---|---|
Free(空闲) | 页面既没有被“提交(committed)”,也没有被“保留(reserved)”。进程无法访问这些页面。它们可以被重新保留、提交,或一次性保留并提交。如果尝试读取或写入空闲页面,会触发“访问冲突异常(Access Violation)”。进程可以调用 VirtualFree / VirtualFreeEx 来释放先前保留或提交的页面,使它们回到空闲状态。 |
Reserved(保留) | 页面被预留以供将来使用。它占用了虚拟地址空间的一段范围,使得其他分配函数不能再使用这一段地址。保留状态下的页面没有任何物理内存或磁盘文件关联,因此也不可访问。进程可以调用 VirtualAlloc / VirtualAllocEx 来保留一段虚拟地址空间,也可以在之后“提交”这些页面。调用 VirtualFree / VirtualFreeEx 可以将已提交的页面“取消提交(decommit)”回到保留状态。 |
Committed(已提交) | 页面已分配了来自 RAM 或分页文件(pagefile.sys)的物理存储空间。页面可以被访问,访问权限由指定的保护标志(如 PAGE_READWRITE)控制。系统会在第一次访问该页面(读或写)时,将内容加载到物理内存。当进程结束时,系统会释放这些已提交页面占用的存储。进程可以通过 VirtualAlloc / VirtualAllocEx 提交页面,也可以在保留的同时立即提交。旧接口 GlobalAlloc / LocalAlloc 实际上就是在内部分配“已提交”且具有读写权限的内存。 |
Free
- 理解方式: 可以把虚拟地址空间想象成一本巨大的空白笔记本。“Free”页就像空白页,没有被任何程序标记、预订或写字。
- 特点:
- 没有物理内存,也没有磁盘文件关联。
- 任何读写操作都会崩溃。
- 操作系统可以自由地把它分配给别的进程。
- 转变方式:
- 可以通过
VirtualAlloc(..., MEM_RESERVE, ...)
→ 变成 Reserved。 - 可以通过
VirtualAlloc(..., MEM_COMMIT, ...)
→ 一步到 Committed。
- 可以通过
Reserved
- 理解方式:
像是你在笔记本上“预定了”第 10 到第 20 页,但暂时还没写内容。
这些页不能被别人占用,但你自己还不能读写。 - 作用:
- 防止内存碎片化。
- 预先占好连续的虚拟地址区域(非常有用,比如在堆或内存映射文件中)。
- 转变方式:
VirtualAlloc(..., MEM_COMMIT, ...)
→ 把保留页“激活”为 Committed。VirtualFree(..., MEM_DECOMMIT, ...)
→ 把 Committed 页“退回”到 Reserved。VirtualFree(..., MEM_RELEASE, ...)
→ 彻底释放为 Free。
Committed
-
理解方式: 现在你不仅预定了那几页笔记本,还真正开始写内容了。
操作系统为这些虚拟页分配了真实的存储(RAM 或分页文件)。 -
特点:
- 可以被读写。
- 有实际物理页(或页文件中的对应页)。
- 第一次访问时会触发“缺页中断”,然后系统把物理页加载进来。
- 程序结束后,这些页会被系统释放。
1.4 内存分配的作用域
进程内存是隔离的。所有通过以下函数分配的内存:HeapAlloc
,VirtualAlloc
,GlobalAlloc
,LocalAlloc
都只能被当前进程访问。每个进程都有独立的虚拟地址空间,互相之间是隔离的。 使两个进程加载了同一个 DLL,它们分配的内存属于各自的地址空间,互不可见。
DLL 中分配的内存也属于调用进程,DLL(动态链接库)中的代码如果调用 HeapAlloc
或 VirtualAlloc
, 它分配的内存也属于调用这个 DLL 的进程。即DLL 只是“帮忙执行代码”,但内存还是分配在调用者的进程地址空间中。不同进程加载同一个 DLL,它们不会共享内存。 想要共享,必须用 文件映射(File Mapping)。
文件映射(File Mapping)实现共享内存,“文件映射”机制可以让多个进程共享一块内存区域。核心思想:多个进程把同一个“文件”映射到各自的虚拟地址空间,操作系统让它们指向相同的物理页。这可以是:
- 真正的磁盘文件(例如某个
.dat
文件), - 或者是“内存文件”(匿名共享内存)。
1.4.1 创建共享内存的步骤
- 创建映射对象
使用
CreateFileMapping
函数,指定一个文件句柄(或INVALID_HANDLE_VALUE
表示纯内存), 并可给它一个名字(lpName
)。
HANDLE hMap = CreateFileMapping(
INVALID_HANDLE_VALUE, // 使用系统分页文件,表示在内存中创建
NULL, // 默认安全属性
PAGE_READWRITE, // 可读可写 0, // 高32位大小 4096, // 低32位大小(4KB)
L"MySharedMemory" // 共享内存名称
);
- 在其他进程中打开相同的映射对象
HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS,FALSE,L"MySharedMemory");
- 将文件映射到当前进程地址空间
void* p = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
虽然不同进程的p
地址可能不同, 但它们实际上指向相同的物理内存页。如果需要让不同进程在“相同虚拟地址”处映射共享内存,可以用MapViewOfFileEx
指定映射地址
1.4.2 共享内存的释放时机
当最后一个映射该共享内存的进程:
- 终止
- 调用
UnmapViewOfFile()
取消映射, 共享内存会被系统释放。
如果这个映射关联了一个真实文件,那么:
- 系统会自动把更改同步回文件。
- 或者可以手动调用
FlushViewOfFile()
强制写回。
1.4.3 同步问题
如果多个进程都有写权限访问共享内存, 你必须手动使用同步机制防止数据竞争:
- 互斥量(
CreateMutex
/WaitForSingleObject
) - 信号量(
CreateSemaphore
) - 事件对象(
CreateEvent
) - 或者低层的原子操作(
InterlockedIncrement
等)
否则可能出现数据被覆盖、逻辑混乱等问题。
概念 | 含义 |
---|---|
进程私有内存 | 用 VirtualAlloc / HeapAlloc 分配,只能被当前进程访问。 |
DLL 内部分配 | 内存归属调用进程,不会跨进程共享。 |
文件映射共享内存 | 允许多个进程共享同一物理页,通过 CreateFileMapping 和 MapViewOfFile 实现。 |
同步机制 | 多进程写入时必须使用互斥量或信号量保护。 |
1.5 数据执行保护
Data Execution Prevention(数据执行保护)是一种内存保护技术,从 Windows XP / Windows Server 2003 开始引入。某些内存区域(如堆、栈、数据区)应该只用来“存数据”, 而不是“执行代码”。 DEP 就是通过标记这些区域为“不可执行”,防止攻击者把恶意代码塞进这些区域后运行。
1.5.1 DEP 如何工作
-
正常情况:
程序的代码放在专门的代码段(Code Segment),CPU 只会在被标记为 可执行 (Executable) 的页上执行指令。
-
异常情况(被攻击时):
攻击者可能利用溢出漏洞,把恶意指令写到堆或栈中,然后通过修改返回地址让 CPU “跳进去执行”。
-
但 DEP 介入了 —— 如果那一页被标记为 不可执行(NX: No eXecute),CPU 会直接拒绝执行,并触发异常。
-
异常类型:
异常码:STATUS_ACCESS_VIOLATION
若程序未捕获此异常,则进程会被系统直接终止。
1.5.2 需要执行动态生成的代码
有些程序(如浏览器、.NET、Java 虚拟机)会动态生成代码,这类情况必须显式地告诉系统:“我需要这块内存可执行”。做法是使用 VirtualAlloc()
:
LPVOID p = VirtualAlloc(
NULL,
size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE // 可执行 + 可读 + 可写
);
然后在写完机器码后,为了安全起见:
再用 VirtualProtect()
改成“只读可执行”:
DWORD oldProtect;
VirtualProtect(p, size, PAGE_EXECUTE_READ, &oldProtect);
这样可以防止恶意代码在运行时再次篡改这段内存。
1.5.3 哪些内存区域受 DEP 保护?
内存区域 | 默认是否可执行 | 说明 |
---|---|---|
栈 (stack) | ❌ 不可执行 | 局部变量存放处 |
默认堆 (default heap) | ❌ 不可执行 | malloc / HeapAlloc 分配的区域 |
内核池 (memory pool) | ❌ 不可执行 | 系统结构存放处 |
代码段 (.text section) | ✅ 可执行 | 正常程序代码存放处 |
使用 VirtualAlloc 分配并标记可执行的区域 | ✅ 可执行 | 手动生成或加载的代码可执行 |
1.5.4 DEP 策略与系统配置
DEP 的启用与系统启动时的配置有关,由 Boot Configuration Data中的“no-execute policy”决定。
你可以用以下 API 查询或设置策略:
函数 | 作用 |
---|---|
GetSystemDEPPolicy() |
获取系统 DEP 策略 |
SetProcessDEPPolicy() |
设置当前进程是否启用 DEP |
1.5.5 编程与安全建议
- 用 VirtualAlloc 设置内存保护属性
- 用
PAGE_EXECUTE_READ
或PAGE_EXECUTE_READWRITE
- 生成代码后,改为只读可执行
- 用
VirtualProtect()
移除写权限
- 尽量减少可执行内存的范围
- 降低攻击面
- 将可执行区域放在低地址
- 防止溢出越界覆盖到代码区(高地址)
1.5.6 兼容性问题(DEP 可能导致旧程序崩溃)
有些老程序或动态生成代码的程序不兼容 DEP:
早期的 ATL 7.1 及更早版本(Active Template Library)会在数据段执行代码 → 触发 NX Fault。
有些程序会把“小跳板代码(thunks)”放在 .data 段。但 DEP 会把 .data 段标记为“不可执行”,因此程序崩溃。
解决办法:
-
把可执行代码移到 .text 段(代码段);
-
或在 .data 段的节头(section header)中显式加上:
IMAGE_SCN_MEM_EXECUTE
让该段变为可执行。
1.6 内存保护
1.6.1 内存保护概念
每个进程的内存 默认是受保护的,因为它们有自己的 私有虚拟地址空间。除此之外,Windows 还利用 虚拟内存硬件 提供进一步保护,比如:
-
代码页可以被标记为 只读,防止用户模式线程修改。
-
内存保护的具体实现依赖 CPU(不同 CPU 支持的页表标记不同)。
也就是说,每个进程默认看不到其他进程的内存,并且代码页可以防止被随意篡改。
1.6.2 写时复制(Copy-on-Write, COW)
核心思想:多个进程可以共享相同的物理内存页面,直到其中一个进程需要修改这个页面。 修改才会触发“复制”动作(copy),其他进程继续保留原页面。
EXAMPLE: 假设 进程 A 和 进程 B 都加载了同一个 DLL:
进程 A 的虚拟页 → 物理页 X
进程 B 的虚拟页 → 物理页 X
-
此时两者共享同一物理页 X,节省内存。
-
-
如果进程 A 写入该页:
-
系统会创建一个新的物理页 Y。
-
把 A 的修改写入 Y。
-
更新 A 的页表,让 A 的虚拟页指向 Y。
-
-
此时 B 仍然指向原来的物理页 X。 这样就保证了进程间内存隔离,同时最大化内存利用率。
1.6.3 应用程序和 DLL 的加载
- 多个实例的应用程序
-
每个实例运行在自己的虚拟地址空间中。
-
它们的
hInstance
(基地址)通常相同。 -
如果可以加载到默认基地址:
- 代码页可以共享物理内存(使用写时复制)。
-
如果默认地址不可用:
-
系统给该实例分配新的物理页。
-
共享就受影响,写时复制可能触发更多复制。
-
- DLL 的加载
-
DLL 有默认基地址。
-
如果多个进程可以在默认地址加载:
- DLL 的物理页可以共享。
-
如果某进程必须加载到其他地址:
-
DLL 内部的跳转修正会修改代码页。
-
这些代码页会被写时复制到新的物理页。
-
如果代码段大量引用数据段,可能导致整个代码段都被复制。
-
2. 内存池
2.1 内存池的概念
内存管理器会创建两类系统内存池,用于系统分配内存:
-
Nonpaged Pool(不可分页池)
-
Paged Pool(可分页池)
2.2 内存池的位置
这两类内存池都位于 系统保留的虚拟地址空间区域,并且映射到 每个进程的虚拟地址空间。也就是说,每个进程都能看到这些内存池的虚拟地址,但这些地址属于系统,不属于进程私有的用户空间。
2.3 非分页池
非分页池包含的虚拟内存地址 始终驻留在物理内存中,只要对应的 内核对象还存在。
-
这些内存不能被换出到磁盘,随时可访问。
-
非分页池主要用于:内核需要立即访问的资源,比如驱动程序和中断处理程序中的数据结构。
2.4 分页池
可分页池包含的虚拟内存 可以被换入或换出到系统分页文件。
-
如果系统内存紧张,可分页池中的页面可以暂时放到磁盘上(page out)。
-
提高了系统内存的利用率,但访问这些页面时可能产生 页缺失(page fault)。
2.5 多处理器优化
为了提高性能,单处理器系统使用 3 个分页池,多处理器系统使用 5 个分页池。
2.6 内核对象句柄
内核对象的句柄(handle)存储在可分页池中,所以你能创建的句柄数量取决于可分页池的剩余内存。
2.7 系统监控
系统会记录 非分页池、分页池 和 分页文件 的限制和当前使用情况。
3. 内存性能信息
3.1 整体目的
Windows 内核的 内存管理器(Memory Manager)负责所有进程和系统的内存分配、回收、分页、缓存等。
为了让开发者或系统工具能够查看系统的内存状态(比如当前用了多少内存、剩多少),Windows 提供了两种方式:
-
性能计数器(Performance Counters) 是 Windows 内部的一个「实时统计系统」,任务管理器、性能监视器等通过它获取信息。
-
内存查询 API 函数 程序可以直接调用,如:
-
GetPerformanceInfo()
-
GetProcessMemoryInfo()
-
GlobalMemoryStatusEx()
这些函数会返回一些结构体,例如:
-
MEMORYSTATUSEX
-
PERFORMANCE_INFORMATION
-
PROCESS_MEMORY_COUNTERS_EX
它们提供系统层面或进程层面的内存统计数据。
3.2 系统级内存性能信息(System Memory Performance Information)
这部分对应任务管理器的「性能」页(Performance Tab)。
性能计数器(Performance Counter) | 对应结构体字段 | 任务管理器显示内容(Vista/Win2008) | 任务管理器显示内容(XP/2003) | 解释 |
---|---|---|---|---|
Available KB | MEMORYSTATUSEX.ullAvailPhys , PERFORMANCE_INFORMATION.PhysicalAvailable |
内存图中 “可用” 部分 | 物理内存: Available | 当前物理内存中空闲的字节数 |
Total Physical Memory | ullTotalPhys , PhysicalTotal |
物理内存总量 | 物理内存总量 | 系统检测到的所有可用物理内存 |
Committed Bytes | CommitTotal |
“Page File” 的第一个值 | Commit Charge: Total | 当前所有进程已提交的虚拟内存总量 |
Commit Limit | ullTotalPageFile , CommitLimit |
“Page File” 的第二个值 | Commit Charge: Limit | 可提交的最大虚拟内存上限(物理内存 + 页面文件) |
Kernel Nonpaged Bytes | KernelNonpaged |
Kernel Memory: Nonpaged | Kernel Memory: Nonpaged | 内核中“不可分页”的内存(常驻物理内存) |
Kernel Paged Bytes | KernelPaged |
Kernel Memory: Paged | Kernel Memory: Paged | 内核中“可分页”的内存(可被换出到磁盘) |
Processes Count | ProcessCount |
System: Processes | Totals: Processes | 当前进程数量 |
Threads Count | ThreadCount |
System: Threads | Totals: Threads | 当前线程数量 |
System Cache | SystemCache |
— | System Cache | 文件缓存、共享页缓存等 |
Commit的概念:指系统为进程分配的虚拟内存量(不代表全部在物理内存中)。
3.3 进程级内存性能信息(Process Memory Performance Information)
这部分对应任务管理器「进程」页(Processes Tab)。
性能计数器 | 对应结构体字段 | Vista/Win2008 显示 | XP/2003 显示 | 说明 |
---|---|---|---|---|
Handle Count | — | Handles | Handles | 此进程打开的系统句柄数量(文件、线程、互斥体等) |
Page File Bytes | PagefileUsage |
Commit Size | VM Size | 该进程占用的虚拟内存(提交的)大小 |
Pool Nonpaged Bytes | QuotaNonPagedPoolUsage |
NP Pool | NP Pool | 该进程使用的不可分页内核池 |
Pool Paged Bytes | QuotaPagedPoolUsage |
Paged Pool | Paged Pool | 该进程使用的可分页内核池 |
Private Bytes | PrivateUsage |
Commit Size | VM Size | 该进程私有的内存量(不能被共享) |
Working Set | WorkingSetSize |
Working Set | Mem Usage | 当前驻留在物理内存中的页数(该进程实际在物理内存的大小) |
Working Set Peak | PeakWorkingSetSize |
Peak Working Set | Peak Mem Usage | 工作集的峰值 |
Working Set - Private | — | Private Working Set | 不支持 | 进程独占驻留的物理内存(不含共享库页) |
Thread Count | — | Threads | Threads | 当前进程的线程数 |
3.4 总结
维度 | 意思 | 示例 |
---|---|---|
Committed Bytes | 虚拟内存已提交的大小(可能部分在磁盘) | 程序 malloc 了 2GB |
Working Set | 实际在物理内存中的部分 | 1.5GB 在 RAM,0.5GB 被分页出 |
Private Bytes | 进程独占的虚拟内存 | 自己 malloc 的,不含 DLL |
Paged / Nonpaged Pool | 内核空间使用的内存 | 驱动、内核结构体等使用的内存 |
System Cache | 系统文件缓存 | 例如文件读写缓冲区 |
4. 虚拟内存函数
4.1 Virtual Memory Functions
4.1.1 总览
每个进程在 Windows 下都有自己的虚拟地址空间,这些函数让程序可以:
-
创建 / 释放 虚拟内存区域;
-
改变页面访问权限;
-
查询内存状态;
-
把虚拟内存页锁在物理内存中; 等等。
4.1.2 Reserve address space
保留进程虚拟地址空间中的一段区域。 这种保留并不会分配物理内存,只是标记这块虚拟地址区段(我将来要用),防止其他分配占用这部分。它不会影响别的进程的地址空间。 保留页面不会浪费物理内存,但允许程序预留出一块区域供将来动态使用(比如增长数据结构用)。常用函数:VirtualAlloc(..., MEM_RESERVE, ...)
,系统不会为它分配 RAM 或 page file,只是保证地址不冲突。
4.1.3 Commit pages
在进程的虚拟地址空间中提交一段已保留的页面, 这时系统会为它分配物理存储(可能是内存,也可能是页面文件), 而且这些页面只属于当前进程。常用:VirtualAlloc(..., MEM_COMMIT, PAGE_READWRITE)
,操作系统分配物理页(RAM 或 pagefile),进程才能真正读写它。
4.1.4 Specify access protection
为一段已提交的页面指定访问权限,比如只读、可读写或不可访问。 这与普通内存分配函数(如 malloc
)不同,后者总是返回可读写的内存。使用 VirtualProtect()
修改权限。权限类型有:
-
PAGE_READWRITE
-
PAGE_READONLY
-
PAGE_NOACCESS
-
PAGE_EXECUTE_READWRITE
等。
这个在漏洞利用和内存保护中非常关键。
4.1.5 Free reserved pages
释放一段保留的虚拟地址,使这些地址可以被后续分配操作重新使用。用 VirtualFree(..., 0, MEM_RELEASE)
。释放后,这些虚拟地址又可以被其他 VirtualAlloc()
使用。
4.1.6 Decommit committed pages
解除一段已提交的页面,释放它们的物理存储(RAM 或页面文件),这些物理页可以被系统分配给别的进程使用。
-
与
MEM_COMMIT
相反。 -
地址仍然属于本进程(保留状态),只是失去了物理页。
-
用
VirtualFree(..., size, MEM_DECOMMIT)
。
4.1.7 Lock pages in RAM
将一段已提交的内存锁定到物理内存(RAM)中,使系统不能把这些页面换出到页面文件(磁盘)。
-
使用
VirtualLock()
。 -
典型用途:
-
加密密钥缓存;
-
实时系统(防止页面置换造成延迟)。
-
-
解锁用
VirtualUnlock()
。
4.1.8 Query page information
获取当前进程或指定进程虚拟地址空间中某一段页面的信息。
-
用
VirtualQuery()
。 -
能得到每页的状态(保留 / 提交 / 空闲)和权限。
-
常用于调试器、内存扫描工具。
4.1.9 Change access protection
修改指定进程中一段已提交页面的访问保护属性。
-
用
VirtualProtect()
。 -
可动态调整可执行 / 可写 / 只读 权限。
-
比如:
-
程序运行时动态加载代码;
-
反调试;
-
修改函数指针表等。
-
4.1.10 注意
功能点 | Specify access protection | Change access protection |
---|---|---|
发生阶段 | 在 分配(commit)时 一起指定 | 在 内存已存在后 动态修改 |
常用函数 | VirtualAlloc(..., MEM_COMMIT, PAGE_XXX) |
VirtualProtect() |
作用对象 | 新提交的页面 | 已提交(已存在)的页面 |
是否创建物理页 | ✅ 会触发物理页分配 | ❌ 只修改访问权限,不重新分配页 |
使用场景 | 初始化时设置权限 | 程序运行过程中调整权限 |
典型权限 | PAGE_READWRITE , PAGE_READONLY , PAGE_EXECUTE_READ , PAGE_NOACCESS , etc. |
同样的权限集合 |
影响范围 | 刚创建的那段内存 | 指定范围内现有内存页 |
底层行为 | 在创建 PTE(页表项)时写入权限标志 | 修改已有 PTE 的权限标志 |
4.2 分配虚拟内存
虚拟内存函数(如 VirtualAlloc
等)操作的是“内存页”。这些函数会根据当前计算机的页大小(通常是 4 KB)来 对齐 传入的地址和大小。VirtualAlloc
函数可以执行以下三种操作之一:
- 保留一段或多段空闲的页地址范围。
-
此时只在“虚拟地址空间”层面占位;
-
并没有分配实际的物理内存;
-
页不可访问(访问会触发异常)。
- 提交一段或多段已保留的页。
-
这一步会为那些虚拟页 分配物理存储(RAM 或分页文件);
-
被提交的页可以被读写(根据你指定的访问权限)。
- 一次性完成“保留 + 提交”。
-
常用于快速分配一块内存并立即使用;
-
等价于“先占虚拟空间,再给它物理内存”。
可以指定要保留或提交的页的起始虚拟地址,如果传入 NULL
可以让系统自动选择一个合适的地址。这个函数会把你提供的地址向下取整到页边界对齐。被“保留”的页不可访问;
被“提交”的页可以根据参数设置成:
-
可读写(
PAGE_READWRITE
)、 -
只读(
PAGE_READONLY
)、 -
不可访问(
PAGE_NOACCESS
)。
当页被提交后,系统会从 RAM 和页面文件(pagefile.sys)中扣除相应的“内存配额”,
但这些页只有在 第一次访问(读/写) 时才会被真正加载到物理内存中。对于通过 VirtualAlloc
提交(commit)的内存页,可以像普通内存一样用指针直接访问。
4.3 释放虚拟内存
取消提交(Decommit)一段或多段已提交的页面,将这些页面的状态改为“已保留”。取消提交会释放与这些页面关联的物理存储,使它们可以被其他进程重新分配使用。任何已提交的页面块都可以被取消提交(没有必须整块释放的限制)。
释放(release)一段或多段已保留的页面,将这些页面状态改为“空闲”。已保留的页面只能整体释放(必须一次性释放 VirtualAlloc
最初保留的整个块)。同时取消提交并释放一段或多段已提交的页面(状态变为 free)。此时要求释放的区域必须是最初 VirtualAlloc
分配的完整块,且该块中的所有页面都已提交。一旦内存块被取消提交或释放,就不能再访问它。若尝试访问(读写)已释放的页面,会触发访问冲突异常。如果那段内存中还有重要数据,就不要释放或取消提交,否则数据将永远丢失。如果你只是想告诉系统“这段内存的数据不再重要”,可以用 VirtualAlloc
+ MEM_RESET
。使用 MEM_RESET
后,页不会再写入/读取分页文件,但这块内存之后仍然可以再次使用。
操作类型 | 结果 | 地址空间 | 物理内存 | 可重新使用 |
---|---|---|---|---|
Decommit | 取消提交 | ✅ 保留 | ❌ 释放 | ✅ 可以再 Commit |
Release | 释放 | ❌ 释放 | ❌ 释放 | ✅ 可以重新 VirtualAlloc |
MEM_RESET | 标记无用 | ✅ 保留 | ⚙️ 内容丢弃但仍可用 | ✅ 可立即重用 |
4.4 Working with Pages
要确定当前计算机上页面的大小,可以使用 GetSystemInfo
函数。
函数 VirtualQuery
和 VirtualQueryEx
会返回从指定地址开始的一段连续页面区域的信息。查询的页面区域从指定地址向下对齐到页面边界开始。查询会延伸到所有具有以下相同属性的连续页面:
-
状态相同(已提交、已保留或空闲)。
-
如果起始页不是空闲的,这段区域中的所有页都属于同一次 VirtualAlloc 分配。
-
所有页的访问保护属性相同(只读、可读写、无访问权限)。
VirtualLock
函数允许进程将一段已提交的内存页锁定到物理内存(RAM)中,防止系统把它交换到分页文件中。锁定页面是危险的,因为它限制了系统的内存管理能力。如果滥用 VirtualLock
,系统可能被迫将代码页换出,导致性能下降。VirtualUnlock
函数用于解锁之前被 VirtualLock
锁定的内存。
VirtualProtect
函数允许进程修改其虚拟地址空间中任何已提交页面的访问保护属性。
函数 | 功能 | 用途 |
---|---|---|
GetSystemInfo |
获取页大小 | 了解分页粒度(一般 4KB) |
VirtualQuery |
查询当前进程的内存页信息 | 调试、自检 |
VirtualQueryEx |
查询其他进程的内存页信息 | 调试器 |
VirtualLock |
锁定页到物理内存 | 实时或关键数据 |
VirtualUnlock |
解锁锁定页 | 释放控制权 |
VirtualProtect |
修改当前进程页保护 | 保护关键数据 |
VirtualProtectEx |
修改其他进程页保护 | 调试、内存注入工具 |