工業相機控制應用跑一個月後突然崩潰時(後篇) - 什麼是 Application Verifier 與異常系測試基盤的做法
· 小村 豪 · Windows 開發, 故障調查, 工業相機, Application Verifier, 異常系測試, handle leak
Application Verifier 是當你想把 Windows 原生代碼與 Win32 邊界上的異常「提前炸出來」時,相當有力的工具。 特別是想測 handle 異常、heap 破壞、低資源時的 failure path,這些在正常系測試裡根本看不到的問題,它能很快把它們浮上檯面。
前篇 工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計 整理了一個案例:長時間運轉後崩潰的控制應用,原因其實是 handle leak。 但光是把日誌加強,只完成一半。真正想要的是 如果未來又因預料之外的程式錯誤出現 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 的 leak 調查,只靠 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失敗
- 類似
- 搭配 debugger 好追
!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 與 debugger
要追 heap 破壞,Heaps + page heap 的組合很強。
特別是 full page heap 用 guard page,壞的那一刻就能停,這點很有利。
但這個組合負擔相當重,適合鎖定再現情境,在 debugger 下跑,而不是拿來做長時間全面掃描。
實務上這樣分比較合理:
- 先用
Basics大範圍打底 - 如果懷疑 heap,就用 full page heap
- 太重時降回 light page heap
- 擬生產長時間測試,交給自家日誌
也就是說 AppVerifier 不是萬能之杖,是依場合換刀刃的工具。
3.4. !avrf / !htrace / 日誌
Application Verifier 不只會 stop 完收工。 debugger 擴充與日誌都能幫你追蹤發生了什麼。
!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 漏了資源
- 日誌能不能留下情境
- 配合 debugger 資訊能不能追到底
- 是否會陷入「搞不懂發生什麼」的狀態
換句話說,我把它當成 檢測器,也當成 觀測基盤的測試。
4.2. 重現「像記憶體不足」的現象
在開發機上真的去弄出記憶體不足很麻煩。 而且整台機器變不穩後,連測試本身都充滿雜訊。
所以改用 Low Resource Simulation 精準地踩到「在低資源狀況下才會走的 failure path」。
這樣可以回答像這種問題:
CreateEvent失敗時,日誌有沒有留cameraId與phase- 半途初始化完後,clean up 是否有跑
VirtualAlloc失敗時 retry 會不會弄壞- 儲存路徑的
CreateFile失敗時,handle 有沒有回收
重點很關鍵:「觸發異常」本身不是目的,「出了異常時能讀懂壞法」才是目的。
4.3. handle 異常能不能追
前篇提到的 handle leak 也是同樣的事。handle 相關問題的特徵是 最後崩掉的地方與真正原因經常錯位。
所以想驗證的是:
- invalid handle stop 出來時,能不能用
!htrace追 open / close - 能否對應上自家日誌的
resourceId/sessionId/phase - 失敗後 handle count 會不會回落
- 用短命 harness 跑時,leak 差異是否看得清楚
做到這,就能從「出了 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 等 |
儲存路徑或 log 檔開啟 |
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 崩了,從別條 thread 去碰
長時間運轉下看「偶爾才錯」的東西,在 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 leak 或 misuse 的難處在於 最後失敗的 API 不是真正的原因。
有 !htrace 就能拿到該 handle 的歷程,具體得多。
6.3. 與自家日誌怎麼搭
話雖如此,光有 Application Verifier 還不夠。 尤其長駐 EXE 的 leak 調查單靠它是相當痛苦的。
實務上要一起用:
- 定期
Handle Count sessionIdresourceIdphase- create/open 與 close/dispose 的 lifecycle log
- verifier stop 時的 dump 與 debugger 輸出
這樣就能:
- 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
- leak 差異清楚
- 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
- 在 debugger 下局部再現
分開之後, 「平常用法本身就壞」 與 「低資源時才壞」 不會混在一起。
尤其有無 fault injection,會讓程式實際走過的 code path 差很多。 所以 有 fault 的 run 與 沒有 fault 的 run 都要跑。
7.3. 要收集的資料
最少應收集:
| 種類 | 想要的內容 |
|---|---|
| 應用日誌 | cameraId、sessionId、phase、handleCount、error code |
| process 狀態 | Handle Count、Private Bytes、Thread Count |
| debugger 資訊 | !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、debugger 資訊
- 長期 leak 的斜率靠自家 counters 觀察
Application Verifier 是 「不被動等難得一遇的異常」,而是「主動去把它迎過來」的工具。
裝置控制應用不只要「不壞」,還要 壞了能解釋發生了什麼。 從這個意義上說,它是相當實務的工具。
前篇:工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計
10. 參考資料
- 前篇:工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計
- 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)
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
工業相機控制應用跑一個月後突然崩潰時(前篇) - handle leak 的找法與長時間運轉用的日誌設計
本文以工業相機控制應用連續運轉約一個月後突然崩潰的案例,整理 handle leak 的特徵、與記憶體流失的差異,以及如何用 Handle Count 斜率與 create/close 配對日誌追出真正洩漏的位置。讀者可學到把長時間運轉的故障切成可觀測形式,並用短迴圈反覆壓...
TCP 重送讓工業相機通訊卡幾秒時 - RFC1323 timestamp 與重送等待的切分
本文整理工業相機 TCP 通訊偶爾卡幾秒的切分思路:先在 wire 上用 Wireshark 確認是封包遺失後的 RTO 重送等待,再核對 SYN/SYN-ACK 是否協商 TCP timestamps option,並說明 RFC1323 系 timestamp 如何消除...
Windows 應用的 crash dump 收集入門 - 先搞清楚 WER / ProcDump / WinDbg 怎麼分工
本文整理在 Windows 應用追難以重現的 crash 時,要先以 WER LocalDumps 應用單位設定為起手式,再依現場狀況追加 ProcDump,最後才考慮 MiniDumpWriteDump 自製收集的決策順序。讀完能理解 mini 與 full dump 的...
開發 COM 元件、OCX/ActiveX 時常見的坑 - 整理 Visual Studio 的 32bit/64bit、註冊、管理員權限
整理開發 COM、OCX、ActiveX 元件時最容易卡關的四個面向:宿主行程的 32bit/64bit、Visual Studio 2022 變成 64bit 後的設計時整合、regsvr32 與 Regasm 的註冊位置、以及管理員權限與 HKCU/HKLM 的關係,協...
ClickOnce 是什麼 - 以實務視角整理機制、更新、適合場面・不適合場面
本文以實務視角整理 ClickOnce 是什麼,從 manifest、快取、更新、簽章的構造,到適合公司內部 .NET 桌面業務應用程式的案件與不適合 machine-wide 或 service、driver 等深度 OS 整合的案件,幫助讀者判斷是否採用並掌握 depl...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
故障調查 & 長期運行故障
整理間歇性故障、通訊診斷、長期運行當機、失敗路徑測試基礎的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
故障調查 & 根本原因分析
調查難以重現的故障、長時間執行後的問題、記憶體洩漏、通訊停滯等棘手的正式環境問題。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。