用 Application Verifier 打造 Windows 異常情境測試基礎架構

· 更新日期: · · Windows 開發, 故障調查, 工業相機, Application Verifier, 失敗路徑測試, 控制代碼洩漏

更新紀錄(2 筆,最後更新 2026年09月04日)

本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。

已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.22279336)
補上了日文原文中已有的諮詢引導(consultation_services)。內文沒有改動。 查看更新前的版本 (DOI: 10.5281/zenodo.21616258)
初次發布
引用本文(DOI: 10.5281/zenodo.21616257)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

Go Komura(2026)。〈用 Application Verifier 打造 Windows 異常情境測試基礎架構〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616257 https://comcomponent.com/zh-TW/blog/2026/03/11/003-application-verifier-abnormal-test-foundation-part2/

DOI(最新版本)
10.5281/zenodo.21616257
DOI(此版本)
10.5281/zenodo.22297086

Application Verifier 是一個很有力的工具,適合用在想提前把 Windows 原生程式碼與 Win32 邊界上發生的異常攤到檯面上的時候。 特別是想測試控制代碼異常、堆積毀損、低資源時的 failure path 的場合,它能把只靠正常情境測試看不到的問題相當早地攤開來。

前篇 工業相機長期運轉當機調查 - 控制代碼洩漏篇 梳理了一個案例:調查長時間運轉後當掉的控制應用程式,最後發現原因是控制代碼洩漏。 不過,只是把日誌加強,事情才做了一半。真正想要的,是能先試出 今後就算因為非預期的程式錯誤而發生記憶體洩漏、控制代碼洩漏、中途失敗或釋放漏掉,是否仍處於「知道發生了什麼」的狀態。

於是我們用上了 Application Verifier。 它是一個能對在 Windows 原生程式碼與 Win32 邊界上執行的處理,在執行階段插入檢查與 fault injection 的工具。實務上特別方便的地方是:就算沒有真的把機器的記憶體吃光,也能提前製造出像記憶體不足或資源不足那樣的壞法。

後篇要在工業相機控制應用程式的脈絡下,梳理 Application Verifier 是什麼、能做什麼,以及要怎麼把它編進異常情境測試基礎架構。

目錄

  1. 先講結論(一句話)
  2. 什麼是 Application Verifier
    • 2.1. 一句話說明
    • 2.2. 在什麼場面派得上用場
    • 2.3. 好處在哪裡
    • 2.4. 取得工具並在自己的機器上啟用
  3. Application Verifier 能做什麼
    • 3.1. Basics:Handles / Heaps / Locks / Memory / TLS 等
    • 3.2. Low Resource Simulation:提前製造記憶體不足與資源不足
    • 3.3. Page Heap 與 debugger
    • 3.4. !avrf / !htrace / 日誌
  4. 這次為什麼導入
    • 4.1. 目的不只是「找出 bug」
    • 4.2. 製造像記憶體不足的現象
    • 4.3. 確認控制代碼異常發生時追不追得動
  5. 怎麼製造記憶體不足或資源不足那樣的現象
    • 5.1. Low Resource Simulation 的思路
    • 5.2. 可以讓什麼失敗
    • 5.3. 實務上怎麼套用
  6. 怎麼看控制代碼異常
    • 6.1. Handles 檢查
    • 6.2. 用 !htrace 看 open / close 的堆疊
    • 6.3. 怎麼和自製日誌搭配
  7. 異常情境測試基礎架構怎麼做
    • 7.1. 把執行單位收攏到 harness
    • 7.2. 把測試選單分開
    • 7.3. 要收集的東西
    • 7.4. 合格條件
    • 7.5. 注意事項
  8. 大致的取捨
  9. 總結
  10. 參考資料

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 17 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

1. 先講結論(一句話)

  • Application Verifier 是一個讓 Windows 非受管理/原生邊界 上發生的誤用更容易在執行階段被找出來的工具
  • 方便的地方不只是「找出 bug」,更在於 能提前製造出平常很難出現的異常情境
  • Handles 可以偵測 invalid handle,Heaps 可以讓堆積毀損顯現出來,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 本來就混在一起的世界,契合度相當高。

Application Verifier 的兩項作用說明 Application Verifier 具有偵測原生邊界的誤用與提前製造平常難以出現的異常情境這兩項作用,可以偵測 invalid handle 與堆積毀損,也可以對像記憶體不足的狀況注入失敗的圖。Application Verifier偵測原生邊界的誤用提前製造難以出現的異常情境invalid handle 與堆積毀損注入像記憶體不足的狀況

圖 1: Application Verifier 的作用有兩根支柱:「偵測誤用」與「提前製造異常情境」。

2. 什麼是 Application Verifier

2.1. 一句話說明

Application Verifier 是針對 Windows user-mode 應用程式的 執行階段驗證工具。 它會監控執行中應用程式如何使用 OS API、如何處理資源,藉此偵測可疑的用法,也可以刻意注入失敗。

