>
>
>
Как создавался PVS-Studio под Linux

Павел Беликов
Статей: 10

Святослав Размыслов
Статей: 90

Как создавался PVS-Studio под Linux

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

Введение

Вообще, консольное ядро анализатора PVS-Studio собрано под Linux довольно давно. Уже около трех лет. Сразу отвечу на вопрос об отсутствии такой версии в общественном доступе - сделать программный продукт, даже на основе уже существующего, это огромная работа и много человеко-часов, куча непредвиденных проблем и нюансов. Тогда мы это только предвидели, поэтому официальная поддержка анализатора для Linux систем не начиналась.

Являясь автором многих статей о проверке проектов, в отличии от моих коллег, я часто черпал вдохновение в проектах из Linux. Эта среда очень богата крупными и интересными проектами с открытым исходным кодом, которые либо очень сложно собрать в Windows, либо вообще невозможно. Именно в рамках таких задач по проверке какого-нибудь открытого проекта до сегодняшнего дня и развивался PVS-Studio для Linux.

Малыми силами портирование кода ядра PVS-Studio на Linux заняло пару месяцев. Замена нескольких обращений к системным функциям и отладка на проекте Chromium дали уже хорошо работающее консольное приложение. Эта версия анализатора была добавлена в регулярные ночные сборки, а также проверялась анализатором Clang Static Analyzer. Периодические проверки открытых проектов и контроль сборки позволили анализатору беспроблемно существовать несколько лет и иногда даже казалось, что этот инструмент уже можно продавать. Но вы ещё не знаете, как мне приходилось проверять проекты на тот момент...

О применении инструментов статического анализа

Прежде чем продолжить рассказ о разработке нашего инструмента, я бы хотел немного рассказать о самой методологии статического анализа. Тем самым, сразу ответив на вопросы в стиле: "Зачем пользоваться сторонними инструментами, если сразу можно писать код без ошибок и делать ревью с коллегами?". К сожалению, такой вопрос довольно часто звучит.

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

Среди наших читателей, пользователей или слушателей на конференциях встречаются люди, которые считают, что обзора кода с коллегами более чем достаточно для выявления ошибок на раннем этапе написания кода. И кое-что в таких "досмотрах" наверняка удаётся найти. Но всё это время мы говорим об одном и том же. Ведь статический анализ можно рассматривать как автоматизированный процесс обзора кода. Представьте, что статический анализатор — это ещё один ваш коллега. Этакий виртуальный человек-робот, который без устали участвует во всех обзорах кода и указывает на подозрительные места. Разве это не полезно?!

Во многих отраслях производства прибегают к автоматизации с целью исключения так называемого "человеческого фактора". И контроль качества кода - не исключение. Мы не призываем отказываться от ручного code-review, если вы его практикуете. Просто использование статического анализатора поможет выявлять ещё больше ошибок на самом раннем этапе.

Ещё важный момент - программа не устаёт и не ленится. В код вносятся ошибки различного характера. Опечатки? Их очень сложно выделить глазами. Языковые ошибки? Сильно зависят от квалификации проверяющего. Ситуацию усугубляют современные объёмы кода. Многие функции не помещаются целиком даже на больших мониторах. При неполноте контекста бдительность проверяющего уменьшается. Плюс человек уже через 15 минут внимательного чтения кода начинает уставать. И чем дальше, тем сильнее. Отсюда и популярность инструментов автоматического анализа, которая растет с каждым годом.

Что просили пользователи PVS-Studio от версии для Linux?

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

Пользователи Linux долго и упорно спрашивали про работу анализатора на этой платформе. Вопросы и аргументы можно обобщить следующим образом:

  • Утилита командной строки - "Интеграция с IDE не нужна!"
  • Инсталлятор не нужен - "Сами все установим!"
  • Документация не нужна - "Сами запустим!"

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

Миф о понимании сборочных скриптов

После общения с людьми из крупных коммерческих проектов выяснилось, что достаточно много разработчиков не знакомы со сборкой проектов и на самом деле такие знания не всегда нужны в полном объёме. Как собрать/отладить свой проект/модуль каждый разработчик знает. Но обычно всё это знание заключается в наборе нескольких магических команд, которые выполняют программисты. Образно говоря, у них есть большая кнопка, на которую достаточно нажать и на выходе они получают собранные модули. Но о том, как всё это работает внутри, они имеют только общее представление. А за сборочными скриптами часто следит специальный человек.

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

Linux версия анализатора появились как раз после того, к в Windows мы сделали систему мониторинга компиляторов, которая позволила проверять любые проекты на этой платформе. Как потом выяснилось, там достаточно много серьёзных проектов, которые собираются с помощью компилятора от Microsoft, но при этом не имеют проекта для Visual Studio. Так мы написали статьи о проверке Qt, Firefox, CryEngine5 и даже сотрудничали с Epic Games, исправляя ошибки в их коде. Наше исследование показало, что если знать информацию о компиляторе: директорию запуска, параметры командной строки и переменные окружения, то этой информации достаточно, чтобы позвать препроцессор и выполнить анализ.

Планируя проверять Linux проекты, я сразу понимал, что не разберусь с интеграцией анализатора в каждый конкретный проект, поэтому сделал аналогичную систему мониторинга для ProcFS (/proc/id's). Брал код из Windows плагина и запускал его в mono для анализа файлов. Несколько лет такой способ использовался для проверки проектов, самые крупные из которых - ядро Linux и FreeBSD. Несмотря на длительное использование такого способа, он ни в коем случае не годится для массового использования. Продукт ещё не готов.

Выбор технологии мониторинга

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

  • (-) Clang scan-build - посмотрев скрипты Clang'a, мы сделали прототип, который аналогичным образом прописывал вызов анализатора в переменные CC/CXX. Мы раньше сталкивались с тем, что таким способом не удаётся проверить некоторые открытые проекты с помощью Clang Static Analyzer. Разобравшись получше в этом способе, мы поняли, что часто в проектах в эти переменные дописывают и флаги компиляции. И при переопределении переменных их не удаётся сохранить. Поэтому мы отказались от этого способа.
  • (+) strace - утилита выдаёт довольно подробный лог трассировки, в котором большая часть залогированных процессов не относится к компиляции. Также в формате вывода этой утилиты отсутствует так необходимая нам рабочая директория процесса. Но директорию удалось найти, связав дочерние и родительские процессы, а приложение на C++ очень быстро парсит такой файл, запуская анализ найденных файлов параллельно. Так можно проверить проекты с любой сборочной системой и познакомиться с анализатором. Например, недавно мы снова перепроверили Linux Kernel, теперь легко и просто.
  • (+) JSON Compilation Database - такой формат можно получить для CMake проекта, указав один дополнительный флаг. В нём есть вся нужная информация для анализа без лишних процессов. Поддержали.
  • (+/-) LD_PRELOAD - интеграция анализатора через замещение функций. Этот способ не будет работать, если сборка проекта уже осуществляется таким образом. Также есть утилиты, позволяющие получить JSON Compilation Database для не CMake-проектов с помощью LD_PRELOAD (Например, Bear). В них есть небольшое отличие от CMake, но мы их тоже поддержали. И если в проекте нет зависимости от каких-то установленных переменных окружения, то мы тоже сможем проверить проект. Поэтому +/-.

Появление регулярных тестов

Существуют разные способы тестирования программного обеспечения. Для тестирования анализатора и диагностик самыми эффективными являются прогоны на большой кодовой базе открытых проектов. Для начала мы выбрали около 30 крупных проектов с открытым исходным кодом. Ранее я упоминал, что собранный анализатор в Linux просуществовал не один год и проекты для статей проверялись регулярно. Казалось, что всё хорошо работает. Но только начав полноценное тестирование, мы увидели полную картину недоработки анализатора. Перед началом анализа необходимо выполнить разбор кода, чтобы найти нужные конструкции. Несмотря на незначительное влияние неразборного кода на качество анализа, всё же ситуация неприятная. Нестандартные расширения есть во всех компиляторах, но MS Visual C/C++ мы их давно поддерживаем, а с GCC нам пришлось начать эту борьбу почти с самого начала. Почему почти? В Windows мы давно поддержали работу с GCC (MinGW), но он там не так сильно распространён, поэтому ни у нас, ни у пользователей проблем не возникало.

Расширения компиляторов

В данном разделе речь пойдёт о коде, который вы, как я надеюсь, больше нигде не увидите: коде, использующем расширения GCC. Казалось бы, зачем они могут нам понадобиться? В большинстве кроссплатформенных проектов их вряд ли станут использовать. Во-первых, как показывает практика, их используют. Разрабатывая систему тестирования проектов под Linux, мы встречали такой код. Но основная проблема возникает при разборе кода стандартной библиотеки: уж там расширения используются во всю силу. В препроцессированных файлах из своего проекта никогда нельзя быть уверенным: ради оптимизации привычная вам функция memset может оказаться макросом, состоящим из statement expression. Но обо всём по порядку. Какие новые конструкции мы встретили, проверяя проекты под Linux?

Одним из первых встреченных расширений стали designated initializers. С помощью них можно проинициализировать массив в произвольном порядке. Особенно это удобно, если индексируется он по enum: мы в явном виде указываем индекс, повышая тем самым читабельность и уменьшая вероятность ошибиться при его модификации в дальнейшем. Выглядит очень красиво и просто:

enum Enum {
  A,
  B,
  C
};

int array[] = {
  [A] = 10,
  [B] = 20,
  [C] = 30,
}

Ради спортивного интереса немного усложним пример:

enum Enum {
  A,
  B,
  C
};

struct Struct {
    int d[3];
};

struct Struct array2[50][50] = {
    [A][42].d[2] = 4
};

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

int array[] = {
  [0 ... 99] = 0,
  [100 ... 199] = 10,
}

Небольшое, но очень полезное с точки зрения безопасности расширение GCC связано с нулевым указателем. О проблеме использования NULL было сказано уже много слов, не буду повторяться. Для GCC ситуация немного лучше, потому что NULL в C++ объявлен как __null. Таким образом, GCC уберегает нас от подобных выстрелов в ногу:

int foo(int *a);
int foo(int a);

void test() {
    int a = foo(NULL);
}

При компиляции получаем ошибку:

test.c: In function 'void test()':
test.c:20:21: error: call of overloaded 'foo(NULL)' is ambiguous
     int a = foo(NULL);
                     ^
test.c:10:5: note: candidate: int foo(int*)
 int foo(int *a) {
     ^
test.c:14:5: note: candidate: int foo(int)
 int foo(int a) {

В GCC есть возможность задавать атрибуты __attribute__(()). Есть целый список атрибутов для функций, переменных и типов, с помощью которых можно управлять линковкой, выравниванием, оптимизациями и многими другими вещами. Одним из интересных атрибутов является transparent_union. Если сделать такой union параметром функции, то в качестве аргумента можно передавать не только сам union, но и указатели из этого перечисления. Вот такой код будет корректен:

typedef union {
    long *d;
    char *ch;
    int  *i;
} Union __attribute((transparent_union));

void foo(Union arg);

void test() {
    long d;
    char ch;
    int i;

    foo(&d);  //ok
    foo(&ch); //ok
    foo(&i);  //ok
}

Примером, который использует transparent_union, может послужить функция wait: она может принимать как int*, так и union wait*. Сделано это в угоду совместимости с POSIX и 4.1BSD.

Про вложенные функции в GCC вы наверняка слышали. В них можно использовать переменные, объявленные до функции. Ещё вложенную функцию можно передавать по указателю (хотя и не стоит, по понятным причинам, вызывать её по этому указателю после завершения работы основной функции).

int foo(int k, int b, int x1, int x2) {
    int bar(int x) {
        return k * x + b;
    }
    return bar(x2) - bar(x1);
}

void test() {
    printf("%d\n", foo(3, 4, 1, 10)); //205
}

Но знали ли вы, что из такой функции можно сделать goto в "функцию-родителя"? Особенно эффектно это смотрится в сочетании с передачей такой функции в другую.

int sum(int (*f)(int), int from, int to) {
    int s = 0;
    for (int i = from; i <= to; ++i) {
        s += f(i);
    }
    return s;
}

int foo(int k, int b, int x1, int x2) {
    __label__ fail;
    int bar(int x) {
        if (x >= 10)
            goto fail;
        return k * x + b;
    }
    return sum(bar, x1, x2);
fail:
    printf("Exceptions in my C?!\n");
    return 42;
}

void test() {
    printf("%d\n", foo(3, 4, 1, 10)); //42
}

На практике правда такой код может привести к крайне печальным последствиям: exception safety - достаточно сложная тема даже для C++ с RAII, не говоря уж о C. Поэтому лучше так не делать.

К слову о goto. В GCC метки можно сохранять в указатели и переходить по ним. А если записать их в массив, получится таблица переходов:

int foo();
int test() {
    __label__ fail1, fail2, fail3;
    static void *errors[] = {&&fail1, &&fail2, &&fail3};
    int rc = foo();
    assert(rc >= 0 && rc < 3);
    if (rc != 0)
        goto *errors[rc];
    return 0;

fail1:
    printf("Fail 1");
    return 1;
fail2:
    printf("Fail 2");
    return 2;
fail3:
    printf("Fail 3");
    return 3;
}

А это небольшое расширение Clang. С этим компилятором PVS-Studio работать умеет уже давно, тем не менее, даже сейчас мы не перестаём удивляться новым конструкциям, появляющимся в языке и компиляторах. Вот одна из них:

void foo(int arr[static 10]);

void test()
{
    int a[9];
    foo(a); //warning

    int b[10];
    foo(b); //ok
}

При такой записи компилятор проверяет, что переданный массив имеет 10 или более элементов и выдаёт предупреждение, если это не так:

test.c:16:5: warning: array argument is too small; contains 9
      elements, callee requires at least 10 [-Warray-bounds]
    foo(a);
    ^   ~
test.c:8:14: note: callee declares array parameter as static here
void foo(int arr[static 10])
             ^  ~~~~~~~~~~~

Закрытое тестирование Beta версии. Волна 1

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

Когда мы начали выдавать анализатор первым тестерам, выяснилось, что предоставлять анализатор исполняемым файлом недостаточно. Мы получали отзывы от "У вас отличный продукт, мы нашли кучу ошибок" до "Я не доверяю вашему приложению и не буду его устанавливать в /usr/bin!". К сожалению, последних было больше. Таким образом, аргументы форумчан о возможности самостоятельной работы с исполняемым файлом были преувеличены. В таком виде работать с анализатором могут или хотят не все. Необходимо воспользоваться какими-нибудь общепринятыми способами распространения ПО в Linux.

Закрытое тестирование Beta версии. Волна 2

Получив первые отзывы, мы остановили тестирование и окунулись в напряжённую работу почти на 2 недели. Тестирование на чужом коде выявило ещё больше проблем с компиляторами. Т.к. на базе GCC создаются компиляторы и кросс-компиляторы под различные платформы, то наш анализатор начали использовать для проверки чего угодно, даже программного обеспечения разных железяк. В принципе анализатор справлялся со своими обязанностями, и мы получали благодарственные отзывы, но некоторые фрагменты кода анализатор пропускал из-за расширений, которые нам приходилось поддерживать.

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

Большой доработкой было создание Deb/Rpm пакетов. После их появления, недовольства по установке PVS-Studio отпали. Был, наверное, всего один человек, которого возмутила установка пакета с использованием sudo. Хотя таким образом ставится почти весь софт.

Закрытое тестирование Beta версии. Волна 3

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

  • Отказ от конфигурационных файлов для быстрой проверки - после введения Deb/Rpm пакетов первое место заняла проблема с заполнением конфигурационного файла для анализатора. Пришлось доработать режим быстрой проверки проектов без конфигурационного файла всего с двумя обязательными параметрами: путь к лицензионному файлу и путь к отчёту анализатора. Возможность расширенной настройки этого режима осталась.
  • Улучшена работа с логом утилиты strace - изначально лог утилиты strace обрабатывался скриптом на языке Perl, на котором был сделан прототип. Скрипт медленно работал и плохо распараллеливал анализ. После переписывания этого функционала на С++ обработка файла ускорилось, также стало легче поддерживать весь код на одном языке программирования.
  • Доработка Deb/Rpm пакетов - т.к. для работы режима быстрой проверки нужна утилита strace и в первые пакеты входили Perl/Python скрипты, то мы не сразу правильно прописали все зависимости, а позже вовсе отказались от скриптов. Позже несколько человек написали про предупреждения при установке анализатора через графические менеджеры, и мы их быстро устранили. Тут хочется отметить пользу от способа тестирования, который мы для себя настроили: в Docker разворачиваются несколько десятков дистрибутивов Linux и в них устанавливаются собранные пакеты. Возможность запуска установленных программ тоже проверялась. Такое тестирование позволило нам оперативно вносить новые изменения в пакеты и тестировать их.
  • Другие доработки по анализатору и документации. Все наши наработки мы отражали в документации. Ну а работа по доработке анализатора не прекращается никогда: это новые диагностики и улучшение существующих.

Закрытое тестирование Beta версии. Волна 4 (Release Candidate)

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

Также пользователи стали больше интересоваться расширенными настройками анализатора. Поэтому мы начали дорабатывать документацию на предмет, как интегрировать анализатор в Makefile/CMake/QMake/QtCreator/CLion. Как это выглядит, я покажу далее.

Проработанные способы интеграции

Интеграция в Makefile/Makefile.am

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

  • Тонкая настройка анализатора;
  • Инкрементальный анализ;
  • Распараллеливание анализа на уровне сборочной системы;
  • Другие преимущества от сборочной системы.

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

Примерно так выглядит интеграция в Makefile:

.cpp.o:
  $(CXX) $(CFLAGS) $(DFLAGS) $(INCLUDES) $< -o $@
  pvs-studio --cfg $(CFG_PATH) --source-file $< --language C++
     --cl-params $(CFLAGS) $(DFLAGS) $(INCLUDES) $<

Интеграция в CMake/CLion

Изучив интеграцию анализатора в CMake, стало возможным использование PVS-Studio в CLion. Можно получать как файл с отчётом анализатора, так и выводить предупреждения в IDE для просмотра проблемных мест.

Интеграция в CMake/QtCreator

Для работы с CMake проектами в QtCreator точно также можно сохранять отчёт или сразу просматривать предупреждения в IDE. В отличии от CLine, QtCreator умеет открывать для просмотра отчёты, сохранённые в формате TaskList.

Интеграция в QMake/QtCreator

Для QMake проектов мы тоже предусмотрели способ простой интеграции:

pvs_studio.target = pvs
pvs_studio.output = true
pvs_studio.license = /path/to/PVS-Studio.lic
pvs_studio.cxxflags = -std=c++14
pvs_studio.sources = $${SOURCES}
include(PVS-Studio.pri)

Заключение

К чему мы пришли за время разработки:

  • Анализатор легко установить из пакета или репозитория;
  • С анализатором легко познакомиться, выполнив проверку без интеграции анализатора в сборочную систему;
  • Для регулярного использования анализатора можно настроить инкрементальный анализ на машине каждого разработчика;
  • Настройка полной проверки на build-сервере;
  • Интеграция с популярными IDE.

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

Скачать и попробовать анализатор можно по ссылке. Следите за нашими новостями и присылайте проекты для проверки, теперь и в Linux!