ReactOS 图形系统分析(23):引擎内存管理 — mem.c
1. 概述
mem.c位于win32ss/gdi/eng/mem.c(文件头自述 “GDI Driver Memory Management Functions”),实现 GDI 引擎的内存管理服务族——既是引擎内部各模块的通用分配入口,也是 DDI 契约中供显示/字体/打印驱动调用的内存服务(win32k.sys 导出)。
按能力分三类:
┌─────────────────────────────────────────────────────────────┐ │ gdi/eng/mem.c(本分析对象) │ │ A. 内核池分配 │ │ EngAllocMem(Paged/NonPaged + 清零)/ EngFreeMem │ │ B. 用户虚拟内存分配 │ │ EngAllocUserMem / EngFreeUserMem(ZwAllocateVirtMem) │ │ C. 用户内存安全锁定(防内核使用中被用户释放) │ │ EngSecureMem / EngSecureMemForRead / EngUnsecureMem │ │ + HackSecureVirtualMemory / HackUnsecureVirtualMemory │ │ (MDL 探测+锁定,供内核访问用户缓冲区) │ ├─────────────────────────────────────────────────────────────┤ │ 底层:ExAllocatePoolWithTag / Zw(Alloc|Free)VirtualMemory / │ │ MmSecureVirtualMemory / IoAllocateMdl+MmProbeAndLock │ └─────────────────────────────────────────────────────────────┘重要观察:EngAllocMem是全引擎最通用的分配服务——全树 32 个文件、65 处调用(eng/ntgdi/字体驱动/显示驱动/DirectX 全在用),是《分析_4》2.x 各对象(SURFACE/EBRUSHOBJ/EXLATEOBJ 等)与《分析_18/19》等文件分配内存的共同底层。
2. 内核池分配 — EngAllocMem / EngFreeMem
PVOIDEngAllocMem(ULONG fl,ULONG cjMemSize,ULONG ulTag){pvBaseAddress=ExAllocatePoolWithTag((fl&FL_NONPAGED_MEMORY)?NonPagedPool:PagedPool,cjMemSize,ulTag);if(pvBaseAddress==NULL)returnNULL;if(fl&FL_ZERO_MEMORY)RtlZeroMemory(pvBaseAddress,cjMemSize);returnpvBaseAddress;}VOIDEngFreeMem(PVOID pvBaseAddress){if(pvBaseAddress)// Windows 允许传 NULLExFreePoolWithTag(pvBaseAddress,0);// tag 0 == ExFreePool(不校验 tag)}标志([winddi.h](file:///d:/reactos/sdk/include/psdk/winddi.h#L1358-L1359)):
| 标志 | 含义 |
|---|---|
FL_ZERO_MEMORY(0x1) | 分配后清零(RtlZeroMemory) |
FL_NONPAGED_MEMORY(0x2) | 从NonPagedPool分配(缺省为 PagedPool) |
设计要点:
- 默认分页池(大部分 GDI 对象可分页换出);需要锁路径/中断上下文时显式
FL_NONPAGED_MEMORY(如信号量 ERESOURCE,见《分析_20》)。 ulTag用于池标签调试(WinDbg!pool可看归属);EngFreeMem用 tag 0 等价ExFreePool,不校验标签。- 与
EngAllocSectionMem(《分析_21》会话空间大块共享分配)形成"小对象用池 / 大共享用节"的分层。
3. 用户虚拟内存分配 — EngAllocUserMem / EngFreeUserMem
PVOIDEngAllocUserMem(SIZE_T cjMemSize,ULONG ulTag){Status=ZwAllocateVirtualMemory(NtCurrentProcess(),&pvBaseAddress,0,&cjMemSize,MEM_COMMIT|MEM_RESERVE,PAGE_READWRITE);.../* TODO: Add allocation info to AVL tree (stored inside W32PROCESS) */}VOIDEngFreeUserMem(PVOID pvBaseAddress){ZwFreeVirtualMemory(NtCurrentProcess(),&pvBaseAddress,&cjSize,MEM_RELEASE);}语义:在当前进程的用户地址空间一次性"保留+提交"一段读写内存(用于内核替用户分配 GDI 位图缓冲等)。树内消费者:
| 消费者 | 用途 |
|---|---|
| [surface.c](file:///d:/reactos/win32ss/gdi/eng/surface.c#L238) | BMF_USERMEM位图的像素内存(CreateBitmap 用户内存模式) |
| [dibobj.c](file:///d:/reactos/win32ss/gdi/ntgdi/dibobj.c#L2017) | 位图信息查询/转换的临时用户缓冲(bm.bmBits) |
已知 TODO(源码标注):分配信息应记入 W32PROCESS 的 AVL 树以便进程退出时清理,当前未实现(若进程中途退出,该用户内存可能泄漏/由 VAD 回收)。
4. 用户内存安全锁定 — EngSecureMem / EngSecureMemForRead / EngUnsecureMem
Windows 语义:当内核正在使用一块用户模式内存(如 DIB Section 位图、系统调用传入的缓冲)时,必须"锁定"它——否则用户线程可能并发释放该内存(VirtualFree/退出进程),内核继续写就崩溃。EngSecureMem返回不透明句柄,用毕EngUnsecureMem解锁。
ReactOS 现状(关键):三个函数的真实现被{// HACK!!!块短路,真正的MmSecureVirtualMemory分支不可达:
HANDLEEngSecureMem(PVOID Address,ULONG Length){{// HACK!!!_SEH2_TRY{ProbeForWrite(Address,Length,1);}// 仅做可写性探测_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER){_SEH2_YIELD(returnNULL);}_SEH2_END;return(HANDLE)-1;// ← 恒返回 -1,假句柄!}returnMmSecureVirtualMemory(Address,Length,PAGE_READWRITE);// 不可达}VOIDEngUnsecureMem(HANDLE Mem){if(Mem==(HANDLE)-1)return;// 假句柄直接跳过MmUnsecureVirtualMemory((PVOID)Mem);}EngSecureMem只做ProbeForWrite校验后返回假句柄-1(无真正锁定);EngSecureMemForRead类似(逐页读探测)。EngUnsecureMem遇-1直接返回——成对调用下行为等价"只探测不锁定"。- 这意味着 ReactOS 当前没有真正防止用户释放正在使用内存的机制(属安全缺口/简化)。
消费者:[dibobj.c](file:///d:/reactos/win32ss/gdi/ntgdi/dibobj.c#L1166)(DIB 位图锁定)、[mapping.c](file:///d:/reactos/win32ss/gdi/eng/mapping.c#L58)(EngMapSectionView 后锁定视图,见《分析_21》)。
5. MDL 锁定 — HackSecureVirtualMemory / HackUnsecureVirtualMemory
供内核访问用户缓冲区(如 DIB 引擎读写用户位图)的MDL 探测+映射辅助:
PVOIDHackSecureVirtualMemory(PVOID Address,SIZE_T Size,ULONG ProbeMode,PVOID*SafeAddress){pmdl=IoAllocateMdl(Address,Size,FALSE,TRUE,NULL);MmProbeAndLockPages(pmdl,UserMode,Operation);// 探测+锁定用户页*SafeAddress=MmGetSystemAddressForMdlSafe(pmdl,NormalPagePriority);returnpmdl;// 返回 MDL 作为"句柄"}VOIDHackUnsecureVirtualMemory(PVOID SecureHandle){MmUnlockPages(pmdl);// 解锁IoFreeMdl(pmdl);// 释放 MDL}- 流程:
IoAllocateMdl→MmProbeAndLockPages(SEH 保护,探测失败返回 NULL)→MmGetSystemAddressForMdlSafe得到系统空间映射地址,内核即可直接读写用户缓冲; - 是 4.3 节"只探测不锁定"的安全替代方案(真正的页锁定 + 系统地址映射),声明于 [ntgdi/misc.h](file:///d:/reactos/win32ss/gdi/ntgdi/misc.h#L29-L30)。
6. 导出与消费路径
6.1 win32k.spec 导出([win32k.spec](file:///d:/reactos/win32ss/win32k.spec))
@ stdcall EngAllocMem(long long long) # L9 @ stdcall EngAllocPrivateUserMem(ptr ptr long) # L10(本文件未实现,spec 保留) @ stdcall EngAllocUserMem(ptr long) # L12 @ stdcall EngFreeMem(ptr) # L57 @ stdcall EngFreePrivateUserMem(ptr ptr) # L59(本文件未实现) @ stdcall EngFreeUserMem(ptr) # L61 @ stdcall EngSecureMem(ptr long) # L118 @ stdcall EngUnsecureMem(ptr) # L1406.2 其他导出路径
| 路径 | 内容 |
|---|---|
| [dxgthk.spec](file:///d:/reactos/win32ss/reactx/dxgthk/dxgthk.spec#L3-L14) | 重新导出EngAllocUserMem/EngFreeUserMem(用户态 DirectX 驱动用) |
| [win32u/umpdstubs](file:///d:/reactos/win32ss/gdi/eng/umpdstubs.c#L9) | NtGdiUMPDEngFreeUserMem(UMPD 用户内存释放) |
6.3 调用链示例
7. 与《分析_4》标注对照
| 标注 | 核对 |
|---|---|
| 《_4》第 43 行:mem.c =「引擎内存分配」 | 准确。补充:三类能力(内核池/用户内存/安全锁定);EngAllocMem为全引擎通用分配入口;EngSecureMem当前为"只探测不锁定"的 HACK 简化 |
8. 源码索引
| 文件 | 关键内容 |
|---|---|
| [mem.c](file:///d:/reactos/win32ss/gdi/eng/mem.c) | EngAllocMem/FreeMem、EngAllocUserMem/FreeUserMem、EngSecureMem(ForRead)/UnsecureMem、HackSecure/UnsecureVirtualMemory |
| [winddi.h](file:///d:/reactos/sdk/include/psdk/winddi.h#L1358-L1359) | FL_ZERO_MEMORY / FL_NONPAGED_MEMORY |
| [misc.h](file:///d:/reactos/win32ss/gdi/ntgdi/misc.h#L29-L30) | Hack* 声明 |
| [surface.c](file:///d:/reactos/win32ss/gdi/eng/surface.c#L238) | BMF_USERMEM 位图用 EngAllocUserMem |
| [mapping.c](file:///d:/reactos/win32ss/gdi/eng/mapping.c#L58) | EngMapSectionView 用 EngSecureMem(《分析_21》) |
| [dibobj.c](file:///d:/reactos/win32ss/gdi/ntgdi/dibobj.c#L1166) | DIB 位图锁定 / 用户缓冲分配 |
关联模块:
- 大块共享分配:
EngAllocSectionMem(《分析_21》mapping.c,会话空间) - 分配对象:SURFACE/EBRUSHOBJ/EXLATEOBJ/PATHOBJ 等(《分析_4/17/18/19》)
- 内存标签:GDITAG_* 系列(ExAllocatePoolWithTag)
9. 各函数使用说明
9.1 EngAllocMem — 分配内核池内存(最常用)
PVOIDEngAllocMem(ULONG fl,ULONG cjMemSize,ULONG ulTag);| 参数 | 说明 |
|---|---|
fl | 标志:FL_ZERO_MEMORY(清零)/FL_NONPAGED_MEMORY(非分页池);可 OR 组合 |
cjMemSize | 请求字节数 |
ulTag | 4 字节池标签(如'rusG'),用于调试!pool归类 |
使用要点:
- 默认是分页池——只有锁路径/中断/DMA 等必须常驻的场景才加
FL_NONPAGED_MEMORY(例:信号量 ERESOURCE 见《分析_20》)。 - 返回 NULL 表示分配失败,必须判空再使用。
- 要求清零内存时传
FL_ZERO_MEMORY,省去手动memset。 - 释放必须用
EngFreeMem(配 tag 0),不要直接用ExFreePool(会丢失"允许 NULL"的语义约定)。
典型用法:
PBRUSH pbr=EngAllocMem(FL_ZERO_MEMORY,sizeof(BRUSH),GDITAG_BRUSH);if(!pbr)returnNULL;// 失败处理...EngFreeMem(pbr);// 释放(NULL 安全)9.2 EngFreeMem — 释放 EngAllocMem 分配的内存
VOIDEngFreeMem(PVOID pvBaseAddress);使用要点:
- 允许传 NULL(与 Windows 一致),无需外层判空。
- 内部用 tag 0 调用
ExFreePoolWithTag,等价ExFreePool,不校验标签。 - 只能释放
EngAllocMem(或驱动直接用ExAllocatePoolWithTag)分配的内核池内存;不能释放用户内存或节映射内存。
9.3 EngAllocUserMem — 在当前进程用户空间分配内存
PVOIDEngAllocUserMem(SIZE_T cjMemSize,ULONG ulTag);使用要点:
- 分配在当前进程的用户地址空间(
ZwAllocateVirtualMemory,MEM_COMMIT|MEM_RESERVE,PAGE_READWRITE),内核可直接写,用户态可读。 - 典型用途:为
BMF_USERMEM位图提供像素缓冲、为位图信息查询提供临时缓冲。 - 释放必须用
EngFreeUserMem。 - 已知局限(源码 TODO):分配记录未纳入 W32PROCESS 的 AVL 树——若分配后进程异常退出,该内存靠进程 VAD 回收,引擎不主动跟踪。
典型用法:
pvBits=EngAllocUserMem(cjBits,0);if(!pvBits)returnFALSE;.../* 内核填像素 */EngFreeUserMem(pvBits);/* 用毕释放 */9.4 EngFreeUserMem — 释放用户空间内存
VOIDEngFreeUserMem(PVOID pvBaseAddress);使用要点:
- 对应
EngAllocUserMem的逆操作(ZwFreeVirtualMemory,MEM_RELEASE)。 - 注意:
EngFreeUserMem不判 NULL(与EngFreeMem不同)——调用前需自行确认指针有效。 - 只释放"由内核替当前进程分配"的用户内存,不要用来释放应用自己
VirtualAlloc的内存。
9.5 EngSecureMem — 锁定用户内存(当前为 HACK 简化版)
HANDLEEngSecureMem(PVOID Address,ULONG Length);使用要点:
- 语义:防止内核使用期间用户并发释放该内存;返回不透明句柄,用毕
EngUnsecureMem解锁。 - ReactOS 现状:只做
ProbeForWrite可写性探测,恒返回假句柄(HANDLE)-1;真正的MmSecureVirtualMemory被{// HACK!!!块短路,没有真正锁定。 - 因此安全校验逻辑要自己写:若后续代码依赖"内存已被锁定",当前实现并不保证——需自行加锁或改用
HackSecureVirtualMemory。 - 返回值判 NULL/
-1均表示失败(探测不通过)。
典型用法:
hSecure=EngSecureMem(pjBits,cjMaxBits);if(!hSecure)returnFALSE;/* 探测失败 */.../* 使用 pjBits */EngUnsecureMem(hSecure);/* 假句柄 -1 会被直接跳过 */9.6 EngSecureMemForRead — 锁定只读用户内存
HANDLEEngSecureMemForRead(PVOID Address,ULONG Length);使用要点:
- 与
EngSecureMem同构,但做的是逐页读探测(ProbeForRead+ 每页读一次),语义为"内核将只读该缓冲"。 - 同样返回假句柄
-1,真实锁定未启用(HACK)。 - 使用场景:只读用户缓冲(如 GetBitmapBits 的输入参数校验)。
9.7 EngUnsecureMem — 解除锁定
VOIDEngUnsecureMem(HANDLE Mem);使用要点:
- 与
EngSecureMem/EngSecureMemForRead成对使用。 - 遇假句柄
(HANDLE)-1直接返回(HACK 配套逻辑);真实句柄才调用MmUnsecureVirtualMemory。 - 不要对 NULL 或未加锁的地址调用(无防护),成对性由调用方保证。
9.8 HackSecureVirtualMemory — 用 MDL 锁定用户缓冲并映射到系统空间
PVOIDHackSecureVirtualMemory(PVOID Address,SIZE_T Size,ULONG ProbeMode,PVOID*SafeAddress);| 参数 | 说明 |
|---|---|
Address/Size | 用户缓冲地址与大小 |
ProbeMode | PAGE_READONLY(读锁)/PAGE_READWRITE(读写锁) |
SafeAddress | 输出:可被内核直接访问的系统空间映射地址 |
使用要点:
- 返回值为PMDL(作为后续释放的句柄),不是映射地址——映射地址在
SafeAddress输出。 - 流程:
IoAllocateMdl→MmProbeAndLockPages(SEH 保护,失败返回 NULL)→MmGetSystemAddressForMdlSafe。 - 用毕必须调用
HackUnsecureVirtualMemory释放(解锁 + 释放 MDL)。 - 这是当前引擎里真正能锁定用户页的方案(优于 9.5 的探测式 HACK),适合 DIB 引擎需要长时间读写用户位图的场景。
典型用法:
pmdl=HackSecureVirtualMemory(pvUserBits,cbSize,PAGE_READWRITE,&pvSafe);if(!pmdl)returnFALSE;memcpy(pvSafe,...);/* 内核通过系统地址访问 */HackUnsecureVirtualMemory(pmdl);9.9 HackUnsecureVirtualMemory — 释放 MDL 锁定
VOIDHackUnsecureVirtualMemory(PVOID SecureHandle);使用要点:
- 参数是
HackSecureVirtualMemory返回的 PMDL。 - 依次
MmUnlockPages+IoFreeMdl,成对释放。 - 必须与 9.8 一一对应,过早释放会导致内核访问已解锁页面的错误。
本文档基于 ReactOS 源代码
win32ss/gdi/eng/mem.c及关联模块分析(2026 年 8 月)