和「靜態分析」、「單元測試」不同,它是用來看 實際走過那條程式碼路徑時會怎麼壞 的工具。 所以很適合把平常的功能測試看不到的 failure path 逼出來。

測試 harness控制應用程式 / SDK 包裝層Application VerifierWin32 API / native DLL / OS 資源verifier stopdebugger outputAppVerifier logs自製 structured log

圖 2: Application Verifier 監控由測試 harness 啟動的控制應用程式,並以 verifier stop、debugger 輸出與日誌留下觀測結果。

2.2. 在什麼場面派得上用場

特別容易派上用場的,是下面這些場面。

  • 正在呼叫 native DLL 或相機 SDK
  • 跨越 P/Invoke 或 COM
  • 直接或間接大量使用 handle、heap、lock、virtual memory
  • 一般的正常情境幾乎不會當掉,但只有異常情境下生命週期管理看起來會垮
  • 比起「當掉」,先出現的是「偶爾回傳奇怪的失敗」

反過來說,它 並不是拿來追蹤純受管理世界裡 object graph 的工具。 所以 C# 應用程式只要 native SDK 或 Win32 邊界夠厚,它一樣很有效,但這不代表純受管理的 heap leak 靠它一支就能全部看完。

判斷 Application Verifier 是否派得上用場說明對 native DLL、P/Invoke 與 Win32 邊界厚重的應用程式它派得上用場,但它不是用來追蹤純受管理世界裡 object graph 的工具,並呈現這條分界的圖。native SDK 或 Win32 邊界純受管理的 object graph問題出在哪一層Application Verifier 派得上用場不在守備範圍〔改用別的工具〕

圖 3: 派不派得上用場的分界,在於 native / Win32 邊界有多厚,它不是只看純受管理世界的工具。

2.3. 好處在哪裡

實務上的好處,大致是下面 3 個。

  1. 能早一步擋下原生邊界的誤用
    • invalid handle
    • heap corruption
    • lock misuse
    • virtual memory API misuse 等
  2. 能提前製造只有低資源時才會出現的壞法
    • 相當於 malloc 的呼叫偶爾失敗
    • CreateEvent 或 CreateFile 偶爾失敗
    • VirtualAlloc 失敗
  3. 搭配 debugger 就好追
    • !avrf
    • !htrace
    • !heap -p -a
    • verifier stop 的日誌

裝置控制應用程式最讓人頭痛的,是「異常情境下不知道發生了什麼」。 Application Verifier 對減少那種「搞不清楚」相當有效。

實務上的三個好處說明能早一步擋下原生邊界的誤用、能提前製造只有低資源時才會出現的壞法、搭配 debugger 就好追這三個好處的圖。Application Verifier能早一步擋下誤用能提前製造壞法用 debugger 好追avrf 與 htrace 等擴充命令

圖 4: 實務上的好處,可以收攏成早期偵測、提前製造異常情境、debugger 連動這三點。

2.4. 取得工具並在自己的機器上啟用

先把備妥工具這件事處理掉。這一段漏掉的話,後面講的全部都會變成紙上談兵。

Application Verifier 隨附於 Windows SDK。 Windows 本身並沒有內建,所以要啟動 SDK 的安裝程式,在功能選擇畫面勾選「Application Verifier」。執行檔的名稱是 appverif.exe。

使用上的前提有 3 個。

  • 執行的使用者必須是該機器上 Administrators 群組的成員
  • ARM64EC 不在支援範圍內
  • 驗證對象必須是 非受管理(原生)程式碼

GUI 與命令列的關係,這樣理解就不會迷路。

  做的是什麼
GUI(appverif.exe) 把目標 EXE 名稱與要啟用的測試組合 寫進登錄檔
命令列(appverif -enable ...) 用命令寫入完全相同的登錄檔設定
執行階段 目標 EXE 啟動時會讀取該設定,載入 verifier 的 DLL,並掛上 Win32 API 的 hook

也就是說 用哪一邊,做的事情都一樣。手動操作的第一次用 GUI,CI 或指令碼用命令列,這樣分工就好。

GUI 與命令列的關係說明 GUI 與命令列都只是寫入相同的登錄檔設定,目標 EXE 啟動時會讀取該設定載入 verifier 的 DLL 並掛上 Win32 API 的 hook 的圖。GUI〔appverif.exe〕把設定寫進登錄檔命令列目標 EXE 啟動時參照載入 verifier 的 DLL 並掛 hook

圖 5: GUI 與命令列都只是在寫同一份登錄檔設定,hook 是在目標 EXE 啟動時才掛上去。

GUI 的操作流程是:在畫面左邊的 Applications 欄位按右鍵,用「Add Application」加入目標 EXE,然後在畫面右邊的 Tests 欄位勾選 Basics 之類的項目,再按「Save」。要解除時,一樣在 Applications 欄位按右鍵選「Delete Application」,然後「Save」。

由此衍生出 2 個重要的限制。

  • 無法對正在執行中的處理程序事後啟用。 hook 是在 DLL 載入時掛上去的,所以順序一定是先設定再啟動。
  • 設定會一直留著,直到明確刪除為止。 抱著「只是試一次」的心態放著不管,那台機器上的那個 EXE 就會一直在 verifier 之下啟動。

