Pull to refresh
222.11
Rating
PVS-Studio
Static Code Analysis for C, C++, C# and Java

Разрушаем мифы о статическом анализе кода

PVS-Studio corporate blog
Общаясь с людьми на форумах, я заметил несколько стойких заблуждений, касающихся методологии статического анализа. Я хочу развеять следующие мифы:
Разрушаем мифы о статическом анализе
  1. Статический анализатор это продукт разового применения.
  2. Профессиональные разработчики не допускают глупых ошибок.
  3. Динамический анализ лучше чем статический.
  4. Программисты хотят добавлять свои правила в статический анализатор.


Миф первый – статический анализатор это продукт разового применения


Вот как выглядит такое утверждение при обсуждении на форуме (собирательный образ):

Имея пробную/крякнутую версию, можно бесплатно прогнать все свои проекты, найти несколько застаревших ошибок и, собственно, на этом успокоиться на какое-то время.

Все счастливы. Люди проверили. Разработчики анализатора не узнали, что их обманули и обокрали.

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

Аналогия:

Ставим уровень предупреждений компилятора /W0. И разрабатываем проект. Ругаемся, правим глупые ошибки и опечатки, тестируем больше и дольше. Потом изредка включаем /W3 и сражаемся с предупреждениями, а потом опять возвращаемся к /W0. При этом то, что мог подсказать компилятор на уровне /W3 мы бесстрашно и долго искали в отладчике и потратили на это в 10-100 раз больше времени. Вдобавок, обратим внимание на то, что теперь программисту не будет нравиться результат /W3. Ведь почти все ошибки он и так поправил методом тестирования и отладки. Компилятор на уровне /W3 выдает теперь в основном ложные срабатывания.

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

Так же, как и ключ /W3, использование статического анализа максимально полезно в случае регулярного использования. Кстати, статический анализ и есть своего рода расширение предупреждений, выдаваемых компилятором. Многие диагностики, которые когда то были реализованы в старых анализаторах, постепенно переходят в компилятор. Конечно, анализаторы всегда будут опережать в диагностическом плане компиляторы. Они разработаны для этого специально. У компилятора больше других забот и вдобавок на него накладываются более жесткие требования по производительности.

Некоторые в пылу дискуссии отвечают так:

Идея верна для начинающих студентов. Для специалистов это уже не так важно. Если я поставлю /W0 я хуже писать не стану. Нужно совершенствовать стиль программирования, а не увеличивать количество инструментов-костылей.

Совершенно согласен со всем написанным выше. Но давайте поиграем и вот так переделаем текст:

Идея верна для начинающих водителей. Для профессионалов это уже не так важно. Если я не пристегнусь за рулем, я хуже ездить не стану. Нужно совершенствовать стиль вождения, а не увеличивать количество страхующих компонентов.

И опять не поспоришь. Однако любой адекватно мыслящий водитель понимает, что пристегиваться все-таки полезно. Также полезен и статический анализ. Ведь от ошибок и опечаток не застрахован даже самый опытный программист. И примеры, приведенные в этой статье, хорошее тому подтверждение. Конечно, все профессиональные программисты уверены, что вот они-то таких глупых ошибок не делают, но про это дальше.

Миф второй – профессиональные разработчики не допускают глупых ошибок


Миф второй: «Профессиональные разработчики не допускают глупых ошибок, которые в основном и ловят статические анализаторы кода».

Вот как выглядит такое утверждение при обсуждении на форуме (собирательный образ):

У меня, профессионального разработчика, уже N лет не было проблем с порчей памяти, временем жизни объектов и так далее. Статический анализ — это инструмент для «макдональдса», а здесь (на профессиональном форуме) сидят гики. Сейчас мои основные проблемы это сложно тестируемые алгоритмы и интеграция с другими разработчиками с использованием неявных контрактов по состояниям объектов.

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

Это не так. Любые программисты делают глупые ошибки. Я знаю, что вы меня не услышали. Повторю еще раз еретические мысли. Все программисты делают глупые ошибки. Неважно, какие они профессионалы. Людям свойственно делать ошибки. И чаще всего эти ошибки просты.

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

Отвлечемся немного и вспомним, почему так живучи различные гороскопы. Первая причина — это очень расплывчатые формулировки, которые человеку легко подстроить под себя. Но нам интересен второй компонент. Люди не запоминают те случаи, когда предсказание не сбылось. Зато очень хорошо помнят и пересказывают те случаи, когда их жизненная ситуация совпала с ситуацией, описанной в гороскопе. В результате получается, что, говоря и вспоминая о гороскопе, мы находим N подтверждений, что гороскопы работают и не вспоминаем про N*10 случаев, когда гороскоп не сработал.

Что-то аналогичное происходит с программистом, когда он занимается поиском ошибок. Он хорошо помнит про сложные и интересные ошибки. Может подискутировать о них с коллегами или написать заметку в блог. Когда же он заметит, что вместо переменной 'AB' он написал 'BA', то он просто исправит ляп, и этот факт сразу исчезнет из его памяти. Фрейд обратил внимание на следующую особенность памяти: человеку свойственно помнить положительные высказывания о себе и забывать отрицательные. Если человек сражается со сложной ошибкой в алгоритмической задаче, то когда он её исправляет, считает себя героем. Это стоит запомнить и даже рассказать другим. Когда он находит глупый баг, то помнить про это нет никакого повода и желания.

Какие у меня есть доказательства? Хотя большинство опечаток и ляпов исправляется, некоторые из них всё-таки остаются незамеченными. И массу примеров таких ошибок можно обнаружить в этой статье. В статье хорошо видно, что ошибки допускали не новички, а квалифицированные программисты.

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

Миф третий – динамический анализ лучше чем статический


Миф третий: «Динамическая проверка такими инструментами, как valgrind для Си/Си++ намного продуктивнее, чем статический анализ кода».

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

Перечислю сильные стороны статического анализа кода.

Диагностика всех ветвей программы
Динамический анализ на практике не может покрыть все ветвления программы. После этих слов поклонники valgrind заявляют, что нужно делать правильные тесты. Теоретически они правы. Но любой, кто пробовал такое осуществить, понимает всю сложность и объем работы. На практике, даже хорошие тесты покрывают не более 80% программного кода.

Особенно хорошо это видно в участках кода, обрабатывающие нестандартные/аварийные ситуации. Если взять старый проект, то большинство ошибок будет выявлено статическим анализатором именно в этих местах. Причина в том, что даже если проект стар, эти участки практически не протестированы. Приведу очень короткий пример, чтобы показать, что я имею в виду (проект FCE Ultra):
fp = fopen(name,"wb");
int x = 0;
if (!fp)
  int x = 1;

Флаг 'x' не станет равен единице, если файл не был открыт. Именно из-за подобных ошибок, когда в программах что-то идёт не так, они вместо адекватных сообщений про ошибки падают или выдают бессмысленные сообщения.

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

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

Более высокоуровневый анализ
У динамического анализатора есть преимущество, что он знает, какая функция с какими аргументами вызывается. Как следствие он может проверить корректность вызова. Статический анализ в большинстве случаев не может это узнать и проверить значения аргументов. Это минус. Зато статический анализ проводит более высокоуровневый анализ, чем динамический. И это позволяет ему искать такие вещи, которые с точки зрения динамического анализа корректны. Простой пример (проект ReactOS):
void Mapdesc::identify( REAL dest[MAXCOORDS][MAXCOORDS] )
{
  memset( dest, 0, sizeof( dest ) );
  for( int i=0; i != hcoords; i++ )
    dest[i][i] = 1.0;
}

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

Или вот другой пример из проекта Clang:
MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;
void clearBottomUpPointers() {
  PerPtrTopDown.clear();
}
void clearTopDownPointers() {
  PerPtrTopDown.clear();
}

Что здесь может заподозрить динамический анализатор? Ничего. А статический анализатор, может заподозрить неладное. Здесь ошибка в том, что внутри clearBottomUpPointers() должно быть: «PerPtrBottomUp.clear();».

Миф четвёртый – программисты хотят добавлять свои правила в статический анализатор


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

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

Я всегда отвечал, что реализация собственных правил это не то, что хотят программисты. И не видел другой альтернативы, кроме реализации диагностик разработчиками анализатора по просьбам программистов (статья на эту тему). Недавно я плотно пообщался с Дмитрием Петуниным. Он является руководителем отдела тестирования компиляторов и разработки инструментов верификации программ Intel. Он расширил мое понимание этой темы и озвучил идею, о которой я думал, но так и не сформулировал в конечном варианте.

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

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

Программисты действительно хотят искать некоторые паттерны/ошибки в своём коде. Им это действительно нужно. Например, человеку нужно найти вся явные приведения от типа int к float. Эту задачу нельзя решить, используя такие инструменты, как grep. Ведь в конструкции вида «float(P->FOO())» неизвестно какой тип вернет функция FOO(). В этот момент программист и приходит к выводу, что он может реализовать поиск таких конструкций, добавив свою проверку в статическом анализаторе.

Здесь и кроется ключевой момент. Человеку не нужно создавать свои правила анализа. Ему нужно решить частную проблему. То, что он хочет, является очень маленькой задачей с точки зрения механизмов статического анализа. Это то же самое, как использовать автомобиль, чтобы зажигать сигареты от прикуривателя.

Именно поэтому и я, и Дмитрий не поддерживаем идеи предоставлять пользователю API для работы с анализатором. Это крайне сложная задача с точки зрения разработки. И при этом от неё человек вряд ли будет использовать более 1%. Нерационально. Разработчику проще и дешевле реализовывать пожелания пользователей, чем создавать сложный API для модулей расширения или создавать специальный язык описания правил.

Читатель заметит: «тогда откройте в API только 1% от функциональности и все будут довольны». Да, всё правильно. Но смотрите, как сместился акцент. От разработки своих правил мы пришли к тому, что на самом деле достаточен инструмент, схожий с grep, но обладающий некоторой дополнительной информацией о коде программы.

Пока такого инструмента нет. Если вы хотите решить какую-то задачу, то можете написать мне, и мы постараемся реализовать её в анализаторе PVS-Studio. Например, недавно мы как раз реализовывали несколько пожеланий по поиску явных приведениё типов: V2003, V2004, V2005. Воплощать в жизнь такие пожелания нам намного проще, чем создавать и поддерживать открытый интерфейс. Проще это и самим пользователям.

Кстати, возможно такой инструмент со временем появится в рамках Intel C++. Дмитрий Петунин говорил, что они обсуждали возможность создания grep-подобного инструмента, обладающего знаниями о структуре кода и типах переменных. Но это обсуждалось абстрактно. Я не знаю, планируется в действительности создать такое или нет.
Tags: статический анализ кодаразрушители мифовстатический анализc plus plus
Hubs: PVS-Studio corporate blog
Total votes 90: ↑51 and ↓39 +12
Comments 29
Comments Comments 29