Application Verifier для программиста: тестирование Windows-приложений

    Возможно в Вашем проекте и не пишут try { /* code */ } catch(...) { } для того чтобы избежать исключений при работе с памятью, умеют закрывать хендлы и знают о виртуализации Windows Vista, а программы никогда не падают по непонятным и редко повторяемым причинам.

    Тогда Вам повезло, можете переходить к следующему топику.

    Но иногда происходят, казалось бы, странные вещи. Программа «падает» на ровном месте, память куда-то утекает, а еще один раз вам звонили с жалобой на странное поведение программы, работающей на сервере 24/7, но вы конечно «завернули» их проблему, убедив, что она аппаратно-зависимая, и ладно. Всё-таки разработка программ под Windows дело нередко хитрое, и от ошибок по невнимательности или из-за незнания архитектуры никто не застрахован. Я не буду учить, как этих ошибок не допускать — сам не знаю. Но вот одно средство для эффективной отладки могу посоветовать.

    Речь пойдет о Microsoft Application Verifier. Но это не отладчик. Напротив, без отладчика, сама по себе, штука относительно бесполезная. А вот в совокупности с ним позволяет детектировать ряд важных платформо-зависимых проблем. Кроме того, не удастся получить сертификат «Сompatible with windows 7» без прохождения тестирования с использованием AppVerifier (собственно для “Vista Certified” так же, но об этом, видимо, говорить не принято). А этот сертификат — для пользователя некоторая гарантия, что получившая его программа, может лучше и не сделает, но хотя бы не навредит. Ладно, «вода» закончилась, приступим к делу.

    Способ применения


    Скачать и установить AppVerifier для Хаброчеловека, уверен, не сложность. Запустим (из под real-администратора, под Vista+ по-другому и не выйдет) его графическую оболочку:



    Слева список приложений для тестирования; справа – список секций на проверку для выбранного приложения. В MSDN утверждается, что AppVerifier предназначен для тестирования программ на C++, но в целом применим для любого native кода.

    Графическая оболочка не производит никаких тестов, только дает возможность выбора нужных пунктов. Сами проверки реализуются благодаря так называемым «слоям», динамически подключаемым библиотекам vfbasics, vfcompat, vfLuaPriv, vfprint (на них можно полюбоваться в папке system32). При запуске тестируемого приложения они подключаются к нему и перехватывают вызов системных функций, таких как HeapAlloc, GetTickCount, CloseHandle и многих других. Перехватчик производит ряд дополнительных проверок, затем вызывает оригинальную функцию, и поэтому, за исключением нескольких рассматриваемых далее случаев, это не скажется на работе тестируемого приложения. Разве что будет заметна некоторая потеря производительности. Субъективно в худшем случае программа «замедлится» в пять раз, а нужны ли какие-нибудь конкретные цифры или нет – оставлю на ваше усмотрение.

    Здесь есть важная особенность: несмотря на то, что мы при добавлении выбираем файл тестируемого приложения, проверки привязываются только к его имени без пути. С одной стороны, можно не беспокоиться в какой конфигурации (и в какую папку) собирать проект (обычно папки для Debug и Release разные), но с другой – можно забыть об установленных проверках, и запуская программу с рабочего стола, удивляться, что она «не работает».

    Про смысл тест-пунктов поговорим чуть позже, а сейчас добавим, к примеру notepad.exe и установим все галки. Запустим блокнот, добавим пару строчек, попробуем сохранить. О-па, неудача:



    Не единственный исход ситуации, возможно, вы получите другое окно предупреждения, или вообще обойдется без него. Что же случилось? Обратимся снова к графической надстройке AppVerifier. На сей раз выберем пункт Logs из главного меню, увидим список лог-файлов ассоциированных с тестируемыми приложениями. По логу на запуск.



    Физически эти лог файлы находятся в папке AppVerifierLogs в корне пользовательского профайла. Прочитать их голыми руками будет трудно (бинарный формат), поэтому тыкаем кнопочку “View” для соответствующего лога. Произойдет его дамп в xml и открытие дефолтной программы просмотра для xml:



    Для тех кто внимательно следил: ошибка изображенная на этом скриншоте не соответствует сообщению об ошибке (которе является нормальным поведением программы) с предыдущего скриншота, а происходит чуть позже.

    Тут и краткое описание проблемы, и stack trace. И от меня подсказка, как искать ошибки, а не предупреждения. Кстати сказать, если ошибки присутствуют, то программа не получает сертификации на совместимость с Vista/Win7. Постойте, но это же блокнот?! Ну да, только тссс.

    Лечение больного


    Теперь запускаем отладчик. Пусть это будет отладчик, встроенный в студию, или бесплатный WinDbg из состава Debugging Tools for Windows (он конечно более навороченный, но сейчас это не имеет значения).

    А вот и наш больной:
    int _tmain(int argc, _TCHAR* argv[])
    {  
      int *p = new int();
       delete p;
      *p = 0; // p = 0 will be OK, but *p = 0 is error!
    }

    Потенциальную опасность этого фрагмента легко оценить, если бы строки с delete и перезаписью памяти были растянуты по времени. Но ни в дебажной, ни в релизной сборке такая проблема не детектируется (Visual Studio, конфигурация по умолчанию).

    Теперь добавляем программу на тестирование группы Basics в Application Verifier. И запускаем её из под отладчика (из студии по F5, например). AppVerifier заговорил с нами голосом студии:



    А в Debug Output показывается соответствующее структурное исключение:
    =======================================
    VERIFIER STOP 00000013: pid 0xB54: First chance access violation for current stack trace.

    02B59FF8 : Invalid address causing the exception.
    0082142F : Code address executing the invalid access.
    0013F670 : Exception record.
    0013F68C : Context record.

    =======================================

    Оно рассказывает, что за исключение (00000013), с каким адресом памяти (02B59FF8) и по какому адресу кода (0082142F) произошло. Счастливчикам, скачавшим Windows Debug Symbols покажут и место в исходном коде, где произошла проблема и Stack Trace, который привел к исключению.

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

    Детектируемые проблемы


    Давайте теперь разберемся, какие проблемы позволит выявить нам AppVerifier. Все опции тестирования разделены на группы. Исключая группу «Low Resource Simulation» и тестов «TimeRollOver» и «HighVersionLie» проверки не меняют поведения приложения (в случае, если не будет обнаружено ошибок).

    1. Искажающие проверки

    1.1. Low Resource Simulation

    Вот она причина падения блокнота. Тесты этой группы позволяют смоделировать поведение системы при нехвате ресурсов. Приложению запросто могут отказать (по датчику случайных чисел) в выделении памяти, создании файла, Event'а, окна, записи в реестр. Обычно есть некоторое «спокойное» время около 2-5 секунд, когда приложению разрешается пользоваться ресурсами в полную силу; сделано это, чтобы приложение вообще смогло запуститься (это придумали не так давно, раньше было грустнее). Нормальным поведением программы является стабильность; показ предупреждающих диалогов, но не «падения». Так что в коде нужно бы предусматривать данные ситуации.

    1.2. TimeRollOver в группе Misc

    Рассмотрим следующий пример кода, который выполняет действие action несколько раз, но не более одной секунды:
    DWORD time_end = GetTickCount() + 1000; // 1s timelimit
    do { action(); } while (GetTickCount() < time_end);

    Подвох виден невооруженным глазом; если time_end очень близко к DWORD_MAX, но меньше чем DWORD_MAX-1000, а action() иногда выполняется больше секунды, то цикл проработает немного дольше, чем хотелось бы. А именно 50 суток (DWORD_MAX / (1000 * 60 * 24)).

    И это не единственный случай, что Вы скажете про следующий фрагмент?
    char buf[8];
    sprintf(buf, "%i", GetTickCount());

    Для диагностики подобных проблем проверка TimeRollOver «прогоняет» значение функции GetTickCount() быстрее. Полный цикл до обнуления значения проходит за 5 минут.

    1.3. HighVersionLie в группе Compatibility

    Если вдруг Вы пользуетесь функцией GetVersionEx, то этот тест поможет вам обнаружить ветки кода с некорректной проверкой допустимой версии ОС.

    OSVERSIONINFO osvi;
    ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    GetVersionEx(&osvi);

    BOOL bIsWindowsXP_or_Later = (osvi.dwMajorVersion >= 5) && (osvi.dwMinorVersion >= 1);
    if (!bIsWindowsXP_or_Later)
        printf("Windows XP or later required.\n");


    В данном фрагменте допущена явная ошибка; с целью отсечь Windows 2000 (5.0) вводится дополнительная проверка на minor версию XP (5.1), но код также отбрасывает и Windows Vista (6.0). На Windows 7 (6.1) работать будет. Неужели это и есть причина плохой совместимости с Windows Vista? Microsoft утверждает, что 70% несовместимых с Vista программ не работают в том числе и из-за этой проблемы.

    Но диагностика такой ситуации на компьютере разработчика затруднительна — у него одна, фиксированная версия ОС. Можно воспользоваться виртуальной машиной с другой версией ОС, а можно просто ткнуть галку HighVersionLie. Тогда значение GetVersionEx будет модифицировано (обычно по правилу dwMajorVersion += 3; dwMinorVersion = 0).

    2. Немодифицирующие проверки

    2.1. Memory в группе Basics

    Проверка корректности вызовов HeapAlloc, GlobalAlloc и других API Windows Heap Manager. За утечками памяти не следит, но это можно решить другими способами.

    2.2. TLS в группе Basics

    Следит за корректностью вызовов Thread Local Storage API.

    2.3. Exceptions в группе Basics

    Следит за уместностью перехвата исключений, в частности попытки «заглушить» исключения Access Violation, «демаскирует» исключения в заглушках вида try { } catch(...) { } .

    2.4. Handles в группе Basics

    Следит за допустимостью операций над хендлами, корректностью хендлов и их временем жизни. Чуть подробнее на английском.

    2.5. Locks в группе Basics

    Проверяет корректность использования критических секций, не допускает сброс критической секции из другого потока относительно установки критической секции.

    2.6. DirtyStacks в группе Misc

    Периодически заполняет неиспользуемую часть стека паттерном 0xCD, что позволяет обнаруживать неинициализированные переменные или параметры функций.

    2.7. DangerousAPIs в группе Misc

    Оповещает об использовании и нежелательных потенциально опасных функций API навроде TerminateThread.

    2.8. LuaPriv

    Limited-user-account privileges test. Проверяет, нужны ли программе административные привилегии, не выполняет ли программа действий, которые допустимы только для real-администратора.

    Состоит из двух частей: предсказывающей (перечисляет все действия программы, которые может выполнить только администратор) и диагностической (отказывает программе в административных действиях с ошибкой ACCESS_DENIED). Таким образом, программисту не обязательно тестировать программу отдельно логинясь гостем. Также проверяет ряд особенностей связанных с виртуализацией под Windows Vista и старше.

    Заключение


    AppVerifier — интересный инструмент, позволяющий выявить и решить ряд «плавающих» и «скрытых» (а иногда и специально спрятанных) проблем. Пользоваться им в целом не сложно, при определнных навыках — удобно. А если вы хотите получить сертификат «Windows compatible», то знакомства с ним не избежать. Лично мне помог уже на двух проектах, надеюсь будет полезен и Вам.

    *All source code was highlighted with Source Code Highlighter.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 20
    • 0
      Программа функциональная, но отделам бэта-тестирования волноваться, думаю, рано)
      • 0
        Это так, пока разработка самого AppVerifier'а ведется в сторону увеличения функциональности и покрытия тестами потенциальных проблем, но тулза всё равно остается удобной только программистам в силу специфики использования. А на этапе бета-тестирования уже поздно узнавать о неправильном вызове CloseHandle…

        Хотя мы внедрили применение тулзы в отделе тестирования, и результат в скором времени очень порадовал :)
        • 0
          А где работаете, если не секрет?
          • +1
            ответил в личку.
          • 0
            У нас уже второй год все беты при установки принудительно делают запись в реестре, дабы запускаться под верифаером. Эту запись отрезают только в релиз кандидатах.
        • +2
          А как быть программам, написанным на C#? Для них другая тулза или такие программы вообще не могут получить логотип? ))
          • +1
            Для managed-кода чаще всего используется FxCop, тоже от MS.
            • 0
              Могут, и скорее всего на них есть какие-то дополнительные требования, но проверки AppVerifier'ом никто не отменяет — и да, группа Basics заведомо проходится любым приложением на C# без unmanaged (а для unmanaged имеют смысл почти все тесты кроме Exceptions), но от ошибок LuaPriv не застраховано не одно приложение имеющее доступ к файлам, реестру, ShellExecute.
            • +2
              Было бы неплохо в самом начале статьи показать область — что это Win32/Win64, поскольку «Application Verifier is a runtime verification tool for unmanaged code.»
              А для дотнета есть FxCop + StyleCop.
              Ну и в целом — средства статического анализа не ограничиваются перечисленным, есть и другие.
              • 0
                Спасибо. Модифицировал теги к топику. На самом деле не стоит так жестко отметать применимость AV к managed коду, он все равно использует тот же API.

                А FxCop решает немного другую задачу, но у managed кода и другие цели. Но инструмент более чем достойный внимания.
              • +5
                Возможно в Вашем проекте и не пишут try { /* code */ } catch(...) { }

                Если бы вы тока знали....
                • 0
                  А вот я не понял, чем оно так уж плохо? Поясните?
                  • +2
                    Процитирую
                    … произойти все что угодно в любой строке. Тогда программист начинает писать вещи типа
                    try {  
                       dosomething(); 
                    } 
                    catch(...){
                       /*do nothing*/ 
                    }
                    которые гасят все исключения, и вот тогда это действительно становится проблемой. Потому что его код, работая в среде, где все доверяют исключениям, начинает врать, что все хорошо, даже когда все плохо. Никогда не съедайте все исключения, съедайте только те, за которые отвечает ваша функция.

                    И от себя добавлю пару слов. На проекте с действительно широкой аудиторией, написанном изначально весьма посредственно, в один из дней решено было обернуть подозрительную область в такой try{}. Потом появился второй, третий… Подобный стиль, в купе с несинхронизированной многопоточностью, привёл к появлению креш репортов указывающих на, скажем, объявление переменной типа int. Стало в порядке вещей, что во время выполнения метода класса, где-то в его серединке, указатель this занулялся, или принимал жуткое значение, указывающие в никуда. Это всё признаки покорапченного стека и прочей нечести.

                    Всегда при ошибке нужно как минимум делать запись в лог, что бы потом найти её источник. Подобный try{}catch{} — это поведение страуса который прячет голову в песок прячась от проблем.
                    • 0
                      О! вот именно этого /*do nothing*/ мне и не хватало для понимания. В таком варианте — это действительно зло.
                • 0
                  Для получения логотипа в логе не должно быть ни одного Warning-a, Error-а?
                  • +1
                    Ни одного Error'а. Просто некоторые из Warning'ов уже к следующей версии AppVerifier'а могут превратиться в Error'ы, особенно, скажем, связанные с DangerousAPI (функция может стать deprecated). Но, поскольку сейчас проверка выполняется стороной самих разработчиков, достаточно просто убедиться что нет Errors перед отправкой лога.
                    Если тема Вас заинтересовала могу посоветовать документ от майкрософт.
                    • 0
                      Спасибо за ссылку.
                      Попробовал прогнать на простейшем приложении на базе WTL, включил LuaPriv, получил кучу ошибок и предупреждений, все связаны с ATL. Стоит ли на это обращать внимание?
                      • 0
                        Если стек вызова растет из MessageBoxW то можете смело игнорировать, но это не ошибка WTL (про это я уже задавал вопрос представителям Microsoft на конференции по совместимости с Vista). Хотелось бы каких-то частностей, чтобы можно было исследовать вопрос.
                  • 0
                    А что с картинками к статье? :(
                    • +1
                      исправил… ох уж этот дропбокс.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.