Тонкости Анализа Исходного Кода C/C++ С Помощью Cppcheck

В предыдущем почта рассмотрены основные возможности статического анализатора с открытым исходным кодом cppcheck .

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

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

На сегодняшний день нет сравнений анализаторов «кто лучше», статья целиком посвящена работе с cppcheck.



Загрузить и установить

Вы можете скачать cppcheck с Официальный веб-сайт , для Windows есть установщик, а для Linux рекомендую скачать исходники с Гита , так как он очень прост в сборке и не имеет зависимостей.

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

В частности, для моей машины с Linux я раздвоенный на GitHub cppcheck и сделал клон git-форка.

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



Сборка для Linux
Сборка на Linux предельно проста: скачиваем, распаковываем, переходим в каталог и запускаем make:
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

unzip cppcheck-master.zip cd cppcheck-master make



Сбор для Windows
Сборка под Windows тоже не должна вызвать затруднений — там есть проект для VS. Открытие, сбор.

Сам не пробовал, ибо нет у меня такой необходимости.



Cppcheck как плагин
Что касается плагинов для IDE, то, конечно, есть плагины для Code::Blocks, CodeLite, Gedit и Eclipse. Но этим дело не ограничивается, поскольку есть плагины для сборочных ферм Hudson, Jenkins, для систем контроля версий Tortoise SVN и Mercurial. Плагина для Visual Studio нет, но на главной странице есть очень красивая фраза:
Плагина для Visual Studio нет, но можно добавить Cppcheck в качестве внешнего инструмента.

Вы также можете попробовать фирменную PVS-Studio (есть бесплатная пробная версия), ориентированную на эту среду.

cppcheck имеет графический интерфейс, написанный на Qt. Это еще больше упрощает процесс анализа, но не будем вдаваться в подробности — крутые программисты не используют графические интерфейсы :) Более того, GUI полностью повторяет возможности командной строки (а в некоторых случаях уступает) и это сложно понять, после знакомства с cppcheck не получится.

Стоит отметить, что cppcheck распространяется под лицензией GNU GPL. Это позволяет вам легко взять исходный код этой программы, перетащить его в свой Git-репозиторий и добавить туда под любые нужды, добавляя свои собственные правила и библиотеки.



Настройка анализатора

Положительным моментом cppcheck является возможность его тонкой настройки.

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

Справедливости ради отмечу, что всю представленную ниже информацию можно получить на сайте документация или выполнив команду cppcheck --help , но на самых важных нюансах остановлюсь подробнее (и по-русски:).

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

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



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

Это означает, что если анализатор обнаружит подозрительную область, но не уверен, что это ошибка, он не сообщит об этом.

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

Пример того, как это работает. Допустим, нам нужно разобрать следующий код:

void f() { char *a = malloc(100); process_a(a); }

На первый взгляд здесь ошибка: нет бесплатно .

Однако если функция процесс_а является библиотечной функцией, нельзя с уверенностью сказать, что процесс_а где-то внутри этого нет бесплатно для указателя а .

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

Поэтому cppcheck сначала попытается найти реализацию функции.

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

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

Однако cppcheck можно «научить» распознавать библиотечные функции, тем самым повысив точность анализа — об этом речь пойдет ниже.

Второй пример:

void f() { char *a = malloc(100); if(random()) g_exit(0); free(a) }

Здесь явно уже есть утечка памяти, поскольку g_exit — обертка библиотеки GLib над стандартной функцией Выход .

Но cppcheck этого не знает g_exit прерывает выполнение программы, поэтому анализатор не сможет распознать здесь ошибки.

Нам нужно каким-то образом предоставить cppcheck информацию о том, что функция g_exit может прервать работу программы, чтобы анализатор научился распознавать такие ошибки.

Из примеров видно, что cppcheck перестраховывается, и планка адекватности высока.

Большинство проверок cppcheck не включены по умолчанию.

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

  • ошибка — очевидные ошибки, которые анализатор считает критическими и обычно приводят к ошибкам (включено по умолчанию);
  • предупреждение — здесь выводятся предупреждения, сообщения о небезопасном коде;
  • стиль — стилистические ошибки, появляются сообщения при неаккуратном кодировании (больше похоже на рекомендации);
  • производительность — проблемы с производительностью, здесь cppcheck предлагает варианты, как сделать код быстрее (но это не всегда даёт прирост производительности);
  • портативность — ошибки совместимости, обычно связанные с разным поведением компиляторов или систем разной разрядности;
  • информация — информационные сообщения, появляющиеся при проверке (не связанные с ошибками в коде);
  • неиспользованная функция — попытка вычисления неиспользуемых функций (мертвый код), не может работать в многопоточном режиме;
  • отсутствуетВключить - проверка на отсутствие пропажи #включать (например, мы используем случайный и подключить stdlib.h забыл).

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

