Ein Testfundament für Windows-Failure-Paths mit Application Verifier
· Go Komura · Windows-Entwicklung, Fehleruntersuchung, Industriekamera, Application Verifier, Failure-Path-Tests, Handle-Leak
Application Verifier ist ein starkes Werkzeug, wenn man Anomalien, die im nativen Windows-Code oder an der Win32-Grenze auftreten, vorzeitig sichtbar machen möchte. Besonders wenn man Handle-Anomalien, Heap-Beschädigung oder Failure Paths bei geringen Ressourcen testen will, lassen sich damit Probleme, die reine Normalpfad-Tests nie zeigen würden, deutlich früher aufdecken.
In Teil 1, Untersuchung eines Absturzes einer Industriekamera-Anwendung nach Langzeitbetrieb - Der Handle-Leak (Teil 1), haben wir den Fall einer Steuerungsanwendung untersucht, die nach langem Dauerbetrieb abstürzte, und festgestellt, dass die Ursache ein Handle-Leak war. Allein die Logs zu verstärken, ist aber erst die halbe Miete. Was man wirklich braucht, ist vorab prüfen zu können, ob man auch dann noch „erkennt, was passiert ist“, wenn künftig ein unerwarteter Programmierfehler einen Memory-Leak, einen Handle-Leak, einen Teilausfall oder eine vergessene Freigabe verursacht.
Dafür haben wir Application Verifier eingesetzt. Es ist ein Werkzeug, mit dem sich zur Laufzeit Prüfungen und Fault Injection in Code einbringen lassen, der im nativen Windows-Code oder an der Win32-Grenze läuft. In der Praxis besonders praktisch ist, dass man speicher- oder ressourcenmangelähnliche Ausfallmuster vorziehen kann, ohne den Speicher der Maschine wirklich aufzubrauchen.
In diesem zweiten Teil erklären wir, was Application Verifier ist, was es kann und wie man es im Kontext einer Industriekamera-Steuerungsanwendung in ein Testfundament für Failure-Path-Tests einbaut.
Inhaltsverzeichnis
- Zuerst das Fazit (in einem Satz)
- Was ist Application Verifier?
- 2.1. In einem Satz
- 2.2. Wann es wirkt
- 2.3. Was man davon hat
- 2.4. Beschaffen und lokal aktivieren
- Was Application Verifier kann
- 3.1. Basics: Handles / Heaps / Locks / Memory / TLS und mehr
- 3.2. Low Resource Simulation: Speicher- und Ressourcenmangel vorziehen
- 3.3. Page Heap und der Debugger
- 3.4.
!avrf/!htrace/ Logs
- Warum wir es diesmal eingeführt haben
- 4.1. Das Ziel ist nicht nur „Bugs finden“
- 4.2. Speichermangelähnliche Phänomene auslösen
- 4.3. Prüfen, ob sich Handle-Anomalien im Ernstfall nachverfolgen lassen
- Wie man speicher- oder ressourcenmangelähnliche Phänomene auslöst
- 5.1. Die Idee hinter Low Resource Simulation
- 5.2. Was sich zum Scheitern bringen lässt
- 5.3. Anwendung in der Praxis
- Wie man Handle-Anomalien betrachtet
- 6.1. Die
Handles-Prüfung - 6.2. Open-/Close-Stacks mit
!htracebetrachten - 6.3. Kombination mit eigenen Logs
- 6.1. Die
- Aufbau eines Testfundaments für Failure Paths
- 7.1. Die Ausführungseinheit auf eine Harness ausrichten
- 7.2. Das Testmenü aufteilen
- 7.3. Was man erfasst
- 7.4. Abnahmekriterien
- 7.5. Hinweise
- Grober Entscheidungsleitfaden
- Zusammenfassung
- Quellen
1. Zuerst das Fazit (in einem Satz)
- Application Verifier ist ein Werkzeug, das Fehlverwendungen an der unmanaged/nativen Grenze von Windows zur Laufzeit leichter auffindbar macht
- Der Nutzen liegt nicht nur im „Bugs finden“, sondern darin, Failure Paths, die sonst selten auftreten, vorzuziehen
Handleserkennt invalid handles,Heapsmacht Heap-Beschädigung sichtbar, undLow Resource Simulationerlaubt Fault Injection für speicher- oder ressourcenmangelähnliche Situationen- Die Leck-Untersuchung einer lange laufenden, dauerhaft aktiven EXE allein Application Verifier zu überlassen, ist keine gute Idee — realistisch ist die Kombination mit
Handle Countund einem eigenen Log des Ressourcen-Lifecycle - In einem Testfundament für Failure Paths liest sich das Ergebnis leichter, wenn man einen normalen Verifier-Run und einen Fault-Injection-Run getrennt durchführt
- Auch wenn man eine DLL testen möchte, wird Application Verifier auf der Test-EXE aktiviert, die diese DLL tatsächlich ausführt
Kurz gesagt ist Application Verifier ein Werkzeug, das die „lästigen Bugs“ rund um Windows’ native/Win32-Grenze ans Licht zerrt. Besonders in einer Welt wie der von Geräte-Steuerungsanwendungen, in der sich native SDKs, P/Invoke und Win32-APIs ganz selbstverständlich mischen, passt es sehr gut.
2. Was ist Application Verifier?
2.1. In einem Satz
Application Verifier ist ein Laufzeitverifikationswerkzeug für Windows-User-Mode-Anwendungen. Es überwacht, wie eine laufende Anwendung OS-APIs nutzt und mit Ressourcen umgeht, erkennt verdächtige Verwendungen und kann gezielt Fehler injizieren.
Anders als „statische Analyse“ oder „Unit-Tests“ ist es ein Werkzeug, das zeigt, wie etwas tatsächlich zusammenbricht, wenn dieser Codepfad wirklich durchlaufen wird. Es eignet sich deshalb hervorragend, um Failure Paths aufzudecken, die im gewöhnlichen Funktionstest unsichtbar bleiben.
flowchart LR
A[Test-Harness] --> B[Steuerungsanwendung / SDK-Wrapper]
B --> C[Application Verifier]
C --> D[Win32-API / native DLL / OS-Ressourcen]
C --> E[verifier stop]
C --> F[Debugger-Ausgabe]
C --> G[AppVerifier-Logs]
B --> H[Eigenes structured Log]
2.2. Wann es wirkt
Besonders wirksam ist es in Situationen wie diesen.
- Native DLLs oder ein Kamera-SDK werden aufgerufen
- P/Invoke- oder COM-Grenzen werden überschritten
- Handles, Heaps, Locks und virtueller Speicher werden direkt oder indirekt stark genutzt
- Auf dem normalen Pfad stürzt kaum etwas ab, aber die Lifetime-Verwaltung wirkt auf Failure Paths brüchig
- „Gibt gelegentlich seltsame Fehler zurück“ tritt eher auf als „stürzt ab“
Umgekehrt ist es kein Werkzeug, um Objektgraphen in einer rein managed Welt nachzuverfolgen. Auch bei einer C#-Anwendung wirkt es also durchaus stark, wenn native SDKs oder die Win32-Grenze eine dicke Schicht bilden — es ist aber nicht dafür gedacht, rein managed Heap-Leaks allein damit vollständig zu untersuchen.
2.3. Was man davon hat
In der Praxis läuft der Nutzen im Wesentlichen auf drei Punkte hinaus.
- Fehlverwendungen an der nativen Grenze werden früh gestoppt
- invalid handle
- heap corruption
- Lock-Fehlverwendung
- Fehlverwendung von Virtual-Memory-APIs und mehr
- Ausfallmuster, die nur bei geringen Ressourcen auftreten, lassen sich vorziehen
- Das
malloc-Äquivalent schlägt gelegentlich fehl CreateEventoderCreateFileschlagen gelegentlich fehlVirtualAllocschlägt fehl
- Das
- In Kombination mit dem Debugger lässt sich leichter nachverfolgen
!avrf!htrace!heap -p -a- Logs der verifier stops
Das Problem bei Geräte-Steuerungsanwendungen ist, dass man „nicht weiß, was auf dem Failure Path passiert ist“. Application Verifier trägt erheblich dazu bei, dieses „Nicht-Wissen“ zu verringern.
2.4. Beschaffen und lokal aktivieren
Zunächst klären wir, wie man sich das Werkzeug beschafft. Fehlt dieser Schritt, bleibt alles Folgende graue Theorie.
Application Verifier ist im Windows SDK enthalten. Windows allein bringt es nicht mit — starten Sie den SDK-Installer und aktivieren Sie im Auswahlbildschirm der Komponenten das Häkchen bei „Application Verifier“. Die ausführbare Datei heißt appverif.exe.
Für den Einsatz gelten drei Voraussetzungen.
- Der ausführende Benutzer muss Mitglied der Administrators-Gruppe auf dieser Maschine sein
- ARM64EC wird nicht unterstützt
- Das Prüfziel muss unmanaged (nativer) Code sein
Das Verhältnis zwischen GUI und Kommandozeile lässt sich am besten so verstehen:
| Was passiert | |
|---|---|
GUI (appverif.exe) |
Schreibt den Namen der Ziel-EXE und die Kombination der zu aktivierenden Tests in die Registry |
Kommandozeile (appverif -enable ...) |
Schreibt genau dieselbe Registry-Einstellung, nur per Befehl |
| Zur Laufzeit | Beim Start der Ziel-EXE wird diese Einstellung gelesen, die Verifier-DLL geladen und die Win32-API-Hooks eingebracht |
Das heißt: Egal welchen Weg Sie wählen, es passiert dasselbe. In der Praxis nutzt man beim ersten manuellen Versuch die GUI und in CI oder Skripten die Kommandozeile.
Die Bedienung der GUI läuft so ab: Im linken Bereich „Applications“ mit Rechtsklick „Add Application“ wählen, um die Ziel-EXE hinzuzufügen, im rechten Bereich „Tests“ Häkchen wie bei Basics setzen und „Save“ klicken. Zum Deaktivieren im selben „Applications“-Bereich mit Rechtsklick „Delete Application“ wählen und erneut „Save“ klicken.
Daraus ergeben sich zwei wichtige Einschränkungen.
- Ein bereits laufender Prozess lässt sich nicht nachträglich aktivieren. Da der Hook beim Laden der DLL eingebracht wird, muss die Reihenfolge „erst konfigurieren, dann starten“ sein.
- Die Einstellung bleibt bestehen, bis sie ausdrücklich entfernt wird. Lässt man sie nach einem vermeintlich einmaligen Test einfach stehen, startet diese EXE auf dieser Maschine fortan immer unter dem Verifier.
Die beim Erkennen entstehenden Logs werden standardmäßig im Binärformat unter %USERPROFILE%\AppVerifierLogs gespeichert und lassen sich über die GUI oder die Kommandozeile in XML umwandeln und auswerten.
3. Was Application Verifier kann
3.1. Basics: Handles / Heaps / Locks / Memory / TLS und mehr
Das Basisset von Application Verifier heißt Basics.
Hier sind die in der Praxis am häufigsten genutzten Prüfungen gebündelt.
| Ebene | Was geprüft wird | Einsatz in diesem Kontext |
|---|---|---|
Handles |
Verwendung eines invalid handle | Ob geschlossene / defekte Handles angefasst werden |
Heaps |
Heap-Beschädigung | Aufdecken von Pufferüberläufen und use-after-free an der native-SDK-Grenze |
Leak |
Nicht freigegebene Ressourcen zum Zeitpunkt des DLL-Unload | Tests kurzlebiger Harnesses und Fälle mit Unload |
Locks / SRWLock |
Fehlverwendung von Locks | Prüfung von Race Conditions zwischen Reconnect und Shutdown |
Memory |
Fehlverwendung von VirtualAlloc / MapViewOfFile usw. |
Prüfung von Anomalien bei großen Puffern und Shared Memory |
TLS |
Fehlverwendung der Thread-Local-Storage-API | Absicherung für nativen Code mit komplexen Thread-Grenzen |
Threadpool |
Konsistenz der Threadpool-API und des Worker-Zustands | Unterstützung bei vielen Callbacks oder asynchroner Verarbeitung |
Entscheidend ist: nicht „hinterher aus dem Absturz herauslesen“, sondern „verdächtige Verwendung sofort an Ort und Stelle stoppen“. Bei Fehlern, die erst nach Langzeitbetrieb auftreten, zahlt sich dieses Vorziehen erheblich aus.
3.2. Low Resource Simulation: Speicher- und Ressourcenmangel vorziehen
Das ist der in der Praxis besonders nützliche Teil, denn man kann Phänomene nahe an Speicher- oder Ressourcenmangel auslösen, ohne wirklich das RAM aufzubrauchen.
Die Idee dahinter ist einfach.
- Einen bestimmten API-Aufruf
- mit einer festgelegten Wahrscheinlichkeit
- absichtlich fehlschlagen lassen
Damit lassen sich Error Paths durchlaufen, die sonst so gut wie nie erreicht werden.
Konkret lassen sich damit gezielt Phänomene wie diese auslösen.
HeapAllocoderVirtualAllocschlägt fehlCreateFileschlägt fehlCreateEventschlägt fehlMapViewOfFileschlägt fehl- OLE/COM-Allokationen wie
SysAllocStringschlagen fehl
Das ist deutlich handhabbarer, als tatsächlich versuchen, die ganze Maschine durch echten Speichermangel zu quälen. Außerdem lässt sich Fault Injection gezielt auf einzelne DLLs beschränken. Bei Konstellationen wie Geräte-Steuerungsanwendungen, in denen eigene Wrapper mit Vendor-SDKs gemischt sind, ist das ausgesprochen praxistauglich.
3.3. Page Heap und der Debugger
Für Heap-Beschädigung ist die Kombination aus Heaps und Page Heap stark.
Besonders full page heap hat den Vorteil, mithilfe von Guard Pages genau im Moment der Beschädigung anzuhalten.
Das ist allerdings ziemlich schwergewichtig. Statt eines langen erschöpfenden Laufs ist es praktikabler, sich auf Szenarien nahe an der Reproduktion zu beschränken und unter dem Debugger laufen zu lassen.
Als Vorgehen ist deshalb folgende Aufteilung realistisch.
- Zunächst breit mit
Basicsansetzen - Wird der Heap verdächtig, full page heap einsetzen
- Wird es zu schwer, auf light page heap zurückfallen
- Bei produktionsnahen Langzeittests hauptsächlich auf eigene Logs setzen
Letztlich ist AppVerifier kein Zauberstab, sondern ein Werkzeug, dessen Klinge man je nach Situation wechselt.
3.4. !avrf / !htrace / Logs
Zunächst ein Begriff. Der bisher schon mehrfach erwähnte verifier stop ist das Erkennungsereignis, das Application Verifier auslöst, wenn es zu dem Schluss kommt: „diese Verwendung ist falsch“. Er ist keine bloße Logzeile — läuft man unter einem Debugger, bricht die Ausführung genau dort ab. Stops sind nummeriert und werden etwa als VERIFIER STOP 00000300 angezeigt. Es gibt Stops, nach denen man fortfahren kann, und solche, nach denen das nicht möglich ist (bei denen nur das Beenden des Prozesses bleibt).
Application Verifier gibt sich nicht mit dem Auslösen eines Stops zufrieden. Dank Debugger-Erweiterungen und Logs lässt sich leichter nachvollziehen, was passiert ist.
!avrf- Zeigt die aktuellen Verifier-Einstellungen und den gerade aufgetretenen Stop
!htrace- Zeigt die Stacks von Open, Close und ungültiger Referenz eines Handles
!heap -p -a- Verfolgt in Kombination mit Page Heap den beschädigten Heap-Block
- AppVerifier-Logs
- Halten die Logs zum Zeitpunkt eines Stops fest
Besonders hilfreich ist, dass bei aktiviertem Handles automatisch auch Handle-Tracing aktiviert wird.
Damit lässt sich im Nachhinein leicht nachvollziehen, „wo dieses Handle geöffnet und wo es geschlossen wurde“.
4. Warum wir es diesmal eingeführt haben
4.1. Das Ziel ist nicht nur „Bugs finden“
Unser Ziel war diesmal nicht einfach, „mit AppVerifier einen einzelnen Bug zu finden“. Praktischer ausgedrückt wollten wir Folgendes prüfen.
- Tritt künftig auf einem anderen Failure Path erneut ein Ressourcenleck auf,
- bleibt der Kontext dann verlässlich im Log erhalten?
- Lässt sich das zusammen mit den Debugger-Informationen vollständig nachverfolgen?
- Landet man nicht in einem Zustand von „unklar, was passiert ist“?
Wir haben es also nicht nur als Detektor, sondern als Test unseres Beobachtungsfundaments eingesetzt.
4.2. Speichermangelähnliche Phänomene auslösen
Auf einer gewöhnlichen Entwicklungsmaschine tatsächlich Speichermangel auszulösen, ist ziemlich umständlich. Wird zudem die ganze Maschine instabil, wird der Test selbst zunehmend von Störgeräuschen überlagert.
Deshalb haben wir uns entschieden, mit Low Resource Simulation gezielt die Failure Paths zu durchlaufen, die bei Speicher- oder Ressourcenmangel wahrscheinlich auftreten würden.
Damit lassen sich Fragen wie diese leichter beantworten.
- Bleiben
cameraIdundphaseim Log, wennCreateEventfehlschlägt? - Läuft der Cleanup korrekt, nachdem die Initialisierung nur halb abgeschlossen wurde?
- Bricht der Retry nicht zusammen, wenn
VirtualAllocfehlschlägt? - Kommt das Handle zurück, wenn
CreateFileauf dem Save-Path fehlschlägt?
Betonen möchten wir: Nicht das Auslösen der Anomalie selbst ist das Ziel, sondern dass sich der Ausfall im Ernstfall nachvollziehen lässt.
4.3. Prüfen, ob sich Handle-Anomalien im Ernstfall nachverfolgen lassen
Wie schon beim Handle-Leak aus Teil 1 gilt bei allem rund um Handles: die Stelle, an der es zuletzt abstürzt, und die eigentliche Ursache klaffen leicht auseinander.
Deshalb wollten wir Folgendes bestätigen.
- Lässt sich beim Auftreten eines invalid handle stop mit
!htraceOpen und Close nachvollziehen? - Lässt sich das mit
resourceId/sessionId/phaseaus dem eigenen Log verknüpfen? - Geht der Handle Count nach dem Fehlschlag wieder zurück?
- Sind Leck-Differenzen leicht erkennbar, wenn die Harness ein kurzlebiger Prozess ist?
Wird das sichtbar, kommt man von einem bloßen „ein Bug ist aufgetreten“ bis zu „in welcher Zuständigkeit die Lifetime-Verwaltung zusammengebrochen ist“.
5. Wie man speicher- oder ressourcenmangelähnliche Phänomene auslöst
5.1. Die Idee hinter Low Resource Simulation
Low Resource Simulation ist, salopp gesagt, Fault Injection. Statt eine Niedrig-Ressourcen-Umgebung originalgetreu nachzubilden, geht es eher darum, die typischen API-Fehlschläge, die bei knappen Ressourcen auftreten, künstlich einzumischen.
Der Einsatzbereich ist entsprechend klar umrissen.
- Prüfen des Aufräumverhaltens auf dem Failure Path
- Prüfen der Robustheit von Retry / Reconnect
- Prüfen der Initialisierung bei gemischtem Teilerfolg und Teilfehlschlag
- Prüfen, ob auch bei „normalerweise nie auftretenden Fehlern“ ein Log erhalten bleibt
Der Trick dabei ist, nicht von Anfang an alles gleichzeitig fehlschlagen zu lassen. Schaltet man sofort alles auf einmal ein, explodiert das Log, und man verliert den Überblick, „was man eigentlich betrachtet“.
5.2. Was sich zum Scheitern bringen lässt
Mit Low Resource Simulation lassen sich typischerweise die folgenden API-Klassen probabilistisch scheitern lassen.
| Klasse | Beispiel | Beispiel in einer Geräte-Steuerungsanwendung |
|---|---|---|
Heap_Alloc |
Heap-Allokation | Temporäre Puffer, Bild-Metadaten, interne Allokationen des SDK-Wrappers |
Virtual_Alloc |
Allokation virtuellen Speichers | Größere Framebuffer, Ringpuffer |
File |
CreateFile u. Ä. |
Öffnen von Speicherpfaden und Logdateien |
Event |
CreateEvent u. Ä. |
Frame-Ready-Benachrichtigung, Stop-/Reconnect-Synchronisation |
MapView |
CreateMapView u. Ä. |
Shared Memory und Memory-Mapped Files |
Ole_Alloc |
SysAllocString u. Ä. |
COM-/OLE-Grenze |
Wait |
WaitForXXX-Familie |
Rund um fehlgeschlagenes synchrones Warten |
Registry |
Registry-Zugriff | Lesen/Schreiben von Einstellungen und treibernahen Konfigurationen |
In der Praxis kommt es darauf an, nicht alles gleichzeitig zu öffnen, sondern gezielt mit dem einzusteigen, was dem aktuell interessierenden Failure Path am nächsten liegt.
5.3. Anwendung in der Praxis
Als Kommandozeilenbeispiel sieht das etwa so aus.
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
Reines Copy-Paste bringt nichts, wenn die Absicht dahinter nicht klar ist — deshalb hier Zeile für Zeile, was jeweils passiert.
| Befehl | Was er tut |
|---|---|
appverif /verify CameraHarness.exe |
Aktiviert die Basics-Testgruppe für CameraHarness.exe |
appverif /verify CameraHarness.exe /faults |
Aktiviert zusätzlich Fault Injection. Betrifft dabei aber nur OLE_ALLOC und HEAP_ALLOC |
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 ... |
Aktiviert lowres (Low Resource Simulation) und legt Art und Wahrscheinlichkeit der fehlschlagenden APIs einzeln fest |
appverif -query lowres -for CameraHarness.exe |
Zeigt an, was aktuell mit welcher Wahrscheinlichkeit eingestellt ist |
appverif /n CameraHarness.exe |
Entfernt die Einstellungen für diese EXE (dasselbe Ziel wie -disable * -for oder -delete settings -for) |
Auch die Bedeutung der Argumente sollte man kennen.
- Die Wahrscheinlichkeit wird in Millionsteln angegeben. Zulässig sind ganze Zahlen von 0 bis 1.000.000;
20000bedeutet20000 / 1.000.000, also 2 %. Es heißt also nicht „einmal alle 20.000 Aufrufe“. Auch Microsofts Dokumentation nennt-with registry=20000 file=20000als Beispiel dafür, Registry- und Datei-APIs mit 2 % Wahrscheinlichkeit fehlschlagen zu lassen. - Nach
/faultslassen sich Wahrscheinlichkeit, Karenzzeit und DLL-Name aneinanderreihen. Das Format ist/faults [Wahrscheinlichkeit [Karenzzeit in ms [DLL ...]]]. Lässt man die Wahrscheinlichkeit weg, gilt 5 %, lässt man die Karenzzeit weg, gelten 500 Millisekunden. Die Karenzzeit bedeutet „in dieser Zeit nach dem Prozessstart keine Fehler injizieren“ und soll verhindern, dass schon der Startvorgang selbst scheitert und sich gar nichts mehr testen lässt. /ndeaktiviert. Man kann sichnin etwa als „no verifier“ vorstellen — der Gegenbefehl, damit nichts dauerhaft aktiviert bleibt.
Die Ausgabe von -query lowres sieht ungefähr so aus. Hier lässt sich prüfen, ob die gewünschten Wahrscheinlichkeiten eingetragen sind und ob nicht versehentlich auch nicht gewollte Klassen geöffnet wurden.
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 und Exclude grenzen die Zielmodule ein, TimeOut ist die Zeit direkt nach dem Start, in der kein Fault injiziert wird. Die Standardwerte hängen davon ab, wie die Einstellung vorgenommen wurde — verlassen Sie sich nicht auf Annahmen, sondern prüfen Sie es sicherheitshalber anhand dieser Ausgabe.
Als Vorgehen bietet sich Folgendes an.
- Zuerst nur mit
Basicsden Normalpfad durchlaufen - Anschließend
Low Resource Simulationhinzufügen und mit Fault Injection durchlaufen - Bei Bedarf nur den gewünschten Fehlern wie
fileodereventeine Wahrscheinlichkeit geben - Soll gezielt eine bestimmte DLL angesprochen werden, die Injektion auf diese DLL beschränken
Die Abkürzung /faults ist praktisch, konzentriert sich allein aber hauptsächlich auf OLE_ALLOC und HEAP_ALLOC.
Wer die Failure Paths von CreateFile oder CreateEvent sehen möchte, ist mit -enable lowres -with file=... event=... sicherer bedient.
Bei Geräte-Steuerungsanwendungen liest sich das Ergebnis meist leichter, wenn man sich auf den Camera-Wrapper oder die DLL des Save-Path beschränkt, statt Fehler über die ganze Anwendung zu verstreuen.
Hier auch gleich die konkrete Schreibweise für dieses „auf eine DLL beschränken“. Das dritte und jedes weitere Argument nach /faults gibt das Zielmodul an.
appverif /verify CameraHarness.exe /faults 50000 1000 CameraSdkWrapper.dll
Damit lässt sich beim Start von CameraHarness.exe nur die 1000 Millisekunden nach dem Start beginnende Verarbeitung, die von CameraSdkWrapper.dll ausgeht, mit 5 % Wahrscheinlichkeit (50000 / 1.000.000) fehlschlagen. Der Modulname wird inklusive Dateiendung angegeben, ohne Pfad. Es lassen sich nicht nur .dll-, sondern auch andere geladene Module wie .ocx angeben.
Ob die Einschränkung wirkt, lässt sich an den Zeilen Include und Exclude von appverif -query lowres -for CameraHarness.exe prüfen. Steht dort noch *, ist weiterhin der gesamte Prozess betroffen.
Läuft der Prozess unter dem Debugger, lässt sich der Umfang auch nachträglich ändern.
!avrf -trg dll CameraSdkWrapper.dll
!avrf -skp dll VendorSdk.dll
-trg bedeutet „hier gezielt ansetzen“, -skp bedeutet „das hier auslassen“. Mit !avrf -flt lässt sich die aktuelle Fault-Injection-Einstellung prüfen, mit !avrf -flt stacks 10 der Stack der zuletzt injizierten Fehler.
So lassen sich beispielsweise Szenarien wie diese bauen.
CreateEvent-Fehlschlag unmittelbar nach Beginn eines ReconnectsCreateFile-Fehlschlag beim Beginn des Speicherns- Fehlschlag bei der Allokation eines temporären Puffers
SysAllocString-Fehlschlag bei einer COM-Konvertierung- Prüfung des Fehlerpfads von Wait-APIs
All das wird von reinen Normalpfad-Tests praktisch nie durchlaufen. Genau deshalb lohnt es sich, es gezielt zu erzwingen.
6. Wie man Handle-Anomalien betrachtet
6.1. Die Handles-Prüfung
Bei allem rund um Handles beginnt man mit Handles.
Damit lässt sich die Verwendung eines invalid handle leichter erkennen.
Typischerweise werden Fehler wie diese erkannt.
- Ein bereits geschlossenes Handle wird erneut verwendet
- Ein defekter Handle-Wert wird übergeben
- Ein durch einen Teilfehlschlag nicht initialisiertes Handle wird verwendet
- Die Lifetime bricht zusammen, und ein anderer Thread greift darauf zu
Was im Langzeitbetrieb nur als „gelegentlich tritt ein seltsamer Fehler auf“ sichtbar wäre, bringt der Verifier direkt an Ort und Stelle zum Stehen. Dieses Vorziehen hilft erheblich.
6.2. Open-/Close-Stacks mit !htrace betrachten
Handles ist deshalb so wertvoll, weil es gut mit Handle-Tracing zusammenspielt.
Ab hier kommt der Debugger zum Einsatz, deshalb installieren wir zuerst WinDbg. Es wird als Debugging Tools for Windows vertrieben und lässt sich über denselben Windows-SDK-Installer wie Application Verifier einrichten.
windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC
Die Optionen in der ersten Zeile sind keine bloße Beschwörungsformel. Application Verifier wirft beim Erkennen drei Arten von Ausnahmen.
| Option | Ausnahme | Wann sie auftritt |
|---|---|---|
av |
Zugriffsverletzung (0xC0000005) |
Bei erkanntem Pufferüberlauf im Heap |
ch |
Ungültiges Handle (0xC0000008) |
Bei erkannter Verwendung eines invalid handle |
sov |
Stack-Überlauf (0xC00000FD) |
Wenn der anfängliche Stack als zu klein erkannt wird |
-xd legt fest, diese Ausnahme erst beim second chance abzufangen. Der Hintergrund: den first chance verarbeitet Application Verifier selbst, um die Stop-Informationen aufzubauen — greift der Debugger schon davor ein, stört das. In einem bereits laufenden Debugger entspricht das der Eingabe von sxd av, sxd ch, sxd sov.
Mit !htrace möchte man im Wesentlichen Folgendes sehen.
- Wo dieses Handle geöffnet wurde
- Wo es geschlossen wurde
- Ob es als invalid handle referenziert wurde
- Ob sich mehr Opens angehäuft haben, als erwartet
Auch das reale Erscheinungsbild sei hier gezeigt. Die folgenden Beispiele stammen aus der offiziellen Dokumentation und nicht aus unserer eigenen Umgebung, die Form bleibt aber dieselbe.
Trifft man auf ein invalid handle, erscheint zunächst dies.
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.
===================================================
Gibt man anschließend !avrf ein, erscheint, was aktuell aktiv ist und welcher Stop aufgetreten ist. Die letzte Zeile ist die Zusammenfassung.
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).
Betrachtet man die Historie dieses Handles mit !htrace, erscheinen OPEN, CLOSE und BAD REFERENCE jeweils mit ihrem Stack aufgelistet.
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
--------------------------------------
Die Lesart ist unkompliziert: Kommt nach CLOSE ein BAD REFERENCE, wird ein bereits geschlossenes Handle erneut verwendet. Der OPEN-Stack zeigt, wo dieses Handle erzeugt wurde.
Das Tückische an Handle-Leaks und Handle-Fehlverwendung ist, dass die API, die zuletzt gestolpert ist, nicht die eigentliche Ursache ist.
Mit !htrace lässt sich die Historie dieses Handles ziemlich konkret nachvollziehen.
6.3. Kombination mit eigenen Logs
Trotzdem reicht Application Verifier allein nicht aus. Insbesondere die Leck-Untersuchung einer lange laufenden, dauerhaft aktiven EXE allein damit zu bewältigen, ist ziemlich mühsam.
Deshalb kombiniert man in der Praxis Folgendes.
- Regelmäßig erfassten
Handle Count sessionIdresourceIdphase- Lifecycle-Logs von Create/Open und Close/Dispose
- Dumps und Debugger-Ausgaben zum Zeitpunkt eines verifier stop
Damit lässt sich zum Beispiel so vorgehen.
- Der Heartbeat zeigt, dass der Trend des
Handle Countverdächtig ist - Das Lifecycle-Log grenzt Ressourcen ein, die zwar ein
Create, aber keinClosehaben - Ein Verifier-Run bringt invalid handle oder Fehlverwendung vorzeitig zum Vorschein
!htracezeigt den Open-/Close-Stack
Diese Kombination macht die Nachverfolgung erheblich einfacher.
7. Aufbau eines Testfundaments für Failure Paths
7.1. Die Ausführungseinheit auf eine Harness ausrichten
Application Verifier lässt sich nicht nachträglich auf einem bereits laufenden Prozess aktivieren. Erst konfigurieren, dann starten.
Zudem bleibt die Einstellung bestehen, bis sie ausdrücklich entfernt wird. In der Praxis lässt sich das deshalb leichter handhaben, wenn man sich eher an einer Test-Harness-EXE als an der eigentlichen Produktivanwendung orientiert.
Zum Beispiel eine Konstellation wie diese.
flowchart LR
A[Szenario-Runner] --> B[CameraHarness.exe]
B --> C[CameraSdkWrapper.dll]
C --> D[Vendor-SDK]
B --> E[Strukturiertes Log]
B --> F[Dump / Debugger]
Damit ergeben sich diese Vorteile:
- Ein Szenario lässt sich pro Prozess ausführen
- Leck-Differenzen sind leicht erkennbar
- Das Ein- und Ausschalten der AppVerifier-Einstellung ist einfach
- Auch beim Testen einer DLL lässt sich das über die EXE-Seite handhaben
Die Befehle sehen etwa so aus.
appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe
/verify aktiviert Basics, /n entfernt die Einstellung (siehe auch die Tabelle in 5.3).
Aktivierung vor dem Start, Deaktivierung ausdrücklich.
Läuft man das auf Basis einer Harness, lassen sich Konfigurationsfehler leichter vermeiden.
7.2. Das Testmenü aufteilen
In einem Testfundament für Failure Paths ist es besser, nicht alles in einem Durchgang zu erledigen. Eine Aufteilung in etwa diese drei Spuren macht das Ergebnis leichter lesbar.
- Normalpfad + Basics
- Keine Fehler injizieren
- Bestätigen, dass kein verifier stop auftritt
- Fault-Injection-Spur
Low Resource Simulationevent/file/heap_alloc/virtual_allocusw. gezielt fehlschlagen lassen
- Heap-Vertiefungsspur
Heaps- full page heap
- Lokal unter dem Debugger reproduzieren
Trennt man das, geraten „bricht es schon bei normaler Nutzung“ und „bricht es nur bei geringen Ressourcen“ nicht so leicht durcheinander.
Besonders mit und ohne Fault Injection ändern sich die durchlaufenen Code-Pfade erheblich. Deshalb sollte man sowohl den Run ohne Fault als auch den Run mit Fault durchführen.
7.3. Was man erfasst
Mindestens Folgendes sollte man festhalten.
| Kategorie | Was benötigt wird |
|---|---|
| Anwendungslog | cameraId, sessionId, phase, handleCount, error code |
| Prozesszustand | Handle Count, Private Bytes, Thread Count |
| Debugger-Informationen | !avrf, !htrace, bei Bedarf !heap -p -a |
| Dumps | Bei einem verifier stop oder einem abnormalen Beenden |
| AppVerifier-Logs | Aufzeichnung der Stops, bei Bedarf zur Auswertung nach XML exportiert |
Bei Bedarf lassen sich auch die AppVerifier-Logs nach XML exportieren und auswerten. Allein daraus lässt sich die Ursache jedoch meist nicht abschließend klären — praxistauglicher ist die Annahme, sie gemeinsam mit den eigenen Logs zu lesen.
Viele Logs sind für sich genommen kein Verdienst. Wichtig ist, dass sich der Zusammenhang später herstellen lässt.
7.4. Abnahmekriterien
Auch als Abnahmekriterium ist „ist nicht abgestürzt“ zu schwach. In diesem Kontext brauchten wir mindestens Folgendes.
- Im Normalpfad +
Basicstritt kein verifier stop auf - Auch bei Fault Injection bleibt der erwartete Fehlschlag im Log erhalten
- Halb initialisierte Ressourcen werden ordentlich aufgeräumt
- Nach Reconnect / Retry kehrt
Handle Countnahe an die Baseline zurück - Tritt ein verifier stop auf, lässt er sich über
sessionId/phase/ Stack nachverfolgen - Kein Fehlschlag endet als „unklar, was passiert ist“
Wichtig ist dabei, „bricht nicht“ und „lässt sich im Fall des Zusammenbruchs nachverfolgen“ getrennt zu bewerten.
7.5. Hinweise
Application Verifier ist zwar sehr praktisch, aber keine Magie.
- Nicht tatsächlich durchlaufene Codepfade werden nicht geprüft
- full page heap ist schwergewichtig
- Auch auf Seiten eines Third-Party-SDK können Stops auftreten
- Mit und ohne Fault Injection unterscheiden sich die durchlaufenen Codepfade erheblich
- Es ist kein Werkzeug, um rein managed Heap-Leaks allein damit zu untersuchen
Die eigene Position lässt sich deshalb so beschreiben.
- Langzeittrend: eigene Logs und Counter
- Fehlverwendung an der nativen Grenze: Application Verifier
- Wiederherstellung der Kausalität im Ernstfall: strukturiertes Log + Dump + Debugger
Diese Arbeitsteilung ist die praxistauglichste.
8. Grober Entscheidungsleitfaden
- invalid handle oder double close wird vermutet
Handles+!htrace
- heap corruption / use-after-free wird vermutet
Heaps+ full page heap +!heap -p -a
- Speicher- oder ressourcenmangelähnliche Phänomene sollen ausgelöst werden
Low Resource Simulation
- Etwas bricht im Langzeitbetrieb allmählich zusammen
- Zuerst das eigene
Handle Count/Private Bytes/ Lifecycle-Log
- Zuerst das eigene
- Eine DLL soll getestet werden
- Application Verifier auf der Harness-EXE aktivieren, die diese DLL aufruft
Schaltet man von Anfang an alles gleichzeitig ein, entsteht meist nur ein Nebel aus Logs. Mit der Klinge anzusetzen, die dem gewünschten Failure Path am nächsten liegt, ist deutlich verständlicher.
9. Zusammenfassung
Application Verifier positioniert sich als Runtime-Verifier für die native/Win32-Grenze von Windows. Mit Handles / Heaps / Locks / Memory / TLS / Low Resource Simulation und mehr lassen sich Failure Paths, die sonst kaum auftreten, vorziehen und gezielt durchlaufen.
In diesem Kontext hat sich vor allem ausgezahlt, dass sich Handle-Anomalien im Ernstfall mit !htrace leicht nachverfolgen lassen, dass sich speicher- oder ressourcenmangelähnliche Phänomene auslösen lassen, ohne die ganze Maschine zu zerstören, und dass wir dabei prüfen konnten, ob unsere eigenen Logs im Ernstfall wirklich nützlich sind.
In der Praxis läuft es so ab, dass man Normalpfad + Basics von der Fault-Injection-Spur trennt, eine Harness-EXE bereitstellt und Szenarien in kurzlebigen Prozessen durchlaufen lässt. Darauf aufbauend kombiniert man das mit eigenen Logs, Dumps und Debugger-Informationen, während man den Trend von Langzeit-Lecks selbst mit eigenen Countern beobachtet — das ist die Arbeitsteilung.
Application Verifier ist ein Werkzeug, um „selten auftretenden Anomalien“ nicht zufällig entgegenzuwarten, sondern ihnen aktiv entgegenzugehen.
Bei Geräte-Steuerungsanwendungen ist es wichtig, dass nichts zusammenbricht — genauso wichtig ist aber, im Ernstfall erklären zu können, was passiert ist. In diesem Sinne halten wir es für ein durchweg praxistaugliches Werkzeug.
10. Quellen
- Teil 1: Untersuchung eines Absturzes einer Industriekamera-Anwendung nach Langzeitbetrieb - Der Handle-Leak (Teil 1)
- Application Verifier - Overview
- Application Verifier - Testing Applications
- Application Verifier - Tests within Application Verifier
- Application Verifier - Debugging Application Verifier Stops
- Application Verifier - Features
- !htrace (WinDbg)
- !avrf (WinDbg)
- Download Debugging Tools for Windows
- Windows SDK herunterladen
- GetProcessHandleCount-Funktion (processthreadsapi.h)
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Untersuchung eines Absturzes einer Industriekamera-Anwendung nach Langzeitbetrieb - Der Handle-Leak (Teil 1)
Wie man vorgeht, wenn eine Windows-Anwendung nach langem Dauerbetrieb plötzlich abstürzt: anhand eines Fallbeispiels einer Industriekamer...
Process Monitor (ProcMon) Praxisleitfaden — „Konfiguration wird nicht gelesen“ und ACCESS DENIED in 10 Minuten identifizieren
„Ich habe die Konfigurationsdatei korrigiert, aber die Änderung wird nicht übernommen“ – „Bis gestern lief es noch, aber heute startet es...
Mit PerfView und dotnet-trace die Ursache von „langsam“ finden — Praxis-Einstieg in die .NET-Performanceanalyse
Wenn eine Geschäftsanwendung langsam ist, die CPU auslastet oder gelegentlich einfriert: Welches Werkzeug zeigt was? Dieser Beitrag ordne...
Design für Windows-Apps, das bei einem Absturz Protokolle und Dumps hinterlässt
Wie sich normale Protokolle, ein letzter Absturzmarker, WER LocalDumps und ein Überwachungsprozess kombinieren lassen, damit auch bei ein...
Einführung in das Sammeln von Windows-Absturzabbildern - WER/ProcDump/WinDbg
Um schwer reproduzierbare Abstürze von Windows-Anwendungen aufzuklären, ordnen wir ein, wann WER LocalDumps, ProcDump, MiniDumpWriteDump ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Fehleranalyse und Langzeitprobleme
Sporadische Fehler, Kommunikationsdiagnose, Langzeitabstürze und Tests von Fehlerpfaden.
Verwandte Fallstudien
Diese Fallstudien zeigen einen ähnlichen Ansatz für Analyse, Priorisierung oder Neugestaltung.
Testinfrastruktur für Fehlerpfade mit Application Verifier
Fallstudie zum Aufbau einer Grundlage für Fehlerpfadtests, die spätere Untersuchungen erleichtert.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Fehleruntersuchung und Ursachenanalyse
Application Verifier und Fundamente für Failure-Path-Tests sind ein zentrales Thema unserer Fehleruntersuchung und Ursachenanalyse, die die Reproduktion von Störungen und die Identifikation ihrer Ursachen vorantreibt.
Technische Beratung und Design-Review
Wenn Sie klären möchten, wie weit Failure-Path-Tests und Beobachtungspunkte in Ihr Design einfließen sollten, lässt sich das im Rahmen einer Technischen Beratung und eines Design-Reviews angehen.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ist Application Verifier?
- Es ist ein Laufzeitverifikationswerkzeug für Windows-User-Mode-Anwendungen. Es überwacht, wie eine laufende Anwendung OS-APIs nutzt und mit Ressourcen umgeht, erkennt verdächtige Verwendungen wie den Einsatz eines invalid handle oder heap corruption und kann gezielt Fehler injizieren. Anders als statische Analyse oder Unit-Tests ist es ein Werkzeug, das zeigt, wie etwas tatsächlich zusammenbricht, wenn dieser Codepfad wirklich durchlaufen wird — deshalb eignet es sich hervorragend, um Failure Paths aufzudecken, die im gewöhnlichen Funktionstest unsichtbar bleiben.
- Lässt sich mit Application Verifier ein Speichermangel nachbilden?
- Mit Low Resource Simulation lassen sich Phänomene nahe an Speicher- oder Ressourcenmangel vorziehen, ohne wirklich das RAM der Maschine aufzubrauchen. Der Mechanismus dahinter ist Fault Injection: API-Aufrufe wie HeapAlloc, VirtualAlloc, CreateFile oder CreateEvent werden mit einer festgelegten Wahrscheinlichkeit absichtlich zum Scheitern gebracht. Da sich die Fehlerinjektion auch gezielt auf einzelne DLLs beschränken lässt, ist das auch bei Konstellationen praktikabel, in denen eigene Wrapper mit Vendor-SDKs gemischt sind. Lässt man von Anfang an alles gleichzeitig scheitern, wird das Log allerdings unlesbar — der Trick ist, gezielt mit dem Failure Path zu beginnen, den man tatsächlich sehen möchte.
- Lässt sich Application Verifier zur Untersuchung von Handle-Leaks einsetzen?
- Aktiviert man die Handles-Prüfung, lässt sich die Verwendung eines invalid handle wie die Wiederverwendung eines bereits geschlossenen Handles erkennen, und Handle-Tracing wird automatisch mitaktiviert, sodass sich mit !htrace der Open-/Close-Stack dieses Handles nachvollziehen lässt. Die Leck-Untersuchung einer lange laufenden, dauerhaft aktiven EXE allein Application Verifier zu überlassen, ist allerdings unrealistisch. Praxistauglich ist eine Arbeitsteilung, bei der regelmäßig protokollierte Handle Counts und ein eigenes Log des Ressourcen-Lifecycle die Trend-Erkennung übernehmen, während der Verifier die Erkennung von Fehlverwendungen übernimmt.
- Wie setzt man Application Verifier zum Testen einer DLL ein?
- Aktiviert wird Application Verifier nicht auf der DLL selbst, sondern auf der Test-EXE, die diese DLL tatsächlich ausführt. Ein bereits laufender Prozess lässt sich nicht nachträglich aktivieren — die Einstellung muss vor dem Start vorgenommen werden. Da die Einstellung außerdem bestehen bleibt, bis sie ausdrücklich entfernt wird, lässt sie sich leichter handhaben, wenn man sie eher auf eine Test-Harness-EXE als auf die eigentliche Produktivanwendung ausrichtet. Führt man ein Szenario pro Prozess aus, lassen sich Leck-Differenzen leichter erkennen, und auch das Ein- und Ausschalten der Einstellung fällt leichter.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.