工业相机控制应用运行一个月后突然崩溃时(下篇) - 什么是 Application Verifier 与异常路径测试基础设施的做法
· 小村 豪 · Windows 开发, 故障排查, 工业相机, Application Verifier, 异常路径测试, handle leak
Application Verifier 是当你想把 Windows 原生代码与 Win32 边界上的异常「提前炸出来」时,相当有力的工具。 特别是想测 handle 异常、heap 破坏、低资源时的 failure path,这些在正常路径测试里根本看不到的问题,它能很快把它们浮上台面。
上篇 工业相机控制应用运行一个月后突然崩溃时(上篇) - handle 泄漏的查找方法与长时间运行用的日志设计 整理了一个案例:长时间运行后崩溃的控制应用,原因其实是 handle 泄漏。 但光是把日志加强,只完成了一半。真正想要的是 如果未来又因预料之外的程序错误出现 memory leak、handle leak、中途失败、释放漏掉,当下能够「知道发生了什么」,而且要能事先验证得到。
这时用的,就是 Application Verifier。 它是在 Windows 原生代码/Win32 边界上运行时加上检查与 fault injection 的工具。实务上最有价值的是:不用真的把机器内存榨干,就能提前再现出「像内存不足/资源不足」的坏法。
下篇整理 Application Verifier 是什么、能做什么、要怎么把它编入异常路径测试基础设施,并放在工业相机控制应用的脉络下讨论。
1. 先下结论(一句话)
- Application Verifier 是在 Windows 的 unmanaged / 原生边界 上,提高运行时误用检测能力的工具
- 比「找 bug」更有用的是,它能 提前触发平常不会踩到的异常路径
Handles能检测 invalid handle;Heaps能把 heap 破坏显性化;Low Resource Simulation能对内存/资源不足的场景做 fault injection- 常驻 EXE 的泄漏排查,只靠 Application Verifier 是自讨苦吃,实务上必须与
Handle Count与 resource lifecycle 的自研日志搭配 - 异常路径测试基础设施上,正常路径的 verifier run 与 fault injection run 要分开跑,比较好读
- 想测 DLL 时,Application Verifier 要启用的是「实际加载该 DLL 的 测试 EXE」
简单说,Application Verifier 是 把 Windows native / Win32 周边的「难缠 bug」强行拽到台面上的工具。 对设备控制类应用这种 native SDK、P/Invoke、Win32 API 混在一起的世界,它特别契合。
2. 什么是 Application Verifier
2.1. 一句话
Application Verifier 是针对 Windows user-mode 应用的 运行时检验工具。 它会监控运行中应用对 OS API 的使用与资源处理方式,找出可疑用法,也能故意注入失败。
和「静态分析」、「单元测试」不同,它看的是 实际走到这段代码时会怎么坏。 所以特别适合把平常功能测试看不到的 failure path 逼出来。
flowchart LR
A[测试 harness] --> B[控制应用 / SDK 包装]
B --> C[Application Verifier]
C --> D[Win32 API / native DLL / OS 资源]
C --> E[verifier stop]
C --> F[debugger output]
C --> G[AppVerifier logs]
B --> H[自研 structured log]
2.2. 什么场合有效
特别有效的情境:
- 在调用 native DLL 或相机 SDK
- 涉及 P/Invoke 或 COM 跨界
- 大量直接或间接使用 handle、heap、lock、虚拟内存
- 一般正常路径不会崩,但看起来异常路径的生命周期管理会崩
- 比起「崩掉」,先出现的是「偶尔返回奇怪的失败」
反过来说,要追纯 managed 世界 object graph 的工具 它并不是。 所以 C# 应用只要有厚重的 native SDK 或 Win32 边界,它仍很有用,但不是「纯 managed heap leak 靠它一招解决」的意思。
2.3. 有什么好处
实务上的好处大致 3 条:
- 能在很早阶段挡下原生边界的误用
- invalid handle
- heap corruption
- lock misuse
- 虚拟内存 API 误用等
- 能提前触发低资源时才会出现的坏法
- 类似
malloc的调用偶尔失败 CreateEvent/CreateFile偶尔失败VirtualAlloc失败
- 类似
- 搭配调试器好追
!avrf!htrace!heap -p -a- verifier stop 的日志
设备控制类应用最棘手的是「异常路径不知道到底发生了什么」。 Application Verifier 能明显降低那种「搞不懂」的感觉。
3. Application Verifier 能做什么
3.1. Basics:Handles / Heaps / Locks / Memory / TLS 等
Application Verifier 的基本组合叫 Basics。
实务常用的检查都在里面。
| 层级 | 看什么 | 在本次脉络中的用途 |
|---|---|---|
Handles |
使用 invalid handle | 是否误踩已 close 或坏掉的 handle |
Heaps |
heap corruption | 在 native SDK 边界抓 buffer 破坏、use-after-free |
Leak |
DLL unload 时仍未释放的资源 | 短命 harness 测试或含 unload 的场景 |
Locks / SRWLock |
锁误用 | 确认 reconnect 与 shutdown 的竞态 |
Memory |
VirtualAlloc / MapViewOfFile 等误用 |
大 buffer 或共享内存周边的异常 |
TLS |
Thread Local Storage API 误用 | 线程边界复杂的 native 代码保险 |
Threadpool |
threadpool API 或 worker state 一致性 | callback 或异步较多时的辅助 |
关键是 不是「崩了之后再来读」,而是「看到可疑用法当下就停下」。 对长时间运行型的 bug,这种提前很有效。
3.2. Low Resource Simulation:提前触发内存/资源不足
实务上特别好用的就是这个。 不用真的把 RAM 吃光,就能重现「像内存/资源不足」的现象。
思路很单纯:
- 对某个 API 调用
- 以一定概率
- 故意让它失败
于是那些平常走不到的 error path 就能走一遍。
例如能故意触发:
HeapAlloc/VirtualAlloc失败CreateFile失败CreateEvent失败MapViewOfFile失败SysAllocString等 OLE/COM 分配失败
比起去把整台机器搞到真的爆内存,这种做法轻松得多。 而且可以 只针对特定 DLL 注入 fault。设备控制应用里自研包装与厂商 SDK 并存的结构中,相当实用。
3.3. Page Heap 与调试器
要追 heap 破坏,Heaps + page heap 的组合很强。
特别是 full page heap 用 guard page,坏的那一刻就能停,这点很有利。
但这个组合负担相当重,适合锁定再现场景,在调试器下运行,而不是拿来做长时间全面扫描。
实务上这样分比较合理:
- 先用
Basics大范围打底 - 如果怀疑 heap,就用 full page heap
- 太重时降回 light page heap
- 拟生产长时间测试,交给自研日志
也就是说 AppVerifier 不是万能法宝,是依场合换刀刃的工具。
3.4. !avrf / !htrace / 日志
Application Verifier 不只会 stop 完就收工。 调试器扩展与日志都能帮你追踪发生了什么。
!avrf- 查看当前 verifier 设置与正在发生的 stop
!htrace- 看 handle 的 open / close / invalid reference 堆栈
!heap -p -a- 搭配 page heap,追坏掉的 heap block
- AppVerifier 日志
- 记录 stop 发生的信息
特别是启用 Handles 时,handle tracing 会自动启用,相当实用。
可以回头查「这个 handle 在哪里 open、在哪里 close」。
4. 为何这次导入
4.1. 目的不只是「找 bug」
这次的目的,不是「靠 AppVerifier 找到 1 个 bug」。 更实务的说法是,我要确认下面几件事:
- 未来又有其他 failure path 漏了资源
- 日志能不能留下场景信息
- 配合调试器信息能不能追到底
- 是否会陷入「搞不懂发生什么」的状态
换句话说,我把它当成 检测器,也当成 观测基础设施的测试。
4.2. 重现「像内存不足」的现象
在开发机上真的去弄出内存不足很麻烦。 而且整台机器变不稳后,连测试本身都充满噪声。
所以改用 Low Resource Simulation 精准地踩到「在低资源状况下才会走的 failure path」。
这样可以回答像这种问题:
CreateEvent失败时,日志有没有留下cameraId与phase- 半途初始化完后,clean up 是否有跑
VirtualAlloc失败时 retry 会不会弄坏- 存储路径的
CreateFile失败时,handle 有没有回收
重点很关键:「触发异常」本身不是目的,「出了异常时能读懂坏法」才是目的。
4.3. handle 异常能不能追
上篇提到的 handle 泄漏也是同样的事。handle 相关问题的特征是 最后崩掉的地方与真正原因经常错位。
所以想验证的是:
- invalid handle stop 出现时,能不能用
!htrace追 open / close - 能否对应上自研日志的
resourceId/sessionId/phase - 失败后 handle count 会不会回落
- 用短命 harness 跑时,泄漏差异是否看得清楚
做到这一点,就能从「出了 bug」变成 「哪个职责的生命周期管理崩了」。
5. 怎么重现「像内存/资源不足」的现象
5.1. Low Resource Simulation 的思路
Low Resource Simulation 就是 fault injection。 与其真的重现低资源环境,不如 人为地插入低资源时常见的 API 失败。
所以用法也很明确:
- 确认 failure path 的善后
- 确认 retry / reconnect 的稳健性
- 确认成功/失败交错的初始化流程
- 确认「平常不会出的错」发生时日志还留不留
诀窍是 别从一开始就全打开。 把所有 fault 都打开,日志会爆,根本看不懂自己在看什么。
5.2. 可以让什么失败
Low Resource Simulation 典型可按概率让下面这些 API 失败:
| 类别 | 例 | 设备控制应用的例子 |
|---|---|---|
Heap_Alloc |
heap 分配 | 临时 buffer、图像 metadata、SDK wrapper 内部 |
Virtual_Alloc |
虚拟内存分配 | 大 frame buffer、ring buffer |
File |
CreateFile 等 |
存储路径或日志文件打开 |
Event |
CreateEvent 等 |
frame ready 通知、stop/reconnect 同步 |
MapView |
CreateMapView 等 |
共享内存或 memory mapped file |
Ole_Alloc |
SysAllocString 等 |
COM / OLE 边界 |
Wait |
WaitForXXX 系列 |
同步等待失败周边 |
Registry |
注册表访问 | 配置读写或驱动周边设置 |
实务上,与其一次全开, 从最贴近本次想看的 failure path 的开始打开,更有效。
5.3. 实务的应用方式
命令行示意大约这样:
appverif /verify CameraHarness.exe
appverif /verify CameraHarness.exe /faults
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 virtual_alloc=20000 file=20000 event=20000
appverif -query lowres -for CameraHarness.exe
思路:
- 先只开
Basics跑正常路径 - 再加
Low Resource Simulation跑 fault injection 版本 - 需要时,只给
file或event等想看的失败设置概率 - 只想针对特定 DLL,就把范围限制在那个 DLL
/faults 的快捷方式方便,但它主要是 以 OLE_ALLOC 与 HEAP_ALLOC 为中心。
想看 CreateFile 或 CreateEvent 的 failure path,最好写成 -enable lowres -with file=... event=...。
设备控制应用里,与其把 fault 撒在整个应用,不如锁在 camera wrapper 或存储路径 DLL,读起来清爽多了。
可以这样设计场景:
- reconnect 开始后立刻的
CreateEvent失败 - 存储开始时的
CreateFile失败 - 临时 buffer 分配失败
- COM 转换中的
SysAllocString失败 - 等待 API 的失败路径验证
这些场景正常路径测试几乎踩不到。 所以更要刻意让它踩到。
6. handle 异常怎么看
6.1. Handles 检查
handle 相关先用 Handles,这样可以让 invalid handle 使用更容易被检测到。
典型可挡下的错:
- 再次使用已 close 的 handle
- 传入坏掉的 handle 值
- 使用中途失败、未正确初始化的 handle
- lifetime 崩了,从别的线程去碰
长时间运行下看「偶尔才错」的东西,在 verifier 下反而会当下停下。 这种提前暴露真的很有帮助。
6.2. 用 !htrace 看 open / close 堆栈
Handles 的另一好处是 与 handle tracing 相性好。
windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC
!htrace 想看的东西,大致是:
- 这个 handle 在哪 open
- 在哪 close
- 有没有被当作 invalid handle 引用
- open 是不是比预期堆得更多
handle 泄漏或 misuse 的难处在于 最后失败的 API 不是真正的原因。
有 !htrace 就能拿到该 handle 的历程,具体得多。
6.3. 与自研日志怎么搭配
话虽如此,光有 Application Verifier 还不够。 尤其常驻 EXE 的泄漏排查单靠它是相当痛苦的。
实务上要一起用:
- 定期采样
Handle Count sessionIdresourceIdphase- create/open 与 close/dispose 的 lifecycle log
- verifier stop 时的 dump 与调试器输出
这样就能:
- heartbeat 里发现
Handle Count斜率异常 - 在 lifecycle log 找到
Create有、Close没有的 resource - 用 verifier run 提前暴露 invalid handle 或 misuse
- 用
!htrace看 open / close stack
四招并用,追踪变得明朗很多。
7. 构建异常路径测试基础设施
7.1. 把运行单元收敛到 harness
Application Verifier 无法对「正在运行」的 process 实时启用。 它是设置之后才启动。
而且设置会保留直到明确清除。 所以实务上,与其改正式主体,不如改成测试用 harness EXE 比较好处理。
示例结构:
flowchart LR
A[Scenario Runner] --> B[CameraHarness.exe]
B --> C[CameraSdkWrapper.dll]
C --> D[Vendor SDK]
B --> E[Structured Log]
B --> F[Dump / Debugger]
这样的好处:
- 1 个场景 1 个 process
- 泄漏差异清楚
- AppVerifier 的 ON/OFF 容易切换
- 要测 DLL 也可以通过 EXE 测
命令示意:
appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe
启用在启动前,解除要明确执行。 把它绑在 harness 上管理,比较不会忘了清除设置。
7.2. 分开测试项
异常路径测试基础设施别想一口气全做。 分成下面 3 条线比较好读:
- 正常路径 + Basics
- 不注入任何 fault
- 确认没有 verifier stop
- fault injection 类
Low Resource Simulation- 专门打
event/file/heap_alloc/virtual_alloc等
- heap 深挖类
Heaps- full page heap
- 在调试器下局部再现
分开之后, 「平常用法本身就坏」 与 「低资源时才坏」 不会混在一起。
尤其有无 fault injection,会让程序实际走过的 code path 差很多。 所以 有 fault 的 run 与 没有 fault 的 run 都要跑。
7.3. 要收集的数据
最少应收集:
| 种类 | 想要的内容 |
|---|---|
| 应用日志 | cameraId、sessionId、phase、handleCount、error code |
| process 状态 | Handle Count、Private Bytes、Thread Count |
| 调试器信息 | !avrf、!htrace、必要时 !heap -p -a |
| dump | verifier stop 或异常结束时 |
| AppVerifier 日志 | stop 记录,必要时汇出为 XML 统计 |
AppVerifier 日志可以汇出为 XML 统计。 但光看它通常收不了尾,要和自研日志一起读比较实用。
日志多本身不代表好,事后因果连得上才重要。
7.4. 合格条件
「没崩」单独拿来当合格条件太弱。本次的合格条件至少:
- 正常路径 + Basics 下无 verifier stop
- 有 fault injection 时,预期的失败都留在日志里
- 半途初始化的资源能被清理干净
- reconnect / retry 后
Handle Count回到 baseline 附近 - 有 verifier stop 时,能靠
sessionId/phase/ stack 追查 - 不出现「搞不懂发生了什么」的失败
这里最重要:把「不会坏」与「坏了能追」当两个独立指标看。
7.5. 注意事项
Application Verifier 很好用,但不是魔法:
- 没走到的 code path 它也无从检验
- full page heap 负担重
- 第三方 SDK 也可能触发 stop
- 有无 fault injection,程序实际走的 path 差很多
- 纯 managed heap leak 不能只靠它
定位上:
- 长期斜率 靠自研日志与 counters
- 原生边界误用 靠 Application Verifier
- 异常时因果还原 靠 structured log + dump + debugger
这样分工最实务。
8. 粗略的分流
- 怀疑 invalid handle 或 double close
Handles+!htrace
- 怀疑 heap corruption / use-after-free
Heaps+ full page heap +!heap -p -a
- 想重现「内存/资源不足」的现象
Low Resource Simulation
- 长时间运行慢慢坏掉
- 先从自研
Handle Count/Private Bytes/ lifecycle log 下手
- 先从自研
- 想测 DLL
- 对调用该 DLL 的 harness EXE 启用 Application Verifier
一开始全开,通常会变成日志雾。 从最贴近想看的 failure path 的刀刃先下手比较清楚。
9. 总结
要记住的重点:
Application Verifier 的定位:
- Windows 原生/Win32 边界的 runtime verifier
- 可用 Handles / Heaps / Locks / Memory / TLS / Low Resource Simulation 等
- 能提前把平常踩不到的 failure path 拖出来跑
本次有用的点:
- handle 异常时用
!htrace追踪很顺 - 不用弄垮机器,就能再现像内存/资源不足
- 可验证自研日志在异常时是否真的能救场
实务上怎么用:
- 正常路径 + Basics 与 fault injection 类要分开跑
- 备好 harness EXE,以短命 process 跑场景
- 搭配自研日志、dump、调试器信息
- 长期泄漏的斜率靠自研 counters 观察
Application Verifier 是 「不被动等难得一遇的异常」,而是「主动把它迎过来」的工具。
设备控制应用不只要「不坏」,还要 坏了能解释发生了什么。 从这个意义上说,它是相当实务的工具。
上篇:工业相机控制应用运行一个月后突然崩溃时(上篇) - handle 泄漏的查找方法与长时间运行用的日志设计
10. 参考资料
- 上篇:工业相机控制应用运行一个月后突然崩溃时(上篇) - handle 泄漏的查找方法与长时间运行用的日志设计
- Application Verifier - Overview
- Application Verifier - Testing Applications
- Application Verifier - Tests within Application Verifier
- Application Verifier - Debugging Application Verifier Stops
- Application Verifier - Features
- !htrace (WinDbg)
- GetProcessHandleCount function (processthreadsapi.h)
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 什么时候需要管理员权限 - UAC、保护区域与设计上的辨别方式
从边界与存储位置的角度,整理 Windows 什么时候真正需要管理员权限:UAC、保护区域、HKLM、服务、驱动、防火墙。同时说明 per-user 与 per-machine 的差异,以及把管理员处理拆成独立 EXE、服务或任务的设计取舍,帮读者判断该不该提升权限。
Windows Forms、WPF、WinUI 该怎么选 - 新建项目、存量资产、发布、UI 表现力判断表
从存量资产的规模、界面是表单为主还是需要表现力、现代 Windows UI 是否是产品刚需、发布与运维怎么落地这四个角度,整理 WinForms、WPF、WinUI 该怎么选的判断表,并提醒只想用 Windows App SDK 不必全面迁移到 WinUI。
Windows应用的crash dump收集入门 - 先搞清楚 WER / ProcDump / WinDbg怎么分工
本文整理在 Windows 应用追查难以复现的 crash 时,要先以 WER LocalDumps 按应用单独配置为起手式,再依现场状况追加 ProcDump,最后才考虑 MiniDumpWriteDump 自研收集的决策顺序。读完能理解 mini 与 full dump...
Windows 应用程序不要把敏感信息以明文存进配置文件的最佳实践
本文整理 Windows 桌面应用保存连接凭证或 API Token 时的实务做法,说明为什么 DPAPI 与 ProtectedData 比明文或自制加密更能切断「文件外泄即等于敏感信息外泄」这条链条,并对比 CurrentUser 与 LocalMachine 的适用场...
将 .NET Framework 迁移到 .NET 之前该确认的事 - 动手前就决定成败的实战检查清单
整理将 .NET Framework 业务应用程序迁移到现代 .NET 之前必须先盘点的论点。涵盖落地版本、Windows 专用前提的取舍、不再支持的 API、共享库切分方式、第三方组件、运维与 CI/CD,帮助在动手前厘清范围并降低迁移风险。
常见问题
汇总了咨询这一主题时常见的问题。
- Application Verifier 是什么?
- Application Verifier 是针对 Windows user-mode 应用的运行时检验工具。它会监控运行中应用对 OS API 的使用与资源处理方式,找出可疑用法,也能故意注入失败(fault injection)。和静态分析或单元测试不同,它看的是实际走到这段代码时会怎么坏,特别适合把正常路径功能测试看不到的 failure path 提前逼出来。对 native SDK、P/Invoke、Win32 API 混在一起的设备控制类应用尤其契合。
- Low Resource Simulation 有什么用?
- 它能不用真的把机器内存榨干,就提前重现「像内存/资源不足」的坏法。原理是对 HeapAlloc、VirtualAlloc、CreateFile、CreateEvent 等 API 调用,以一定概率故意让它失败,让平常走不到的 error path 走一遍。也可以只针对特定 DLL 注入 fault。诀窍是别从一开始就全打开,从最贴近想看的 failure path 的项目开始,否则日志会爆。
- Application Verifier 怎么检测 handle 异常?
- 启用 Handles 检查后,再次使用已 close 的 handle、传入坏掉的 handle 值等 invalid handle 使用会当下停止(verifier stop)。启用 Handles 时 handle tracing 会自动启用,可在调试器中用 !htrace 查看该 handle 在哪里 open、在哪里 close。不过常驻 EXE 的泄漏排查光靠它是自讨苦吃,实务上必须搭配 Handle Count 与 resource lifecycle 的自研日志。
- 怎么把 Application Verifier 编入测试基础设施?
- Application Verifier 无法对正在运行的 process 实时启用,设置也会保留直到明确清除,所以实务上准备测试用 harness EXE、1 个场景 1 个 process 比较好处理。测试项分成三条线:正常路径 + Basics(确认无 verifier stop)、fault injection 类(Low Resource Simulation)、heap 深挖类(full page heap 在调试器下局部再现)。想测 DLL 时,要对实际加载该 DLL 的测试 EXE 启用。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。