Интеграция PVS-Studio в uVision Keil

Я занимаюсь разработкой для встраиваемых систем (в основном, под STM32 и Миландр), в качестве основной среды я использую uVision Keil. И, поскольку пишу я на С и С++, уже долгое время меня мучает вопрос — правильно ли я пишу код? Можно ли так?

Не, он конечно компилируется, но это же С++, язык, где «program is ill-formed, no diagnostic required» — это норма.

Соответственно, на протяжении нескольких лет я донимал руководство просьбами купить нам лицензию PVS-Studio и, наконец, когда моя просьба неожиданно совпала с моментом, когда нужно было срочно потратить выделенные на закупку ПО деньги, нам ее все-таки купили!

Радости моей с одной стороны не было предела, но с другой оказалось, что не все так хорошо; сходу PVS-Studio встраивается только в Visual Studio (что порадовало отдел разработки под десктопы) и продукты от Jetbrains (CLion, Rider, Idea, Android Studio), для некоторых других систем сборки тоже предусмотрены готовые сценарии, а вот для Keil«a заявлена только поддержка компилятора — и все. А значит, нужно заниматься интеграцией. Кто будет этим заниматься? Ну, мне же больше всех надо…

image-loader.svg

Поскольку я не придумал отдельный кусок текста до ката, я просто повторю его тут в качестве введения :)
Я занимаюсь разработкой для встраиваемых систем (в основном, под STM32 и Миландр), в качестве основной среды я использую uVision Keil. И, поскольку пишу я на С и С++, уже долгое время меня мучает вопрос — правильно ли я пишу код? Можно ли так?
Не, он конечно компилируется, но это же С++, язык, где «program is ill-formed, no diagnostic required» — это норма.

Соответственно, на протяжении нескольких лет я донимал руководство просьбами купить нам лицензию PVS-Studio и, наконец, когда моя просьба неожиданно совпала с моментом, когда нужно было срочно потратить выделенные на закупку ПО деньги, нам ее все-таки купили!

Радости моей с одной стороны не было предела, но с другой оказалось, что не все так хорошо; сходу PVS-Studio встраивается только в Visual Studio (что порадовало отдел разработки под десктопы) и продукты от Jetbrains (CLion, Rider, Idea, Android Studio), для некоторых других систем сборки тоже предусмотрены готовые сценарии, а вот для Keil«a заявлена только поддержка компилятора — и все. А значит, нужно заниматься интеграцией. Кто будет этим заниматься? Ну, мне же больше всех надо…

Разумеется, рабочие задачи при этом с меня не спали, поэтому процесс затягивался довольно сильно. Поначалу я, вопреки всем рекомендациям, занимался проверками «только по праздникам», без всякой автоматизации по самому универсальному сценарию — запускать PVS-Studio Standalone, нажимать «Мониторить запуск компилятора», компилировать проект и читать результаты анализа.

Так продолжалось до тех пор, пока однажды я не потратил 3 дня на отладку очень неприятного бага, который отличался совершенно дикими проявлениями в случайные моменты времени. Баг оказался банальным чтением по нулевому указателю (которое на микроконтроллерах зачастую не приводит ни к каким мгновенным ошибкам типа Access Violation).

Быстренько убедившись, что PVS-Studio находит этот баг, я понял, что хватит это терпеть! — и решил-таки заняться интеграцией с Keil«ом.

И давайте сразу же определимся, что я понимаю под интеграцией:


  • анализ запускается автоматически, после нажатия на кнопку «скомпилировать»
  • результаты анализа выдаются автоматически, желательно в то же окно, что и обычные ошибки компиляции
  • двойной клик на ошибку или предупреждение должен автоматически перемещать нас к проблемному месту

Как вы узнаете к концу статьи, в принципе, более-менее получилось –, но с нюансами :)

Keil, насколько мне известно, не предусматривает никаких «нормальных» способов кастомизации типа плагинов или расширений, поэтому единственный способ встроиться в сборку — это Custom Build Steps, которые в Keil«e называются «User Scripts».
В опциях проекта во вкладке Users предусмотрена возможность запуска сторонних программ (только .bat или .exe, даже .cmd нет!) по трем событиям:


  • перед сборкой всего проекта
  • перед компиляцией каждого файла
  • после сборки всего проекта

Вроде бы, первого и последнего должно быть достаточно. План вырисовывается вроде бы несложный:


  1. перед сборкой всего проекта нужно запустить мониторинг
  2. после сборки нужно мониторинг остановить
  3. запустить анализ
  4. вывести результаты в окно Build Output

Быстрые эксперименты показали, что Build Output (ожидаемо) ловит весь вывод в stout и stderr для пользовательских скриптов, правда кириллицу показывать отказывается напрочь, поэтому ошибки в этих скриптах превращаются в нечитаемые козябры. Я это быстро подкостылил с помощью смены кодовой страницы на какую-то англоязычную, чтобы ошибки выдавались на английском.

Окей, пройдемся по шагам.


  1. Запустить мониторинг можно с помощью консольной утилиты CLMonitor
  2. После того, как сборка завершена, запускаем анализ и сохраняем его результат в формате обычного текста.
  3. А потом выводим результаты просто с помощью more.
  4. И вуаля, вроде бы все работает!

Благодаря удачному совпадению (а может быть и не совпадению, а разработчики PVS-Studio сделали это специально?), формат строк с предупреждениями совпадает с форматом, который использует Keil, поэтому переход к проблемной строке по двойному клику просто заработал.
Казалось бы, пост на этом можно завершать?
К сожалению, нет.

Через некоторое время я заметил небольшую странность — пересобираю один и тот же проект без изменений, целиком, а результаты анализа PVS-Studio — разные! В них то пропадала, то появлялась ошибка в одном из файлов.

И началась эпическая переписка с техподдержкой, которая — исключительно по моей вине — растянулась почти на год (!). Вот честное слово — техподдержка у PVS-Studio — натурально лучшая из всех, с кем я общался, а общался я со многими, от российских производителей микросхем, где человек поздравлял меня с «днём пирожков с малиновым вареньем» (нет, это не шутка) до крупнейших зарубежных компаний, где меня месяцами футболили от человека к человеку :)
Тут же я со стыдом признаюсь, что отвечал существенно медленнее, чем отвечали мне… частично меня оправдывает необходимость заниматься основными рабочими задачами, но только частично.

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

Окей. Что же с этим делать?


Решение «в лоб»

Вариант «в лоб» — выключить параллельную сборку (ну или выключать ее иногда, для анализа). Плохой вариант, потому что:


  • В Keil«e это делается глобально, не для каждого проекта в отдельности; т.е. замедлена будет сборка всех проектов вообще
  • Замедление сборки весьма ощутимое; конечно, кому-то время в 1.5–2 минуты кажется не очень большим, но все же это достаточно неприятно, успеваешь отвлечься и потерять фокус
    Если же параллельную сборку выключать только иногда, то мы по сути возвращаемся к сценарию «проверки только по праздникам», которого хотим избежать.


Парсинг файл проекта

Едем дальше. Мне довольно быстро показалось, что использовать мониторинг вообще как-то глупо, ведь в файле проекта содержится вся нужная информация — какие файлы будут скомпилированы, с какими ключами и тому подобным. Почему бы просто не парсить этот файл?
Но этот вариант хорош только на бумаге. С одной стороны, непонятно, кто должен этим парсингом заниматься? Конечно, мы купили лицензию, но это не значит, что представителей PVS-Studio можно теперь бесконечно эксплуатировать. Для них мы со своим Keil«ом явно не в приоритете, интегрироваться в каждую встречную-поперечную среду экономически нецелесообразно, собственно, поэтому и предлагается универсальное решение с мониторингом.

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


Batch-файл

В Keil«е есть странная функция, которой я так и не смог найти применения — создание batch-файла сборки. В этот файл попадает вся необходимая PVS-Studio информация и включается этот файл одной галкой!

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


Замедлитель компиляции

Мониторинг не успевает отловить запуск компилятора? Так давайте заставим его компилировать подольше!


  • Можно просто запускать Process Explorer вместе с Keil«ом. Но не очень понятно, насколько это помогает и почему.
  • Поскольку один мой коллега упарывался по шаблонам, я попросил его накидать что-нибудь, что хорошенько грузило компилятор, не оказывая никакого влияния на бинарный файл; он мне предложил свое шаблонное вычисление табличного синуса, которое я, пожалуй не рискну выкладывать на всеобщее обозрение дабы не шокировать почтенную публику (и потому что код не я писал :)

С помощью флага --preinclude это принудительно инклудилось в каждый.срр-файл в проекте.

Эти варианты отметаются, поскольку замедляют компиляцию (а еще, потому что это наркомания).

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


Дамп

Первый вариант — не мониторить компиляцию каждый раз. Вполне достаточно ведь получить набор файлов, которые компилируются. Меняется этот набор не так уж часто — только когда в проект добавляют новые файлы (ну, или убирают старые).

Таким образом, этот вариант делится на две стадии:


  • как-то детектировать, что набор файлов в проекте поменялся; в таком случае запускать мониторинг и сохранять результат мониторинга (не анализа)
  • если набор файлов не менялся, то просто запускать анализ по сохраненному результату

Как детектировать изменение списка файлов? Наверняка можно разными способами, я же пошел первым пришедшим мне на ум путем — через git, поскольку все равно все проекты должны гитоваться.
Если файл проекта менялся с последнего коммита, значит, добавлялись файлы!

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

was_changed=$(git diff *.uvproj* | grep "[+,-]\s*" | sed -e 's###g')

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

Окей, мы поняли, что набор файлов поменялся — как запускать мониторинг?
А вот тут я не придумал ничего лучше, как выдать пользователю предупреждение и потребовать некоторых ручных манипуляций:


  1. Выключить параллельную сборку (зайти в Edit->Configuration->Other и поставить галку Disable parallel build)
  2. Сменить «обычные» скрипты на «мониторящие» — снять и поставить еще две галки в Options->User
  3. Выполнить полную пересборку проекта
  4. Вернуть галки обратно

Минусы этого подхода:


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


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


  • Если в проекте несколько конфигураций сборки (в Keil это называется «Targets»), то при переключении может быть нужно перегенирировать дамп — если в конфигурациях участвуют разные файлы, разные ключи компиляции, активны разные дефайны и т.д. И вот за этим приходится следить самостоятельно, к сожалению, из Кейла автоматически имя текущей конфигурации никак не вытащить (ну, или я не смог найти как).



Всякие не очень интересные подробности:
  • Для проверки измененности нужен git, bash и sed — к счастью, все это входит в комплект поставки git for Windows;, но ограничивает применимость скрипта. Плюс, чтобы проверять измененность файлов через git, нужно, чтобы проект лежал в репозитории; проверить просто произвольную папку не получится.
  • Поскольку Keil умеет вызывать только .bat и .exe, приходится оборачивать вызов shell-скрипта в батник.
  • Git может быть установлен где попало, но может быть и прописан в Path. Чтобы закрыть оба случая я придумал такой немножко упоротый вариант: "%GIT_BASH_PATH%bash.exe" — если пукть к bash.exe прописан в Path, то это просто прокатит, но опционально можно создать переменную окружения GIT_BASH_PATH и глобальный Path не засорять. Только в GIT_BASH_PATH нужно будет поставить слеш в конце пути.
  • С PVS-Studio та же история
  • Если проект не скомпилировался, то clmonitor может остаться запущенным, нужно не забывать его прибивать при запуске компиляции. Это означает, что компилировать сразу два проекта со сбором дампа — нельзя. Впрочем, вроде не очень-то и хочется.

Собственно собирание дампа делается вот так:

CLMonitor.exe saveDump -d "путь_к_дампу\pvs_dump.zip"

Когда дамп уже есть, анализ запускается вот так:

CLMonitor.exe analyzeFromDump -d "путь_к_дампу\pvs_dump.zip" 
                              -l "путь_к_результату\pvs.plog"  
                              -t "путь_к_конфигу\pvs_settings.xml" 
                              -c "путь_к_конфигу\ignore_warnings.pvsconfig"

PlogConverter.exe "путь_к_результату\pvs.plog" --renderTypes=Txt -o "путь_к_результату"

more "путь_к_результату\pvs.plog.txt"

Конфиги pvs_settings.xml и ignore_warnings.pvsconfig позволяют подавлять предупреждения, о них подробнее позже.

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


Утилита CLMonitorDumpFilter

Поскольку ручные манипуляции — это все-таки довольно грустно, после перебрасывания идеями, команда PVS-Studio разработала для нас специальную утилиту и несколько скриптов.
Основная идея состоит в следующем:


  1. Запуск скрипта перед сборкой (с одним набором параметров), чтобы сформировать дамп окружения, ключей компиляции и т.д. Этот запуск создает копию файла проекта, включает в нем формирование Batch-файла, собирает этот проект, анализирует batch-файл и удаляет копию.
  2. Запуск скрипта перед компиляцией каждого файла вместо мониторинга запусков компилятора.
  3. Запуск скрипта (с другим параметром) после сборки проекта, чтобы выполнить анализ и вывести результат.
    Собственно основной скрипт достаточно объемный и я его здесь целиком приводить не буду (но вот он на гитхабе); к тому же, его мне предоставили представители PVS-Studio:) Я немножко подправил его, в частности, убрал необходимость вручную указывать путь до папки Keil«a.

Соответственно, вызовы в данном случае выглядят так:

Здесь "Target 1" — имя вашего текущего таргета, должно быть в кавычках


  • After Build .\scripts\_after_build.bat #X #P

Буквы с решетками — Keil называет это «Key Sequence» — по сути это build variables, переменные окружения для сборки.


  • #X — это путь до папки Keil«a,
  • #E — путь до текущего файла
  • #P — путь до файла проекта

Плюсы по сравнению с предыдущим:


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

Минусы:


  • В данный момент не поддерживается ARM Compiler version 6 (т.е. armclang)
  • Название текущей конфигурации нужно указывать вручную в строке вызова скрипта.

Это минус только по сравнению с предыдущим вариантом, где это название указывать не нужно вообще:) К счастью, делать это нужно только при создании конфигурации –, но вручную.


  • Окно Build Output замусоривается

image-loader.svg

Убрать эти надписи у меня не получилось :(

А поскольку в Keil нет нормального окна «Errors», как в большинстве других IDE, окно Build Output приходится читать постоянно, фильтровать его невозможно; соответственно, эти надписи мешают визуально находить ошибки компиляции и предупреждения от компилятора, особенно если в проекте много файлов.


  • Поскольку спецутилита трогает файл проекта, после компиляции Keil решает, что файл проекта изменился, и предлагает проект перезагрузить. Если вы согласитесь, то все надписи из Build Output (в т.ч. результаты анализа) пропадут.

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

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

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

Итак, подавлять предупреждения можно на нескольких уровнях:

Поскольку в embedded есть реальная возможность делать каждый проект самодостаточным (т.е. не зависящим от каких-либо внешних библиотек, просто клонишь его и он компилируется), эту самодостаточность хочется сохранять. Поэтому все скрипты для анализа и файлы для подавления предупреждений тоже нужно будет хранить внутри папки проекта (разумеется, саму PVS-Studio придется ставить отдельно).


Один конкретный варнинг для одной конкретной строчки

До нужной строчки или справа от нее пишется комментарий вида

// -V::XXX - Пояснение, почему варнинг подавлен

Здесь ХХХ — это номер варнинга.
Пояснение, на мой взгляд, крайне важно писать; иначе совершенно непонятно, почему варнинг подавлен — потому что он ложный или потому, что он просто раздражал программиста, который не смог понять, в чем проблема.


Все варнинги для какой-нибудь папки только в текущем проекте и классы предупреждений для текущего проекта

Это делается с помощью xml-файла (который я обычно называю pvs_settings.xml). Этот файл путешествует вместе с проектом.


Пример:


  
  true
  
    
    \cmsis\
    \spl\
  
  
  true


Конкретные варнинги в текущем проекте

Это делается с помощью файла ignore_warnings.pvsconfig. Этот файл тоже путешествует вместе с проектом. Разумеется, пояснения о причинах игнорирования приветствуются!


Пример
###### Common warnings

# ignore 64-bit warnings
// -V::4

# allow C-style cast for primitive integer types (and void)
// -V:int:2005
// -V:char:2005
// -V:short:2005
// -V:uint8_t:2005
// -V:int8_t:2005
// -V:uint16_t:2005
// -V:int16_t:2005
// -V:uint32_t:2005
// -V:int32_t:2005
// -V:uint64_t:2005
// -V:int64_t:2005
// -V:void:2005

# ignore 'The body of the statement should be enclosed in braces'; that doesn't look like a source of errors for us
// -V::2507 

###### MISRA

# ignore MISRA C++ 6-6-5 'A function should have a single point of exit at the end.'
# this goes againts our best practises and generally seems outdated
// -V::2506


Конкретные папки на всем компе

Это делается с помощью xml-файлов в папке текущего пользователя. Чтобы они применялись вместе с локальным xml-файлом, в локальном файле должна быть строка true. PVS-Studio просто смотрит в папку %APPDATA%\PVS-Studio\SettingsImports и применяет все файлы оттуда подряд.


Пример:


  
    
    \boost\
    \zlib\
    \png\
    \libpng\
    \pnglib\
    \freetype\
    \ImageMagick\
    \jpeglib\
    \libxml\
    \libxslt\
    \tifflib\
    \wxWidgets\
    \libtiff\
    \mesa\
    \cximage\
    \bzip2\
  

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

Отмечу, что подсчитать пользу от постоянного анализа достаточно сложно — ведь ошибка исправляется практически сразу, как только анализатор выдает соответствующее предупреждение. Оценить, сколько времени бы я эту проблему искал сам, без PVS-Studio, достаточно сложно.

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

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


  • Не проще ли было интегрироваться в Eclipse?
  • Не проще ли было встроиться в CI, а не в IDE?
  • Может быть стоило выработать рефлекс «есть баг — сегодня праздник запусти PVS, а сам думай потом».

И, разумеется, примеры на гитхабе.

P.S. Если кто-нибудь знает, как теперь на хабре делать работающее оглавление, подскажите, пожалуйста, я пост поправлю.

© Habrahabr.ru