V576. Incorrect format. Consider checking the Nth 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'.
Вся эта история с широкими символами достаточно запутанная и выходит за пределы документации. Чтобы лучше разобраться в вопросе, предлагаем ознакомиться со следующими ссылками:
- Bug 1121290 - distinguish specifier s and ls in the printf family of functions
- Visual Studio swprintf is making all my %s formatters want wchar_t * instead of char *
- Update. В 2019 году появилась статья, которая рассказывает, почему получилась путаница: The sad history of Unicode printf-style format specifiers in Visual C++.
Дополнительная настройка диагностики
Чтобы самостоятельно указать имена своих собственных функций, для которых следует выполнять проверку формата, можно использовать пользовательские аннотации. Подробнее об этом можно почитать здесь.
Дополнительные ресурсы:
- Wikipedia. Printf.
- MSDN. Format Specification Fields: printf and wprintf Functions.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования форматной строки. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V576. |