另外,偵測到問題時的日誌預設會以二進位格式存放在 %USERPROFILE%\AppVerifierLogs,可以用 GUI 或命令列轉成 XML 之後彙總。

啟用的順序與設定的殘留說明 hook 是在 DLL 載入時掛上去因此無法對執行中的處理程序事後啟用,順序必須是先設定再啟動,而設定會一直留到明確刪除為止的圖。寫入設定啟動目標 EXE在 verifier 之下執行設定會留到刪除為止放著不管就會一直在 verifier 之下啟動執行中的處理程序無法事後啟用

圖 6: 「先設定再啟動」的順序不能顛倒,而且設定會一直留在那台機器上,直到明確刪除為止。

3. Application Verifier 能做什麼

3.1. Basics:Handles / Heaps / Locks / Memory / TLS 等

Application Verifier 的基本組合是 Basics。 實務上常用的檢查都收在這裡。

層 看什麼 在本次脈絡下的用處
Handles invalid handle 的使用 有沒有踩到已 close 或已壞掉的 handle
Heaps heap corruption 把 native SDK 邊界的緩衝區毀損與 use-after-free 逼出來
Leak DLL unload 時仍未釋放的資源 短命 harness 的測試,或含 unload 的情境檢查
Locks / SRWLock lock 的誤用 檢查 reconnect 與 shutdown 的競爭
Memory VirtualAlloc / MapViewOfFile 等的誤用 檢查大型緩衝區與共用記憶體周邊的異常
TLS Thread Local Storage API 的誤用 執行緒邊界複雜的 native 程式碼的保險
Threadpool threadpool API 與 worker state 的完整性 callback 或非同步處理較多時的輔助

重點在於,不是「當掉之後再讀就知道」,而是「看到可疑的用法就當場停住」。 在長時間運轉型的缺陷上,這種提前相當有效。

Basics 提前偵測的思路說明不是等當掉之後讀日誌來推測,而是看到可疑用法就當場停住,藉此把長時間運轉型的缺陷提前攤開來的圖。可疑的 API 用法Basics 的檢查群當場停住提前把問題攤開過去只能等當掉之後再讀

圖 7: Basics 的價值,在於把「當掉之後再讀」換成「當場停住」。

3.2. Low Resource Simulation:提前製造記憶體不足與資源不足

實務上相當方便的就是這裡。 因為 就算沒有真的把 RAM 吃光,也能製造出接近記憶體不足或資源不足的現象。

思路很單純。

  • 對某個 API 呼叫
  • 以一定機率
  • 刻意讓它失敗

這樣就能走到平常幾乎不會走的 error path。

具體來說,下面這些現象會變得容易刻意製造出來。

  • HeapAlloc 或 VirtualAlloc 失敗
  • CreateFile 失敗
  • CreateEvent 失敗
  • MapViewOfFile 失敗
  • SysAllocString 這類 OLE/COM 系的配置失敗

比起真的想弄出記憶體不足而讓整台機器受苦,這種做法好處理得多。 而且,還可以 只鎖定特定 DLL 注入 fault。像裝置控制應用程式這種自製包裝層與 vendor SDK 混在一起的結構,這點相當貼近實務。

Low Resource Simulation 的機制說明以一定機率刻意讓某類 API 呼叫失敗,藉此刻意走到平常幾乎不會走的 error path,並且可以把對象縮小到特定 DLL 的圖。是否API 呼叫命中設定的機率了嗎?刻意回傳失敗照常處理走到平常不會走的 error path也可以只鎖定特定 DLL

圖 8: Low Resource Simulation 的本質,就是以一定機率讓 API 呼叫失敗的 fault injection。

3.3. Page Heap 與 debugger

要看堆積毀損,Heaps 與 page heap 的組合很強。 特別是 full page heap 會使用 guard page,在壞掉的那一瞬間就容易停住,這是它的優點。

不過,這個組合相當重。 與其拿來做長時間的地毯式測試,不如 鎖定接近重現的情境,在 debugger 之下跑,會更好用。

所以在維運上,這樣分比較實際。

  • 先用 Basics 大範圍打
  • 覺得堆積有問題時再用 full page heap
  • 太重的時候降到 light page heap
  • 等同正式環境的長時間測試,以自製日誌為主來看

說到底,AppVerifier 不是萬能的魔杖,而是 依場面換刀刃的工具。

page heap 的取捨流程說明先用 Basics 大範圍打,覺得堆積有問題就用 full page heap 在壞掉的瞬間停住,太重就降到 light page heap,等同正式環境的長時間測試則以自製日誌為主的圖。是否是否用 Basics 大範圍打堆積有問題嗎?用 full page heap 停住長時間測試以自製日誌為主太重了嗎?降到 light page heap在 debugger 之下做局部重現

圖 9: page heap 不是隨時都開著的工具,先用 Basics 大範圍打,再依場面換刀刃。