Например:

cppcheck -q -j4 --enable=warning,style,performance,portability .

/source

Именно так я обычно включаю самые важные проверки.

Есть ключевое слово все , который включает в себя все перечисленные проверки.

Примечание .

Параметры -j и проверьте режим неиспользованная функция несовместимы, поэтому -j отключит проверку unusedFunction, даже если она указана явно.

Пример команды, которая «запускает» код по всем правилам:

cppcheck -q --enable=all .

/source

И это еще не все.

Если ваша программа безошибочна с точки зрения анализатора, попробуйте запустить cppcheck с параметром --безрезультатно .

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

cppcheck -q --enable=all --inconclusive .

/source



Не забывайте о кроссплатформенности!
Cppcheck изначально создавался как инструмент, работающий в разных операционных системах и платформах.

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

Различные платформы:

  • unix32 — все 32-битные *nix (включая Linux);
  • unix64 — все 64-битные *nix;
  • win32A — семейство 32-разрядных версий Windows с кодировкой ASCII;
  • win32W — семейство 32-разрядных версий Windows с кодировкой UNICODE;
  • win64 — семейство 64-разрядных версий Windows.
Если вам нужно протестировать код, написанный для Win32, с использованием Linux, необходимо указать платформу:

cppcheck --platform=win32A .

/source

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

Используйте параметр --std со следующими возможными вариантами:

  • posix — для операционных систем, совместимых со стандартом POSIX (в том числе Linux);
  • c89 — язык C, стандарт 1989 года;
  • c99 — язык C, стандарт 1999 года;
  • c11 — язык C, стандарт 11-го курса (по умолчанию для C);
  • c++03 — язык C++, стандарт 03;
  • c++11 — язык C++, стандарт 11-го курса (по умолчанию для C++).

Вы можете использовать сразу два стандарта:

cppcheck --std=c99 --std=posix .

/source



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

-q , -j .

Для чего они нужны? Давайте рассмотрим самые интересные из них.

-j — очень полезный параметр, позволяющий запустить проверку в многопоточном режиме.

Пользоваться очень просто — количество процессоров передается как параметр и проверка пройдет веселее.

-q - Тихий режим.

По умолчанию cppcheck выдает информационные сообщения о ходе проверки (которых может быть много).

Эта опция полностью отключает информационные сообщения, оставляя только сообщения об ошибках.

или --сила — включить перебор всех вариантов директив ifdef (по умолчанию cppcheck проверяет десяток вариантов).

О том, что это такое, поговорим отдельно позже.

-v — режим отладки — cppcheck предоставляет внутреннюю информацию о ходе проверки.

--xml — вывести результат проверки в формате XML. --template=gcc — отображать ошибки в формате предупреждения компилятора gcc (удобно для интеграции с IDE, поддерживающей такой компилятор).

--подавить — режим подавления ошибок с заданными идентификаторами (требуется повторный анализ).

-час — выдает сертификат по всем параметрам на чистейшем английском языке.



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

Самое полезное — это возможность отключить конкретное предупреждение в конкретном файле (и, возможно, в конкретной строке).

Отключение сообщений реализовано с помощью параметра --подавить , где вам нужно указать исключение, или --suppress-файл указание текстового файла, содержащего список исключений.

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

--подавить подряд, но лучше для таких целей создать файл с исключениями.

Формат исключения:

id[:file:[line]]

Обязательный идентификатор параметра (идентификатор ошибки), после которого следует двоеточие, при желании можно указать имя файла, после имени файла также при желании можно указать номер строки.

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

The scope of the variable '%VAR' can be reduced.

Эта ошибка присуща большинству проектов и чтобы она не отговаривала вас от рефакторинга, я рекомендую сначала ее отключить.

Это можно сделать следующим образом:

cppcheck -q -j4 --enable=all --suppress=variableScope .

/source

Как видно из этого примера, cppcheck использует удобочитаемые идентификаторы ошибок, а не числа, которые гораздо легче запомнить.

Параметр поможет узнать список всех возможных ошибок.

--errorlist , который создает полный список в формате XML. Но я могу порекомендовать другой метод выявления «нежелательных» сообщений.

Для этого вам нужно будет изменить формат вывода сообщения с помощью параметра --шаблон :

cppcheck -q -j4 --enable=all --template='{id} {file}:{line} {message}' .

/source

Мой шаблон теперь сначала выводит идентификатор сообщения, затем файл с номером строки и, наконец, само сообщение.

Узнать ID мешающего сообщения несложно.

Пример вывода

