Зачем нужен динамический анализ кода, если есть статический?




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

Рисунок 1

Наша команда много пишет о пользе статического анализа и о том, какую выгоду может принести его использование в вашем проекте. Мы любим искать ошибки в различных проектах с открытым исходным кодом с помощью нашего инструмента, тем самым популяризируя методологию статического анализа кода. В свою очередь эта методология помогает улучшать качество программ, делает их более надёжными и уменьшает количество потенциальных уязвимостей. Наверное, каждый, кто непосредственно работает с кодом, получает внутреннее удовлетворение от исправления ошибок. Но даже в том случае, если вы не получаете выброс эндорфинов от того, что вам удалось найти (и исправить) очередной баг, то, скорее всего, вам нравится мысль о том, что у вас получилось сократить денежные затраты на разработку путём того, что статический анализатор помог более продуктивно использовать время программистов. Подробнее о том, какую пользу в денежном эквиваленте может принести использование инструмента статического анализа, можно узнать из данной статьи. Приблизительные расчёты показаны на примере использования анализатора PVS-Studio, но подобное можно проэкстраполировать и для других статических анализаторов, имеющихся на рынке.

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

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

К основным преимуществам статического анализа можно отнести:

  • Обнаружение ошибок на ранних этапах разработки программного обеспечения. Это существенно снижает стоимость устранения дефектов в программе, так как чем раньше выявлена ошибка, тем легче и, как следствие, дешевле её исправить.
  • Позволяет точно определять местонахождение потенциальной ошибки в исходном коде.
  • Полное покрытие кода. Вне зависимости от того, как часто получают управление те или иные участки кода во время исполнения программы, весь исходный код будет полностью проанализирован.
  • Простота использования. Для запуска статического анализа не нужно заранее подготавливать какие-либо наборы входных данных.
  • С помощью статического анализа кода можно достаточно легко и быстро обнаруживать опечатки и последствия использования Copy-Paste.

К объективным недостаткам статического анализа кода относятся следующие факторы:

  • Неизбежное появление так называемых ложно-позитивных срабатываний. Статический анализатор кода может указывать на те места, где на самом деле нет никаких ошибок. Разрешить подобную ситуацию и дать понять анализатору, что он дал ложное срабатывание, может только программист, тем самым потратив своё рабочее время.
  • Статический анализ, как правило, слаб в диагностике утечек памяти и параллельных ошибок. Чтобы выявлять подобные ошибки, фактически необходимо виртуально выполнить часть программы. Это крайне сложно реализовать. Также подобные алгоритмы требуют очень много памяти и процессорного времени. Как правило, статические анализаторы ограничиваются диагностикой простых случаев. Более эффективным способом выявления утечек памяти и параллельных ошибок является использование инструментов динамического анализа.

Отметим, что использование статического анализа кода не ограничивается только выявлением ошибок в программе. Например, используя инструменты статического анализа, можно получать рекомендации по оформлению кода. Некоторые статические анализаторы позволяют проверять, соответствует ли исходный код принятому в компании стандарту оформления кода. Имеется в виду контроль количества отступов в различных конструкциях, использование пробелов/символов табуляции и так далее. Помимо этого, статический анализ можно использовать для подсчёта метрик. Метрика программного обеспечения — это мера, позволяющая получить численное значение некоторого свойства программного обеспечения или его спецификаций. Если вас интересует, каким ещё образом можно использовать статический анализатор кода, вы можете обратиться к этой статье.

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

Используя динамическое тестирование, можно получить следующие метрики и предупреждения:

  • Используемые ресурсы: время выполнения программы в целом или ее отдельных модулей, количество внешних запросов (например, к базе данных), количество используемой оперативной памяти и других ресурсов.
  • Степень покрытия кода тестами и другие метрики программы.
  • Программные ошибки: деление на ноль, разыменование нулевого указателя, утечки памяти, "состояние гонки".
  • Детектировать некоторые уязвимости.

К основным преимуществам динамического анализа кода относят:

  • Возможность проводить анализ программы без необходимости доступа к её исходному коду. Здесь стоит сделать оговорку, так как программы для динамического анализа различают по способу взаимодействия с проверяемой программой (подробнее с этим можно ознакомиться в этой статье). Например, распространён способ проведения динамического анализа путём предварительного инструментирования исходного кода, то есть добавления специального кода в исходный текст приложения для обнаружения ошибок. В этом случае доступ к коду проверяемой программы будет необходим.
  • Возможность обнаружения сложных ошибок, связанных с работой с памятью: выход за границу массива, обнаружение утечек памяти.
  • Возможность проводить анализ многопоточного кода непосредственно в момент выполнения программы, тем самым обнаруживать потенциальные проблемы, связанные с доступом к разделяемым ресурсами, возможные deadlock ситуации.
  • В большинстве реализаций появление ложных срабатываний исключено, так как обнаружение ошибки происходит в момент ее возникновения в программе; таким образом, обнаруженная ошибка является не предсказанием, сделанным на основе анализа модели программы, а констатацией факта ее возникновения.

Перечислим недостатки, которые присущи динамическому анализу кода:

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

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

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

Это пример кода из проекта Clang:

MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;
void clearBottomUpPointers() {
  PerPtrTopDown.clear();
}
void clearTopDownPointers() {
  PerPtrTopDown.clear();
}

Здесь статический анализ укажет на то, что тела двух функций абсолютно идентичны. Конечно, нельзя с абсолютной уверенностью говорить, что если тела функций одинаковы, то это ошибка. Однако существует вероятность, что это был результат копипаста, совмещённый с невнимательностью разработчика, что уже и приведёт к непредвиденному поведению программы. В данном случае, внутри метода clearBottomUpPointers должен был быть осуществлён вызов PerPtrBottomUp.clear. В приведённом примере динамический анализ кода не сможет увидеть ничего подозрительного, ведь с его точки зрения это абсолютно рабочий код.

Другой пример. Представим, что у нас есть следующая функция:

void OutstandingIssue(const char *strCount)
{
  unsigned nCount;
  sscanf_s(strCount, "%u", &nCount);
  
  int array[10];
  memset(array, 0, nCount * sizeof(int));
}

Теоретически возможно, что статический анализатор смог бы заподозрить что-то неладное с этим кодом, но это очень трудно и нецелесообразно реализовывать. Данный пример взят из следующей статьи, там же можно более подробно ознакомиться с тем, почему поиск подобных ошибок не стоит имплементировать в статических анализаторах. Вкратце отмечу, что статическому анализатору чрезвычайно сложно определить, что в функции memset может произойти выход за границу массива, так как он не может заранее предвидеть, какое число будет считано из строки strCount, а в случае, если значение strCount было получено из файла, это и вовсе невозможно. В свою очередь, динамический анализатор (при правильном наборе входных данных) смог бы легко указать на то, что в данной программе есть ошибка при работе с памятью.

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

Не хочется казаться чрезмерно предвзятым и как-то особенно выделять технику статического анализа, но в последнее время именно о ней всё больше говорят и, что более важно, внедряют в свои CI процессы многие компании. Статический анализ выступает как один из этапов так называемого барьера или ворот качества (quality gates) к построению надёжного и качественного программного обеспечения. Рекомендую обратить внимание на интересную лекцию по этой теме тут. Нам кажется, что статический анализ через пару лет станет стандартной практикой при разработке программ, такой же, как когда-то стало юнит-тестирование.

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

Дополнительные ссылки:



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;


Найденные ошибки

Проверено проектов
361
Собрано ошибок
13 417

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;