3.4. !avrf / !htrace / 日誌

先講一個名詞。前面出現過好幾次的 verifier stop,是 Application Verifier 判斷「這種用法不對」時發出的偵測事件。它不只是一行日誌,特徵是 只要是在偵錯器之下執行,它就會當場中斷。stop 都有編號,會顯示成 VERIFIER STOP 00000300 這樣的形式。有些 stop 可以直接繼續執行,有些則不能繼續(只能把處理程序結束掉)。

Application Verifier 並不是發出 stop 就結束了。 它有偵錯器擴充命令與日誌,所以更容易追出發生了什麼。

  • !avrf
    • 查看目前的 verifier 設定,以及現在發生的 stop
  • !htrace
    • 查看 handle 的 open / close / invalid reference 的堆疊
  • !heap -p -a
    • 搭配 page heap,追出壞掉的堆積區塊
  • AppVerifier 的日誌
    • 可以留下 stop 發生時的日誌

特別是啟用 Handles 時,handle tracing 會自動啟用,這點很值得感謝。 這樣就能事後追出「這個 handle 是在哪裡開的、在哪裡關的」。

從 verifier stop 到調查的流程說明偵測到不對的用法就會發出帶編號的 verifier stop,在偵錯器之下會當場中斷,接著可以用 avrf 查看設定與 stop、用 htrace 查看控制代碼的履歷的圖。偵測到不對的用法verifier stop〔帶編號〕在偵錯器之下會中斷用 avrf 查看設定與 stop用 htrace 查看 handle 履歷stop 分成可繼續與不可繼續

圖 10: verifier stop 不只是一行日誌,在偵錯器之下會當場中斷,成為調查的起點。

4. 這次為什麼導入

4.1. 目的不只是「找出 bug」

這次的目的,不只是「用 AppVerifier 找出 1 個 bug」。 用更貼近實務的說法,想確認的是下面這幾點。

  • 將來又在別的 failure path 發生資源洩漏時
  • 日誌裡有沒有好好留下脈絡
  • 能不能配合偵錯器的資訊追到底
  • 會不會落到「不知道發生了什麼」的狀態

換句話說,我們不只是把它當成 偵測器,也把它當成 觀測基礎架構的測試 來用。

4.2. 製造像記憶體不足的現象

在平常的開發機上真的弄出記憶體不足,其實相當麻煩。 而且整台機器一旦變得不穩定,測試本身就會充滿雜訊。

所以改成用 Low Resource Simulation,刻意去踩那些在記憶體不足或資源不足時可能會走到的 failure path。

這樣就比較容易回答下面這些問題。

  • CreateEvent 失敗時,日誌裡還會留下 cameraId 與 phase 嗎
  • 在半吊子的初始化之後,clean up 有確實跑起來嗎
  • VirtualAlloc 失敗時,重試會不會把東西弄壞
  • save path 的 CreateFile 失敗時,控制代碼有回收嗎

要強調的是,製造異常本身不是目的,異常發生時能讀懂壞法才是目的。

用 fault injection 檢驗觀測基礎架構說明用 Low Resource Simulation 刻意踩到失敗,檢查日誌會不會留下脈絡、後續收拾會不會跑、重試會不會弄壞東西,目的不是製造異常而是壞了能讀懂的圖。刻意踩到失敗日誌會留下脈絡嗎clean up 會跑嗎重試會不會弄壞壞法讀得懂的狀態

圖 11: fault injection 的目的不是製造異常,而是確認異常時的壞法讀不讀得懂。

4.3. 確認控制代碼異常發生時追不追得動

前篇出現的控制代碼洩漏也是如此,控制代碼相關的問題 最後當掉的地方和真正的原因很容易對不上。

所以想確認的是下面這些事。

  • 出現 invalid handle stop 時,能不能用 !htrace 追 open / close
  • 能不能和自製日誌的 resourceId / sessionId / phase 連起來
  • 失敗之後 handle count 會不會回落
  • 把 harness 做成短命處理程序時,洩漏的差異看不看得清楚

看到這個程度,就能從單純的「出 bug 了」,走到「是哪個職責的生命週期管理垮掉」。

確認控制代碼異常時追不追得動說明出現 invalid handle stop 時能否用 htrace 追 open 與 close、能否和自製日誌的脈絡連起來、handle count 會不會回落,並一路走到鎖定生命週期管理垮掉的職責的圖。invalid handle stop用 htrace 追 open 與 close和自製日誌的脈絡連起來確認 handle count 是否回落鎖定生命週期管理垮掉的職責

圖 12: 控制代碼異常不要停在「出 bug 了」,要確認能否追到是哪個職責的生命週期管理垮掉。

5. 怎麼製造記憶體不足或資源不足那樣的現象

5.1. Low Resource Simulation 的思路

Low Resource Simulation 就是所謂的 fault injection。 與其說是把低資源環境原樣重現,不如說是 人為地摻入低資源時會發生的代表性 API 失敗。

