- Зачем идете в горы вы?
Ведь Эльбрус и с самолета видно здорово!
Приветствую! Я Владислав Щапов и я обожаю манулов. А еще я разработчик в компании НИЦ ЦТ, которая разрабатывает операционную систему для российских процессоров Эльбрус. Одной из моих рабочих задач было провести тестирование статического анализатора PVS-Studio для проверки С и С++ кода на Эльбрусе. Эта задачка была непростой и очень напомнила мне восхождение на гору, когда за каждым пройденным испытанием сразу возникает какой-то новый вызов. Но манулы не сдаются!
Немного вводных: что такое CollabOS?
Компания НИЦ ЦТ разрабатывает операционную систему CollabOS, которая создана для работы на базе отечественных процессоров с архитектурой Эльбрус (e2k). CollabOS основана на исходных кодах CentOS 9 и EPEL, которые мы портируем и патчим для работы на платформе Эльбрус. Сборка всех пакетов дистрибутива ведется нативно, без использования кросскомпиляции. Нативная сборка позволяет нам использовать CollabOS в процессе разработки и сборки CollabOS следствием чего является возможность как можно раньше выявлять возникающие при портировании проблемы.
Следующим шагом в разработке CollabOS и программного обеспечения мы планируем внедрить процессы безопасной разработки, которые с 2024 года стали регламентироваться принятыми ГОСТами. Одним из этапов конвейера безопасной разработки является статический анализ кода. С учетом того, что мы разрабатываем операционную систему, нам необходим инструмент, который позволил бы проверять не просто проекты на машинах разработчиков, но и который можно было бы встроить в процесс сборки пакетов.
В рамках тестирования статических анализаторов мне была поставлена задача провести тестирование применимости статического анализатора PVS-Studio для проверки C и C++ кода, который нативно компилируется на Эльбрусах.
Статический анализатор PVS-Studio
PVS-Studio — это статический анализатор кода, который может выявлять опечатки, мертвый код, другие ошибки и потенциальные уязвимости в коде программ на C, C++, C# и Java. Этот сервис анализирует препроцессированные файлы, те исходные файлы, в которых с помощью препроцессора развернуты все макросы и директивы #include
. Применение статического анализа помогает контролировать качество кода и обеспечивать безопасность проекта.
К тому же применение статического анализа требуется при организации процесса безопасной разработки по ГОСТу. И если наша ОС должна получить сертификацию ФСТЭК, то без статического анализатора никак не обойтись. Так что хотим мы или нет, но нам придется применять статический анализатор кода на Эльбрусах. И вот тут-то манул и столкнулся с первый проблемой: в операционной системе Linux PVS-Studio работает только на архитектуре x86_64.
Способы запуска анализатора PVS-Studio в Linux
Для запуска статического анализатора PVS-Studio в Linux предусмотрено три основных способа:
С использованием JSON Compilation Database.
С использованием трассировки компиляции.
Прямая интеграция анализатора в сборочную систему.
Рассмотрим их подробнее.
Запуск анализа с использованием JSON Compilation Database
В этом случае анализатор PVS-Studio использует содержимое файлов compile_commands.json
. Файл compile_commands.json
может выглядеть следующим образом:
[
{
"directory": "/home/user/development/project",
"command": "/usr/bin/c++ ... -c ../foo/foo.cc",
"file": "../foo/foo.cc",
"output": "../foo.dir/foo.cc.o"
},
...
{
"directory": "/home/user/development/project",
"command": "/usr/bin/c++ ... -c ../foo/bar.cc",
"file": "../foo/bar.cc",
"output": "../foo.dir/bar.cc.o"
}
]
Как видно из приведенного фрагмента, файл содержит информацию обо всех вызовах компилятора, включая текущий рабочий каталог, информацию о входных и выходных файлах и полную команду компилятора вместе с параметрами.
Файл compile_commands.json
умеют создавать многие широко используемые инструменты сборки: CMake, Ninja, GNU Make, Qt Build System, Xcode. Запуск систем сборки с генерацией файла compile_commands.json описан в документации.
pvs-studio-analyzer
разбирает файл compile_commands.json
, извлекает из него информацию о компилируемых файлах, о компиляторе и его параметрах, после чего запускает генерацию препроцессированных файлов и проводит статический анализ, результат которого записывается в лог.
Таким образом, обработка идет в две стадии:
Сборка проекта с генерацией
compile_commands.json
файла.Анализ проекта.
При этом на стадии анализа должно быть доступно то же самое окружение, что и на стадии компиляции. А для корректного разбора параметров командной строки вызова компилятора, подготовки препроцессированных файлов и анализа PVS-Studio должен знать и поддерживать используемый для сборки проекта компилятор и препроцессор.
Запуск анализа с использованием трассировки компиляции
В том случае, когда сборочная система не поддерживает генерацию compile_commands.json
, возможно извлечение необходимой информации непосредственно из процесса сборки с использованием трассировки системных вызовов приложением strace.
В этом случае для сборки проекта и отслеживания процесса его компиляции необходимо выполнить команду:
pvs-studio-analyzer trace -- make
где вместо make может быть вызов любой команды запуска сборки проекта со всеми необходимыми параметрами.
В результате трассировки по умолчанию будет сформирован файл strace_out.
Запуск процесса анализа выполняется точно так же, как и при использовании файла compile_commands.json
. Если pvs-studio-analyzer
не найдет в текущем каталоге фай compile_commands.json
, то будет проведен поиск и использование файла с результатами трассировки strace_out
. Все остальные действия анализатор выполняет точно так же, как и при использовании compile_commands.json
.
Прямая интеграция анализатора в сборочную систему
Прямая интеграция анализатора в сборочную систему требует внесения соответствующих изменений в сборочной системе. Разберем приведенный в документации пример прямой интеграции анализатора в систему сборки на базе Make-файлов:
.cpp.o:
$(CXX) $(CFLAGS) $(DFLAGS) $(INCLUDES) $< -o $@
$(CXX) $(CFLAGS) $< $(DFLAGS) $(INCLUDES) -E -o $@.PVS-Studio.i
pvs-studio --cfg $(PVS_CFG) --source-file $< --i-file $@.PVS-Studio.i --output-file $@.PVS-Studio.log
Во второй строке выполняется команда компиляции исходного кода - это команда из немодифицированной системы сборки и она не зависит от PVS-Studio.
В третьей строке вызывается препроцессор, который создает препроцессированный файл с именем, в которое добавлен суффикс .PVS-Studio.i
.
В четвертой строке вызывается ядро статического анализатора pvs-studio
, которому передается препроцессированный файл и параметры анализа.
При таком способе интеграции анализатор будет запускаться каждый раз, когда make будет выполнять соответствующую цель.
Подробнее все стандартные способы запуска анализатора рассмотрены в документации.
А что с запуском на Эльбрусе?
PVS-Studio ничего не знает о:
платформе Эльбрус;
компиляторе LCC;
системе сборки RPM-пакетов.
PVS-Studio запускает генерацию препроцессированных файлов на этапе анализа, но анализ не может быть запущен на Эльбрусе, а генерация препроцессированных файлов невозможна на x86_64, так как при разработке не используется кросскомпиляция.
Но, даже если реализовать поддержку платформы Эльбрус в PVS-Studio, это не поможет неинвазивно проверять проекты в процессе сборки RPM-пакетов. Стандартные способы интеграции потребуют модификации spec-файлов параметров сборки пакетов.
Получается, типовые способы запуска анализатора нам не подходят и с этим надо что-то делать.

Путь на вершину Эльбруса
После обдумывания ситуации появилась идея! Почему бы не провести границу между системой сборки проекта и анализатором по другому - перенести подготовку препроцессированных файлов со стадии анализа на стадию сборки?
Действительно, если провести границу между стадиями таким образом, то анализ препроцессированных файлов можно проводить на x86_64, а сборку проектов и генерацию препроцессированных файлов на Эльбрусе.
Для решения этой задачи был разработан инструмент Build Tracer, который состоит из двух основных компонентов:
трассировщик процесса сборки RPM-пакетов (кроссплатформенный: e2k, x86_64 и т.д.);
враппер для запуска анализатора (x86_64).
В результате трассировки генерируется каталог с файлами, который необходимо перенести на x86_64-сервер с PVS-Studio и передать анализатору.
Рассмотрим подробнее разработанные модули.
Трассировщик процесса сборки RPM-пакетов
Трассировщик процесса сборки RPM-пакетов реализован в виде приложения build-tracer-rpmbuild.py
на Python, которое является оберткой над rpmbuild.
Для запуска трассировки процесса сборки RPM-пакета вместо rpmbuild необходимо вызвать build-tracer-rpmbuild.py
и все.
На рисунке представлена блоксхема работы приложения build-tracer-rpmbuild.py
.

Если приложение build-tracer-rpmbuild.py
вызвано для сборки бинарного RPM-пакета (в параметрах командной строки присутствует одна из следующих опций: -ba
, -bb
, -ra
, -rb
, -ta
, -tb
, --rebuild
, --recompile
, то производится запуск rpmbuild
с передачей всех параметров и сбор трассировки, в остальных случаях вызывается только rpmbuild
.
Трассировка rpmbuild
проводится с помощью strace
. Для упрощения последующего анализа, результат работы strace
для каждого порождаемого процесса записывается в отдельный файл. Также передаются параметры, которые включают запись абсолютного времени, экранирование выводимых строк, декодирование файловых дискрипторов, отсутствие обрезки строк и так далее.
Для построения дерева процессов, списков открытых файлов и текущих рабочих каталогов процессов необходимо, чтобы strace отслеживал следующие системные вызовы: fork
, vfork
, clone
, clone2
, clone3
, execve
, execveat
, chdir
, fchdir
, open
, openat
, openat2
.
На этапе парсинга файлов трассировки strace
производится разбор параметров системных вызовов, построение дерева процессов и идентификация тех процессов, которые являются процессами компилятора.
В текущей реализации процессы компилятора идентифицируются по имени исполняемого файла и информации, которую возвращает этот исполняемый файл при запуске с параметром -version
.
Поддерживаются компиляторы LCC, GCC и Clang.
На этапе подготовки препроцессированных файлов список процессов компиляции фильтруется от вызовов, которые ничего не компилируют (например, система сборки проверяет версию компилятора), от известных артефактов систем сборки, по списку открываемых файлов файл с исходным кодом и все его include-зависимости копируются в каталог выходных данных, после чего генерируется препроцессированный файл.
На последнем этапе информация обо всех вызовах компиляторов и сгенерированных препроцессированных файлов записывается в результирующий файл в формате JSON.
В результате получается каталог с данными, имеющий следующую внутреннюю структуру:
preprocessed
- каталог с препроцессированными файлами исходного кода;root
- каталог, в который с сохранением путей скопированы все исходные файлы, открываемые компилятором в процессе сборки;result.json
- JSON с информацией о вызовах компилятора;cmd
- командаstrace
(для отладки);strace
- каталог файлов трассировкиstrace
;cwd
- текущий рабочий каталог, в котором запущенbuild-tracer-rpmbuild.py
;rpmbuild.cmd
- командаrpmbuild
(для отладки);rpmbuild.ret
- код возврата изrpmbuild
.
Этот каталог необходимо перенести на x86_64
машину для дальнейшего анализа.
Интеграция в Mock
Mock - это инструмент для сборки RPM-пакетов в изолированном одноразовом окружении.
Так как окружение очищается после сборки, то извлечение данных трассировки должно проводиться штатными средствами Mock. Для этого был разработан плагин build_tracer
для Mock, который устанавливает все необходимые для build-tracer-rpmbuild.py
пакеты, копирует файл build-tracer-rpmbuild.py
внутрь сборочного контейнера, а после завершения сборки извлекает полученные данные трассировки в виде tar-архива.
С использованием плагина подготовка данных для последующего анализа сокращается до добавления в конфигурационный файл mock следующих параметров:
config_opts['plugin_conf']['build_tracer_enable'] = True
config_opts['plugin_conf']['build_tracer_opts'] = {
# Путь к трассирующему rpmbuild на хосте
"host_trace_rpmbuild_command": '/usr/bin/build-tracer-rpmbuild.py',
}
После этого, в каталог с результатами сборки будет сгенерирован архив с именем build_trace.tar
.
Анализатор
Для запуска ядра анализатора pvs-studio
в качестве параметров необходимо передать информацию об используемом языке программирования (C или C++), версии стандарта, путь к препроцессированному файлу, путь к файлу исходных кодов и pvs-studio
необходимо запускать в каталоге, в котором запускался компилятор, компилирующий соответствующий файл исходных кодов.
В препроцессированных файлах используются полные пути. В связи с этим мы не может просто скопировать содержимое каталога root
из полученного архива в корень файловой системы машины, где будет запускаться анализ.
Техподдержка PVS-Studio предложила отредактировать препроцессированные файлы, чтобы пути в них (в директивах препроцессора #line
) совпадали с путями в файловой системе на x86_64 хосте.
Но, к счастью, ядра анализатора pvs-studio
является статически скомпилированным исполняемым файлом, а это значит, что его можно запускать из почти пустого chroot без необходимости копировать в него какие-либо динамические библиотеки.
В chroot должно быть следующее:
содержимое каталога
root
- исходные коды для сборки, этот каталог должен отображаться на/
;содержимое каталога
preprocessed
- препроцессированне файлы, этот каталог должен отображаться на каталог внутри/
;исполняемый файл
pvs-studio
;каталог для записи результатов анализа.
Или полная схема каталогов внутри chroot:
Каталог препроцессированных файлов:
/pvs/preprocessed
Каталог приложений (
pvs-studio
иchexec
):/pvs/bin
Каталог результатов анализа:
/pvs/result
/Дополнительный каталог для
pvs-studio
:/tmp
Каталоги исходных кодов:
/home
/opt
/usr
...
Простейший способ собрать такую иерархию - это копирование файлов, однако, на больших пакетах объем копируемых данных может измеряться десятками гигабайт. На помощь приходит OverlayFS, которая позволяет собрать итоговую файловую систему из нескольких слоев.

Но использование OverlayFS потребует много ручной работы. И тут нам на помощь приходит Podman, который поддерживает Overlay Volume!
При помощи overlay volume внутри контейнера можно собрать общий каталог, удовлетворяющий требованиям выше, а дальше запустить приложение build-tracer-analyzer-pvs.py
, которое на основе данных из файла result.json
для каждого препроцессированного файла вызовет PVS-studio внутри chroot (внутри контейнера).
Рассмотрим, как можно запустить контейнер с необходимой иерархией каталогов при использовании операционной системы CentOS 9 и новее и podman версии выше 4.5.1:
podman run --rm -it --security-opt label=disable --cap-add=SYS_CHROOT \
-v ~/data/iperf/build_trace/iperf3-3.12-1.cos1:/data:O \
-v ~/data/iperf/build_trace/iperf3-3.12-1.cos1/preprocessed:/data/root/pvs/preprocessed:O \
-v ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1:/data/root/pvs/result:rw \
-v ~/pvs/pvs-studio-7.36.91321.455-x86_64/bin:/data/root/pvs/bin:O \
build-tracer-analyzer-pvs [pvs-studio args...]
Где:
- ~/data/iperf/build_trace/iperf3-3.12-1.cos1
- путь к каталогу с исходными данными, который был сгенерирован запуском сборки пакета с помощью build-tracer-rpmbuild.py
.
- ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1
- путь к каталогу с результатами анализа (ВНИМАНИЕ: Не может быть вложен в каталог с исходными данными).
- ~/pvs/pvs-studio-7.36.91321.455-x86_64/bin
- путь к каталогу, где расположен исполняемый файл ядра анализатора с именем pvs-studio
.
Обратите внимание, что для томов указывается флаг :O
, который включает использование OverlayFS. В этом случае при изменении каталога в контейнере эти изменения не влияют на состояние каталога на хосте.
Том с результатами подключается как :rw
, те он будет отображен непосредственно с хоста и все изменения в нем останутся после работы контейнера.
Тома монтируются в порядке перечисления в командной строке и каждый следующий будет монтироваться поверх предыдущего.
Однако, каждый раз писать команду podman run
со всеми параметрами – это утомительно и чревато ошибками. Поэтому для автоматизации пересборки контейнера и запуска анализатора был написан скрипт build-tracer-analyzer-pvs.sh
. С его использованием команда запуска анализа сокращается до:
build-tracer-analyzer-pvs.sh analyze \
~/data/iperf/build_trace \
~/data/iperf/build_trace-result \
~/pvs/pvs-studio-7.36.91321.455-x86_64 \
[pvs-studio args...]
После того, как скрипт build-tracer-analyzer-pvs.sh
завершит работу, в каталоге каталоге ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1
будут созданы выходные файлы по одному на каждый изначальный препроцессированный файл. и создан общий выходной файл с именем~/data/iperf/build_trace-result/iperf3-3.12-1.cos1.PVS-Studio.log
, который можно сконвертировать в читаемые форматы:
# Конвертация в CSV
~/pvs/pvs-studio-7.36.91321.455-x86_64/bin/plog-converter -t csv -o ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1.PVS-Studio.csv ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1.PVS-Studio.log
# Конвертация в JSON
~/pvs/pvs-studio-7.36.91321.455-x86_64/bin/plog-converter -t json -o ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1.PVS-Studio.json ~/data/iperf/build_trace-result/iperf3-3.12-1.cos1.PVS-Studio.log
Тестирование
После всех подготовительных работ попробуем протестировать. Рассмотрим результат работы PVS-Studio на примере пакета iperf3-3.12-1.cos1
.
Ошибка зануления буфера
Сообщение об ошибке:
error,
"=HYPERLINK(""https://pvs-studio.com/en/docs/warnings/v568/"", ""V568"")",
"It's odd that the argument of sizeof() operator is the 'sizeof (iperf_size_t) * 60.0' expression.",
"=HYPERLINK(""file:///builddir/build/BUILD/iperf-3.12/src/iperf_api.c"", "" Open file"")",
"2725",
/builddir/build/BUILD/iperf-3.12/src/iperf_api.c
Код в строке 2725:
memset(test->bitrate_limit_intervals_traffic_bytes, 0, sizeof(sizeof(iperf_size_t) * MAX_INTERVAL));
Действительно, дублирование sizeof
, из-за чего будут занулены только первые 4 или 8 байт буфера на 32битной и 64битной системах соответственно.
Ошибка была исправлена в версии iperf3-3.18
Утечка памяти в обработчике ошибки нехватки памяти
Сообщение об ошибке:
error,
"=HYPERLINK(""https://pvs-studio.com/en/docs/warnings/v773/"", ""V773"")",
"The function was exited without releasing the 'xbe' pointer. A memory leak is possible.",
"=HYPERLINK(""file:///builddir/build/BUILD/iperf-3.12/src/iperf_api.c"", "" Open file"")",
"1393",
/builddir/build/BUILD/iperf-3.12/src/iperf_api.c
Код:
xbe = (struct xbind_entry *)malloc(sizeof(struct xbind_entry));
if (!xbe) {
i_errno = IESETSCTPBINDX;
return -1;
}
memset(xbe, 0, sizeof(*xbe));
xbe->name = strdup(optarg);
if (!xbe->name) {
i_errno = IESETSCTPBINDX;
return -1; /* xbe не освобождается */
}
Не исправлено в апстриме.
Возможно чтение неинициализированной переменной
Сообщение об ошибке:
warning,
"=HYPERLINK(""https://pvs-studio.com/en/docs/warnings/v614/"", ""V614"")",
"Potentially uninitialized variable 'flag' used.",
"=HYPERLINK(""file:///builddir/build/BUILD/iperf-3.12/src/iperf_server_api.c"", "" Open file"")",
"698",
/builddir/build/BUILD/iperf-3.12/src/iperf_server_api.c
Код:
int flag;
...
if (rec_streams_accepted != streams_to_rec) {
flag = 0;
++rec_streams_accepted;
} else if (send_streams_accepted != streams_to_send) {
flag = 1;
++send_streams_accepted;
}
if (flag != -1) {
Переменная flag
не инициализируется при объявлении, а присваивания условные. После них проверка flag
на значения, не равные -1
.
Возможно, пропущена инициализация переменной в значение -1
.
Не исправлено в апстриме.
Ложноположительные предупреждения
error,
"=HYPERLINK(""https://pvs-studio.com/en/docs/warnings/v595/"", ""V595"")",
"The 'input_buffer' pointer was utilized before it was verified against nullptr. Check lines: 1470\, 1484.",
"=HYPERLINK(""file:///builddir/build/BUILD/iperf-3.12/src/cjson.c"", "" Open file"")",
"1470",
/builddir/build/BUILD/iperf-3.12/src/cjson.c
error,
"=HYPERLINK(""https://pvs-studio.com/en/docs/warnings/v595/"", ""V595"")",
"The 'input_buffer' pointer was utilized before it was verified against nullptr. Check lines: 1630\, 1636.",
"=HYPERLINK(""file:///builddir/build/BUILD/iperf-3.12/src/cjson.c"", "" Open file"")",
"1630",
/builddir/build/BUILD/iperf-3.12/src/cjson.c
Проверка input_buffer
проводится до вызова функций. Функции используют универсальные макросы, которые дополнительно проверяют input_buffer
на nullptr
.
PVS-Studio видит эти проверки после использования переменной input_buffer
и выводит ошибку.
OpenSource
Исходный код Build Tracer доступен на GitHub под лицензией GPLv2+:
https://github.com/e2k-community/build-tracer.
Заключение
PVS-Studio может быть использован для статического анализа кода, сборка которого осуществляется на неподдерживаемых архитектурах, таких, как Эльбрус и другие.

Для этого потребуется чуть больше действий и разработка средств автоматизации. Но это позволит использовать PVS-Studio не только для локального анализа кода х86_64 на машинах разработчиков, но и встроить его в пайплайн сборки пакетов для дистрибутива.
Из подводных камней, которые встретились на пути к вершине, можно отметить тот факт, что strace
замедляет работу процессов сборки и иногда это приводит к проявлению ошибок в логике сборки или в тестах.
Например, при проверке пакета gnutls
был обнаружен тест naked-alerts
, который под strace
работает нестабильно. Похоже, в нем есть состояние гонки, которое проявляется, если системные вызовы начинают работать непредсказуемо дольше.
Комментарии (6)
kemocca
07.06.2025 11:47Кажется, в таком случае подошло бы использование rtc. Не пробовали?
phprus Автор
07.06.2025 11:47Да, с помощью rtc можно встроить анализ в процесс сборки отдельных проектов, например, с использованием
compile_commands.json
.Но, ключевой частью задачи была интеграция анализа в процесс сборки пакетов дистрибутива операционной системы. И в этом случае штатные способы интеграции pvs-studio с запуском под rtc пришлось бы внедрять в
spec
-файлы для каждого пакета по отдельности (да, что-то можно было бы унифицировать макросами, но было бы очень много постоянной ручной работы).Вынос процесса проверки за пределы сборки позволил избежать ручного труда по доработке каждого пакета. По сути все изменения на стороне сборочницы были локализованы в конфигурации Mock.
nerfur
интересно можно ли эти наработки использовать не для разных архитектур, но и для разных ОС (совсем разных имеется в виду, не дистрибов линукса разных) ?
phprus Автор
Если на целевой системе есть возможность трассировать процесс сборки (чтобы извлечь дерево процессов сборки и информацию об открываемых ими файлах), то наработки можно адаптировать под такую систему.