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
- MBCS to Unicode conversion in swprintf
- 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++.
Дополнительные возможности
Можно самостоятельно указать имена своих собственных функций, для которых следует выполнять проверку формата. Подразумевается, что принцип форматирования строк совпадает с функцией 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
Дополнительные ресурсы:
- Wikipedia. Printf.
- MSDN. Format Specification Fields: printf and wprintf Functions.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V576. |