所以,它的用處相當明確。

  • 檢查 failure path 的善後
  • 檢查 retry / reconnect 夠不夠硬
  • 檢查中途成功與中途失敗交錯的初始化
  • 檢查「平常不會發生的失敗」是否也會留下日誌

這裡的訣竅是 不要一開始就什麼都讓它失敗。 一下子全開,日誌會爆掉,變得搞不清楚「自己在看什麼」。

fault injection 的縮小範圍方式說明一開始就什麼都讓它失敗會導致日誌爆掉而搞不清楚自己在看什麼,因此要從最接近想看的 failure path 的失敗開始逐步打開的圖。一開始就全部讓它失敗日誌爆掉讀不下去只打開想看的失敗清楚知道自己在看什麼

圖 13: fault injection 的訣竅是不要全開,從最接近想看的 failure path 的失敗開始打開。

5.2. 可以讓什麼失敗

Low Resource Simulation 具代表性地可以讓下面這些種類的 API 以機率的方式失敗。

種類 例子 裝置控制應用程式的例子
Heap_Alloc 堆積配置 暫時緩衝區、影像中繼資料、SDK 包裝層內部的配置
Virtual_Alloc 虛擬記憶體配置 較大的影格緩衝區、環形緩衝區
File CreateFile 等 儲存路徑或日誌檔的 open
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

光是複製貼上而讀不懂用意就沒有意義,所以逐行寫下每一行在做什麼。

命令 做什麼
appverif /verify CameraHarness.exe 對 CameraHarness.exe 啟用 Basics 這組測試
appverif /verify CameraHarness.exe /faults 在上面的基礎上再啟用 fault injection。不過對象只有 OLE_ALLOC 與 HEAP_ALLOC
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 ... 啟用 lowres(Low Resource Simulation),並逐項指定要讓哪些種類的 API 失敗以及各自的機率
appverif -query lowres -for CameraHarness.exe 顯示目前設定了哪些項目、各自的機率是多少
appverif /n CameraHarness.exe 刪除該 EXE 的設定(與 -disable * -for、-delete settings -for 目的相同)

引數的讀法也先弄清楚。

  • 機率的單位是百萬分率。 可以指定的是 0 〜 1,000,000 的整數,20000 就是 20000 / 1,000,000,也就是 2%,並不是「兩萬次失敗一次」。Microsoft 的文件也把 -with registry=20000 file=20000 當成讓登錄檔與檔案的 API 以 2% 失敗的範例。
  • /faults 後面可以接機率、寬限時間與 DLL 名稱。 格式是 /faults [機率 [寬限毫秒 [DLL ...]]]。省略機率時是 5%,省略寬限時間時是 500 毫秒。寬限時間的意思是「從處理程序啟動起算的這段時間內不注入 fault」,用來避免啟動處理本身就失敗,結果什麼都測不了。
  • /n 是解除。 可以把 n 想成大概是「no verifier」的意思。它是為了不要一直開著不關,而成對存在的命令。

-query lowres 的輸出,大致會以這樣的形式回來。設定的機率有沒有進去、有沒有連沒打算開的種類也開了,都可以在這裡查看。

Settings for CameraHarness.exe:
Test [lowres] enabled.

Include = *
Exclude =
TimeOut = 2000 (0x7D0)
WAIT = 0 (0x0)
HEAP_ALLOC = 20000 (0x4E20)
VIRTUAL_ALLOC = 0 (0x0)
REGISTRY = 0 (0x0)
FILE = 20000 (0x4E20)
EVENT = 20000 (0x4E20)
MAP_VIEW = 0 (0x0)
OLE_ALLOC = 0 (0x0)
STACKS = false

Include 與 Exclude 是對象模組的篩選,TimeOut 是啟動後不注入 fault 的時間。預設值會隨設定的寫法而變,所以不要憑印象,用這份輸出來查看最保險。

思路大致是這樣。

  1. 先只用 Basics 跑正常情境
  2. 接著加上 Low Resource Simulation,跑有 fault injection 的版本
  3. 必要時只給 file 或 event 等想看的失敗設定機率
  4. 只想鎖定特定 DLL 的話,就把注入範圍縮到那個 DLL

/faults 這個捷徑很方便,但只用它的話 會以 OLE_ALLOC 與 HEAP_ALLOC 為主。 想看 CreateFile 或 CreateEvent 的 failure path,還是寫到 -enable lowres -with file=... event=... 比較保險。

在裝置控制應用程式上,與其把 fault 撒滿整個應用程式,不如縮到 camera wrapper 或 save path 的 DLL,結果通常好讀得多。

套用 fault injection 的順序說明先只用 Basics 跑正常情境,接著加上 Low Resource Simulation 跑有 fault injection 的版本,只給想看的失敗設定機率,必要時再縮到特定 DLL 的階段式套用方式的圖。只用 Basics 跑正常情境加上 Low Resource 再跑一次只給想看的失敗設定機率縮到特定 DLL 再注入

圖 14: 不要一開始就狙擊,從 Basics 的正常情境出發,階段性地把 fault injection 的範圍縮小。