variableScope geany/src/document.c:1099 The scope of the variable 'use_ft' can be reduced. variableScope geany/src/document.c:1257 The scope of the variable 'filename' can be reduced. variableScope geany/src/document.c:2306 The scope of the variable 'keywords' can be reduced. variableScope geany/src/document.c:3011 The scope of the variable 'old_status' can be reduced. variableScope geany/src/editor.c:194 The scope of the variable 'specials' can be reduced. variableScope geany/src/editor.c:248 The scope of the variable 'ptr' can be reduced. variableScope geany/src/editor.c:1545 The scope of the variable 'text' can be reduced. variableScope geany/src/editor.c:4309 The scope of the variable 'tab_str' can be reduced.

И напоследок рецепт автоматического создания файла для использования в параметре --suppress-файл .

В командной строке это делается в два этапа:

cppcheck -q --enable=all --template='{id}:{file}:{line}' .

/source > suppress-list.txt

Теперь полученный файл можно отправить на вход cppcheck и на выходе не будет ни одной ошибки.

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

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



Понимание включает и определяет
Cppcheck понимает некоторые параметры компилятора, что позволяет уточнить, какие пути проверяются.

Поскольку cppcheck не использует компилятор, у него есть собственный препроцессор.

Этот препроцессор не требует ни наличия всех заголовочных файлов, ни корректности исходного кода.

Если где-то встречается неизвестный человек включать — cppcheck его просто не обрабатывает. Неизвестность может сыграть злую шутку.

Обычной практикой в GLib является проверка аргументов:

void f(gchar *s1, gchar *s1) { g_return_if_fail(s1); gchar *a = g_strdup(s1); g_return_if_fail(s2); gchar *b = g_strdup(s2); }

Все хорошо, кроме этого g_return* макросы, которые прерывают выполнение функции в случае ошибки.

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

Cppcheck понятия не имеет об этом, потому что думает g_return_if_fail по умолчанию используется «хорошая функция», а не макрос.

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

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

За это отвечает параметр , который аналогичен одноименной опции компилятора gcc. Для GLib и Linux это вполне предсказуемый путь:

cppcheck -q -I/usr/include/glib-2.0/ .

/source

Интересной особенностью (значительно увеличивающей время анализа) является перечисление опций ifdef. Если в программе есть один ifdef, cppcheck выполнит два варианта предварительной обработки и просканирует обе версии исходного кода.

Чем больше ветвей ifdef в исходном коде, тем больше возможностей вам нужно попробовать.

Вы можете контролировать это поведение с помощью параметров И .

Параметр А означает макрос А определенный.

Параметр Б означает макрос Б не определен.

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

Это число можно изменить с помощью параметра --max-configs .

Чтобы проверить только одну конфигурацию, можно указать одну проверку конфигурации.

Параметр --сила проверит все конфигурации (очень медленно).

Cppcheck не отличает макросы из заголовочных файлов от макросов, определенных в исходном коде.

Если анализ усилен предварительной обработкой всех #включать , указав каталог с заголовочными файлами - готовьтесь к очень долгому анализу - cppcheck облезет Все макросы из всех заголовочных файлов, к которым может обратиться препроцессор.

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



Мы сами пишем реализации функций

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

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

Для создания такого файла придется немного потрудиться, но анализ пройдет гораздо быстрее и точнее.

Более того, с макросами можно использовать несколько интересных трюков.

Включение файла с реализацией функций реализуется параметром --append .

Указанный файл автоматически вставляется В конце концов каждый файл проекта.

Включение файла с определениями макросов реализуется параметром --включать .

Указанный файл автоматически вставляется к началу каждый файл проекта.

Например, программа использует библиотеку GLib и вам нужно сообщить cppcheck, что g_return_if_fail это макрос.

Попробуем проанализировать следующий код:

void f(char *s1, char *s1) { g_return_if_fail(s1); char *a = g_strdup(s1); g_return_if_fail(s2); char *b = g_strdup(s2); free(a); free(b); }

Запустите cppcheck:

cppcheck -q test.c

Ничего.

Создайте файл gtk.h со следующим содержимым:

#define g_return_if_fail(expr) do{if(!(expr)){return;}}while(0)

Так как это макрос, его нужно включить в начале:

cppcheck -q test.c --include=gtk.h

Хм.

Опять ничего? Если вы внимательно посмотрите на код в примере, вы заметите функцию g_strdup , о котором cppcheck пока ничего не знает. Попробуем написать простейшую реализацию (файл gtk.c ):

char * g_strdup(const char *s) {

Теги: #C++ #язык c #cppcheck #рефакторинг #статический анализ кода #ошибки #программирование #C++ #C++

Вместе с данным постом часто просматривают: