Application Verifier: инфраструктура тестов нештатных путей Windows

· Обновлено: · · Разработка Windows, Расследование сбоев, Промышленная камера, Application Verifier, Тестирование нештатных путей, Утечка хендлов

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI: 10.5281/zenodo.21619656)

Статья заархивирована на Zenodo. Ниже приведены DOI, который всегда ведёт к последней версии, и DOI, закреплённый за версией, которую вы читаете.

Го Комура (2026). Application Verifier: инфраструктура тестов нештатных путей Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21619656 https://comcomponent.com/ru/blog/2026/03/11/003-application-verifier-abnormal-test-foundation-part2/

DOI (последняя версия)
10.5281/zenodo.21619656
DOI (эта версия)
10.5281/zenodo.21619657

Application Verifier — сильный инструмент, когда нужно заранее вытащить наружу аномалии, которые возникают в нативном коде Windows и на границе Win32. Особенно если нужно тестировать аномалии хендлов, повреждение кучи и failure path при нехватке ресурсов, он довольно быстро поднимает на поверхность проблемы, которых не видно при одном лишь тестировании штатного пути.

В первой части, «Утечка хендлов: почему приложение промышленной камеры падает после месяца работы», мы разобрали случай, когда причиной падения управляющего приложения после долгой работы оказалась утечка хендлов. Но усилить логи — это только половина дела. По-настоящему нужно заранее проверить, останется ли в будущем состояние «понятно, что произошло», если из-за очередной непредвиденной ошибки программиста случится утечка памяти, утечка хендлов, частичный отказ или пропущенное освобождение.

Именно для этого мы использовали Application Verifier. Это инструмент, который на этапе выполнения может вставлять проверки и fault injection в код, работающий в нативном Windows и на границе Win32. Особенно удобно на практике то, что можно заранее вызвать поломки, похожие на нехватку памяти или ресурсов, не выжигая реальную память машины.

Во второй части разберём, что такое 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 и отладчик
    • 3.4. !avrf / !htrace / логи
  4. Зачем мы внедрили его в этот раз
    • 4.1. Цель — не только «найти баг»
    • 4.2. Вызвать явление, похожее на нехватку памяти
    • 4.3. Проверить, можно ли проследить аномалию хендла, когда она случится
  5. Как вызывать явления, похожие на нехватку памяти и ресурсов
    • 5.1. Идея Low Resource Simulation
    • 5.2. Что можно заставить отказать
    • 5.3. Как применять это на практике
  6. Как смотреть на аномалии хендлов
    • 6.1. Проверка Handles
    • 6.2. Смотреть стек open / close через !htrace
    • 6.3. Как сочетать это с собственными логами
  7. Как строить инфраструктуру тестов нештатных путей
    • 7.1. Перенести единицу запуска в harness
    • 7.2. Разделить меню тестов
    • 7.3. Что собирать
    • 7.4. Критерии приёмки
    • 7.5. На что обратить внимание
  8. Как выбирать, вкратце
  9. Итог
  10. Справочные материалы

Карта знаний этой статьи

Application Verifier — инструмент рантайм-проверки user-mode приложений Windows: во время выполнения он следит за вызовами API ОС и управлением ресурсами и сообщает об использовании недействительного хендла и повреждении кучи как verifier stop. Fault injection под именем Low Resource Simulation с заданной вероятностью валит вызовы API, поэтому failure path при нехватке ресурсов можно пройти заранее, не вызывая настоящую нехватку памяти. Если включить проверку Handles, автоматически работает handle tracing: историю хендла смотрят через !htrace в WinDbg, настройки и stop — через !avrf. Целевой EXE нужно настроить до запуска, задним числом включить нельзя. Расследование утечки хендлов в долго работающем приложении нельзя отдавать одному Application Verifier, поэтому на практике сочетают отдельный harness EXE и собственные логи.

Карта знаний: тесты нештатных путей с Application VerifierРисунок показывает, как Application Verifier через fault injection Low Resource Simulation и handle tracing обнаруживает использование недействительного хендла и повреждение кучи как verifier stop; как причину можно проследить через !avrf и !htrace в WinDbg; и как на практике включают verifier для отдельного harness EXE, а расследование утечки хендлов при долгой работе нельзя отдавать одному Application Verifier без собственных логов.используетиспользуетиспользуетиспользуетиспользуетпроверяетсяпроверяетсяпроверяетсяпроверяетсяпроверяетсяможет вызватьможет вызватьрекомендуется дляне рекомендуетсятребуетнастраиваетсярекомендуется дляApplication Verifierтестирование нештатных путейfault injectionLow Resource Simulationhandle tracingWinDbgиспользование недействительного хендлаповреждение кучи (heap corruption)Page Heapverifier stop!avrf (расширение WinDbg)паттерн harness EXEутечка хендлов

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 17, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

