Общаясь с людьми на форумах, я заметил несколько стойких заблуждений, касающихся методологии статического анализа. Я решил сделать небольшой цикл статей, в которых попробую показать, как всё обстоит на самом деле.
Миф третий: "Динамическая проверка такими инструментами, как 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();".