>
>
Статический и динамический анализ кода

Андрей Карпов
Статей: 674

Статический и динамический анализ кода

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

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

Динамический анализ кода - это способ анализа программы непосредственно при ее выполнении.

Я часто слышу приблизительно следующую мысль:

У вас отличный инструмент. Но он не находит некоторые ошибки. Например, недавно Valgrind нашёл вот такую ошибку, а анализатор кода PVS-Studio промолчал. Было бы замечательно, добавить в PVS-Studio диагностику вот таких и таких ошибок. Тогда станет возможным использовать только один инструмент, что будет удобнее.

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

Предположим, что есть функция вида:

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

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

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

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

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

Более того, динамический анализатор решит задачу выхода за границу массива, даже если строка прочитана из файла!

Означает ли это, что динамический анализ лучше? Быть может стоит лучше усовершенствовать динамический анализатор, чтобы он умел тоже самое, что и статический?

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

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

Для динамического анализатора в приведённом ниже коде нет никаких проблем. Сравнивается часть буфера. Ничего подозрительного. Полным полно ситуаций, когда функция memcmp() сравнивает не весь выделенный буфер памяти, а только его часть. Это очень частое явление, когда используется только часть буфера. Ругаться динамическому анализатору здесь не на что.

А вот статический анализатор смотрит на код и понимает, что количество сравниваемых байт, скорее всего вычисляется неверно. Пример, взятый из реального open source проекта:

const unsigned char stopSgn[2] = {0x04, 0x66};
....
if (memcmp(stopSgn, answer, sizeof(stopSgn) != 0))
  return ERR_UNRECOGNIZED_ANSWER;

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

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

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