воскресенье, 22 января 2012 г.

PVS под лупой

В этой заметке будет рассмотрен PVS-Studio более внимательно. Для оценки работы инструментов статического анализа я создал несколько простеньких примеров и собираюсь рассказать как себя на них проявил этот инструмент. Итак, тестируется версия PVS-Studio 4.51, работа общего анализатора.


Для начала попытаемся его запустить на каком-либо файле без использования VS:
"C:\Program Files\PVS-Studio\x86\PVS-Studio.exe" --source-file "F:\tools_testing\static\C\calc_checks\calc_checks.c" --cfg Settings.xml --output-file out.txt
Incorrect parameter syntax: unknown option яю<
Оно немного странно, но ладно, можно создать проект для VS и посмотреть.

Итак, из того, что PVS смог найти:
  • случай возврата указателя на локальный объект
  • отсутствие возвращаемого значения
  • выход за границу статического значения
  • присваивание внутри условного оператора (например if( a = true ))
  • множественное изменение одной переменной
К сожалению, почти полностью отсутствует работа с памятью. Не определяются случаи утечек, двойного освобождения или некорректного применения new и delete[]. Вообще, оно не декларировалось в списках возможностей, но могло бы значительно повысить качество анализа.

Также отсутствует анализ использования переменных. Например, не проверяется факт инициализации переменной или факт её неиспользования.

Отсутствует проверка соответствия типов, что тоже несколько напрягает. Особенно странно, что на моих примерах не обработался typedef. [прим автора: фраза вычёркивается, т.к. пруфа нет, а без него смысл теряется.]

Резюме: довольно странно, что в инструменте анализа C++ не реализованы обработки многих распространённых ошибок. Поэтому пока что он плохо подходит для общего анализа кода (тот же свободный cppcheck выполняет многое из перечисленного). Сейчас я не стал бы покупать PVS для улучшения качества своих приложений, а воспользовался бы альтернативными решениями.

UPD: ошибка при запуске без создания VS проекта была из-за неправильного параметра в --cfg. Необходимо, чтобы там был файл со строками вида
exclude-path = C:\Program Files (x86)\Microsoft Visual Studio 10.0
vcinstalldir = C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\
platform = Win32
Причём если используется UTF-8 то необходим режим без BOM. Иначе PVS ругнётся на некорректные параметры.

13 комментариев:

  1. У меня уже заготовлен ответ. :)

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

    http://www.viva64.com/ru/b/0119/

    ОтветитьУдалить
    Ответы
    1. Не могу полностью с Вами согласиться. В рамках Вашей статьи:

      1. "Программисты думают, что не допускают простых ошибок."
      Тестирование проводилось на тривиальных примерах. Цели обмануть анализатор любым способом не ставилось. Например на вход подавались такие функции:

      void func5()
      {
      int * a = (int *) malloc(20 * sizeof(int));
      free(a);

      a[10] = 10;
      *a = 5;

      free(a);
      }

      или такие:

      void func3()
      {
      int * a = new int[10];
      delete a;
      }

      Последнее, кстати, встречалось в реальном проекте почти в таком же виде.

      2. "Составленные примеры носят случайный характер."
      Примеры были подобраны по распространённым ошибкам, которые делают неопытные (а иногда даже очень опытные) программисты. Да, примеров пока что слишком мало, но надо же с чего-то начинать.

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

      И да, табличка составляется ) Медленно, правда, но занятие это весьма интересное.

      Удалить
    2. 1. Мы не пытаемся искать ошибки (пока по крайней мере), которые намного лучше находятся динамическим анализом. Во-первых, это очень сложно. Во-вторых, все равно высока вероятность ложного срабатывания или наоборот отсутствие позитивного срабатывания. В реальном приложении толку от статического анализа в поиске таких ошибок мало. Достаточно разнести выделение/использование/освобождение памяти по разным функциям, добавить логики, и фиг какой статический анализатор что-то найдет. Практически всегда они могут обнаружить только очень простые ситуации. Подобные, как в этих примерах. Так зачем делать заведомо то, от чего будет мало толку? Чтобы найти описываемые ошибки в реальном приложении, фактически нужно виртуально выполнить часть программы. Так не проще ли сразу использовать динамический анализатор кода?

      2. Беда в том, что программисты только думают, что они знают, какие ошибки распространены. Это как с оптимизацией. Программист уверенно назовет несколько мест, где его программа тормозит. Вот только с вероятностью 90% он ошибается. Именно для этого и служат профилировщики, которые находят настоящие медленные места.

      Аналогично и с типами ошибок. Все называют утечки памяти, выход за границы массива, нулевые указатели и тому подобное. И ещё ни один не назвал, опечатки, Copy-Paste или скажем сравнение вида unsigned_var >= 0. А я уверен, что именно эти ошибки самые распространенные и именно мы их учимся ловить в первую очередь. Программистам, не ловко что ли, даже себе признаться, что они просто ошибаются в словах и буквах. :)

      К этому и относится мое утверждение, что подобные синтетические примеры имеют мало смысла. Они не отражают картину того, какие на самом деле дефекты распространены. Правильный вариант сравнения только один. Взять проект и проверить его двумя анализаторами. И сравнить, сколько реальных дефектов найдено. Ещё потом это число можно поделить на количество ложных срабатываний. А то выиграет анализатор, который ругается на каждую строчку. :)

      Удалить
    3. 1. И статический и динамический анализы имеют свои плюсы, минусы и ограничения. Для статики, как Вы уже указали - наличие ложных срабатываний. С динамикой несколько сложней, т.к. она во-первых не покрывает все ветви выполнения программы, во-вторых требует больше затрат по времени и ресурсам. Поэтому я использую в большинстве случаев статический анализ, а динамический только в случае возникновения явной ошибки (и то обычно обходится просмотром кода в месте возникновения дефекта).

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

      Касательно работы с памятью, cppcheck, например, такие случаи выявлял и даже при наличие прочей логики в функциях (конечно, new[] и delete не были разнесены по разным функциям).

      2. Ну про то как думают программисты, даже не поспоришь )

      А про типы ошибок - хорошо, что Вы напомнили, добавлю в список тестов ) Единственное что - статистику тут очень сложно вести - ведь ошибки в проекте заранее неизвестны, а гарантии, что анализатор обнаружит все случаи нет.

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

      Удалить
    4. Ваша мысль понятна.

      Ещё благодарю за статью. Хоть она и отрицательная, но спасибо за усилия и то, что решили написать про нас :-). Хочется вознаградить, но не знаю как. Могу, если это интересно, подарить инвайт для Хабрахабр. Берегу один на крайний случай. Возможно, там и опубликуете конечные исследования по сравнению.

      Кстати, на тему "типы ошибок", возможно, Вам будет интересна моя статья "Реклама PVS-Studio - статический анализ кода на языке Си и Си++". В ней я выделил несколько классов ошибок (см. раздел "Примеры выявленных ошибок в различных open-source проектах"). Возможно, Вы что-то возьмете себе для составления тестовой базы.

      Удалить
    5. Эх, не хотел я, чтобы она имела совсем негативный окрас. Всё таки я очень симпатизирую людям и проектам, помогающим opensourse'у. Спасибо за предложение, но материалов по исследованиям пока крайне мало, и они пополняются крайне медленно, так что статья на хабре будет не скоро (надеюсь, что пополнение материалов ускорится).

      За статью спасибо, почитаю. Заключение у неё просто шикарно ;)

      Удалить
  2. Добрый день, спасибо за попытку протестировать PVS-Studio. Вы не правильно пробовали запускать из командной строки без наличия sln-файла. Вот здесь описано как это нужно делать:

    Использование PVS-Studio из командной строки (без solution-файла Visual Studio)
    http://www.viva64.com/ru/d/0007/

    ОтветитьУдалить
    Ответы
    1. Да, я собственно по этой статье и делал. В главе "Независимый режим работы анализатора PVS-Studio" в примере сказано, что необходимо запускать следующим образом:
      PVS-Studio.exe --cl-params %ClArgs% --source-file %cppFile% --cfg %cfgPath% --output-file %ExtFilePath%

      За исключением --cl-params все флаги присутствуют. Т.к. при компиляции clang я не указываю аргументов, этот ключ я опустил.

      Удалить
    2. Такой режим работы конечно же не очевиден, но тем не менее, Вы делали неправильно. Вы указали Settings.xml, а надо было PVS-Studio.cfg примерно такого вида:

      exclude-path = C:\Program Files (x86)\Microsoft Visual Studio 10.0
      vcinstalldir = C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\
      platform = Win32

      Об этом конечно же написано в справочной статье.

      Удалить
    3. Действительно, не очевиден. Написано про файл конфига в другой главе, но да, мануалы надо читать лучше и полностью.

      Только, он на UTF-8 с BOM ругается. Поправка добавлена к основной статье.

      Удалить
  3. Ещё хочу уточнить, а что с typedef не так? К чему относится "особенно странно, что на моих примерах не обработался typedef"?

    ОтветитьУдалить
    Ответы
    1. Долго пытался найти этот пример, так и не нашёл. Рассказываю суть. Анализаторы, которые сами строят AST (или вообще его не строят) не всегда раскрывают typedef'ы. Я был уверен, что при использовании компилятора для построения AST проблем с этим быть не должно. Но судя данным в моей таблице, обработки типов в PVS нет и фраза по typedef'ы висит в воздухе. Тут 2 варианта. Либо я нашёл в каком-то реальном проекте пример, где из-за typedef'а не была выявлена ошибка, либо меня глюкануло. За отсутствием доказательств считаю, что второе, фразу про typedef вычёркиваю.

      И заодно у меня к Вам вопрос. Есть интересная статья http://blogs.msdn.com/b/vcblog/archive/2006/08/16/thoughts-on-the-visual-c-abstract-syntax-tree-ast.aspx
      в которой один из разработчиков Visual C++ говорит, что в VS нельзя получить полное AST. Но, если я правильно понимаю, в PVS используется AST и получается оно именно средствами компилятора. Отсюда вопрос: Вам удалось таки заставить его строить AST, или использовалось что-то на подобие IntelliSense?

      Удалить
    2. Мы не используем компилятор для построения AST. Мы используем компилятор Visual C++ для создания препроцессированных файлов (*.i). Сейчас для этой цели потихоньку приспосабливаем Clang, так как он выполняет препроцессирование значительно быстрее. К сожалению, он пока не полностью совместим с Win-проекоами.

      В PVS-Studio AST мы строим самостоятельно. Вернее не AST, а дерево разбора ( http://www.viva64.com/ru/t/0039/ ). Типы соответственно мы тоже раскрываем самостоятельно. Не идеально в случае шаблонов, но в целом всё работает весьма хорошо. Кстати, AST и раскрытие typedef это несколько разные вещи. Если есть AST, это еще не значит, что легко получить тип. Это осуществляет большой и сложный механизм. А вот если AST нет, то это вообще практически сделать невозможно. Вернее можно сделать только для очень ограниченного простого набора случаев.

      По поводу, почему нельзя получить AST от Visual C++. Я думаю, основная причина в том, что его просто в Visual C++ нет. В том смысле, что там важна экономия памяти. И хранить дерево целиком нет смысла. Построили небольшой самодостаточный фрагмент AST, превратили его в код и удалили. Пошли работать со следующим фрагментом.

      Удалить