Продукт PVS-Studio содержит набор правил статического анализа общего назначения, предназначенный для выявления широкого спектра различных дефектов в коде программ на языке C/C++/C++11.
Набор правил общего назначения позволяет обнаружить логические ошибки, опечатки, фрагменты кода, приводящие к access violation, некорректное использование алгоритмов из библиотеки STL и многое другое.
При выборе правил, реализуемых в PVS-Studio, мы руководствуемся в первую очередь их актуальностью. Мы сознательно не реализуем такие стандарты как MISRA, так как большинство описанных в них рекомендаций мало помогают разработчикам, использующим современные инструменты, такие как Visual Studio и такие библиотеки как STL, MFC, Qt, Boost. В PVS-Studio мы стараемся реализовать диагностики, учитывающие не только особенности конструкций языка Си/Си++, но и более высокоуровневые паттерны, а также взаимодействие кода с библиотеками. Многие из проверок уникальны и не реализованы в известных нам анализаторах.
Паттерны ошибок, выявляемые PVS-Studio весьма разнообразны. Поэтому далее будут продемонстрированы только некоторые разновидности диагностируемых ошибок. С другими типами ошибок, выявляемыми анализатором, можно познакомиться в документации. Приведенные примеры ошибок были обнаружены PVS-Studio в реальных проектах.
Ошибка, когда "+1" написан не там, где следует
if ((t=(char *)realloc(next->name, strlen(name+1))))
{
next->name=t;
strcpy(next->name, name);
}
Здесь анализатор обнаружил ошибку, допущенную по невнимательности. Программа выделяет на 2 байта меньше памяти, чем ей в действительности необходима. Исправление ошибки состоит в том, чтобы вынести "+1" за скобки. Корректный вариант: "realloc(next->name, strlen(name) + 1)".
Не до конца очищенный массив
#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
memset(_iContMap, -1, CONT_MAP_MAX);
Данная ошибка относится к классу buffer underrun / overrun. Эти ошибки возникают, когда массив обрабатывается не полностью или наоборот модифицируется память за пределами массива. В приведенном примере функция memset() очищает далеко не весь массив _iContMap. Ошибка заключается забытом sizeof():
memset(_iContMap, -1, CONT_MAP_MAX * sizeof(_iContMap[0]));
Забытое разыменование указателя
if (pSlash != NULL) {
pSlash++;
if (pSlash != '\0') {
...
Анализатор часто помогает обнаружить подозрительную работу с указателем, как например, в данном примере. Сравниваем указатель с 0. Если указатель не равен 0, то увеличиваем его на единицу. И затем опять сравниваем его с 0. Очень странные действия. И естественно они содержат ошибку, так как забыто обыкновенное разыменование указателя: "if (*pSlash != '\0') {".
Некорректный цикл
int i, destlen = 0, l, k;
for (i=0; i<srclen; i++)
{
...
for (k=i; i<srclen; k++)
{
if (src[k]=='>')
break;
}
i = k;
}
В данном коде анализатор обнаружил опечатку - во вложенном цикле для сравнения используется переменная "i", а не "k". Это код с большой вероятностью приведет к обращению к памяти за пределами обрабатываемого массива.
Опечатки в именах переменных
class CSize : public SIZE
{
...
CSize(POINT pt) { cx = pt.x; cx = pt.y; }
Данная ошибка обнаружена в проекте, где реализуется свой собственный класс CSize. Из-за опечатки одной и той же переменной по очереди присваиваются различные значений. Это и привлекло к себе внимание анализатора. Корректный вариант второго присваивания: "cy = pt.y;".
Некорректное использование Windows API
OPENFILENAME lofn;
...
lofn.lpstrFilter = L"Equalizer Preset (*.feq)\0*.feq";
В Windows API есть структуры, в которых указатели на строки должны заканчиваться двойным нулем. В данном примере это член lpstrFilter в структуре OPENFILENAME. Подобный код приведет к тому, что в диалоге работы с файлом в поле фильтров мы можем увидеть мусор. Исправленный код:
lofn.lpstrFilter = L"Equalizer Preset (*.feq)\0*.feq\0";
В конце строки явно написан 0, и еще один ноль добавит компилятор.
Ошибка работы со стандартными алгоритмами
До этого все рассмотренные примеры ошибок являются низкоуровневыми. Рассмотрим теперь проверки в PVS-Studio, которые работают с более высокими понятиями, такими как блок кода или алгоритм.
void unregisterThread() {
Guard<TaskQueue> g(_taskQueue);
std::remove(_threads.begin(), _threads.end(), ThreadImpl::current());
}
Функция std::remove не удаляет элементы из контейнера. Она только сдвигает элементы и возвращает итератор на начало мусора. Пусть мы имеем контейнер vector<int>, содержащий элементы 1,2,3,1,2,3,1,2,3. Если выполнить код "remove( v.begin(), v.end(), 2 )", то контейнер будет содержать элементы 1,3,1,3,?,?,?, где ? - некий мусор. При этом функция вернет итератор на первый мусорный элемент, и если мы хотим удалить эти мусорные элементы, то должны написать код: "v.erase(remove(v.begin(), v.end(), 2), v.end())". Корректный вариант кода:
auto trash = std::remove(_threads.begin(), _threads.end(),
ThreadImpl::current());
_threads.erase(trash, _threads.end());
Ошибка при написании кода методом Copy-Paste
void KeyWordsStyleDialog::updateDlg()
{
...
Style & w1Style =
_pUserLang->_styleArray.getStyler(STYLE_WORD1_INDEX);
styleUpdate(w1Style, _pFgColour[0], _pBgColour[0],
IDC_KEYWORD1_FONT_COMBO, IDC_KEYWORD1_FONTSIZE_COMBO,
IDC_KEYWORD1_BOLD_CHECK, IDC_KEYWORD1_ITALIC_CHECK,
IDC_KEYWORD1_UNDERLINE_CHECK);
Style & w2Style =
_pUserLang->_styleArray.getStyler(STYLE_WORD2_INDEX);
styleUpdate(w2Style, _pFgColour[1], _pBgColour[1],
IDC_KEYWORD2_FONT_COMBO, IDC_KEYWORD2_FONTSIZE_COMBO,
IDC_KEYWORD2_BOLD_CHECK, IDC_KEYWORD2_ITALIC_CHECK,
IDC_KEYWORD2_UNDERLINE_CHECK);
Style & w3Style =
_pUserLang->_styleArray.getStyler(STYLE_WORD3_INDEX);
styleUpdate(w3Style, _pFgColour[2], _pBgColour[2],
IDC_KEYWORD3_FONT_COMBO, IDC_KEYWORD3_FONTSIZE_COMBO,
IDC_KEYWORD3_BOLD_CHECK, IDC_KEYWORD3_BOLD_CHECK,
IDC_KEYWORD3_UNDERLINE_CHECK);
Style & w4Style =
_pUserLang->_styleArray.getStyler(STYLE_WORD4_INDEX);
styleUpdate(w4Style, _pFgColour[3], _pBgColour[3],
IDC_KEYWORD4_FONT_COMBO, IDC_KEYWORD4_FONTSIZE_COMBO,
IDC_KEYWORD4_BOLD_CHECK, IDC_KEYWORD4_ITALIC_CHECK,
IDC_KEYWORD4_UNDERLINE_CHECK);
...
}
Заметить ошибку в этом коде непросто. Однако PVS-Studio терпелив и педантичен: "V525: The code containing the collection of similar blocks. Check items '7', '7', '6', '7' in lines 576, 580, 584, 588". Сократим код, чтобы было проще заметить ошибку:
styleUpdate(...
IDC_KEYWORD1_BOLD_CHECK, IDC_KEYWORD1_ITALIC_CHECK,
...);
styleUpdate(...
IDC_KEYWORD2_BOLD_CHECK, IDC_KEYWORD2_ITALIC_CHECK,
...);
styleUpdate(...
IDC_KEYWORD3_BOLD_CHECK, IDC_KEYWORD3_BOLD_CHECK,
...);
styleUpdate(...
IDC_KEYWORD4_BOLD_CHECK, IDC_KEYWORD4_ITALIC_CHECK,
...);
Код, скорее всего, писался методикой Copy-Paste. Результатом стало использование IDC_KEYWORD3_BOLD_CHECK вместо IDC_KEYWORD3_ITALIC_CHECK. Числа в диагностическом сообщении берутся из макросов вида:
#define IDC_KEYWORD1_ITALIC_CHECK (IDC_KEYWORD1 + 7)
#define IDC_KEYWORD3_BOLD_CHECK (IDC_KEYWORD3 + 6)
Последний приведенный пример показателен тем, что анализатор PVS-Studio осуществил обработку одновременно целого большого участка кода, выявил в нем повторяющиеся структуры и на основе эвристики посчитал этот код потенциально опасным.
Преимущества использования статического анализа общего назначения
Наибольшая ценность статического анализа заключается в том, что ряд ошибок может быть выявлен на самых ранних этапах разработки. Чем раньше выявлена ошибка, тем проще, быстрее и дешевле она может быть устранена. Это сокращает расходы на этапах тестирования и поддержки.
Статический анализатор работает с исходным кодом и просматривает все пути возможного выполнения программы. Таким образом, ошибки могут быть обнаружены в редко используемом коде, или в коде который планируется использовать на следующих этапах проекта.
PVS-Studio – современный анализатор без тяжелого груза прошлого. Он активно развивается и учится выявлять новые паттерны ошибок, с которыми сталкиваются в данный момент программисты, использующие Microsoft Visual C++. Вы можете внести свой вклад и предложить нам придуманные вами правила для диагностики. Присылайте ваши пожелания через страницу обратной связи.