Межмодульный Анализ C++-Проектов В Pvs-Studio

В PVS-Studio есть одно существенное изменение — поддержка межмодульного анализа C++-проектов.

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



Межмодульный анализ C++-проектов в PVS-Studio

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

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

Это очень похоже на проблему оптимизации времени соединения (LTO).

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

Чтобы понять, почему реализация кросс-модульного анализа — непростая задача, следует сначала ознакомиться со структурой проектов C++.



Краткая теория компиляции C++-проектов

До появления стандарта C++20 в языке использовалась только одна схема компиляции.

Как правило, программный код содержится отдельно в файлах заголовков и файлах исходного кода.

Давайте рассмотрим все поэтапно:

Межмодульный анализ C++-проектов в PVS-Studio

  1. Препроцессор выполняет предварительные операции над каждым скомпилированным файлом (единицей перевода) перед передачей его компилятору.

    На этом этапе вместо директив #include подставляется текст всех заголовочных файлов и раскрываются все макросы.

    Результатом этого этапа являются так называемые предварительно обработанные файлы.

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

    Такие файлы называются объектными файлами.

  3. Компоновщик объединяет все объектные файлы в двоичный исполняемый файл, разрешая при этом конфликты при совпадении символов.

    Только на этом этапе код, написанный в разных файлах, связывается в единое целое.

Преимущество этого подхода — параллелизм.

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

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

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

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

После этого с ним можно работать и проводить различные диагностики.

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

Сбор этой информации представляет собой межмодульный анализ.

Стоит отметить, что стандарт C++20 внес изменения в конвейер компиляции.

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

Это отдельная тема для обсуждения и, очевидно, головная боль разработчиков C++-инструментов.

Однако на момент написания эта функциональность недостаточно поддерживается системами сборки.

По этой причине мы сосредоточимся на классическом методе компиляции.



Межмодульный анализ в компиляторах

Одним из самых популярных инструментов в мире переводчиков является ЛЛВМ — набор инструментов для создания компиляторов и работы с кодом.

На его основе построены многие компиляторы для таких языков, как C/C++ (Clang), Rust, Haskell, Fortran, Swift и многих других.

Это стало возможным благодаря тому, что промежуточное представление LLVM абстрагирует как конкретный язык программирования, так и платформу.

Межмодульный анализ в LLVM выполняется именно на промежуточном представлении при оптимизации времени соединения (LTO — Link Time Optimization).

В документация LLVM описывает четыре фазы LTO:

Межмодульный анализ C++-проектов в PVS-Studio

  1. Чтение файлов с промежуточным представлением.

    Компоновщик читает объектные файлы в случайном порядке и собирает информацию о встречающихся символах в глобальную таблицу символов.

  2. Разрешение символа.

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

    Обычно именно здесь обнаруживается большинство ошибок времени компоновки.

  3. Оптимизация файлов с промежуточным представлением.

    Компоновщик выполняет эквивалентные преобразования файлов промежуточного представления на основе собираемой им информации.

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

  4. Разрешение символов после оптимизации.

    Необходимо создать новую таблицу символов для объединенного объектного файла.

    После этого компоновщик продолжает работать в обычном режиме.

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

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

Нельзя игнорировать GCC — второй популярный компилятор языков C/C++.

Он также представляет оптимизацию времени привязки.

Однако они устроены немного иначе.

  1. Первым шагом GCC является создание собственного внутреннего промежуточного представления, называемого GIMPLE, для каждого файла.

    Он хранится в специальных объектных файлах в формате ELF. По умолчанию эти файлы содержат только байт-код. Но если вы укажете флаг -ffat-lto-объекты , то GCC поместит промежуточный код в отдельный раздел рядом с готовым объектным кодом.

    Это необходимо для поддержки связывания без включения LTO. На этом этапе создается потоковое представление всех внутренних структур данных, необходимых для оптимизации кода.

  2. Далее GCC второй раз перебирает объектные модули с уже записанной в них межмодульной информацией и выполняет оптимизации.

    Затем они связываются в один объектный файл.

Кроме того, GCC поддерживает режим WHOPR, в котором связывание объектных файлов происходит поэтапно на основе графа вызовов.

Это позволяет выполнить второй этап параллельно и не загружать всю программу в память.



Наша реализация

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

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

).

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

Выполнение этой задачи может занять слишком много времени.

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

Такая информация представляется как набор фактов для конкретного символа.

На основе этой идеи был разработан описанный ниже подход. Межмодульный анализ выполняется в три этапа:

Межмодульный анализ C++-проектов в PVS-Studio

  1. Семантический анализ каждой отдельной единицы перевода.

    Анализатор собирает информацию о каждом программном символе, для которого найдены потенциально интересные факты.

    После чего эта информация записывается в файлы специального формата.

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

  2. Слияние персонажей.

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

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

  3. Запустите диагностику.

    Анализатор заново проходит каждую единицу трансляции.

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

    Это делает фактическую информацию доступной для символов из других модулей.

К сожалению, при такой реализации некоторая информация теряется.

Дело в том, что для анализа потока данных может потребоваться информация о зависимостях между модулями для расчета виртуальных значений (возможных диапазонов/наборов значений).

Но это невозможно, так как к каждому модулю обращаются только один раз.

Для решения этой проблемы потребуется предварительный анализ вызова функции, как это делает GCC (граф вызовов).

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



Как попробовать

Режим межмодульного анализа можно запустить на всех трех поддерживаемых нами платформах.

Важное уточнение : На данный момент межмодульный анализ несовместим с режимами запуска анализа по списку файлов и с режимом инкрементного анализа.



Работа на Linux/macOS

Утилита используется для анализа проектов под Linux/macOS. pvs-studio-анализатор .

Чтобы включить режим межмодульного анализа, достаточно добавить флаг --межмодульный команде анализатор pvs-studio-analyzer .

В этом случае анализатор сформирует отчет и сам удалит все временные файлы.

Межмодульный анализ также поддерживается в плагинах IDE. В Linux и macOS он доступен в среде IDE JetBrains CLion. Чтобы включить кросс-модульный анализ, достаточно включить соответствующую галочку в настройках плагина.



Межмодульный анализ C++-проектов в PVS-Studio

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

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

V013 : «Межмодульный анализ может быть неполным, так как он запускается не для всех исходных файлов».

Плагин также синхронизирует свои настройки с глобальным файлом.

Настройки.

xml .

Это позволяет иметь одинаковые настройки для всех IDE, в которые интегрирован PVS-Studio. Поэтому остается возможность вручную включить в нем несовместимые настройки.

В этом случае при попытке запустить анализ плагин выдаст в окне предупреждения ошибку «Ошибка: Флаги --incremental и --intermodular не могут использоваться вместе».



Работа в Windows

Под Windows анализ можно запустить двумя способами: через консольные утилиты *PVS-Studio_Cmd* и CLMonitor или через плагин.

Для запуска утилит PVS-Studio_Cmd / CLMonitor просто установите значение истинный по тегу в конфигурации Настройки.

xml .

Анализ в плагине Visual Studio включается следующей опцией:

Межмодульный анализ C++-проектов в PVS-Studio



Что мы нашли

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



zlib

В522 Может произойти разыменование нулевого указателя.

Нулевой указатель передается в функцию «_tr_stored_block».

Проверьте второй аргумент. Проверьте строки: «trees.c:873», «deflate.c:1690».

  
  
   

// trees.c void ZLIB_INTERNAL _tr_stored_block(s, buf, stored_len, last) deflate_state *s; charf *buf; /* input block */ ulg stored_len; /* length of input block */ int last; /* one if this is the last block for a file */ { // .

zmemcpy(s->pending_buf + s->pending, (Bytef *)buf, stored_len); // <= // .

} // deflate.c local block_state deflate_stored(s, flush) deflate_state *s; int flush; { .

/* Make a dummy stored block in pending to get the header bytes, * including any pending bits. This also updates the debugging counts. */ last = flush == Z_FINISH && len == left + s->strm->avail_in ? 1 : 0; _tr_stored_block(s, (char *)0, 0L, last); // <= .

}

Нулевой указатель (символ*)0 Впадать в память второй аргумент через функцию _tr_stored_block .

Хотя настоящей проблемы здесь нет (поскольку копируются нулевые байты), стандарт явно сказал , что при вызове таких функций, как память указатели всегда должны указывать на действительные данные, даже если счетчик равен нулю.

Если это не так, то мы имеем дело с неопределенное поведение .

Ошибка уже есть зафиксированный , но не в релизной версии, а в ветке разработки.

У проекта не было ни одного релиза на протяжении 4 лет. Изначально ошибка была найденный под санитайзерами.



MC

В774 Указатель 'w' использовался после освобождения памяти.

редактироватьcmd.c 2258

// editcmd.c gboolean edit_close_cmd (WEdit * edit) { // .

Widget *w = WIDGET (edit); WGroup *g = w->owner; if (edit->locked != 0) unlock_file (edit->filename_vpath); group_remove_widget (w); widget_destroy (w); // <= if (edit_widget_is_editor (CONST_WIDGET (g->current->data))) edit = (WEdit *) (g->current->data); else { edit = find_editor (DIALOG (g)); if (edit != NULL) widget_select (w); // <= } } // widget-common.c void widget_destroy (Widget * w) { send_message (w, NULL, MSG_DESTROY, 0, NULL); g_free (w); } void widget_select (Widget * w) { WGroup *g; if (!widget_get_options (w, WOP_SELECTABLE)) return; // .

} // widget-common.h static inline gboolean widget_get_options (const Widget * w, widget_options_t options) { return ((w->options & options) == options); }

Функция widget_destroy освобождает память от указателя, делая его недействительным.

Однако после вызова он передается виджет_выбрать , откуда он входит widget_get_options , где происходит разыменование.

Оригинал Виджет *w взято из параметра редактировать и перед звонком виджет_выбрать есть звонок find_editor , который переопределяет переданный параметр.

Скорее всего, переменная ш Это просто используется для оптимизации и упрощения кода, чтобы исправленный вызов выглядел как *widget_select(WIDGET(edit))*.

Ошибка присутствует в владелец -ветвь.



коделит

В597 Компилятор может удалить вызов функции memset, который используется для очистки текущего объекта.

Функцию memset_s() следует использовать для удаления личных данных.

args.c 269 Был интересный случай с удалением звонка Мемсет :

// args.c extern void eFree (void *const ptr); extern void argDelete (Arguments* const current) { Assert (current != NULL); if (current->type == ARG_STRING && current->item != NULL) eFree (current->item); memset (current, 0, sizeof (Arguments)); // <= eFree (current); // <= } // routines.c extern void eFree (void *const ptr) { Assert (ptr != NULL); free (ptr); }

Вызов Мемсет можно удалить при использовании оптимизации LTO, поскольку компилятор может это проанализировать.

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

Без вызова LTO eFree выглядит как неизвестная внешняя функция, поэтому Мемсет останется.



Заключение

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

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

Вы можете попробовать новый режим прямо сейчас.

Он доступен начиная с версии PVS-Studio v7.14, которую можно скачать с нашего сайта.

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

Если у вас есть вопросы, вы можете Напишите нам .

Надеемся, что этот режим будет полезен при исправлении ошибок в вашем проекте.

Если вы хотите поделиться этой статьей с англоязычной аудиторией, воспользуйтесь ссылкой для перевода: Сергей Ларин, Олег Лисий.

Межмодульный анализ C++-проектов в PVS-Studio .

Теги: #C++ #компиляторы #pvs-studio #lto #компилятор #статический анализ #статический анализатор #компиляция

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.