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

Amomum
Статей: 1

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

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

Эта статья была опубликована на habr.com. Перепечатка и перевод сделаны с разрешения автора.

Введение

Не, он, конечно, компилируется, но это же С++, язык, где "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 нет!) по трем событиям:

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

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

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

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

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

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

Благодаря удачному совпадению (а может быть и не совпадению, а разработчики 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*<FileName>" \
                                 | sed -e 's#</*FileName>##g')

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

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

Окей, мы поняли, что набор файлов поменялся – как запускать мониторинг?

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

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

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

  • Необходимость ручных манипуляций при изменении набора файлов. В принципе, файлы в проект добавляются не так уж часто, но все равно неприятно.
  • Тут мы неявно надеемся, что выключения параллельной сборки будет достаточно для безошибочного мониторинга.
  • Если в проекте несколько конфигураций сборки (в 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 разработала для нас специальную утилиту и несколько скриптов.

Основная идея состоит в следующем:

  • Запуск скрипта перед сборкой (с одним набором параметров), чтобы сформировать дамп окружения, ключей компиляции и т.д. Этот запуск создает копию файла проекта, включает в нем формирование Batch-файла, собирает этот проект, анализирует batch-файл и удаляет копию.
  • Запуск скрипта перед компиляцией каждого файла вместо мониторинга запусков компилятора.
  • Запуск скрипта (с другим параметром) после сборки проекта, чтобы выполнить анализ и вывести результат.

Собственно основной скрипт достаточно объемный и я его здесь целиком приводить не буду (но вот он на гитхабе); к тому же, его мне предоставили представители PVS-Studio :) Я немножко подправил его, в частности, убрал необходимость вручную указывать путь до папки Keil'a.

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

  • Before Compile .\scripts\_before_compile.bat #X #E
  • Before Build/Rebuild .\scripts\_before_build_dump.bat #X #P "Target 1"

Здесь "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 замусоривается

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

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

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

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

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

Подавление предупреждений

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

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

Способ подавления

Когда нужно

Одно конкретное предупреждение для одной конкретной строчки

Если вы точно знаете, что это не ошибка

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

Если это подключенная библиотека внутри папки с проектом

Класс предупреждений для текущего проекта

Если этот класс анализов все равно не работает

Конкретные виды предупреждений в текущем проекте

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

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

Для постоянно используемых библиотек, которые не живут внутри папки с проектом

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

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

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

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

Здесь ХХХ — это номер варнинга.

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

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

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

Пример:

<?xml version="1.0" encoding="utf-8"?>
<ApplicationSettings xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- Import settings (mostly exclude paths) from global settings -->
  <AutoSettingsImport>true</AutoSettingsImport>
  <PathMasks>
    <!-- Exclude this paths from analysis -->
    <string>\cmsis\</string>
    <string>\spl\</string>
  </PathMasks>
  <!-- Disable 64-bit errors -->
  <Disable64BitAnalysis>true</Disable64BitAnalysis>
</ApplicationSettings>

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

Это делается с помощью файла 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-файлом, в локальном файле должна быть строка <AutoSettingsImport>true</AutoSettingsImport>. PVS-Studio просто смотрит в папку %APPDATA%\PVS-Studio\SettingsImports и применяет все файлы оттуда подряд.

Пример:

<?xml version="1.0" encoding="utf-8"?>
<ApplicationSettings xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <PathMasks>
    <!-- Exclude this paths from analysis -->
    <string>\boost\</string>
    <string>\zlib\</string>
    <string>\png\</string>
    <string>\libpng\</string>
    <string>\pnglib\</string>
    <string>\freetype\</string>
    <string>\ImageMagick\</string>
    <string>\jpeglib\</string>
    <string>\libxml\</string>
    <string>\libxslt\</string>
    <string>\tifflib\</string>
    <string>\wxWidgets\</string>
    <string>\libtiff\</string>
    <string>\mesa\</string>
    <string>\cximage\</string>
    <string>\bzip2\</string>
  </PathMasks>
</ApplicationSettings>

Подведем итоги

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

Я пользуюсь ей уже несколько лет и в целом доволен, потому что чувствую себя в большей безопасности от собственной глупости :)

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

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

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

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

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