Привет, Хабр! Меня зовут Давид, еще недавно я был стажером 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-странице. 

Это statistics.html — таблица соответствия найденных ошибок их количеству. 
Это statistics.html — таблица соответствия найденных ошибок их количеству. 
Это index.html — список конкретных ошибок с описанием и ссылкой на файл, где можно посмотреть, что именно происходит в коде.
Это index.html — список конкретных ошибок с описанием и ссылкой на файл, где можно посмотреть, что именно происходит в коде.
  • В колонке File можно нажать на соответствующую запись — откроется вкладка с кодом. В ней будет поэтапно показано, что не понравилось анализатору и приводит к ошибке. 

  • В колонке Severity указывается степень «тяжести» той или иной ошибки. 

  • В колонке Message описана проблема, иногда с указанием конкретных переменных/структур. 

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

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

Вместо заключения

Статические анализаторы в составе CodeChecker помогли нам найти и исправить более 300 ошибок (без учета false-positive результатов). Для сравнения, коммерческое решение подсветило около 120 проблем, 99% из которых false-positive. А интерфейс инструмента в разы облегчил их использование.

Если хотите максимальной пользы для проекта в короткие сроки, рекомендую запустить ClangSA на sensitive/extreme настройках, а потом дополнительно прогнать код через Facebook Infer. Если есть время и хочется немного «причесать» код, можно подключить и остальные анализаторы — Clang-Tidy, CppCheck и даже GCC SA. Главное, что все это не требует ни покупки лицензий, ни долгих настроек.

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


  1. lazy_val
    18.09.2024 09:50
    +1

    есть большой проект на более 100 000 строк

    работает — не трогай

    Зачем в итоге понадобилось что-то "анализировать"? Оно работать перестало?


    1. Nullix
      18.09.2024 09:50
      +5

      работает — не трогай

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


      1. lazy_val
        18.09.2024 09:50

        работает - не трогай

        ))


        1. Nullix
          18.09.2024 09:50
          +4

          Но ведь этот принцип "осуждается" в статье.

          за самим проектом на правах легаси особо никто не следил: работает — не трогай


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


  1. SIISII
    18.09.2024 09:50
    +3

    Вообще, на такие вещи, как отсутствие break, компиляторы без всяких анализаторов вполне себе ругаться умеют -- надо лишь компилировать со всеми предупреждениями, превращёнными в ошибки (что, естественно, не отменяет пользу от анализаторов -- но не для таких элементарных ошибок, как мне кажется)


  1. dalerank
    18.09.2024 09:50
    +2

    В команде проекта джун, мидл, лид и архитектор.
    Произошёл баг на проде. Джун, не раздумывая, сразу полез в код чинить, думает, 
    что сейчас всё быстренько поправит. Мидл сидит рядом, смотрит на это дело, но 
    пальцем не шевелит — у него кофе и отпуск на носу. Лид смотрит логи.
    Архитектор, заходя в комнату, видит это безобразие, медленно поднимает бровь
    и спокойно спрашивает: — "И нафига ты туда полез?"
    Джун, слегка нервничая: — "Ну... я подумал..."
    Архитектор, улыбнувшись: — "Вот когда начнёшь думать, тогда и полезешь."


  1. 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

    Вполне рабочий вариант, но не работает под отладкой. Может кто подскажет что не так?


    1. 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