1. Сначала вывод

  • Application Verifier — инструмент, который упрощает выявление во время выполнения неправильного использования на неуправляемой / нативной границе Windows
  • Ценность не только в том, чтобы «найти баг», но и в том, чтобы заранее вызвать редко проявляющиеся нештатные пути
  • Handles ловит invalid handle, Heaps проявляет повреждение кучи, Low Resource Simulation делает fault injection ситуаций, похожих на нехватку памяти и ресурсов
  • Полностью отдать расследование утечки долго работающего EXE одному Application Verifier — плохая идея; реалистичный путь — сочетать его с собственными логами Handle Count и жизненного цикла ресурсов
  • В инфраструктуре тестов нештатных путей результаты проще читать, если разделить прогон verifier по штатному пути и прогон с fault injection
  • Даже если нужно проверить DLL, Application Verifier включают для тестового EXE, который реально её запускает

Иначе говоря, Application Verifier — это инструмент, который вытаскивает на свет противные баги вокруг native / Win32-границы Windows. Он особенно хорошо стыкуется с миром приложений управления оборудованием, где native SDK, P/Invoke и Win32 API обычно смешаны.

Две роли Application VerifierУ Application Verifier две роли — ловить неправильное использование на нативной границе и заранее вызывать редко проявляющиеся нештатные пути. Первое даёт invalid handle и повреждение кучи, второе — fault injection ситуаций, похожих на нехватку памяти.Application VerifierЛовить неправильное использование на нативной границеЗаранее вызывать редкие нештатные путиinvalid handle и повреждение кучиВнедрение ситуации, похожей на нехватку памяти

Рис. 1: Две опоры Application Verifier — «ловить неправильное использование» и «заранее вызывать нештатные пути».

2. Что такое Application Verifier

2.1. Если коротко

Application Verifier — это инструмент рантайм-проверки для user-mode приложений Windows. Он следит, как работающее приложение вызывает API ОС и обращается с ресурсами: ловит подозрительное использование и умеет намеренно вносить отказы.

В отличие от «статического анализа» и «модульных тестов», это инструмент, который показывает, как код ломается, когда данный путь реально пройден. Поэтому он хорошо подходит, чтобы вытащить failure path, невидимые в обычном функциональном тестировании.

Тестовый harnessУправляющее приложение / обёртка SDKApplication VerifierWin32 API / native DLL / ресурсы ОСverifier stopвывод отладчикалоги AppVerifierСобственный structured log

Рис. 2: Application Verifier наблюдает управляющее приложение, которое гоняет тестовый harness, и оставляет результат как verifier stop, вывод отладчика и логи.

2.2. В каких ситуациях он полезен

Особенно хорошо он работает в таких ситуациях.

  • вызываете native DLL или SDK камеры;
  • пересекаете границы P/Invoke или COM;
  • прямо или косвенно много используете хендлы, кучу, блокировки, виртуальную память;
  • на обычном штатном пути почти не падает, но на нештатных путях управление временем жизни выглядит хрупким;
  • «изредка возвращает странный отказ» проявляется раньше, чем «падает».

И наоборот, это не инструмент для того, чтобы ходить по графу объектов в чисто управляемом мире. Поэтому даже в C#-приложении он сильно помогает, если граница native SDK или Win32 толстая, но одним им утечку чисто управляемой кучи целиком не разобрать.

Когда Application Verifier полезен, а когда нетЕсли проблема на границе native SDK или Win32, Application Verifier полезен. Если нужно ходить только по графу объектов в чисто управляемом мире, это уже другой инструмент.native SDK или граница Win32Чисто управляемый object graphНа каком слое проблема?Application Verifier полезенНе тот инструмент (смотреть другим)

Рис. 3: Развилка — толщина native / Win32-границы. Это не инструмент, чтобы смотреть только управляемый мир.

2.3. В чём практическая польза

На практике польза сводится примерно к трём пунктам.

  1. Быстро останавливать неправильное использование на нативной границе
    • invalid handle
    • повреждение кучи
    • неправильное использование блокировок
    • неправильное использование API виртуальной памяти и т. д.
  2. Заранее вызывать поломки, которые проявляются только при нехватке ресурсов
    • эквиваленты malloc изредка отказывают;
    • CreateEvent и CreateFile изредка отказывают;
    • отказывает VirtualAlloc.
  3. Проще расследовать в связке с отладчиком
    • !avrf
    • !htrace
    • !heap -p -a
    • логи verifier stop

В приложениях управления оборудованием мешает именно «непонятно, что произошло на нештатном пути». Application Verifier довольно хорошо уменьшает эту «непонятность».

Три практические выгодыБыстро останавливать неправильное использование на нативной границе, заранее вызывать поломки, которые видны только при нехватке ресурсов, и проще расследовать в связке с отладчиком.Application VerifierБыстро останавливать неправильное использованиеЗаранее вызывать форму поломкиПроще расследовать в отладчикеРасширения вроде avrf и htrace

Рис. 4: На практике выгода сводится к раннему обнаружению, заранее вызванным нештатным путям и связке с отладчиком.

2.4. Как достать и включить у себя

Сначала закроем вопрос, как собрать инструмент. Без этого весь дальнейший разговор остаётся теорией.

Application Verifier поставляется в составе Windows SDK. В самой Windows его нет, поэтому запускаете установщик SDK и на экране выбора компонентов отмечаете «Application Verifier». Имя исполняемого файла — appverif.exe.

Перед использованием есть три условия.

  • пользователь, который запускает, входит в группу Administrators на этой машине;
  • ARM64EC не поддерживается;
  • проверяемый код — неуправляемый (нативный).

Связь GUI и командной строки удобно держать в голове так.

  Что делает
GUI (appverif.exe) Пишет в реестр имя целевого EXE и набор включаемых тестов
Командная строка (appverif -enable ...) Пишет ровно ту же настройку реестра командой
Во время выполнения При старте целевого EXE по этой настройке загружается DLL verifier и ставятся хуки Win32 API

То есть что GUI, что командная строка — делают одно и то же. Для первого ручного раза удобнее GUI, для CI и скриптов — командная строка.

Связь GUI и командной строкиИ GUI, и командная строка только пишут одну и ту же настройку в реестр. При старте целевого EXE эта настройка читается, загружается DLL verifier и ставятся хуки Win32 API.GUI (appverif.exe)Пишет настройку в реестрКомандная строкаЧитается при старте целевого EXEЗагрузка DLL verifier и хуки

Рис. 5: GUI и командная строка пишут одну и ту же настройку в реестр. Хуки встают при старте целевого EXE.

В GUI: в левой колонке Applications правый щелчок → «Add Application», добавляете целевой EXE, справа в Tests отмечаете, например, Basics, и жмёте «Save». Снимаете так же: в Applications правый щелчок → «Delete Application», затем «Save».

Из этого следуют два важных ограничения.

  • Для уже работающего процесса включить задним числом нельзя. Хуки встают при загрузке DLL, поэтому порядок такой: сначала настройка, затем запуск.
  • Настройка живёт, пока её явно не удалят. Если «один раз попробовали» и забыли, на этой машине этот EXE будет стартовать под verifier всегда.

Логи обнаружения по умолчанию пишутся в бинарном виде в %USERPROFILE%\AppVerifierLogs; GUI или командной строкой их можно превратить в XML и сводить.

Порядок включения и то, что настройка остаётсяХуки встают при загрузке DLL, поэтому для уже работающего процесса включить задним числом нельзя. Порядок — сначала настройка, затем запуск. Настройка живёт, пока её явно не удалят.Пишем настройкуЗапускаем целевой EXEРаботает под verifierНастройка живёт, пока не удалятЕсли забыть, всегда стартует под verifierУже работающий процессВключить задним числом нельзя

Рис. 6: Порядок «сначала настройка, затем запуск» ломать нельзя. Настройка остаётся на машине, пока её явно не удалят.

3. Что умеет Application Verifier

3.1. Basics: Handles / Heaps / Locks / Memory / TLS и другое

Базовый набор Application Verifier — это Basics. Здесь собраны проверки, которые чаще всего нужны на практике.

Слой Что смотрит Где это нужно в нашем контексте
Handles Использование недействительных хендлов Не наступаем ли на уже закрытый / повреждённый handle
Heaps Повреждение кучи Вытащить порчу буфера и use-after-free на границе native SDK
Leak Ресурсы, не освобождённые к выгрузке DLL Тесты короткоживущего harness и случаи с unload
Locks / SRWLock Неправильное использование блокировок Проверка гонки reconnect и shutdown
Memory Неправильное использование VirtualAlloc / MapViewOfFile и подобного Аномалии вокруг больших буферов и разделяемой памяти
TLS Неправильное использование API Thread Local Storage Подстраховка для нативного кода со сложными границами потоков
Threadpool Согласованность API threadpool и состояния worker Подспорье, когда много callback и асинхронной обработки

Суть не в том, чтобы «понять по логам уже после падения», а в том, чтобы остановить подозрительное использование на месте. Для дефектов длительной работы такое опережение очень помогает.

Идея раннего обнаружения в BasicsНе читать логи уже после падения, а останавливать подозрительное использование на месте — так дефекты длительной работы всплывают заранее.Подозрительное использование APIНабор проверок BasicsОстановить на местеПроблема всплывает заранееРаньше читали только после падения

Рис. 7: Ценность Basics в том, что «читать после падения» сменяется на «остановить на месте».

3.2. Low Resource Simulation: заранее вызвать нехватку памяти и ресурсов

На практике это особенно удобный кусок. Потому что можно вызвать явление, близкое к нехватке памяти или ресурсов, не выжигая реальный RAM.

Идея простая.

  • берём определённый вызов API;
  • с определённой вероятностью;
  • намеренно заставляем его отказать.