「縮到 DLL」的具體寫法也放在這裡。/faults 第 3 個之後的引數,就是對象模組的指定。

appverif /verify CameraHarness.exe /faults 50000 1000 CameraSdkWrapper.dll

這樣一來,啟動 CameraHarness.exe 時,在啟動後經過 1000 毫秒之後,只有從 CameraSdkWrapper.dll 開始的操作 會以 5%(50000 / 1,000,000)的機率失敗。模組名稱要寫到副檔名為止,不加路徑。除了 .dll,.ocx 這類會被載入的模組也可以指定。

篩選有沒有生效,可以用 appverif -query lowres -for CameraHarness.exe 的 Include 與 Exclude 這兩行查看。這裡還是 * 的話,對象就仍是整個處理程序。

縮到 DLL 的 fault injection 如何運作說明用 faults 的引數指定機率、寬限時間與對象模組後,啟動經過寬限時間才會只讓指定 DLL 發起的操作以指定機率失敗,並可用 query 的 Include 與 Exclude 查看篩選結果的圖。指定機率、寬限時間與 DLL 名稱啟動後的寬限時間內不注入只讓指定 DLL 發起的操作失敗用 query 的 Include 與 Exclude 查看

圖 15: 縮到 DLL 的 fault injection,會在寬限時間過後只讓從指定模組開始的操作失敗。

如果是在偵錯器之下執行,也可以中途改變範圍。

!avrf -trg dll CameraSdkWrapper.dll
!avrf -skp dll VendorSdk.dll

-trg 是「鎖定這裡」,-skp 是「跳過這裡」。也可以用 !avrf -flt 查看目前的 fault injection 設定,或用 !avrf -flt stacks 10 查看最近注入的失敗的堆疊。

舉例來說,可以做出這樣的情境。

  • reconnect 開始後緊接著的 CreateEvent 失敗
  • 開始儲存時的 CreateFile 失敗
  • 暫時緩衝區配置失敗
  • COM 轉換時的 SysAllocString 失敗
  • 等待 API 的失敗路徑檢查

這一帶的路徑,光靠平常的正常情境測試幾乎踩不到。 正因如此,刻意讓它踩到才有價值。

6. 怎麼看控制代碼異常

6.1. Handles 檢查

控制代碼周邊,先用 Handles。 這樣 invalid handle 的使用就比較容易被偵測到。

典型上派得上用場的,是這類出包。

  • 再次使用已 close 的 handle
  • 傳入壞掉的 handle 值
  • 使用因中途失敗而未初始化的 handle
  • 生命週期垮掉,從別的執行緒去碰

在長時間運轉下看只是「偶爾出現奇怪的錯誤」的狀況,在 verifier 之下有時會當場停住。 這種提前相當有幫助。

Handles 檢查派得上用場的出包說明重複使用已 close 的控制代碼、壞掉的控制代碼值、因中途失敗而未初始化的控制代碼、生命週期垮掉後從別的執行緒誤用等出包,在 verifier 之下可以當場停住的圖。重複使用已 close 的當場 verifier stop壞掉的 handle 值未初始化的 handle生命週期垮掉的誤用

圖 16: 在長時間運轉下只會表現成「偶爾出現奇怪錯誤」的出包,Handles 檢查會當場停住。

6.2. 用 !htrace 看 open / close 的堆疊

Handles 值得感謝的地方,是 它和 handle tracing 很合。

從這裡開始要用偵錯器,所以先把 WinDbg 裝好。它以 Debugging Tools for Windows 的形式提供,可以從和 Application Verifier 同一個 Windows SDK 安裝程式裝進來。

windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC

第 1 行的選項不是拿來拜拜的咒語。Application Verifier 在偵測到問題時拋出的例外有 3 種。

選項 例外 什麼時候出現
av 存取違規(0xC0000005) 偵測到 heap 的緩衝區越界時
ch 無效的控制代碼(0xC0000008) 偵測到 invalid handle 的使用時
sov 堆疊溢位(0xC00000FD) 判定初始堆疊不足時

而 -xd 是指定 在 second chance 才攔下那個例外。原因是 first chance 由 Application Verifier 自己處理,用來組出 stop 的資訊,偵錯器要是先插進來就會礙事。如果是在已經啟動的偵錯器裡設定,那就等同於打 sxd av、sxd ch、sxd sov。

用 !htrace 想看的,大致是這一帶。

  • 那個 handle 是在哪裡 open 的
  • 在哪裡 close 的
  • 有沒有被當成 invalid handle 參照
  • open 有沒有堆得比預期多

實際看起來的樣子也放上來。以下是官方文件上刊登的輸出範例,不是本公司環境的輸出,但形式就是這樣。

踩到 invalid handle 時,首先會出現這樣的內容。

Invalid handle - code c0000008 (first chance)
===================================================
VERIFIER STOP 00000300: pid 0x558: invalid handle exception for current stack trace
        C0000008 : Exception code.
        0012FBF8 : Exception record. Use .exr to display it.
        0012FC0C : Context record. Use .cxr to display it.
        00000000 :
