Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо
to the top
>
>
Что нельзя найти с помощью статического…

Что нельзя найти с помощью статического анализа

16 Мар 2023

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

1037_What_static_analysis_cannot_find_ru/image1.png

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

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

Инструменты статического анализа применяют достаточно сложные технологии для обнаружения ошибок: анализ потока данных, символьное выполнение программы, межпроцедурный анализ и так далее. Более подробное их описание вы можете найти в статье "Технологии статического анализа кода PVS-Studio".

Однако, статические анализаторы кода имеют две слабые стороны:

Перечисленные недостатки методологии статического анализа на самом деле неизбежны. Сейчас мы разберём две причины, из-за которых они возникают.

Отсутствие высокоуровневой информации о коде

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

Рассмотрим простейший абстрактный пример:

double Calc(double a, double b)
{
  return a * sin(b);
}

Есть здесь ошибка? Неизвестно. Возможно, автор ошибся и на самом деле в формуле должна использоваться функция для вычисления косинуса, а не синуса.

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

int Foo(int x, int y)
{
  if (x == 0)
    x++; y++;
  return x * y;
}

Этот код выглядит очень подозрительно. Высока вероятность, что здесь забыты фигурные скобки, и анализатор справедливо обращает внимание на этот код. Так писать не нужно, но нельзя однозначно утверждать, что код работает не так, как требуется. Только автор кода может сказать, должен ли этот код выглядеть так:

int Foo(int x, int y)
{
  if (x == 0)
  {
    x++;
    y++;
  }
  return x * y;
}

Или так:

int Foo(int x, int y)
{
  if (x == 0)
    x++;
  y++;
  return x * y;
}

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

  • Классический обзор кода. Автор кода и его коллеги знают как должна работать программа и могут обнаружить ошибки, используя эти знания. Другими словами, они могут проверить с высокоуровневой точки зрения, что код делает именно то, что задумывалось.
  • Юнит-тесты. Можно обнаружить ошибку в рассмотренной выше функции Calc, выявив, что результат её работы на тестовых данных отличается от контрольных значений.
  • Ручное тестирование приложений. Тестировщик может заметить, что программа ведёт себя не так, как надо.
  • Доказательство корректности программы. На специальном языке формально описывается, как должна вести себя программа. После чего проверяется, что реализованная в коде логика, соответствует формальному описанию. Данный подход не нашёл широкого распространения, в силу больших трудозатрат. Иногда получается, что формальное описание функции больше и сложнее, чем само тело функции.

Вычислительная сложность

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

Рассмотрим тело простой функции:

void foo(int *p)
{
  *p = 1;
}

Рассматривая только эту функцию, невозможно сказать, может или нет здесь произойти разыменование нулевого указателя.

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

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

void foo(int *p)
{
  if (p != NULL)
  {
    *p = 1;
  }
}

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

Полезнее отследить, действительно ли в функцию foo где-то может быть передан нулевой указатель. Например, возможно, функция используется только в одном месте следующим образом:

void X(int *p)
{
  if (p == NULL)
    return;
  foo(p);
}

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

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

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

Кстати, анализ входных данных порождает отдельную тему, связанную с выявлением потенциальных уязвимостей. Анализаторы применят для этого специальную технологию, называемую Taint-анализом (taint checking).

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

Заключение

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

Популярные статьи по теме


Комментарии (0)

Следующие комментарии next comments
close comment form
close form

Заполните форму в два простых шага ниже:

Ваши контактные данные:

Шаг 1
Поздравляем! У вас есть промокод!

Тип желаемой лицензии:

Шаг 2
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности
close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS‑Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

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

close form
Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте, отфильтровано ли письмо в одну из следующих стандартных папок:

  • Промоакции
  • Оповещения
  • Спам