software-development
November 24, 2023

PVS Studio и FreeBSD 

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

"Hero shot" , на фоне - очередной шедерв нейросети с Двачей.

Что это и зачем

Если вы никогда не слышали термин «анализатор кода» и ничего не знаете о проекте PVS Studio — вам это 100% не надо, забудьте как страшный сон и спите себе спокойно.

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

анализатор кода — это такая программа для поиска проблем в исходном коде других программ.

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

Пока еще.

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

Стати́ческий ана́лиз ко́да (англ. static code analysis) — анализ программного обеспечения, производимый без реального выполнения исследуемых программ (в отличие от динамического анализа). В большинстве случаев анализ производится над исходным кодом, хотя, иногда анализу подвергается объектный код, например P-код или код на MSIL. Термин обычно применяют к анализу, производимому специальным программным обеспечением (ПО), тогда как ручной анализ называют «program understanding», «program comprehension» (пониманием или постижением программы).

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

И прямо сейчас мы будем его запускать, прямо вот этими руками да.

Запуск на FreeBSD

Разумеется FreeBSD не поддерживается создателями «PVS-Studio» (ну кто бы сомневался), поэтому будем использовать бинарник версии для Линукса.

Скачиваются все версии PVS Studio по этой ссылке.

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

В статье речь пойдет разумеется о самом хардкорном варианте анализатора — для языков Си и C++

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

Распаковываем архив:

cd /opt/app
tar xvf ~/Downloads/pvs-studio-7.27.75620.346-x86_64.tgz

Как видно по выдаче команды file, бинарники являются 64-битными, со статичной сборкой:

cd pvs-studio-7.27.75620.346-x86_64/ 
[alex@zodiac /opt/app/pvs-studio-7.27.75620.346-x86_64]$ file ./bin/pvs-studio-analyzer  
./bin/pvs-studio-analyzer: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for GNU/Li
nux 3.2.0, BuildID[xxHash]=328d76629f3a5b28, not stripped

Что дает отличный шанс на их использование во FreeBSD через эмулятор линукса:

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

Устанавливаем пакет эмуляции:

pkg install linux_base-c7

Прописываем в /etc/fstab линуксовые /proc и /sys виртуальные файловые системы:

linprocfs   /compat/linux/proc  linprocfs       rw      0       0 
linsysfs    /compat/linux/sys   linsysfs        rw      0       0

Если надо чтоб работало без перезапуска, выполняем:

mount -a

Прописываем в /etc/rc.conf активацию сервиса эмуляции:

linux_enable="YES" 
linux64_enable="YES"

Либо перезагружаем систему либо запускаем вручную сервис эмуляции:

service linux start

Для проверки можно выполнить:

Вот такой вот разрыв шаблона, в выдаче uname сразу и Linux и FreeBSD.

Теперь возвращаемся к «PVS Studio», если вы все настроили и включили правильно, то сможете запустить бинарник анализатора:

Минутка внутреннего устройства

Немного расскажу как оно вообще все работает:

Существуют специальные инструменты трассировки, которые записывают в лог-файл все вызовы компилятора, со всеми ключами, заголовочными файлами и библиотеками.

Вот по такому сформированному логу вызовов и происходит анализ проекта. Выглядит это как-то так (фрагмент):

[
  {
    "arguments": [
      "/usr/bin/clang++",
      "-c",
      "-pipe",
      "-std=c++11",
      "-O2",
      "-std=gnu++11",
      "-Wall",
      "-Wextra",
      "-pthread",
      "-fPIC",
      "-DQT_NO_DEBUG",
      "-DQT_WIDGETS_LIB",
      "-DQT_MULTIMEDIA_LIB",
      "-DQT_GUI_LIB",
      "-DQT_SCRIPT_LIB",
      "-DQT_TESTLIB_LIB",
      "-DQT_NETWORK_LIB",
      "-DQT_CORE_LIB",
      "-DQT_TESTCASE_BUILDDIR=\"/opt/src/ukncbtl-qt/emulator\"",
      "-I.",
      "-I/usr/local/include/qt5",
      "-I/usr/local/include/qt5/QtWidgets",
      "-I/usr/local/include/qt5/QtMultimedia",
      "-I/usr/local/include/qt5/QtGui",
      "-I/usr/local/include/qt5/QtScript",
      "-I/usr/local/include/qt5/QtTest",
      "-I/usr/local/include/qt5/QtNetwork",
      "-I/usr/local/include/qt5/QtCore",
      "-I.",
      "-I/usr/local/include",
      "-I.",
      "-I/usr/local/include",
      "-I/usr/local/lib/qt5/mkspecs/freebsd-clang",
      "-o",
      "main.o",
      "main.cpp"
    ],
    "directory": "/opt/src/ukncbtl-qt/emulator",
    "file": "/opt/src/ukncbtl-qt/emulator/main.cpp",
    "output": "/opt/src/ukncbtl-qt/emulator/main.o"
  },
...

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

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

Очень похоже на Perl, поэтому думаю для Perl-программиста не будет особой проблемы его прочитать:

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

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

pvs-studio.com/en/docs/warnings/	1	err	Help: The documentation for all analyzer warnings is available here: https://pvs-studio.com/en/docs/warnings/.
/opt/src/ukncbtl-qt/emulator/emubase/Memory.h	101	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0160000, Dec: 57344.
/opt/src/ukncbtl-qt/emulator/emubase/Memory.h	143	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0160000, Dec: 57344.
/opt/src/ukncbtl-qt/emulator/emubase/Processor.h	289	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0600, Dec: 384.
/opt/src/ukncbtl-qt/emulator/emubase/Processor.h	294	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0600, Dec: 384.
/opt/src/ukncbtl-qt/emulator/emubase/Processor.h	299	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0600, Dec: 384.
/opt/src/ukncbtl-qt/emulator/emubase/Processor.h	304	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0600, Dec: 384.
/opt/src/ukncbtl-qt/emulator/emubase/Processor.h	311	err	V536 Be advised that the utilized constant value is represented by an octal form. Oct: 0600, Dec: 384.

Тут уж совсем все просто и очевидно:

конкретный файл, номер строки, уровень п#здеца, код предупреждения PS-Studio, текст сообщения.

Все для дебилов (владеющих английским).

Тестовые проекты

Я взял три разных открытых проекта с разными системами сборки, для иллюстрации всех основных вариантов использования: cmake, qt5 и чистый make.

Что характерно — ни один из проектов (кроме последнего) официально не поддерживается на FreeBSD.

Хотя когда это меня останавливало.

Начнем с самого наверное популярного варианта — с cmake.

86Box при запуске с пустой виртуалкой по-умолчанию.

Проект на cmake: 86Box

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

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

Забираем исходники:

git clone https://github.com/86Box/86Box

Запускаем первую стадию сборки с записью команд компилятора:

mkdir build & cd build
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ..

В каталоге build должен появиться файл compile_commands.json:

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

Теперь запускаем стадию анализа:

/opt/app/pvs-studio-7.27.75620.346-x86_64/bin/pvs-studio-analyzer analyze 

Выполняться будет достаточно долго:

Когда закончит, в каталоге build появится файл PVS-Studio.log с найденными в проекте проблемами и ошибками:

Разумеется название этого и всех остальных файлов можно менять, но я ленивый, поэтому использовал вариант по-умолчанию.

Если вы не являетесь Perl-разработчиком, то придется все же сконвертировать полученный файл в формат доступный для чтения обычными живыми программистами:

/opt/app/pvs-studio-7.27.75620.346-x86_64/bin/plog-converter -a GA:1,2 -t
 tasklist -o le-putain.txt PVS-Studio.log

Если все прошло хорошо, то будет создан файл «le-putain.txt» с читабельными результатами работы анализатора:

Конвертация кстати очень быстро отрабатывает
Название файла — отсылка к любимой фразе моего бывшего CTO родом из Франции: некая непереводимая игра слов про продажную любовь и женское коварство.

Не гуглите перевод, не надо.

В моем случае получилось 1624 строки, каждая (кроме первой) с информацией о потенциальной жопе проблеме в проекте:

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

Чтобы не быть голословным давайте откроем несколько найденных проблем.

Начнем с простого и очевидного:

/opt/src/86Box/src/86box.c 571 warn V755 Copying potentially tainted data from 'argv' to buffer 'log_path'. Buffer overflow is possible.

Cмотрим в код:

Видим потенциальное переполение буфера, поскольку длина входящего аргумента неизвестна:

If the copied data size exceeds the buffer size, the buffer overflows. To avoid this, pre-calculate the required amount of memory:

Спросите что такое «переполнение буфера»?

Ну как минимум программа «упадет» — перестанет функционировать, как максимум — можно получить уязвимость, ту самую через которые злые хакеры к вам залезут и все украдут.

Теперь более критичный пример:

/opt/src/86Box/src/chipset/via_apollo.c 548 err V547 Expression is always true.

Смотрим:

Что же тут не так?

А не так тут тот простой факт что проверяются границы значения dev->id, по константам:

А чуть выше по коду происходит разовая инициализация dev->id:

Проще говоря это как выражение: «1 меньше 2 и 2 меньше 3» зашитое в код. Очевидно что такое выражение использованное в виде условия всегда будет верным (возвращать true)

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

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

Поэтому плавно переходим ко второму тестовому проекту.

Вот так выглядит запущенный эмулятор УКНЦ на FreeBSD

Проект на QT5: UKNCBTL – эмулятор УКНЦ

Следующий проект это тоже эмулятор, только в этот раз старого советского компьютера:

«Электроника МС 0511» (также известный под названием «УК НЦ») — персональная микроЭВМ, советский учебный компьютер (УК), разработанный для учебных классов. Входит в состав КУВТ «Электроника МС 0202»[⇨].
Разработан в НПО «Научный центр», г. Зеленоград. Главный конструктор — А. Е. Абрамов, зам. ГК А. Н. Полосин[1], ведущие разработчики: Н. Г. Карпинский, А. И. Половянюк, О. Л. Семичастнов, Б. Г. Бекетов, А. Д. Развязнев, И. О. Лозовой, М. И. Дябин, В. Л. Сафонов, И. Н. Селянко, В. Н. Дронов и др.[2]

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

Забираем исходный код проекта:

git clone https://github.com/nzeemin/ukncbtl-qt.git

С этим проектом есть один нюанс:

в проекте активно используется модуль «Qt Scripting», который был убран из текущей 6й версии Qt Framework, на который перевели основную среду разработки для QT — QT Creator.

Вообщем собрать из среды разработки наверное как-то можно, но поскольку речь про анализ кода, а не саму разработку, я не стал заморачиваться — вместо функционала QT Creator:

To generate 'compile_commands.json' in the project that uses qmake, you can use IDE QtCreator version 4.8 or higher. Open the desired project and select 'Build->Generate Compilation Database for %project_name%' in the menu bar:

использовал внешнюю утилиту Bear.

Что в некотором смысле даже ближе к реальному применению в боевых проектах.

Утилита Bear есть в FreeBSD в виде готового пакета, поэтому для установки достаточно выполнить:

pkg install bear

Запускаем qmake:

cd ukncbtl-qt/emulator
/usr/local/bin/qmake-qt5

Следующим шагом запускаем сборку, но через bear для трассировки вызовов компилятора:

bear — gmake

В результате должен сформироваться готовый бинарник в этом же каталоге и все тот же файл compile_commands.json:

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

Дальше все та же стадия анализа:

Готовый результат (для Perl-программистов) и необходимая конвертация в человеческий:

Получилось 96 строк с описанием проблем, сильно меньше чем в предыдущем, но тут и сам проект куда скромнее.

Разберем пару найденных предупреждений.

/opt/src/ukncbtl-qt/emulator/Emulator.cpp 604 warn V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!okCursorType' and 'okCursorType'.

Что тут происходит:

Вся проблема вот в этом выражении:

!okCursorType || (okCursorType && bit == cursorAddress)

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

По логике этого выражения надо оставить лишь bit == cursorAddress, убрав проверку на okCursorType.

Опять же, самая частая причина появления такого кода — рефакторинг, когда из-за цепочки правок теряется изначальный смысл.

Еще один характерный пример:

/opt/src/ukncbtl-qt/emulator/emubase/Memory.cpp 470 warn V1037 Two or more case-branches perform the same actions. Check lines: 470, 484, 487

Смотрим код:

Вообще такая простыня case-условий называется "state machine", часто это результат генерации из лексера/

И ниже еще два условия case, первое:

 case 0176644: case 0176645:
        SetPortWord(address, word);
        break;
   ..

и второе:

case 0176646: case 0176647:
        SetPortWord(address, word);
        break;
..

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

Правильный (с точки зрения анализатора же) вариант вот такой:

case 0176641: case 0176644: case 0176645: case 0176646: case 0176647:
        SetPortWord(address, word);
        break;
 ..           

Выглядит конечно монструозно, но что поделать )

И на самое сладкое - третий тестовый проект с чистой make сборкой.

Проект с чистым make: ядро FreeBSD

Разумеется я не мог упустить такого шанса и загнал в анализатор исходники cамого ядра FreeBSD. Заодно устроил стресс-тест анализатору.

Сейчас будет весело, поехали :)

Забираем исходники:

git clone -b releng/14.0 --depth 1 https://git.freebsd.org/src.git /usr/src

Cкачиваем только релизную ветку текущей 14й версии и без истории, поскольку полная версия репозитория занимает ~2.5Гб и скачивается достаточно долго.

В FreeBSD нет разделения на ядро и саму ОС с окружением, вся операционная система разрабатывается в одном репозитории.

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

cd /usr/src
bear — make buildkernel

Работать будет долго, сильно дольше чем без трассировки:

Время сборки немного неверное поскольку ноутбук пару раз засыпал в процессе

В результате получился достаточно объемный compile_commands.json:

Дальше запускаем все тот же:

/opt/app/pvs-studio-7.27.75620.346-x86_64/bin/pvs-studio-analyzer analyze

Затем конвертируем полученный результат:

/opt/app/pvs-studio-7.27.75620.346-x86_64/bin/plog-converter -a GA:1,2 -t tasklis
t -o le-putain.txt PVS-Studio.log

Получилось 4.3Мб всякого интересного:

30к предупреждений, вот это я называю "хорошее начало"!

Давайте разберем пару-тройку-десятку найденных проблем:

/usr/src/sys/amd64/amd64/machdep.c 1061 err V547 Expression 'page_bad == 1' is always false.

Что внутри:

Скажу честно, скорее всего мои выводы будут неверны, поскольку код достаточно сложный:

	page_bad = FALSE;
			if (memtest == 0)
				goto skip_memtest;

			/*
			 * Print a "." every GB to show we're making
			 * progress.
			 */
			page_counter++;
			if ((page_counter % PAGES_PER_GB) == 0)
				printf(".");

			/*
			 * map page into kernel: valid, read/write,non-cacheable
			 */
			*pte = pa | PG_V | PG_RW | PG_NC_PWT | PG_NC_PCD;
			invltlb();

			tmp = *(int *)ptr;
			/*
			 * Test for alternating 1's and 0's
			 */
			*(volatile int *)ptr = 0xaaaaaaaa;
			if (*(volatile int *)ptr != 0xaaaaaaaa)
				page_bad = TRUE;
			/*
			 * Test for alternating 0's and 1's
			 */
			*(volatile int *)ptr = 0x55555555;
			if (*(volatile int *)ptr != 0x55555555)
				page_bad = TRUE;
			/*
			 * Test for all 1's
			 */
			*(volatile int *)ptr = 0xffffffff;
			if (*(volatile int *)ptr != 0xffffffff)
				page_bad = TRUE;
			/*
			 * Test for all 0's
			 */
			*(volatile int *)ptr = 0x0;
			if (*(volatile int *)ptr != 0x0)
				page_bad = TRUE;
			/*
			 * Restore original value.
			 */
			*(int *)ptr = tmp;

skip_memtest:
			/*
			 * Adjust array of valid/good pages.
			 */
			if (page_bad == TRUE)
				continue;
	

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

Но скорее всего причина в другом, чуть выше по коду есть вот такое:

	/*
	 * The boot memory test is disabled by default, as it takes a
	 * significant amount of time on large-memory systems, and is
	 * unfriendly to virtual machines as it unnecessarily touches all
	 * pages.
	 *
	 * A general name is used as the code may be extended to support
	 * additional tests beyond the current "page present" test.
	 */
	memtest = 0;
	TUNABLE_ULONG_FETCH("hw.memtest.tests", &memtest);

Те тут ставится значение memtest в 0, затем происходит проверка настройки sysctl, если она есть — значение меняется.

А при значении 0 происходит пропуск всего блока условий и переход по метке:

	if (memtest == 0)
				goto skip_memtest;

Где уже есть та самая подозрительная (с точки зрения анализатора) проверка:

skip_memtest:
			/*
			 * Adjust array of valid/good pages.
			 */
			if (page_bad == TRUE)
				continue;

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

Следующий случай:

/usr/src/sys/kern/uipc_mqueue.c 670 warn V666 Consider inspecting seventh argument of the function 'uma_zcreate'. It is possible that the value does not correspond with the length of a string which was passed with the first argument.

Ну как я мог пройти мимо предупреждения с номером 666 еще и в ядре FreeBSD?

Вот что тут внутри:

Вообщем увы, но тут полный фальстарт:

The analyzer suspects that an incorrect argument has been passed into a function. An argument whose numerical value doesn't coincide with the string length found in the previous argument is considered incorrect. The analyzer draws this conclusion examining pairs of arguments consisting of a string literal and an integer constant. Analysis is performed over all the function calls of the same name.

Анализатор принял вот этот макрос:

/* Definitions for align */
#define UMA_ALIGN_PTR	(sizeof(void *) - 1)	/* Alignment fit for ptr */

за длину строковой константы, которая передается первым аргументом. С кем не бывает.

Что мы говорим богу рефакторинга?

Не сегодня PVS-Studio, не сегодня ;)

Чтобы вы не подумали будто я все манипуляции проводил на виртуалке, выкладываю фото того самого ноутбука на котором все это делал:

В фоне работающий PVS-Studio, по центру - клубничка.