Привет, Хабр! Меня зовут Давид, еще недавно я был стажером YADRO, а сейчас работаю в отделе разработки ПО поддержки сетевой аппаратной части. У нас в команде есть большой проект на более 100 000 строк, написан на C++ (и частично на С). Код переписывался много раз, а за самим проектом на правах легаси особо никто не следил: работает — не трогай. Когда я пришел в команду, у меня была задача: привести код в порядок и отловить ошибки, которые пропускает компилятор — например, возможное разыменование нулевого указателя, неинициализированные переменные или простые опечатки.
Одним из очевидных решений было использование статического анализатора. Выбрали довольно известное коммерческое решение, но долгие прогоны не привели ни к чему дельному. Решили поэкспериментировать с другими вариантами статических анализаторов, сделав ставку на open source. Поиски привели к инфраструктуре CodeCheсker, которая предоставляет удобный интерфейс запуска и настройки статических анализаторов через аргументы командной строки. С помощью инструмента удалось достичь результатов, которые значительно превысили значения, полученные на коммерческом решении.
Под катом расскажу, что же такое CodeCheсker, как с ним работать и почему его точно стоит попробовать на большом проекте.
CodeCheсker: что за инструмент и чем он хорош
CodeCheсker предоставляет удобный интерфейс запуска и конфигурации статических анализаторов на Linux или macOS. С помощью него можно запускать анализаторы кода на C/C++ — Clang-Tidy, Clang Static Analyzer, Cppcheck и GCC Static Analyzer — в любой комбинации. А еще — включать и выключать различные проверки этих анализаторов. Результаты проверок можно изучать в удобном веб-интерфейсе.
Чем он нам понравился:
С ним просто запускать анализаторы. Статических анализаторов немало, но иногда их неудобно использовать: настраивать конфигурационные файлы долго, нет простого способа запуска чекера и парсинга результатов. CodeChecker исключает эти проблемы, предоставляя удобный интерфейс запуска и конфигурации через аргументы командной строки.
Его просто установить. CodeChecker — это python-пакет, ставится просто и не требует никаких настроек. Можно использовать «из коробки».
Скрытый текст
Поскольку CodeChecker — это python-пакет, устанавливается он самым обычным pip3 (сам пакет доступен на pypi):
pip3 install codechecker
Можно установить пакет в виртуальном окружении или воспользоваться pipx, который самостоятельно создаст venv:
pipx install CodeChecker
Еще один вариант — установка через пакетный менеджер Snap:
sudo snap install codechecker --classic
Обратите внимание, что название инструмента в консоли в таком случае нужно писать именно прописными буквами.
Еще больше способов установки (в основном напрямую) описаны в документации CodeChecker. Также его можно запустить в Docker-контейнере или в редакторе Visual Studio Code.
Запускаем анализаторы в CodeChecker
Существует несколько способов запуска анализаторов с помощью CodeChecker, но мы выберем самый простой — через передачу compilation database (файла compile_commands.json
). По сути, это просто данные о структуре проекта в одном файле. Их можно передавать, например, своей IDE, если она не видит зависимостей и из-за этого работает некорректно.
Сгенерировать этот файл можно разными способами. Например, если вы используете CMake, то можно просто прописать cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON.
в рабочей директории. Но у нас Linux, поэтому воспользуемся, возможно, более сложным способом — утилитой Bear, которая генерирует базу данных компиляции для инструментов clang. Чтобы сгенерить compile_commands.json
, заходим в рабочую директорию и пишем: bear -- <ваша-команда-для-билда-проекта>
, в нашем случае bear -- make
. Готово, в этой же директории получаем compile_commands.json
.
Для запуска анализаторов будем использовать команду analyze
. В общей команде нужно указать следующее:
путь до файла
compile_commands.json
,названия анализаторов, которые хотим запустить,
настройки чекеров,
папку, куда сгенерировать результат.
Файл compile_commands.json
у нас уже есть, разберемся c анализаторами.
Анализаторы
В CodeChecker доступны четыре анализатора — подробнее о каждом я расскажу позже:
Clang-Tidy — ставится дефолтным пакетом или вместе с Сlang,
Clang Static Analyzer — ставится вместе с Сlang,
Cppcheck — ставится дефолтным пакетом,
GCC Static Analyzer — ставится вместе с GNU Compiler Collection.
GCC нужен от версии 13.0.0. Если в PATH указана другая версия, то можно выставить в переменную окружения
CC_ANALYZER_BIN
путь до бинарного файла с GCC нужной версии:
CC_ANALYZER_BIN='gcc:<путь-до-бинарного-файла-gcc>'
По дефолту CodeChecker запустит все доступные анализаторы. Прописать конкретный набор анализаторов можно во флаг --analyzers
. Например, --analyzers gcc clangsa
.
Чекеры
Чекеры — самая важная часть команды запуска, ведь они устанавливают, на что именно будет смотреть анализатор и какие ошибки подмечать.
Есть три способа включить или выключить проверку:
через конкретный чекер,
через группу чекеров, которые просто объединяют чекеры в одну сущность,
через соответствующий профиль.
Профили — это наборы групп чекеров. Полезны, потому что групп много и прописывать их всех трудозатратно. Изначально доступны три профиля: default (стандартный набор групп), sensitive (увеличенный набор групп), extreme (почти все группы включены). Но можно собирать и свои профили. Однако важно помнить, что с ростом количества проверок растет и количество false-positive срабатываний.
Для включения/выключения чекеров используют флаги --enable
и --disable
и их комбинации. Так, с помощью --enable-all
и --disable-all
можно включить или выключить все чекеры (за исключением тех, что включаются вручную). Эти флаги применяются последовательно, так что их можно комбинировать для нужных настроек.
Например, после --disable-all --enable alpha.unix.PthreadLock
включится только чекер PthreadLock на многопоточность.
Итоговая команда запуска может выглядеть так:
CodeChecker analyze ./compile_commands.json --analyzers clangsa --enable=alpha --enable=sensitive --output ./reports
Здесь мы запускаем Clang Static Analyzer в режиме sensitive с alpha-чекерами. Результаты анализа можно будет посмотреть в папке reports.
Больше про запуски анализаторов можно почитать здесь. Перед тем как парсить результат, давайте ближе познакомимся с самими анализаторами.
Доступные анализаторы и как их лучше использовать
Clang Static Analyzer
Из четырех анализаторов, входящих в CodeChecker, этот дал нам больше всего полезного «выхлопа», причем разностороннего. Тут и проверки использования STL (например, использование итератора, вышедшего за конец контейнера), и обнаружение дедлоков при работе с мьютексами, и проверки на NULL dereference. Чтобы посмотреть полезные чекеры и группы чекеров, доступные для этого анализатора, используйте команду:
clang -cc1 -analyzer-checker-help
Анализатор прорабатывает код довольно быстро, даже с полным листом чекеров, — на наш проект с немалым числом строк кода он тратил 2-3 минуты. А еще он яснее всего подсказывает, где именно в коде он нашел ошибку.
Кроме того, у анализатора есть уникальная проверка cross-translation unit, которая включается с помощью флага --ctu
. Этот режим позволяет ему расширить область видимости анализа на соседние единицы трансляции. Подробнее про конфигурирование этого анализатора можно прочитать здесь.
В итоге большинство багов и опечаток нам помог исправить именно Clang SA.
Clang-Tidy
При использовании Clang-Tidy нужно быть готовым к километровому списку ошибок даже на дефолтных настройках чекеров. Анализатор ругается буквально на каждый чих, будь то неявный каст у целочисленных типов или неиспользованное значение snprintf(). Это не делает его бесполезным, но надо быть готовым, что результаты придется разгребать и выцеплять «пинцетом» действительно полезную информацию.
Еще Clang-Tidy — самый долгий из анализаторов: с дефолтными чекерами проверка может занять 30 минут. А с включением всех чекеров анализ кода может зависнуть на долгое время. Например, в наших тестах он не «проснулся» даже за час.
Но если есть время, можно подключить этот анализатор. От него мы получили несколько полезных рекомендаций — в основном из разряда, где стоит «причесать» код. Иногда указывал на ошибки, но реже. Так, он обнаружил пропуски скобок в логических операциях и пропущенный break у switch.
Чекеры для Clang-Tidy можно увидеть с помощью команды:
clang-tidy --list-checks
CppCheck
На бумаге CppCheck считают сильным анализатором, который минимизирует false-positive срабатывания. На деле он указывает на синтаксические ошибки там, где их нет, — например, при использовании varargs (variable arguments list). И в целом, дает довольно мало полезного знания о коде.
При анализе кода нашего проекта мы отметили буквально два места, где он действительно нашел ошибку в синтаксе, связанную с форматной строкой. Все это утяжеляется тем, что работает анализатор не очень быстро. В общем, рекомендую включать, только если есть свободное время — так, на всякий случай.
GCC Static Analyzer
Этот анализатор для нашего проекта не подошел совсем, потому что GCC SА не заточен под C++, а большая часть проекта написана именно на «плюсах». В теории код на C этот анализатор должен анализировать хорошо и выдавать что-то полезное. Если это ваш случай, проверьте ради эксперимента. Но в плюсах, увы, он ничего не замечает, причем не имеет значения, путь до какого бинарника мы ему передаем.
Бонус: чем можно заменять «слабые» статические анализаторы
Хочется упомянуть еще один статический анализатор, про который мы узнали из описания CodeChecker — Facebook* Infer (*компания Facebook принадлежит организации Meta, признанной экстремистской на территории РФ). Он не запускается из интерфейса CodeChecker, но мы решили опробовать его отдельно. В отличие от предыдущих анализаторов, он может проверять код не только на C/C++, но и на других языках — например, Java и Objective-C.
Для запуска нам потребуется все тот же compile_commands.json. Просто прописываем:
infer --compilation-database ./compile_commands.json
И вот, красивый прогресс-бар на время анализа скрашивает наше время ожидания.
Этот анализатор нам понравился. Facebook Infer показал много мест с неинициализированными и неиспользованными переменными, ситуациями гонки и лишними копированиями значений. Работает быстро, примерно как ClangSA (2-3 минуты), хоть настроек у него и мало.
Результаты можно посмотреть в .txt-файлике, сгенеренном анализаторм. А если хочется более приятного интерфейса, можно спарсить результаты Facebook Infer с помощью CodeChecker, Как парсить результаты — расскажу далее.
Парсинг результатов
После успешного анализа результаты необходимо обработать. В CodeChecker это легко сделать с помощью команды parse
:
CodeChecker parse --export html --output ./reports_html ./reports
Во флаге --export
указываем формат, в котором хотим получить результат, в нашем случае это html. Также прописываем папку, куда сгенерить результаты обработки, и, конечно, указываем путь до папки, где находятся результаты анализа.
Теперь можно посмотреть результаты и статистику по анализу на красивой html-странице.
В колонке File можно нажать на соответствующую запись — откроется вкладка с кодом. В ней будет поэтапно показано, что не понравилось анализатору и приводит к ошибке.
В колонке Severity указывается степень «тяжести» той или иной ошибки.
В колонке Message описана проблема, иногда с указанием конкретных переменных/структур.
Результаты анализа можно отсортировать по этим колонкам — например, если хотим группами рассматривать отчеты одной и той же проверки.
Перед повторным запуском советуем удалить или переместить папку со старым результатом в другую директорию. На время анализа это не повлияет, зато на выходе будет актуальное состояние кода. Если этого не сделать, CodeChecker будет добавлять результаты в старый файл и не уберет уже исправленные ошибки.
Вместо заключения
Статические анализаторы в составе CodeChecker помогли нам найти и исправить более 300 ошибок (без учета false-positive результатов). Для сравнения, коммерческое решение подсветило около 120 проблем, 99% из которых false-positive. А интерфейс инструмента в разы облегчил их использование.
Если хотите максимальной пользы для проекта в короткие сроки, рекомендую запустить ClangSA на sensitive/extreme настройках, а потом дополнительно прогнать код через Facebook Infer. Если есть время и хочется немного «причесать» код, можно подключить и остальные анализаторы — Clang-Tidy, CppCheck и даже GCC SA. Главное, что все это не требует ни покупки лицензий, ни долгих настроек.
Комментарии (8)
SIISII
18.09.2024 09:50+3Вообще, на такие вещи, как отсутствие break, компиляторы без всяких анализаторов вполне себе ругаться умеют -- надо лишь компилировать со всеми предупреждениями, превращёнными в ошибки (что, естественно, не отменяет пользу от анализаторов -- но не для таких элементарных ошибок, как мне кажется)
dalerank
18.09.2024 09:50+2В команде проекта джун, мидл, лид и архитектор. Произошёл баг на проде. Джун, не раздумывая, сразу полез в код чинить, думает, что сейчас всё быстренько поправит. Мидл сидит рядом, смотрит на это дело, но пальцем не шевелит — у него кофе и отпуск на носу. Лид смотрит логи. Архитектор, заходя в комнату, видит это безобразие, медленно поднимает бровь и спокойно спрашивает: — "И нафига ты туда полез?" Джун, слегка нервничая: — "Ну... я подумал..." Архитектор, улыбнувшись: — "Вот когда начнёшь думать, тогда и полезешь."
UFO_01
18.09.2024 09:50Не знаю правильно ли поступаю, но делаю так. Если нужен статический анализатор — PVS и clang-tidy без вариантов. Чтобы отловить ошибки памяти, использую address sanitizer
target_compile_options(${PROJECT_NAME}_compiler_flags_c INTERFACE "$<${gcc_like_c}:$<BUILD_INTERFACE:-fsanitize=address>>" ) target_compile_options(${PROJECT_NAME}_compiler_flags_cxx INTERFACE "$<${gcc_like_cxx}:$<BUILD_INTERFACE:-fsanitize=address>>" ) set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address")
Чтобы он нормально выводил текст, а не адреса, прямо в main пишу
#ifndef __has_feature // GCC does not have __has_feature... #define __has_feature(feature) 0 #endif #if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) #ifdef __cplusplus extern "C" #endif const char *__asan_default_options() { // Clang reports ODR Violation errors in mbedtls/library/certs.c. // NEED TO REPORT THIS ISSUE return "symbolize=1:detect_stack_use_after_return=1:external_symbolizer_path=/usr/bin/llvm-symbolizer"; } const char *__lsan_default_options() { // Clang reports ODR Violation errors in mbedtls/library/certs.c. // NEED TO REPORT THIS ISSUE return "verbosity=1:log_threads=1:fast_unwind_on_malloc=0"; } #endif
Вполне рабочий вариант, но не работает под отладкой. Может кто подскажет что не так?
UFO_01
18.09.2024 09:50Код почему-то съехал
#ifndef __has_feature // GCC does not have __has_feature... #define __has_feature(feature) 0 #endif #if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) #ifdef __cplusplus extern "C" #endif const char *__asan_default_options() { // Clang reports ODR Violation errors in mbedtls/library/certs.c. // NEED TO REPORT THIS ISSUE return "symbolize=1:detect_stack_use_after_return=1:external_symbolizer_path=/usr/bin/llvm-symbolizer"; } const char *__lsan_default_options() { // Clang reports ODR Violation errors in mbedtls/library/certs.c. // NEED TO REPORT THIS ISSUE return "verbosity=1:log_threads=1:fast_unwind_on_malloc=0"; } #endif
lazy_val
Зачем в итоге понадобилось что-то "анализировать"? Оно работать перестало?
Nullix
Возможно оно работает, только потому что-то никто пока не использовал какой-то ультра редкий кейс приводящий к ошибкам. Но при этом, неплохо было бы проанализировать код, и понять что у тебя нет таких кейсов с потенциальными ошибками.
lazy_val
))
Nullix
Но ведь этот принцип "осуждается" в статье.
Никто не следил за проектом, так как все жили по правилу "работает - не трогай", а надо бы и за легаси кодом последить. Вот и решили анализировать.