工業相機控制應用跑一個月後突然崩潰時(後篇) - 什麼是 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 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. 搭配 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 失敗時,日誌有沒有留 cameraIdphase
  • 半途初始化完後,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

思路:

  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 崩了,從別條 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
  • sessionId
  • resourceId
  • phase
  • create/open 與 close/dispose 的 lifecycle log
  • verifier stop 時的 dump 與 debugger 輸出

這樣就能:

  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
  • leak 差異清楚
  • 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
    • 在 debugger 下局部再現

分開之後, 「平常用法本身就壞」「低資源時才壞」 不會混在一起。

尤其有無 fault injection,會讓程式實際走過的 code path 差很多。 所以 有 fault 的 run沒有 fault 的 run 都要跑。

7.3. 要收集的資料

最少應收集:

種類 想要的內容
應用日誌 cameraIdsessionIdphasehandleCounterror code
process 狀態 Handle CountPrivate BytesThread 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. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

在整理與改善方式上相近的案例頁面。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