Так можно пройти error path, которые обычно почти никогда не проходятся.

Конкретно так становится легко намеренно вызывать такие явления.

  • отказывают HeapAlloc и VirtualAlloc;
  • отказывает CreateFile;
  • отказывает CreateEvent;
  • отказывает MapViewOfFile;
  • отказывают выделения OLE/COM вроде SysAllocString.

Это заметно удобнее, чем пытаться реально вызвать нехватку памяти и мучить всю машину. Более того, можно нацелить fault injection на конкретную DLL. Для конфигураций вроде приложений управления оборудованием, где смешаны собственные обёртки и vendor SDK, это весьма практично.

Как устроен Low Resource SimulationОпределённый класс вызовов API с заданной вероятностью намеренно завершается отказом. Так можно пройти error path, которые обычно не проходятся. Цель можно сузить до конкретной DLL.ДаНетВызов APIПопали в заданную вероятность?Намеренно вернуть отказОбработать как обычноНа error path, который обычно не проходятМожно сузить до конкретной DLL

Рис. 8: Low Resource Simulation по сути — fault injection, который с заданной вероятностью валит вызов API.

3.3. Page Heap и отладчик

Для повреждения кучи сильна связка Heaps и page heap. В частности, full page heap за счёт guard page даёт преимущество — останавливаться близко к моменту повреждения.

Однако это довольно тяжеловесно. Вместо длительного полного перебора удобнее сузиться до сценариев, близких к воспроизведению, и гонять их под отладчиком.

Поэтому реалистичен такой порядок работы.

  • сначала широко включить Basics;
  • если куча выглядит подозрительно, использовать full page heap;
  • если слишком тяжело, опуститься до light page heap;
  • для длительных тестов, близких к продакшену, опираться в основном на собственные логи.

В конечном счёте AppVerifier — не волшебная палочка, а инструмент, у которого лезвие меняют под задачу.

Как выбирать page heapСначала широко включают Basics. Если куча подозрительна, останавливают в момент повреждения через full page heap. Если слишком тяжело — light page heap. Длительные тесты, близкие к продакшену, смотрят в основном своими логами.ДаНетДаНетШироко включить BasicsКуча подозрительна?Остановить через full page heapДлительные тесты — в основном свои логиСлишком тяжело?Опуститься до light page heapЛокально воспроизвести под отладчиком

Рис. 9: Page heap не держат включённым всегда. Сначала широко Basics, потом меняют лезвие под задачу.

3.4. !avrf / !htrace / логи

Сначала одно слово. verifier stop, который уже несколько раз встречался, — это событие обнаружения: Application Verifier решил, что «так использовать нельзя». Это не просто строка лога: если процесс идёт под отладчиком, он останавливается на месте. У stop есть номер, он показывается как VERIFIER STOP 00000300. Одни stop можно продолжить, другие — нельзя (остаётся только завершить процесс).

Application Verifier не заканчивает работу, выдав stop. Есть расширения отладчика и логи, поэтому проще понять, что произошло.

  • !avrf
    • смотреть текущие настройки verifier и stop, который происходит сейчас;
  • !htrace
    • смотреть стеки open / close / обращения к недействительному хендлу;
  • !heap -p -a
    • в связке с page heap идти по повреждённому блоку кучи;
  • логи AppVerifier
    • можно сохранить логи на момент возникновения stop.

Особенно удобно, что при включении Handles автоматически включается handle tracing. Тогда потом проще выяснить, где этот хендл открыли и где закрыли.

От verifier stop к расследованиюПри обнаружении неправильного использования выходит verifier stop с номером. Под отладчиком процесс останавливается на месте. Настройки и stop смотрят через avrf, историю хендла — через htrace.Обнаружено неправильное использованиеverifier stop (с номером)Под отладчиком — остановavrf: настройки и stophtrace: история handleЕсть stop, которые можно продолжить, и которые нельзя

Рис. 10: verifier stop — не просто строка лога. Под отладчиком это останов на месте и точка входа в расследование.

4. Зачем мы внедрили его в этот раз

4.1. Цель — не только «найти баг»

Цель в этот раз была не просто «найти один баг с помощью AppVerifier». Говоря практичнее, мы хотели проверить следующее.

  • если в будущем на каком-то другом failure path снова случится утечка ресурса;
  • останется ли в логах нужный контекст;
  • сможем ли мы дойти до конца вместе с информацией отладчика;
  • не окажемся ли в состоянии «непонятно, что произошло».

Иначе говоря, мы использовали его не только как детектор, но и как тест собственной инфраструктуры наблюдения.

4.2. Вызвать явление, похожее на нехватку памяти

По-настоящему вызвать нехватку памяти на обычной машине разработки довольно хлопотно. Более того, если из-за этого вся машина станет нестабильной, сам тест захлебнётся в шуме.

Поэтому мы пошли через Low Resource Simulation: намеренно наступать на failure path, которые с высокой вероятностью вызывает нехватка памяти или ресурсов.

Это заметно упрощает ответы на такие вопросы.

  • если CreateEvent отказывает, остаются ли в логах cameraId и phase?
  • действительно ли после половинчатой инициализации выполняется clean up?
  • если VirtualAlloc отказывает, не ломает ли состояние повторная попытка?
  • возвращается ли хендл, если CreateFile отказывает на пути сохранения?

Хотим подчеркнуть: сама по себе цель — не вызвать аномалию, а сделать так, чтобы форму поломки можно было прочитать, когда она случится.

Проверка инфраструктуры наблюдения через fault injectionLow Resource Simulation намеренно проводит через отказ. Проверяют, остаётся ли в логах контекст, выполняется ли зачистка, не ломает ли состояние повтор. Цель — не вызвать аномалию, а убедиться, что форму поломки можно прочитать.Намеренно пройти через отказОстаётся ли в логах контекст?Выполняется ли clean up?Не ломает ли повтор?Состояние, в котором форму поломки можно прочитать

Рис. 11: Цель fault injection — не вызвать аномалию, а проверить, читаема ли форма поломки, когда она случится.

4.3. Проверить, можно ли проследить аномалию хендла, когда она случится

Как и с утечкой хендлов из первой части, вокруг хендлов место, где всё в итоге упало, и истинная причина легко расходятся.

Поэтому мы хотели подтвердить следующее.

  • если выходит stop из-за invalid handle, можно ли через !htrace проследить open / close;
  • связывается ли это с resourceId / sessionId / phase в собственных логах;
  • возвращается ли handle count после отказа;
  • легче ли читать разницу утечки, если harness сделать короткоживущим процессом.

Если это видно, можно перейти от простого «баг проявился» к тому, у какой именно ответственности разъехалось управление временем жизни.

Можно ли проследить аномалию хендлаКогда выходит invalid handle stop, проверяют, можно ли через htrace проследить open и close, связать это с контекстом собственных логов и увидеть, вернулся ли handle count — и дойти до ответственности, у которой разъехалось время жизни.invalid handle stopЧерез htrace проследить open и closeСвязать с контекстом своих логовПроверить, вернулся ли handle countНайти ответственность, у которой разъехалось время жизни

Рис. 12: При аномалии хендла не останавливаться на «баг проявился», а проверять, можно ли дойти до ответственности, у которой разъехалось время жизни.

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 и подобное Открытие путей сохранения и файлов логов
Event CreateEvent и подобное Уведомление о готовности кадра, синхронизация 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%. Это не «один раз на 20 000 вызовов». В документации 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, ограничить injection этой DLL.

Сокращение /faults удобно, но само по себе оно сосредоточено в основном на OLE_ALLOC и HEAP_ALLOC. Если нужно посмотреть failure path CreateFile или CreateEvent, надёжнее явно прописать -enable lowres -with file=... event=....

В приложениях управления оборудованием часто проще читать результаты, если сузить injection до обёртки камеры или DLL пути сохранения, а не разбрасывать отказы по всему приложению.

Порядок, в котором накладывают fault injectionСначала штатный путь только с Basics, затем добавляют Low Resource Simulation и гоняют с fault injection, задают вероятность только интересующим отказам и при необходимости сужают до конкретной DLL.Штатный путь только с BasicsДобавить Low Resource и прогнатьЗадать вероятность только интересующим отказамПри необходимости сузить до конкретной DLL

Рис. 14: Не целиться сразу. От штатного пути с Basics постепенно сужают fault injection.

Конкретная запись «сузить до DLL» тоже стоит положить рядом. Третий и последующие аргументы /faults — это целевые модули.

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

Тогда при старте CameraHarness.exe после 1000 миллисекунд от запуска отказывать с вероятностью 5% (50000 / 1 000 000) будут только операции, начавшиеся из CameraSdkWrapper.dll. Имя модуля пишут с расширением, путь не ставят. Кроме .dll можно указать и загружаемый модуль вроде .ocx.

Сработало ли сужение, смотрят по строкам Include и Exclude в appverif -query lowres -for CameraHarness.exe. Если там по-прежнему *, целью всё ещё является весь процесс.

Как работает fault injection, суженный до DLLВ аргументах faults задают вероятность, льготное время и целевой модуль. После льготного времени от старта отказывают только операции, начавшиеся из указанной DLL. Сужение проверяют по Include и Exclude в query.Задать вероятность, льготное время и имя DLLПосле старта льготное время injection нетОтказывают только операции из указанной DLLПроверить по Include и Exclude в query

Рис. 15: Fault injection, суженный до DLL, после льготного времени валит только операции, начавшиеся из указанного модуля.

Если процесс идёт под отладчиком, диапазон можно менять уже на ходу.

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

-trg — «целиться сюда», -skp — «это пропустить». Через !avrf -flt смотрят текущие настройки fault injection, через !avrf -flt stacks 10 — стеки недавних внедрённых отказов.

Например, можно собрать такие сценарии.

  • отказ CreateEvent сразу после начала reconnect;
  • отказ CreateFile при начале сохранения;
  • отказ выделения временного буфера;
  • отказ SysAllocString при преобразовании COM;
  • проверка путей отказа API ожидания.

Обычным тестированием штатного пути сюда почти никогда не попасть. Именно поэтому намеренно наступать на них оправданно.

6. Как смотреть на аномалии хендлов

6.1. Проверка Handles

Для всего, что связано с хендлами, начинают с Handles. Это упрощает ловлю использования недействительных хендлов.

Типично он ловит такие происшествия.

  • повторное использование уже закрытого handle;
  • передачу повреждённого значения handle;
  • использование handle, который так и не инициализировали из-за частичного отказа;
  • обращение из другого потока при разъехавшемся времени жизни.

Там, где долгая работа показала бы лишь «изредка появляется странная ошибка», под verifier можно остановиться прямо на месте. Это опережение помогает очень сильно.

Какие происшествия ловит проверка HandlesПовторное использование уже закрытого хендла, повреждённое значение, неинициализированный хендл после частичного отказа, неправильное использование из другого потока из-за разъехавшегося времени жизни — под verifier это останавливается на месте.Повторное использование после closeverifier stop на местеПовреждённое значение handleНеинициализированный handleНеправильное использование из-за разъехавшегося времени жизни

Рис. 16: Происшествия, которые при долгой работе выглядят лишь как «изредка странная ошибка», проверка Handles останавливает на месте.

6.2. Смотреть стек open / close через !htrace

Handles ценен ещё и тем, что хорошо стыкуется с handle tracing.

Дальше нужен отладчик, поэтому сначала ставят WinDbg. Он распространяется как Debugging Tools for Windows и ставится из того же установщика Windows SDK, что и Application Verifier.

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

Параметры первой строки — не заклинание. Application Verifier при обнаружении бросает три вида исключений.

Параметр Исключение Когда выходит
av Нарушение доступа (0xC0000005) Обнаружено переполнение буфера кучи
ch Недействительный хендл (0xC0000008) Обнаружено использование invalid handle
sov Переполнение стека (0xC00000FD) Решено, что начального стека не хватает

А -xd значит ловить это исключение на second chance. First chance обрабатывает сам Application Verifier, собирая информацию stop, поэтому если отладчик вмешался раньше, это мешает. Если отладчик уже запущен, то же самое делают команды sxd av, sxd ch, sxd sov.

Через !htrace обычно хотят увидеть следующее.

  • где этот хендл открыли;
  • где его закрыли;
  • использовали ли его как недействительный хендл;
  • не накапливается ли 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
--------------------------------------

Читать прямо: если после CLOSE идёт BAD REFERENCE, закрытый хендл использовали ещё раз. По стеку OPEN видно и место, где хендл создали.

Утечки хендлов и неправильное использование неприятны тем, что API, на котором всё в итоге упало, не является истинной причиной. С !htrace историю этого хендла можно проследить довольно конкретно.

Как читать историю хендла через htracehtrace выстраивает OPEN, CLOSE и BAD REFERENCE — каждый со стеком. Если после CLOSE идёт BAD REFERENCE, закрытый хендл использовали ещё раз. По стеку OPEN видно, где его создали.OPEN (где создали)CLOSE (где закрыли)BAD REFERENCEЗначит, закрытый handle использовали ещё разУ каждой записи есть стек

Рис. 17: Читать htrace прямо: если после CLOSE стоит BAD REFERENCE, закрытый хендл использовали ещё раз.

6.3. Как сочетать это с собственными логами

Тем не менее одного Application Verifier недостаточно. В частности, вести расследование утечки долго работающего EXE только на нём одном довольно тяжело.

Поэтому на практике сочетают следующее.

  • периодический Handle Count;
  • sessionId;
  • resourceId;
  • phase;
  • логи жизненного цикла create/open и close/dispose;
  • дампы и вывод отладчика в момент verifier stop.

Тогда можно идти, например, так.

  1. heartbeat показывает, что наклон Handle Count подозрителен;
  2. логи жизненного цикла сужают круг до ресурса, у которого есть Create, но нет Close;
  3. прогон verifier заранее выявляет invalid handle или неправильное использование;
  4. !htrace показывает стеки open / close.

Эта комбинация заметно упрощает расследование.

Порядок связки своих логов и verifierПо heartbeat замечают наклон Handle Count, по lifecycle log сужают ресурс без Close, прогоном verifier заранее вытаскивают неправильное использование, через htrace смотрят стеки open и close.Заметить наклон Handle CountСузить ресурс по lifecycle logПрогоном verifier заранее вытащить неправильное использованиеСмотреть стек через htrace

Рис. 18: Наклон ловят своими логами, неправильное использование — verifier. Связывают в таком порядке.

7. Как строить инфраструктуру тестов нештатных путей

