4. 内核内存管理:内存分配与释放、分页与非分页内存、内存池标签、内存泄漏检测
驱动开发里,内存管理是基本功。说句实话,用户态程序里内存泄漏了,顶多进程退出时系统帮你回收。但在内核里,你泄漏一次,系统可能就蓝给你看。我早年刚转内核开发时,就因为一个内存泄漏,让测试机跑了一周后死机,被老大骂得狗血淋头。从那以后,我对内存管理就格外上心。
这一章,咱们就把内核内存管理的几个核心点掰开揉碎讲清楚。包括怎么分配、怎么释放、分页与非分页内存的区别、内存池标签怎么用,以及如何检测泄漏。
4.1 分页内存与非分页内存
先搞清楚这两个概念。这是内核开发里最基础、也最容易踩坑的地方。
分页内存(Paged Pool):说白了就是可以被换出到磁盘的内存。系统内存紧张时,这部分数据会被写到页面文件里。优点是省物理内存,缺点是你不能在DISPATCH_LEVEL及以上的IRQL级别访问它。为什么?因为访问时如果发生缺页中断,系统会尝试从磁盘读回数据,而这个操作需要等待,在高IRQL下是不允许等待的。
非分页内存(Non-Paged Pool):永远驻留在物理内存中,不会被换出。访问它没有任何IRQL限制。代价就是占用宝贵的物理内存,不能释放给其他用途。
我个人习惯是:能用分页就用分页,但一旦涉及DPC、中断处理、自旋锁保护的临界区,必须用非分页。我曾经在一个项目的DPC例程里用了分页内存,结果系统在内存压力下频繁蓝屏,查了两天才定位到问题。
| 特性 | 分页内存 | 非分页内存 |
|---|---|---|
| 可被换出 | 是 | 否 |
| IRQL限制 | ≤ APC_LEVEL | 无限制 |
| 物理内存占用 | 按需 | 始终占用 |
| 分配函数 | ExAllocatePoolWithTag(PagedPool, ...) | ExAllocatePoolWithTag(NonPagedPool, ...) |
4.2 内存分配与释放
内核里分配内存,最常用的就是 ExAllocatePoolWithTag 和 ExFreePoolWithTag。Windows 10 1809之后,微软推荐用 ExAllocatePool2 和 ExAllocatePool3,但老代码里还是前者居多。咱们先讲经典的。
// 分配非分页内存,大小为 1024 字节,标签为 'TAG1'
PVOID pBuffer = ExAllocatePoolWithTag(NonPagedPool, 1024, 'TAG1');
if (pBuffer == NULL) {
// 分配失败处理
DbgPrint("内存分配失败!\n");
return STATUS_INSUFFICIENT_RESOURCES;
}
// 使用内存...
RtlZeroMemory(pBuffer, 1024);
// 释放内存
ExFreePoolWithTag(pBuffer, 'TAG1');
pBuffer = NULL; // 我习惯释放后立即置空,防止野指针
你想想看,如果忘了释放,或者释放了两次,后果都很严重。忘了释放就是内存泄漏,释放两次就是双重释放,系统可能直接蓝屏。我见过最离谱的案例,有人把同一个指针释放了三次,第三次时系统直接崩溃。
4.3 内存池标签
标签是什么?就是一个4字节的整数,通常用ASCII字符表示,比如 'TAG1'。它的作用是帮你追踪内存的来源。用 !poolused 或 !poolfind 调试命令时,你可以根据标签找到哪些内存是哪个驱动分配的。
我个人习惯用驱动名称缩写加序号,比如我的驱动叫 MyDrv,标签就用 'Md01'、'Md02' 这样。注意标签在内存中是倒序存储的,所以 'TAG1' 实际存的是 0x31474154。
// 分配时指定标签
PVOID pData = ExAllocatePoolWithTag(NonPagedPool, sizeof(MY_STRUCT), 'Md01');
// 释放时必须使用相同的标签
ExFreePoolWithTag(pData, 'Md01');
4.4 内存泄漏检测
说实话,内核内存泄漏比用户态难查得多。用户态有各种工具,内核里主要靠调试器和驱动验证器(Driver Verifier)。
方法一:Driver Verifier
这是微软官方提供的驱动验证工具。启用后,它会监控你的驱动内存操作,一旦发现泄漏或非法访问,立即触发蓝屏并生成dump文件。我建议开发阶段一直开着它。
// 在命令行启用 Driver Verifier
verifier.exe /flags 0x209BB /driver MyDriver.sys
常用的flags包括:
- 0x1:特殊内存池(Special Pool)—— 分配内存前后加保护页,检测越界
- 0x8:强制IRQL检查
- 0x200:内存泄漏检测
方法二:使用 !poolused 调试命令
在WinDbg里,你可以用这个命令查看所有内存池的使用情况。按标签分组,能看到每个标签分配了多少内存、有多少未释放。
kd> !poolused 2
Pool Used:
NonPaged Paged
Tag Allocs Used Allocs Used
Md01 5 512 0 0
Md02 3 256 0 0
如果某个标签的 Allocs 和 Used 一直在增长,而你的驱动又没有对应的释放逻辑,那基本就是泄漏了。
方法三:手动跟踪
我自己的项目里,会写一个简单的内存跟踪模块。每次分配时记录调用栈和大小,释放时删除记录。驱动卸载时,打印所有未释放的记录。虽然有点笨,但很有效。
// 伪代码示例
typedef struct _MEM_TRACK {
PVOID Address;
SIZE_T Size;
ULONG Tag;
char File[64];
ULONG Line;
} MEM_TRACK, *PMEM_TRACK;
// 分配时记录
PMEM_TRACK Track = ExAllocatePoolWithTag(NonPagedPool, sizeof(MEM_TRACK), 'Trk1');
Track->Address = pBuffer;
Track->Size = 1024;
Track->Tag = 'TAG1';
// 插入到全局链表...
// 释放时删除
// 从链表中找到对应记录,删除并释放Track结构
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到,内存分配从选择分页还是非分页开始,然后通过带标签的分配函数获取内存,使用完毕后必须用相同标签释放。泄漏检测则从Driver Verifier、调试命令和手动跟踪三个维度入手。
4.6 总结
内核内存管理,说白了就是三件事:选对类型、带好标签、记得释放。分页和非分页的选择决定了你的代码能在什么IRQL下运行。内存池标签是调试时的救命稻草。泄漏检测则靠工具和好习惯。
我个人建议,每个驱动项目从一开始就建立内存跟踪机制。哪怕只是简单的链表记录,也比出了问题再查要省事得多。毕竟,内核里没有垃圾回收,全靠开发者自觉。