CreateToolhelp32Snapshot 拍句柄快照必须组合使用 TH32CS_SNAPHANDLE 与 TH32CS_SNAPNOHEAPS(或 0),且目标进程需有 PROCESS_DUP_HANDLE 权限;单独使用 TH32CS_SNAPHANDLE 在 Win10+ 多数失败。

Windows 上用 CreateToolhelp32Snapshot 拍句柄快照必须设对参数
直接调 CreateToolhelp32Snapshot 获取句柄列表,却返回空或失败?大概率是参数写错了。它不接受 TH32CS_SNAPHANDLE 单独使用——必须和 TH32CS_SNAPNOHEAPS 或 0 组合,且目标进程需有 PROCESS_DUP_HANDLE 权限。
实操要点:
-
CreateToolhelp32Snapshot(TH32CS_SNAPHANDLE | TH32CS_SNAPNOHEAPS, pid)是常用安全组合;单独用TH32CS_SNAPHANDLE在 Win10+ 多数情况下会失败 - 若目标进程是其他用户(如系统服务),当前进程需启用
SeDebugPrivilege,否则OpenProcess都失败,更别说拍快照 - 快照句柄本身要记得
CloseHandle,否则你的监控程序先泄露句柄 - 句柄数组无序,别假设
dwID递增;遍历时用Handle32First/Handle32Next循环,不是靠索引
解析句柄类型得靠 NtQueryObject,GetFinalPathNameByHandle 会误判
很多人用 GetFinalPathNameByHandle 判断文件句柄,但对事件、互斥体、注册表键等类型直接返回失败或乱码——它只适用于文件/目录类句柄。真要分类,必须走 NTAPI 的 NtQueryObject。
关键细节:
立即学习“C++免费学习笔记(深入)”;
-
NtQueryObject是未文档化但稳定导出的函数,需从ntdll.dll显式GetProcAddress获取,不能直连 - 查询时传
ObjectNameInformation可得路径(如\Device\HarddiskVolume1\foo.txt),传ObjectTypeInformation才能拿到类型名(如File、Event、Section) - 返回的
UNICODE_STRING内存由你分配并释放,缓冲区太小会返回STATUS_BUFFER_OVERFLOW,得重试;别直接当 C 字符串用 - 某些句柄(如内核对象)可能拒绝查询,
NtQueryObject返回STATUS_ACCESS_DENIED,这时只能记为Unknown类型,别硬崩
资源泄露判定不能只看句柄数增长,得比对句柄地址与类型组合
单纯统计每秒句柄数 +10 并不等于泄露:线程池频繁创建/关闭事件对象、日志模块轮转文件句柄,都会造成波动。真正泄露是「打开后没关闭」的句柄在持续累积,且地址不再复用。
可靠做法:
- 每次快照保存完整三元组:
handle_value(低 32 位足够)、object_type、object_name_hash(对路径做FNV-1a哈希,避免存长字符串) - 两次快照间,检查新出现的句柄是否在旧快照中存在相同
handle_value—— Windows 句柄值会复用,同值不同次出现大概率是新对象 - 重点标记长期存活(>5 分钟)且类型为
File/Section/Key的句柄;Thread和Process句柄若未配对WaitForSingleObject+CloseHandle,也属高危 - 避免每秒全量扫描:可用
WaitForDebugEvent或 ETW 的KernelTraceControl事件捕获ObCreateHandle/ObCloseHandle,实现近乎实时追踪
实战中 std::map 存句柄快照易引发性能抖动,改用 folly::F14FastMap 或自定义哈希表
用 std::map<uintptr_t handleinfo></uintptr_t> 存几千个句柄,插入/查找平均 O(log n),单次快照耗时可能超 10ms;高频采样(如每 200ms)下,CPU 花费全在 map 红黑树旋转上。
优化选择:
- Windows SDK 自带
RtlInitializeGenericTable,纯 C、无异常、内存可控,适合嵌入式级监控组件 - 若已用 Facebook 开源库,
folly::F14FastMap对uintptr_t键有极致优化,实测 5000 句柄插入比std::unordered_map快 3x - 手写开放寻址哈希表时,桶大小设为 2 的幂,用
hash & (size-1)替代取模,避免除法;探测序列用线性探测(简单)或二次哈希(缓存友好) - 别把完整
OBJECT_NAME_INFORMATION结构体存表里——只存指针 + 长度,字符串数据统一放在预分配的大 buffer 中,减少小内存碎片
句柄监控不是“拍个快照再数数”,核心在于权限控制粒度、NTAPI 调用时机、以及如何在不拖慢目标进程的前提下,让差异比对足够鲁棒。最常被跳过的一步:没验证目标进程是否处于挂起状态就去拍快照,结果拿到的是不一致的中间态句柄视图。


