7.1. Перенести единицу запуска в harness

Application Verifier нельзя включить задним числом для уже работающего процесса. Сначала настройка, затем запуск.

К тому же настройка живёт, пока её явно не удалят. Поэтому на практике удобнее целиться в тестовый harness EXE, а не в само продакшен-приложение.

Например, такая конфигурация.

Scenario RunnerCameraHarness.exeCameraSdkWrapper.dllVendor SDKStructured LogДамп / отладчик

Рис. 19: Конфигурация harness: один сценарий — один процесс. Цель verifier — не DLL, а harness EXE, который её запускает.

Это даёт такие преимущества:

  • можно гонять по одному сценарию на процесс;
  • разницу утечки легко видеть;
  • легко переключать включение и выключение настроек AppVerifier;
  • при проверке DLL можно работать со стороны EXE.

Команды выглядят так.

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

/verify включает Basics, /n удаляет настройку (см. также таблицу в 5.3). Включение — до запуска, снятие — явное. Если гонять это, исходя из harness, легче избежать и происшествий с настройками.

7.2. Разделить меню тестов

В инфраструктуре тестов нештатных путей лучше не делать всё за один прогон. Разделение примерно на три направления делает результаты понятнее.

  1. Штатный путь + Basics
    • не вносят ни одного отказа;
    • подтверждают, что verifier stop не возникает.
  2. Направление fault injection
    • Low Resource Simulation;
    • целенаправленно вызывают отказы event / file / heap_alloc / virtual_alloc и подобных.
  3. Направление углублённого анализа кучи
    • Heaps;
    • full page heap;
    • воспроизводят локально под отладчиком.

Такое разделение не даёт смешаться «ломается ли это при обычном использовании» и «ломается ли это только при нехватке ресурсов».

Наличие или отсутствие fault injection особенно сильно меняет проходимые пути кода. Поэтому стоит гонять и прогон без fault, и прогон с fault.

Меню тестов из трёх направленийТесты делят на три направления: штатный путь плюс Basics без внедрения отказов, fault injection через Low Resource Simulation и углублённый анализ кучи с full page heap под отладчиком.Как гонять тесты нештатных путейШтатный путь и BasicsНаправление fault injectionУглублённый анализ кучиПодтвердить, что stop не возникаетВнедрить целевой отказЛокально воспроизвести под отладчиком

Рис. 20: Не делать всё за один прогон. Три направления меню не дают смешать, где именно ломается.

7.3. Что собирать

Как минимум стоит фиксировать следующее.

Категория Что нужно
Логи приложения cameraId, sessionId, phase, handleCount, код ошибки
Состояние процесса Handle Count, Private Bytes, Thread Count
Информация отладчика !avrf, !htrace, при необходимости !heap -p -a
Дампы В момент verifier stop или при аварийном завершении
Логи AppVerifier Записи stop, при необходимости экспорт в XML для сведения

При необходимости логи со стороны AppVerifier тоже можно выгрузить в XML и сводить. Но одних их часто недостаточно, чтобы замкнуть причину, поэтому практичнее читать их вместе с собственными логами.

Сам по себе большой объём логов ничего не стоит. Важно, чтобы причинно-следственную связь можно было восстановить позже.

7.4. Критерии приёмки

Критерий приёмки «не упало» тоже слишком слаб. В этом контексте нам требовалось как минимум следующее.

  • в прогоне штатного пути + Basics нет verifier stop;
  • даже при fault injection ожидаемые отказы остаются в логах;
  • половинчато инициализированные ресурсы аккуратно приводятся в порядок;
  • после reconnect / retry Handle Count возвращается близко к baseline;
  • при возникновении verifier stop его можно проследить по sessionId / phase / стеку;
  • ни один отказ не превращается в «непонятно, что произошло».

Здесь важно оценивать «не ломается» и «прослеживаемо, когда сломалось» как отдельные оси.

Две оси критериев приёмкиКритерии приёмки делят на ось «не ломается» — нет verifier stop на штатном пути, ресурсы зачищаются — и ось «прослеживаемо, когда сломалось» — ожидаемый отказ остаётся в логе, его можно пройти по контексту и стеку.Критерии приёмкиНе ломаетсяПрослеживаемо, когда сломалосьstop не возникаетРесурсы зачищаютсяОтказ остаётся в логеМожно пройти по стеку

Рис. 21: Одного «не упало» мало. «Не ломается» и «можно проследить» оценивают как разные оси.

7.5. На что обратить внимание

Application Verifier весьма удобен, но это не волшебство.

  • пути кода, которые реально не прошли, не проверяются;
  • full page heap тяжеловесен;
  • stop может возникать и внутри стороннего SDK;
  • проходимые пути кода заметно отличаются с fault injection и без;
  • это не единственный инструмент для расследования утечки чисто управляемой кучи.