===================================================
This verifier stop is continuable.
After debugging it use 'go' to continue.
===================================================

接著打 !avrf,就會顯示現在有哪些項目啟用、發生了哪個 stop。最後一行就是摘要。

0:000> !avrf
Global flags: 00000100
Application verifier global flag is set.
Application verifier settings (00000004):
   - no heap checking enabled!
   - handle checks
Page heap is not active for this process.
Current stop 00000300 : c0000008 0012fbf8 0012fc0c 00000000 .
    Using an invalid handle (either closed or simply bad).

用 !htrace 看那個控制代碼的履歷,OPEN / CLOSE / BAD REFERENCE 會各自帶著堆疊排出來。

0:000> !htrace 7DC

--------------------------------------
Handle 0x7DC - BAD REFERENCE:
0x801902BE: ntoskrnl!NtSetEvent+0x6C
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - CLOSE:
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - OPEN:
0x77DE265C: KERNEL32!CreateEventA+0x66
0x010011A0: badhandle!main+0x20
--------------------------------------

讀法很直接了當:只要 BAD REFERENCE 出現在 CLOSE 之後,就表示已經關掉的控制代碼又被用了一次。看 OPEN 的堆疊,也能知道那個控制代碼是在哪裡建立的。

控制代碼洩漏或 handle misuse 麻煩的地方,在於 最後跌倒的那個 API 並不是真正的原因。 有了 !htrace,就能相當具體地追出那個 handle 的履歷。

用 htrace 讀控制代碼履歷的方法說明 htrace 會把控制代碼的 OPEN、CLOSE、BAD REFERENCE 各自帶著堆疊排出來,只要 BAD REFERENCE 出現在 CLOSE 之後就是關掉的控制代碼被重複使用,看 OPEN 的堆疊也能知道建立的位置的圖。OPEN〔建立的位置〕CLOSE〔關閉的位置〕BAD REFERENCE判定為已關閉 handle 的重複使用每一筆紀錄都帶著堆疊

圖 17: htrace 的讀法很直接了當,只要 BAD REFERENCE 排在 CLOSE 之後,就是已關閉控制代碼的重複使用。

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

這套組合技一上,就好追多了。

自製日誌與 verifier 的組合技步驟說明先從 heartbeat 察覺 Handle Count 的斜率,再用 lifecycle log 鎖定沒有 Close 的資源,接著用 verifier run 提前逼出誤用,最後用 htrace 看 open 與 close 的堆疊的順序的圖。察覺 Handle Count 的斜率用 lifecycle log 鎖定資源用 verifier run 提前逼出誤用用 htrace 看堆疊

圖 18: 斜率的偵測交給自製日誌,誤用的偵測交給 verifier,這樣的分工就照這個順序串起來。

7. 異常情境測試基礎架構怎麼做

7.1. 把執行單位收攏到 harness

Application Verifier 無法對正在執行中的處理程序事後啟用。 順序是先設定再啟動。

而且,設定會一直留到明確刪除為止。 所以實務上,把對象 從正式應用程式本體收攏到測試用的 harness EXE 會更好處理。

舉例來說,是這樣的結構。

Scenario RunnerCameraHarness.exeCameraSdkWrapper.dllVendor SDKStructured LogDump / Debugger

圖 19: 一個情境一個處理程序來跑的 harness 結構。verifier 的對象不是 DLL,而是驅動它的 harness EXE。

這樣一來,

  • 可以一個情境一個處理程序來跑
  • leak 的差異容易看
  • AppVerifier 設定的 ON/OFF 容易切換
  • 想測試 DLL 時也能從 EXE 這一側處理

有這些好處。

命令的樣子是這樣。

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

/verify 是啟用 Basics,/n 是刪除設定(也請參照 5.3 的表)。 啟用要在啟動前,解除要明確執行。 把這一帶都放在 harness 的前提下運作,設定造成的出包也比較容易減少。

7.2. 把測試選單分開

在異常情境測試基礎架構裡,最好不要一次全部做完。 大致分成下面 3 條線,結果比較好讀。

  1. 正常情境 + Basics
    • 不注入任何失敗
    • 確認不會出現 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 兩邊都跑比較好。

分成三條線的測試選單說明把測試分成不注入任何失敗的正常情境加 Basics、用 Low Resource Simulation 鎖定目標讓它失敗的 fault injection 系列、把 full page heap 放在 debugger 之下跑的 heap 深挖系列這三條線的圖。異常情境測試的跑法正常情境與 Basicsfault injection 系列heap 深挖系列確認不會出現 stop注入鎖定好的失敗在 debugger 之下局部重現

圖 20: 不要一次全做完,分成三份選單,哪裡在壞就不會攪在一起。

7.3. 要收集的東西

最低限度,希望至少留下這些。

種類 想要的東西
應用程式日誌 cameraId、sessionId、phase、handleCount、error code
處理程序狀態 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 追查
  • 不會變成「不知道發生了什麼」的失敗

