V576. Incorrect format. Consider checking the N actual argument of the 'Foo' function.


Анализатор обнаружил потенциальную ошибку при использовании функций форматного вывода (printf, sprintf, wprintf и так далее). Строка форматирования не соответствует передаваемым в функцию фактическим аргументам. Рассмотрим простой пример:

int A = 10;
double B = 20.0;
printf("%i %i\n", A, B);

Согласно строке форматирования функция 'printf' ожидает два фактических аргумента типа 'int'. Однако второй аргумент имеет значение типа 'double'. Подобное несоответствие приводит к неопределённому поведению программы. Например, к распечатке бессмысленных значений.

Корректный вариант:

int A = 10;
double B = 20.0;
printf("%i %f\n", A, B);

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

Распечатка адреса.

Очень часто значение указателя пытаются распечатать, используя следующий код:

int *ptr = new int[100];
printf("0x%0.8X\n", ptr);

Этот код ошибочен, поскольку будет работать только в тех системах, где размер указателя совпадает с размером типа 'int'. А, например, в Win64 этот код уже распечатает только младшую часть указателя 'ptr'. Корректный вариант кода:

int *ptr = new int[100];
printf("0x%p\n", ptr);

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

Неиспользуемые аргументы.

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

int nDOW;
#define KEY_ENABLED "Enabled"
...
wsprintf(cDowKey, L"EnableDOW%d", nDOW, KEY_ENABLED);

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

wsprintf(cDowKey, L"EnableDOW%d%s", nDOW, KEY_ENABLED);

Недостаточное количество аргументов.

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

char* salloc(register int nbytes)
{
    register char* p;
    p = (char*) malloc((unsigned)nbytes);
    if (p == (char *)NULL)
    {
        fprintf(stderr, "%s: out of memory\n");
        exit(1);
    }
    return (p);
}

Если функция 'malloc' вернёт значение NULL, то программа не сможет корректно сообщить о нехватке памяти и завершить свою работу. Она аварийно завершится или распечатает непонятный текст. В любом случае, подобное поведение усложнит анализ причины неработоспособности программы.

Путаница с signed/unsigned

Очень часто программисты используют спецификатор печати знаковых значений (например '%i') для печати переменных типа unsigned. И наоборот. Эта ошибка, как правило, не критична и так сильно распространена, что в анализаторе она имеет низкий приоритет. Во многих случаях подобный код успешно работает и даёт сбой только при больших или отрицательных значениях. Рассмотрим код, который хотя не корректен, но успешно работает:

int A = 10;
printf("A = %u\n", A);
for (unsigned i = 0; i != 5; ++i)
  printf("i = %d\n", i);

Хотя здесь имеется несоответствие, это код на практике печатает корректные значения. Кончено всё равно так лучше не делать и написать корректно:

int A = 10;
printf("A = %d\n", A);
for (unsigned i = 0; i != 5; ++i)
  printf("i = %u\n", i);

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

int A = -1;
printf("A = %u", A);

Вместо строки "A = -1" программа распечатает "A = 4294967295". Корректный вариант:

printf("A = %i", A);

Широкие строки (Wide character string)

У Visual Studio есть неприятная особенность, что он нестандартно интерпретирует формат строки для печати широких символов. В результате анализатор помогает диагностировать ошибку, например, в таком коде:

const wchar_t *p = L"abcdef";
wprintf(L"%S", p);

В Visual C++ считается, что "S" предназначен для печати строки типа "const char *". Поэтому с точки зрения Visual C++ правильным является код:

wprintf(L"%s", p);

Начиная с Visual Studio 2015 предлагается решение этой проблемы, чтобы писать переносимый код. Для совместимости с ISO C (C99) следует указать препроцессору макрос _CRT_STDIO_ISO_WIDE_SPECIFIERS.

В этом случае, код:

const wchar_t *p = L"abcdef";
wprintf(L"%S", p);

является правильным.

Анализатор знает про _CRT_STDIO_ISO_WIDE_SPECIFIERS и учитывает его при анализе.

Кстати, если вы включили режим совместимости с ISO C (объявлен макрос _CRT_STDIO_ISO_WIDE_SPECIFIERS), вы можете в отдельных местах вернуть старое приведение, используя спецификатор формата "%Ts".

Вся эта история с широкими символами достаточно запутанная и выходит за пределы документации. Что-бы лучше разобраться в вопросе предлагаем ознакомиться со следующими ссылками:

Дополнительные возможности

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

Возле прототипа функции (или возле её реализации, или в общем заголовочном файле) пишется комментарий специального вида. Пример использования:

//+V576, function:Mylog, format_arg:1, ellipsis_arg:2
Mylog("%f", time(NULL)); // warning V576

Формат:

  • Ключи function, class и namespace задают имя функции, имя класса (если нужно анализировать вызов только этого метода класса) и имя пространства имён (если требуется анализировать функцию или метод класса только этого пространства имён).
  • Ключ format_arg задаёт номер аргумента функции, в котором будет находиться форматная строка. Это обязательный аргумент. Номера также считаются с единицы и также не должны превышать число 14.
  • Ключ ellipsis_arg задаёт номер аргумента функции с эллипсисом (то есть многоточием). К этому номеру предъявляются те же ограничения, что и к номеру форматной строки. Более того, номер аргумента с эллипсисом должен быть больше номера аргумента с форматной строкой (всё-таки эллипсис – исключительно последний аргумент). Это также обязательный аргумент.

Напоследок, дадим наиболее полный пример использования:

// Предупреждать, когда в методе C класса B
// из пространства имён A аргументы, начиная с
// третьего, не совпадают с типом, заданным в
// форматной строке из второго аргумента. 
//+V576,namespace:A,class:B,function:C,format_arg:2,ellipsis_arg:3

Дополнительные ресурсы:



А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++ и C#

goto PVS-Studio;
Мы используем cookie-файлы для анализа событий на нашем веб-сайте, что позволяет улучшить наш контент и сделать взаимодействие с пользователем более удобным. Продолжая просмотр страниц нашего веб-сайта, вы принимаете условия использования этих файлов. Узнайте подробнее о cookie-файлах и политике конфиденциальности или скройте это уведомление, нажав на кнопку. Подробнее →
Не показывать