HidHide 驱动分析 - drivers 篇(四):错误处理与诊断基础设施
一、错误处理哲学
HidHide 的错误处理遵循防御性编程原则:
- 所有 API 返回值必须检查:任何 NTSTATUS 失败都被记录并向上传播。
- 失败即返回:不尝试在错误状态中继续执行,避免级联故障。
- 详细日志:每次错误都记录文件名、行号、函数名和错误码,便于事后分析。
- 优雅降级:某些非致命错误(如注册表键值不存在)被转换为成功状态,使用默认值继续运行。
二、NTSTATUS 返回值的语义
HidHide 使用 NTSTATUS 码不仅报告错误,还传递逻辑状态信息:
| NTSTATUS 码 | 语义 | 使用场景 |
|---|---|---|
STATUS_SUCCESS |
操作成功,无额外信息 | 默认成功 |
STATUS_PROCESS_IN_JOB |
进程在白名单中 | Whitelisted 返回 |
STATUS_PROCESS_NOT_IN_JOB |
进程不在白名单中 | Whitelisted 返回 |
STATUS_OBJECT_NAME_NOT_FOUND |
注册表键值不存在 | 使用默认值 |
STATUS_ALREADY_INITIALIZED |
BST 节点已存在 | 重复插入检测 |
STATUS_ACCESS_DENIED |
设备访问被拒绝 | 向上层应用返回 |
STATUS_INVALID_PARAMETER |
参数无效 | IOCTL 验证失败 |
这种设计让调用者可以通过统一的 NTSTATUS 接口获知详细结果,而无需额外输出参数。
三、日志框架的实现
3.1 日志级别与宏
| 日志宏 | 用途 | 启用以触发 ETW 关键字 |
|---|---|---|
TRACE_ALWAYS |
始终记录的关键事件 | EtwEventTraceAlways |
TRACE_PERFORMANCE |
性能追踪(函数进出) | EtwEventTracePerformance |
TRACE_DETAILED |
详细调试信息 | EtwEventTraceDetailed |
LOG_AND_RETURN_NTSTATUS |
错误记录 + 返回 | EtwEventLogException |
cpp
#define TRACE_ALWAYS(message) { \
TraceEvent(&__FILE__[ProjectDirLength], __LINE__, __FUNCTION__, &EtwEventTraceAlways, message, ""); \
if (MCGEN_EVENT_ENABLED(EtwEventTraceAlways) && MCGEN_EVENT_ENABLED(EtwEventTraceDebugging)) \
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "%s(%d) %s\n", ...); \
}
ProjectDirLength 是编译期常量,用于裁剪源文件绝对路径,使日志输出更简洁。
3.2 ETW 事件写入
LogWriteTransfer 是底层写入函数,将事件同时写入日志提供者和追踪提供者:
cpp
NTSTATUS LogWriteTransfer(..., PCEVENT_DESCRIPTOR eventDescriptor, ...) {
EventDataDescCreate(&eventDataDescriptor[1], fileName, (ULONG)(strlen(fileName) + 1));
EventDataDescCreate(&eventDataDescriptor[2], &lineNumber, sizeof(unsigned int));
EventDataDescCreate(&eventDataDescriptor[3], functionName, (ULONG)(strlen(functionName) + 1));
EventDataDescCreate(&eventDataDescriptor[4], messageW, (ULONG)(wcslen(messageW) + 1) * sizeof(WCHAR));
EventDataDescCreate(&eventDataDescriptor[5], messageA, (ULONG)(strlen(messageA) + 1));
return MCGEN_EVENTWRITETRANSFER(context->RegistrationHandle, eventDescriptor, ...);
}
3.3 事件描述符的精简
对于追踪提供者,LogEvent 会使用精简的事件描述符:
cpp
eventDescriptor.Id = event->Id;
eventDescriptor.Level = event->Level;
eventDescriptor.Task = EtwTaskLog;
eventDescriptor.Keyword = EtwEventTraceAlways.Keyword;
这确保了日志事件以一致的格式出现在追踪日志中。
四、错误记录的标准模式
4.1 内存分配错误处理
cpp
temp = ExAllocatePoolZero(NonPagedPool, sizeof(*temp), CONFIG_TAG);
if (NULL == temp) LOG_AND_RETURN_NTSTATUS(L"ExAllocatePoolWithTag", STATUS_NO_MEMORY);
分配失败时记录分配位置和错误码,立即返回 STATUS_NO_MEMORY。
4.2 注册表操作错误处理
cpp
ntstatus = WdfRegistryQueryULong(wdfKey, valueName, &unsignedLong);
if (!NT_SUCCESS(ntstatus)) {
if (STATUS_OBJECT_NAME_NOT_FOUND == ntstatus) return (STATUS_PROCESS_NOT_IN_JOB);
WdfRegistryClose(wdfKey);
LOG_AND_RETURN_NTSTATUS(L"WdfRegistryQueryULong", ntstatus);
}
键值不存在被视为正常情况(使用默认值),其他错误则记录并返回。
4.3 进程/镜像回调错误处理
PsSetCreateProcessNotifyRoutine 失败时,驱动加载将中止:
cpp
ntstatus = PsSetCreateProcessNotifyRoutine(OnSystemProcessChange, FALSE);
if (!NT_SUCCESS(ntstatus)) LOG_AND_RETURN_NTSTATUS(L"PsSetCreateProcessNotifyRoutine", ntstatus);
进程追踪是核心功能,失败后驱动无法正确工作,因此选择加载失败。
五、调试支持
5.1 DbgPrintEx 集成
所有 TRACE_XXX 宏在启用调试关键字时,会调用 DbgPrintEx 输出到内核调试器:
cpp
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "%s(%d) %s\n", fileName, lineNumber, functionName);
开发者可在 WinDbg 中实时看到驱动执行流程,无需借助 ETW 查看器。
5.2 自检输出的调试信息
HidHideVerifyInternalConsistency 中的失败会通过 TRACE_ALWAYS 输出具体哪一步失败:
cpp
if (!NT_SUCCESS(ntstatus)) {
TRACE_ALWAYS(L"Adding a unique node should succeed");
break;
}
这帮助开发者快速定位 BST 算法的实现问题。
六、驱动验证器(Driver Verifier)兼容性
HidHide 的设计充分考虑了 Driver Verifier 的检查:
- 正确的 IRQL 标注 :所有函数声明了
_IRQL_requires_same_和_IRQL_requires_max_,帮助 Verifier 检测 IRQL 违规。 - 无分页内存访问 :所有分配使用
NonPagedPool,避免在 DISPATCH_LEVEL 访问分页内存。 - 锁使用规范 :
WdfWaitLock的获取和释放成对出现,且不在 DISPATCH_LEVEL 持有。 - 对象生命周期:WDF 对象通过父子关系自动管理,减少了内存泄漏风险。
七、诊断信息汇总
| 信息类型 | 输出方式 | 适用场景 |
|---|---|---|
| 错误详情 | ETW + 事件日志 | 生产环境故障排查 |
| 性能追踪 | ETW(Detailed/Performance) | 性能瓶颈分析 |
| 调试输出 | DbgPrintEx | 驱动开发/调试 |
| 版本信息 | ETW Started 事件 | 确认部署版本 |
| 配置变更 | ETW Enabled/Disabled | 审计配置历史 |
这种多层次的诊断架构确保驱动在开发、测试和生产环境中都能提供足够的可见性。