三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

ReactOS 图形系统分析(23):引擎内存管理 — mem.c

ReactOS 图形系统分析(23):引擎内存管理 — mem.c

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}
  • 流程:IoAllocateMdlMmProbeAndLockPages(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) # L140

6.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 调用链示例

EngAllocMem(65 处调用:eng/ntgdi/驱动)

ExAllocatePoolWithTag
Paged/NonPaged

EngAllocUserMem

ZwAllocateVirtualMemory
用户地址空间

EngSecureMem

ProbeForWrite + 假句柄-1
(真实锁定未启用)

HackSecureVirtualMemory

MDL 探测锁定
+ 系统地址映射


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请求字节数
ulTag4 字节池标签(如'rusG'),用于调试!pool归类

使用要点:

  1. 默认是分页池——只有锁路径/中断/DMA 等必须常驻的场景才加FL_NONPAGED_MEMORY(例:信号量 ERESOURCE 见《分析_20》)。
  2. 返回 NULL 表示分配失败,必须判空再使用。
  3. 要求清零内存时传FL_ZERO_MEMORY,省去手动memset
  4. 释放必须用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);

使用要点:

  1. 允许传 NULL(与 Windows 一致),无需外层判空。
  2. 内部用 tag 0 调用ExFreePoolWithTag,等价ExFreePool,不校验标签。
  3. 只能释放EngAllocMem(或驱动直接用ExAllocatePoolWithTag)分配的内核池内存;不能释放用户内存或节映射内存。

9.3 EngAllocUserMem — 在当前进程用户空间分配内存

PVOIDEngAllocUserMem(SIZE_T cjMemSize,ULONG ulTag);

使用要点:

  1. 分配在当前进程的用户地址空间ZwAllocateVirtualMemoryMEM_COMMIT|MEM_RESERVE,PAGE_READWRITE),内核可直接写,用户态可读。
  2. 典型用途:为BMF_USERMEM位图提供像素缓冲、为位图信息查询提供临时缓冲。
  3. 释放必须用EngFreeUserMem
  4. 已知局限(源码 TODO):分配记录未纳入 W32PROCESS 的 AVL 树——若分配后进程异常退出,该内存靠进程 VAD 回收,引擎不主动跟踪。

典型用法:

pvBits=EngAllocUserMem(cjBits,0);if(!pvBits)returnFALSE;.../* 内核填像素 */EngFreeUserMem(pvBits);/* 用毕释放 */

9.4 EngFreeUserMem — 释放用户空间内存

VOIDEngFreeUserMem(PVOID pvBaseAddress);

使用要点:

  1. 对应EngAllocUserMem的逆操作(ZwFreeVirtualMemoryMEM_RELEASE)。
  2. 注意:EngFreeUserMem不判 NULL(与EngFreeMem不同)——调用前需自行确认指针有效。
  3. 只释放"由内核替当前进程分配"的用户内存,不要用来释放应用自己VirtualAlloc的内存。

9.5 EngSecureMem — 锁定用户内存(当前为 HACK 简化版)

HANDLEEngSecureMem(PVOID Address,ULONG Length);

使用要点:

  1. 语义:防止内核使用期间用户并发释放该内存;返回不透明句柄,用毕EngUnsecureMem解锁。
  2. ReactOS 现状:只做ProbeForWrite可写性探测,恒返回假句柄(HANDLE)-1;真正的MmSecureVirtualMemory{// HACK!!!块短路,没有真正锁定
  3. 因此安全校验逻辑要自己写:若后续代码依赖"内存已被锁定",当前实现并不保证——需自行加锁或改用HackSecureVirtualMemory
  4. 返回值判 NULL/-1均表示失败(探测不通过)。

典型用法:

hSecure=EngSecureMem(pjBits,cjMaxBits);if(!hSecure)returnFALSE;/* 探测失败 */.../* 使用 pjBits */EngUnsecureMem(hSecure);/* 假句柄 -1 会被直接跳过 */

9.6 EngSecureMemForRead — 锁定只读用户内存

HANDLEEngSecureMemForRead(PVOID Address,ULONG Length);

使用要点:

  1. EngSecureMem同构,但做的是逐页读探测ProbeForRead+ 每页读一次),语义为"内核将只读该缓冲"。
  2. 同样返回假句柄-1,真实锁定未启用(HACK)。
  3. 使用场景:只读用户缓冲(如 GetBitmapBits 的输入参数校验)。

9.7 EngUnsecureMem — 解除锁定

VOIDEngUnsecureMem(HANDLE Mem);

使用要点:

  1. EngSecureMem/EngSecureMemForRead成对使用。
  2. 遇假句柄(HANDLE)-1直接返回(HACK 配套逻辑);真实句柄才调用MmUnsecureVirtualMemory
  3. 不要对 NULL 或未加锁的地址调用(无防护),成对性由调用方保证。

9.8 HackSecureVirtualMemory — 用 MDL 锁定用户缓冲并映射到系统空间

PVOIDHackSecureVirtualMemory(PVOID Address,SIZE_T Size,ULONG ProbeMode,PVOID*SafeAddress);
参数说明
Address/Size用户缓冲地址与大小
ProbeModePAGE_READONLY(读锁)/PAGE_READWRITE(读写锁)
SafeAddress输出:可被内核直接访问的系统空间映射地址

使用要点:

  1. 返回值为PMDL(作为后续释放的句柄),不是映射地址——映射地址在SafeAddress输出。
  2. 流程:IoAllocateMdlMmProbeAndLockPages(SEH 保护,失败返回 NULL)→MmGetSystemAddressForMdlSafe
  3. 用毕必须调用HackUnsecureVirtualMemory释放(解锁 + 释放 MDL)。
  4. 这是当前引擎里真正能锁定用户页的方案(优于 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);

使用要点:

  1. 参数是HackSecureVirtualMemory返回的 PMDL。
  2. 依次MmUnlockPages+IoFreeMdl,成对释放。
  3. 必须与 9.8 一一对应,过早释放会导致内核访问已解锁页面的错误。

本文档基于 ReactOS 源代码win32ss/gdi/eng/mem.c及关联模块分析(2026 年 8 月)

← 返回列表