Поэтому позиционирование такое.

  • наклон долгой работы — собственные логи и счётчики;
  • неправильное использование на нативной границе — Application Verifier;
  • восстановление причинно-следственной связи при аномалии — structured log + дамп + отладчик.

Такое разделение труда наиболее практично.

Общая картина разделения труда в расследованииНаклон долгой работы смотрят своими логами и счётчиками, неправильное использование на нативной границе — Application Verifier, восстановление причинно-следственной связи при аномалии — structured log, дамп и отладчик.Наклон долгой работыСвои логи и счётчикиНеправильное использование на нативной границеApplication VerifierВосстановление причинно-следственной связи при аномалииЛоги, дамп и отладчик

Рис. 22: Application Verifier — не волшебная палочка. Под то, что хотите увидеть, инструменты разделяют.

8. Как выбирать, вкратце

  • Подозреваете недействительный хендл или двойной close
    • Handles + !htrace
  • Подозреваете повреждение кучи / use-after-free
    • Heaps + full page heap + !heap -p -a
  • Хотите вызвать явление, похожее на нехватку памяти или ресурсов
    • Low Resource Simulation
  • Постепенно ломается при долгой работе
    • сначала собственные Handle Count / Private Bytes / лог жизненного цикла
  • Хотите проверить DLL
    • включите Application Verifier для harness EXE, который вызывает эту DLL

Если сразу включить всё подряд, обычно получается туман из логов. Намного понятнее подставлять лезвие, ближайшее к интересующему failure path.

9. Итог

Позиционирование Application Verifier — рантайм-проверка native / Win32-границы Windows. С помощью Handles / Heaps / Locks / Memory / TLS / Low Resource Simulation и остального можно заранее провести через редко проявляющиеся failure path.

В этом контексте окупились следующие моменты: аномалии хендлов стало легко прослеживать через !htrace в момент их возникновения; явления, похожие на нехватку памяти и ресурсов, удалось вызывать, не ломая всю машину; и мы смогли подтвердить, действительно ли собственные логи будут полезны в этот момент.

Практический способ прогона: разделить прогон штатного пути + Basics и прогоны с fault injection, подготовить harness EXE и гонять сценарии короткоживущими процессами. Дополнительно сочетать это с собственными логами, дампами и информацией отладчика, а сам наклон долгих утечек смотреть своими счётчиками — таково разделение труда.

Application Verifier — это инструмент, чтобы не ждать редкую аномалию, а выходить ей навстречу.

В приложениях управления оборудованием важно не только не ломаться, но и одинаково важно уметь объяснить, что произошло, когда что-то всё же сломалось. В этом смысле мы считаем его весьма практичным инструментом.

Часть 1: Утечка хендлов: почему приложение промышленной камеры падает после месяца работы

10. Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

В этих примерах используется сходный подход к анализу, расстановке приоритетов или переработке.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое Application Verifier?
Инструмент рантайм-проверки для user-mode приложений Windows. Он следит, как работающее приложение вызывает API ОС и обращается с ресурсами: ловит подозрительное использование вроде invalid handle или повреждения кучи и умеет намеренно вносить отказы. В отличие от статического анализа и модульных тестов, это инструмент, который показывает, как код ломается, когда данный путь реально пройден. Поэтому он хорошо подходит, чтобы вытащить failure path, невидимые в обычном функциональном тестировании.
Можно ли с помощью Application Verifier воспроизвести нехватку памяти?
С Low Resource Simulation можно заранее вызвать явление, близкое к нехватке памяти или ресурсов, не выжигая реальный RAM машины. Механизм — fault injection: вызовы API вроде HeapAlloc, VirtualAlloc, CreateFile, CreateEvent намеренно завершаются отказом с заданной вероятностью. Можно нацелить отказы и на конкретную DLL, поэтому это удобно и в конфигурации, где смешаны собственная обёртка и vendor SDK. Если сразу заставить отказывать всё подряд, логи станет невозможно читать, поэтому начинают с того, что ближе к интересующему failure path.
Можно ли использовать Application Verifier для расследования утечки хендлов?
Если включить проверку Handles, можно ловить использование недействительных хендлов — например, повторное использование уже закрытого хендла. При этом автоматически включается handle tracing, и через !htrace можно посмотреть стек open / close этого хендла. Но полностью отдать расследование утечки долго работающего EXE одному Application Verifier нереалистично. На практике его сочетают с периодической записью Handle Count и собственными логами жизненного цикла ресурсов: наклон ловят своими логами, неправильное использование — verifier.
Как использовать Application Verifier для тестирования DLL?
Application Verifier включают для тестового EXE, который реально запускает эту DLL. Включить его для уже работающего процесса задним числом нельзя: сначала настройка, затем запуск. Настройка сохраняется, пока её явно не удалят, поэтому удобнее целиться в тестовый harness EXE, а не в само продакшен-приложение. Если гонять один сценарий на один процесс, разницу утечки легче увидеть, а включение и выключение настройки — легче переключать.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог