Содержание статьи
Зачем нужен статический анализатор
Современные приложения — это огромные массивы кода и библиотек. Уследить за ошибками в них — дело непростое. Можно забыть высвободить ресурсы, сделать «удачную» опечатку, при которой код собирается, но ведет себя неправильно, допустить утечку памяти... Короче, есть множество ситуаций, которые способны превратить отладку в изматывающее приключение. И если ты оказался в одной из них, это вовсе не значит, что ты плохой программист. Просто люди могут уставать, отвлекаться и допускать ошибки.
Для помощи программистам придумали специальные тулзы — статические анализаторы кода, которые помогают в разработке и выявляют разные ошибки: логические баги, опечатки, опасные конструкции, несоответствия каким‑то стандартам и конвенциям и так далее. Статические анализаторы применяются именно на этапе разработки, а не тестирования, поэтому позволяют править код на месте, что намного проще и быстрее.
Давай посмотрим, как обращаться с анализатором на примере PVS-Studio. Я покажу, какие ошибки будут им ловиться, а за чем придется следить самостоятельно. PVS-Studio «понимает» C/C++, C# и Java, поддерживает Windows, Linux и macOS. Но поскольку показывать я буду всё на рабочем проекте, то сфокусируюсь на C++ и Windows.
Установка и настройка
Итак, качаем PVS-Studio с сайта разработчика. На момент написания статьи актуальная версия — 7.19.
Промо
Чтобы получить бесплатную пробную лицензию на 30 дней, перейди по ссылке. В поле «промокод» будет вставлен код xakep, который компания‑разработчик предоставила для читателей «Хакера».
Что интересно, инсталлятор сразу нашел у меня на машине Visual Studio 2022 и IntelliJ IDEA Ultimate 2022 и сразу предложил интегрироваться в них. Не отказываемся, потому что будет удобно использовать статанализатор не как отдельную тулзу, а именно как плагин для IDE.

Теперь откроем Visual Studio и пройдемся по самым интересным настройкам.

Обрати внимание на IntermoduladAnalysisCpp, что означает «межмодульный анализ». Эта настройка применяется только к проектам на C++. Разработчики уверяют, что при включении этой опции анализатор производит анализ более эффективно. Проверим! Вот два сканирования: без этой опции и вместе с ней.


Как видишь, стало больше на одну ошибку класса Low. Не очень впечатляет, но галочку лучше оставить.
Следующая интересная опция — No Noise. Она исключает низкоприоритетные предупреждения, чтобы было проще ориентироваться в выдаче анализатора.
Рекомендую заглянуть и во вкладку Detectable Errors — здесь можно включать или выключать обнаружение определенных групп ошибок.
PVS-Studio умеет определять потенциальные уязвимости, связанные с безопасностью кода. Среди нацеленных на это наборов правил:
- CWE — база конструкций языка, которые содержат потенциальные уязвимости;
- AUTOSAR — набор правил для стандарта C++ 14 для встраиваемых отказоустойчивых систем;
- MISRA — правила, выработанные компанией MISRA для встраиваемых систем, где важны безопасность и отказоустойчивость;
- OWASP — правила для безопасной разработки веб‑приложений;
- SEI-CERT — правила, созданные центром CERT, опять же, для повышения надежности ПО.
Рекомендую включить все эти подсказки и обращать на них внимание при написании кода. Они значительно увеличат шанс не пропустить потенциальный баг.
Помимо этого, PVS-Studio определяет проблемы 64-битного кода, предлагает оптимизации производительности и даже содержит несколько правил (Custom), которые сделаны специально по заявкам трудящихся.
Анализатор на практике: разбираем примеры
Теперь перейдем к обзору найденных при помощи PVS-Studio ошибок и недочетов. Весь код, который будет тут представлен для примера, — реальный, из крупного опенсорсного проекта, который развивается несколько лет. «Синтетических» ошибок, искусственно созданных для демонстрации возможностей статанализатора, я приводить не буду, ведь это не так интересно.
Запускаем сканирование! Первый недочет, найденный анализатором и имеющий рейтинг Medium:
V560 A part of conditional expression is always true: threadArg
Если нажать на номер ошибки, откроется сайт разработчика с пояснением (как на русском, так и на английском языках): «Анализатор обнаружил потенциально возможную ошибку внутри логического условия. Часть логического выражения всегда истинно и оценено как опасное». Разберемся...
EncryptionThreadPool.c 205:static TC_THREAD_PROC EncryptionThreadProc (void *threadArg){ EncryptionThreadPoolWorkItem *workItem; if (threadArg) {#ifdef DEVICE_DRIVER SetThreadCpuGroupAffinity ((USHORT) *(WORD*)(threadArg));#else SetThreadGroupAffinityFn SetThreadGroupAffinityPtr = (SetThreadGroupAffinityFn) GetProcAddress (GetModuleHandle (L"kernel32.dll"), "SetThreadGroupAffinity"); if (SetThreadGroupAffinityPtr && threadArg) { GROUP_AFFINITY groupAffinity = {0}; groupAffinity.Mask = ~0ULL; groupAffinity.Group = *(WORD*)(threadArg); SetThreadGroupAffinityPtr(GetCurrentThread(), &groupAffinity, NULL); }#endif }Анализатор ругается вот на эту строку:
if (SetThreadGroupAffinityPtr && threadArg)Просмотрим начало всей функции: если указатель threadArg валиден, мы входим в тело if. Далее, в нем есть еще один вложенный if с условием SetThreadGroupAffinityPtr , на который как раз и жалуется анализатор. Но threadArg мы ведь уже проверили, нет нужды его проверять опять! Код избыточный, хоть и не опасный. Смело убираем часть условия && и идем дальше.
Анализатор жалуется вот на этот код:
ci = crypto_open (); if (!ci) return FALSE; if (QueryPerformanceFrequency (&benchmarkPerformanceFrequency) == 0) { if (ci) crypto_close (ci); MessageBoxW (hwndDlg, GetString ("ERR_PERF_COUNTER"), lpszTitle, ICON_HAND); return FALSE; }Ошибка:
V547. Expression is always true/false
«Условие всегда истинно или ложно». В этом примере проверка существования ci уже есть:
if (!ci) return FALSE;Далее идет еще одна, хотя никаких манипуляций с ci нет:
if (ci) crypto_close (ci);Опять же некритично, но код можно почистить и похвалить анализатор за помощь. Едем дальше, нашли сразу три недостатка в такой функции:
void DisableCPUExtendedFeatures (){ g_hasSSE2 = 0; g_hasISSE = 0; g_hasMMX = 0; g_hasSSE2 = 0; g_hasISSE = 0; g_hasMMX = 0; g_hasAVX = 0; g_hasAVX2 = 0; g_hasBMI2 = 0; g_hasSSE42 = 0; g_hasSSE41 = 0; g_hasSSSE3 = 0; g_hasAESNI = 0; g_hasCLMUL = 0;}Ошибка:
V1048. Variable 'g_hasSSE2' was assigned the same value
Подсказка с сайта: «Анализатор обнаружил присваивание переменной значения, которое уже содержится в ней. Скорее всего, это логическая ошибка». При этом ошибки одинаковые для g_hasSSE2, g_hasISSE, g_hasMMX.
Смотрим внимательнее и понимаем, что безболезненно можем удалить вот эти строки:
g_hasSSE2 = 0; g_hasISSE = 0; g_hasMMX = 0;Дальше вот такое интересное предупреждение:
V1042 This file is marked with copyleft license, which requires you to open the derived source code. jitterentropy.h 23Оно сработало на стандартный текст лицензии GNU. Анализатор поясняет: «Анализатор обнаружил в файле copyleft лицензию, которая обязывает открыть остальной исходный код. Это может быть неприемлемо для многих коммерческих проектов». С одной стороны, мы тут вроде бы анализируем код, а не юридические тонкости. С другой — в мире еще есть разработчики, которые не знают о копилефте и думают, что раз код открытый, значит можно брать. Опять же, если ошибка не полезна, ты можешь ее отключить.
А вот интересно сработали правила OWASP (правда на тестовом коде проекта):
derive_key_streebog ("password", 8, "\x12\x34\x56\x78", 4, 5, dk, 96);Ошибка:
V5013 [OWASP-2.10.4] Storing credentials inside source code can lead to security issues. Tests.c 1762
Пояснение: «Анализатор обнаружил в коде данные, которые могут являться конфиденциальными». Ну, тут и добавить нечего. Анализатор заподозрил, что в коде прямым текстом указаны пароли, и оказался прав! Но в данном случае это механизм тестирования, и пароль некритичный.
Теперь включим анализ 64-битного кода. Анализатор сразу заприметил вот такую строчку:
GlobalMemoryStatus (&memoryStatus);Ошибка:
V303 The function 'GlobalMemoryStatus' is deprecated in the Win64 system. It is safer to use the 'GlobalMemoryStatusEx' function. Random.c 875
Другими словами, на 64-битной платформе функцию GlobalMemoryStatus применять не рекомендуется и ее лучше заменить на GlobalMemoryStatusEx. Впрочем, о той же ошибке сообщает и сам Visual Studio.
У анализатора могут быть и ложные срабатывания. К примеру, получаем вот такую ошибку уровня High:
V547 Expression is always true. Crypto.c 1465)
Подозрение вызвала вот эта строка:
VcProtectMemory (encID, pCryptoInfo->ks, MAX_EXPANDED_KEY,И следующая:
pCryptoInfo->ks2, MAX_EXPANDED_KEY,Взглянем на всю функцию.
static void VcInternalProtectKeys (PCRYPTO_INFO pCryptoInfo, uint64 encID){#ifdef TC_WINDOWS_DRIVER VcProtectMemory (encID, pCryptoInfo->ks, MAX_EXPANDED_KEY, pCryptoInfo->ks2, MAX_EXPANDED_KEY);#else VcProtectMemory (encID, pCryptoInfo->ks, MAX_EXPANDED_KEY, pCryptoInfo->ks2, MAX_EXPANDED_KEY, pCryptoInfo->master_keydata, MASTER_KEYDATA_SIZE, pCryptoInfo->k2, MASTER_KEYDATA_SIZE);#endif}Далее посмотрим на прототип VcProtectMemory:
void VcProtectMemory (uint64 encID, unsigned char* pbData, size_t cbData, unsigned char* pbData2, size_t cbData2, unsigned char* pbData3, size_t cbData3, unsigned char* pbData4, size_t cbData4)Очевидно, это ошибка анализатора (точнее, синтаксического парсера, как я понимаю), потому что никаких условий здесь нет и всегда истинными они быть не могут. Видимо, причина такого поведения — крупный макрос, который передается в качестве аргумента.
Или вот интересное поведение. Вопросы вызвал такой код.
BOOL bNewDesktopSet = FALSE; // wait for SwitchDesktop to succeed before using it for current thread while (true) { if (SwitchDesktop (pParam->hDesk)) { bNewDesktopSet = TRUE; break; } Sleep (SECUREDESKTOP_MONOTIR_PERIOD); } if (bNewDesktopSet) { SetThreadDesktop (pParam->hDesk);Анализатор ругается на строку if ( со следующим вердиктом:
V547 Expression 'bNewDesktopSet' is always true. Dlgcode.c 14113
Давай разбираться: bNewDesktopSet инициализируется как FALSE при объявлении, далее входим в цикл, в котором переключение bNewDesktopSet на TRUE возможно только в том случае, если сработает WinAPI SwitchDesktop. Де‑юре анализатор прав, но прав ли он по сути? Во‑первых, мы не можем быть уверены, произойдет ли событие SwitchDesktop (, потому что за поведение WinAPI мы не отвечаем. Во‑вторых, взгляни на архитектуру кода: выполнение тела if отдано на откуп поведению функции WinAPI SwitchDesktop, которая или сработает (будет переход), или образует вечный цикл, потому как while (. На мой взгляд, ошибки «Expression ... is always true» в таком случае быть не должно.
Дальше встречаем такую ошибку:
V112 Dangerous magic number
А вот строка‑нарушитель:
unsigned __int32 CRC = 0xffffffff;Ошибка означает, что используется «магическая» константа, о предназначении который знает только тот, кто ее написал.
При разработке кода системного уровня подобные ошибки будут мельтешить в глазах и мешать обращать внимание на более важные вещи. Поэтому просто отключай их, чтобы не мешали.
Выводы
Проверив наш проект, анализатор сделал несколько сотен замечаний — и это только по категории General, без включения дополнительных проверок, иначе счет пошел бы на тысячи! Разбирать их все в статье нет смысла, так она превратится в многостраничный отчет по разбору выдачи PVS-Studio — со ссылками на документацию и выдержками из Страуструпа.
Возможно, у тебя возникнет вопрос о том, зачем вообще нужны сторонние статические анализаторы, если во многие IDE они и так встроены. Все дело в количестве и качестве проверок. Когда анализатор — это основной проект для разработчика, то можно ожидать, что анализ будет более тщательным и глубоким (примерно как Windows Defender не заменит настоящий продвинутый файрвол). Так и оказалось в случае с PVS-Studio.
Не думай при этом, что статический анализатор — это волшебная палочка, которая выручит тебя из любой беды и будет писать хороший код за тебя. Работа с анализатором — это прежде всего разгребание отчетов и вдумчивое чтение кода. Автоматический инструмент тоже будет ошибаться, просто не так же, как человек. Зато вместе вы достигнете большего! Для разработчиков больших проектов это однозначно мастхэв.
