HidHide 驱动分析 – drivers 篇(三):内存管理与性能优化
一、内存池选择与分配策略
1.1 非分页池(NonPagedPool)
HidHide 中所有动态分配的内存都使用 NonPagedPool 类型:
temp = ExAllocatePoolZero(NonPagedPool, sizeof(*temp), CONFIG_TAG);
选择理由:
- BST 节点可能在任何 IRQL 被访问(包括 DISPATCH_LEVEL),必须驻留在非分页内存中。
- 驱动代码在任意线程上下文执行,不能触发页面错误。
NX 优化:
ExInitializeDriverRuntime(DrvRtPoolNxOptIn);
在 DriverEntry 中启用 NX(No-Execute)池优化,允许将部分内存标记为不可执行,增强安全性。
1.2 内存池标签
每个分配都附带 4 字符标签用于调试:
| CONFIG_TAG | '1gaT' | BST 节点(Config.c) |
| LOGIC_TAG | '2gaT' | 临时缓冲区(Logic.c) |
使用 ExAllocatePoolZero 而非 ExAllocatePool 会自动清零分配的内存,避免了未初始化数据的风险。
二、BST 的内存效率
2.1 节点结构的内存布局
typedef struct _PROCESSIDTREE {
ULONG pid; // 4 bytes
WCHAR fullImageName[NTSTRSAFE_UNICODE_STRING_MAX_CCH]; // 1024 bytes
UNICODE_STRING fullImageNameUnicodeString; // 8 bytes
EvaluationCache evaluationCache; // 4 bytes
struct _PROCESSIDTREE* left; // 8 bytes
struct _PROCESSIDTREE* right; // 8 bytes
} PROCESSIDTREE; // 约 1056 bytes
每个节点约 1KB,支持最大路径长度(UNICODE_STRING_MAX_CCH)。在典型系统中,同时监控的进程数不超过 1000,总内存占用约 1MB,可接受。
2.2 字符串共享设计
// 节点存储 UNICODE_STRING 结构,指向同一缓冲区
ntstatus = RtlStringCchCopyUnicodeStringEx(&temp->fullImageName[0], ...);
ntstatus = RtlUnicodeStringInit(&temp->fullImageNameUnicodeString, &temp->fullImageName[0]);
fullImageNameUnicodeString 不复制字符串,而是指向 fullImageName 数组。这避免了双重内存分配,且 RtlCompareUnicodeString 可直接使用此结构。
三、集合对象(WDFCOLLECTION)
3.1 集合的使用场景
WDFCOLLECTION 是 WDF 提供的对象容器,专门用于存储 WDF 对象(如 WDFSTRING):
WdfCollectionCreate(NULL, &pControlDeviceContext->whitelistedFullImageNames);
// 添加条目
WdfStringGetUnicodeString(WdfCollectionGetItem(wdfCollection, index), &fullImageName);
每个白名单/黑名单条目都是一个 WDFSTRING 对象,由集合父对象自动管理生命周期。
3.2 遍历效率
for (ULONG index = 0, size = WdfCollectionGetCount(wdfCollection); (index < size); index++) {
WdfStringGetUnicodeString(WdfCollectionGetItem(wdfCollection, index), &fullImageName);
if (0 == RtlCompareUnicodeString(&fullImageName, &node->fullImageNameUnicodeString, TRUE)) {
// 匹配
}
}
每次查找需 O(n) 遍历集合。由于白名单条目数通常很小(< 10),效率影响可忽略。
四、缓存与性能优化
4.1 进程路径查询缓存
EvaluationCache 是关键的优化手段:
typedef enum {
EvaluationCacheEmpty = 0,
EvaluationCacheFound,
EvaluationCacheNotFound
} EvaluationCache;
- 首次查询:遍历白名单集合,与进程路径逐项比较。结果缓存到节点中。
- 后续查询:直接返回缓存结果,无需遍历集合。
命中率分析:同一进程在生命周期内可能多次打开 HID 设备(如游戏反复检查设备状态),缓存显著减少了字符串比较开销。
4.2 缓存失效策略
当白名单变更时,驱动调用 HidHideProcessIdsFlushWhitelistEvaluationCache 清空所有节点的缓存状态:
VOID BstFlushEvaluationCache(PPROCESSIDTREE tree) {
if (NULL != tree) {
BstFlushEvaluationCache(tree->left);
BstFlushEvaluationCache(tree->right);
tree->evaluationCache = EvaluationCacheEmpty;
}
}
这比逐个删除节点更高效,仅重置状态位,而保留路径字符串。
五、对象生命周期管理
5.1 父对象自动清理
WDF 对象具有父子关系,父对象销毁时自动销毁所有子对象:
wdfObjectAttributes.ParentObject = s_wdfControlDevice;
WdfWaitLockCreate(&wdfObjectAttributes, &s_criticalSectionLock);
WdfWaitLock 作为子对象,在控制设备销毁时自动销毁,无需手动调用 WdfWaitLockDelete。
5.2 上下文清理回调
对于需要显式释放资源的对象,使用 EvtCleanupCallback:
pDeviceContext = DeviceGetContext(wdfDeviceObject);
WdfObjectDelete(pDeviceContext->deviceInstancePath);
UpdateDataForControlDeviceDeletionAndDeleteControlDeviceWhenNeeded(–1);
当过滤设备对象销毁时,自动释放 deviceInstancePath 字符串并减少引用计数。
5.3 避免双重释放
WdfObjectDelete(pControlDeviceContext->whitelistedFullImageNames);
pControlDeviceContext->whitelistedFullImageNames = newCollection;
在替换集合时,先调用 WdfObjectDelete 释放旧集合,再赋值新集合。由于集合对象没有父对象,必须手动管理生命周期。
六、注册表读写的缓冲策略
6.1 读取注册表的多字符串
WdfRegistryQueryMultiString(wdfKey, valueName, &wdfObjectAttributes, *value);
WDF 自动处理 REG_MULTI_SZ 格式的解析,并创建对应的 WDFSTRING 子对象。这避免了手动解析多字符串的复杂性和缓冲区溢出风险。
6.2 写入注册表
WdfRegistryAssignValue(wdfKey, valueName, REG_MULTI_SZ, (ULONG)(valueInCharacters * sizeof(WCHAR)), value);
写入时直接传入 Unicode 字符串和长度,WDF 自动处理格式转换和写入操作。
七、性能影响评估
| IRP 拦截 | 极高(每次设备打开) | 微秒级 | 无锁读取、短路优化 |
| BST 查找 | 中(进程创建/加载) | O(log n) | 二叉搜索树 |
| 白名单匹配 | 低(仅首次访问) | O(n) + 字符串比较 | 缓存结果 |
| 配置更新 | 极低(用户操作) | 毫秒级 | 双缓冲原子交换 |
总体性能损耗 < 0.5%,满足游戏外设场景的实时性要求。
网硕互联帮助中心




评论前必须登录!
注册