這裡重要的是, 把 不會壞 和 壞了追得動 分開來評估。

合格條件的兩個軸說明把合格條件分成正常情境下不出現 verifier stop 且資源會被收拾掉的不會壞這一軸,以及預期中的失敗會留在日誌並能用脈絡與堆疊追查的壞了追得動這一軸來評估的圖。合格條件不會壞壞了追得動不出現 stop資源會被收拾掉失敗會留在日誌可以用堆疊追查

圖 21: 只有「沒當掉」太弱,要把不會壞與追得動當成兩條不同的軸來評估。

7.5. 注意事項

Application Verifier 相當方便,但它不是魔法。

  • 實際沒有走過的程式碼路徑不會被驗證
  • full page heap 很重
  • 有時 stop 會出在 third-party SDK 那一側
  • 有沒有 fault injection,走過的程式碼路徑差很多
  • 它不是用來單靠一支處理純受管理 heap leak 調查的工具

所以,它的定位是這樣。

  • 長時間的斜率 交給自製日誌與 counters
  • native 邊界的誤用 交給 Application Verifier
  • 異常時的因果還原 交給 structured log + dump + debugger

這樣的分工最貼近實務。

調查分工的全貌說明長時間的斜率交給自製日誌與 counters、native 邊界的誤用交給 Application Verifier、異常時的因果還原交給 structured log 與 dump 與 debugger 的分工的圖。長時間的斜率自製日誌與 countersnative 邊界的誤用Application Verifier異常時的因果還原日誌與 dump 與 debugger

圖 22: Application Verifier 不是萬能的魔杖,而是依想看的東西讓工具分工。

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 native / Win32 邊界的 runtime verifier。使用 Handles / Heaps / Locks / Memory / TLS / Low Resource Simulation 等功能,可以提前讓平常很難走到的 failure path 被走過一遍。

在這次的脈絡下派上用場的是:控制代碼異常發生時可以用 !htrace 順利追查;不必把整台機器搞壞就能製造出像記憶體不足或資源不足的現象;以及能藉此確認自製日誌在那種時候是不是真的派得上用場。

實務上的跑法是:把正常情境 + Basics 與 fault injection 系列分開,準備 harness EXE,用短命的處理程序跑情境。在這之上再搭配自製日誌、dump 與 debugger 資訊,而長時間 leak 的斜率本身則交給自製 counters 看,這樣分工。

Application Verifier 是 不去「碰運氣等」那些難得出現的異常,而是「主動去把它迎過來」的工具。

在裝置控制應用程式上,不會壞當然重要, 但壞掉時 能說明發生了什麼,同樣重要。 從這個意義上說,我認為它是相當貼近實務的工具。

前篇:工業相機長期運轉當機調查 - 控制代碼洩漏篇

10. 參考資料

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

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

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

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

常見問題

整理諮詢這個主題時常見的問題。

Application Verifier 是什麼?
它是針對 Windows user-mode 應用程式的執行階段驗證工具。它會監控執行中應用程式如何使用 OS API、如何處理資源,因此可以偵測 invalid handle 的使用或 heap corruption 這類可疑用法,也可以刻意注入失敗。和靜態分析或單元測試不同,它看的是實際走過那條程式碼路徑時會怎麼壞,所以很適合把平常的功能測試看不到的 failure path 逼出來。
Application Verifier 可以重現記憶體不足嗎?
使用 Low Resource Simulation,就算沒有真的把機器的 RAM 吃光,也能提前製造出接近記憶體不足或資源不足的現象。原理是 fault injection,讓 HeapAlloc、VirtualAlloc、CreateFile、CreateEvent 等 API 呼叫以一定機率刻意失敗。也可以只鎖定特定 DLL 注入失敗,所以在自製包裝層與 vendor SDK 混在一起的結構裡也很好處理。不過一開始就什麼都讓它失敗,日誌會變得讀不下去,訣竅是從最接近想看的 failure path 的項目開始逐步打開。
Application Verifier 可以用來調查控制代碼洩漏嗎?
啟用 Handles 檢查後,就能偵測到重複使用已 close 的控制代碼這類 invalid handle 的使用,而且 handle tracing 也會自動啟用,可以用 !htrace 追那個控制代碼的 open / close 堆疊。不過,把長時間常駐的 EXE 的洩漏調查全部丟給 Application Verifier 並不實際。實務上要搭配定期記錄的 Handle Count 與 resource lifecycle 的自製日誌,斜率的偵測交給自製日誌,誤用的偵測交給 verifier,這樣分工才可行。
要用 Application Verifier 測試 DLL 該怎麼做?
啟用 Application Verifier 的對象,是實際會執行那個 DLL 的測試用 EXE。無法對已經在執行的處理程序事後啟用,必須先設定再啟動。而且設定會一直保留到明確刪除為止,所以比起正式應用程式本體,把對象放在測試用的 harness EXE 上更好處理。以一個情境一個處理程序來跑,洩漏的差異也比較容易看出來,設定的開關也比較好切換。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