工业相机控制应用运行一个月后突然崩溃时(下篇) - 什么是 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 runfault 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 条:

  1. 能在很早阶段挡下原生边界的误用
    • invalid handle
    • heap corruption
    • lock misuse
    • 虚拟内存 API 误用等
  2. 能提前触发低资源时才会出现的坏法
    • 类似 malloc 的调用偶尔失败
    • CreateEvent / CreateFile 偶尔失败
    • VirtualAlloc 失败
  3. 搭配调试器好追
    • !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 失败时,日志有没有留下 cameraIdphase
  • 半途初始化完后,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

思路:

  1. 先只开 Basics 跑正常路径
  2. 再加 Low Resource Simulation 跑 fault injection 版本
  3. 需要时,只给 fileevent 等想看的失败设置概率
  4. 只想针对特定 DLL,就把范围限制在那个 DLL

/faults 的快捷方式方便,但它主要是 OLE_ALLOCHEAP_ALLOC 为中心。 想看 CreateFileCreateEvent 的 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
  • sessionId
  • resourceId
  • phase
  • create/open 与 close/dispose 的 lifecycle log
  • verifier stop 时的 dump 与调试器输出

这样就能:

  1. heartbeat 里发现 Handle Count 斜率异常
  2. 在 lifecycle log 找到 Create 有、Close 没有的 resource
  3. 用 verifier run 提前暴露 invalid handle 或 misuse
  4. !htrace 看 open / close stack

四招并用,追踪变得明朗很多。

7. 构建异常路径测试基础设施

7.1. 把运行单元收敛到 harness

Application Verifier 无法对「正在运行」的 process 实时启用。 它是设置之后才启动。

而且设置会保留直到明确清除。 所以实务上,与其改正式主体,不如改成测试用 harness EXE 比较好处理。

示例结构:

Scenario RunnerCameraHarness.exeCameraSdkWrapper.dllVendor SDKStructured LogDump / Debugger

这样的好处:

  • 1 个场景 1 个 process
  • 泄漏差异清楚
  • AppVerifier 的 ON/OFF 容易切换
  • 要测 DLL 也可以通过 EXE 测

命令示意:

appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe

启用在启动前,解除要明确执行。 把它绑在 harness 上管理,比较不会忘了清除设置。

7.2. 分开测试项

异常路径测试基础设施别想一口气全做。 分成下面 3 条线比较好读:

  1. 正常路径 + Basics
    • 不注入任何 fault
    • 确认没有 verifier stop
  2. fault injection 类
    • Low Resource Simulation
    • 专门打 event / file / heap_alloc / virtual_alloc
  3. heap 深挖类
    • Heaps
    • full page heap
    • 在调试器下局部再现

分开之后, 「平常用法本身就坏」「低资源时才坏」 不会混在一起。

尤其有无 fault injection,会让程序实际走过的 code path 差很多。 所以 有 fault 的 run没有 fault 的 run 都要跑。

7.3. 要收集的数据

最少应收集:

种类 想要的内容
应用日志 cameraIdsessionIdphasehandleCounterror code
process 状态 Handle CountPrivate BytesThread 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. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表