Урок 10. Паттерн 2. Функции с переменным количеством аргументов
Классическими примерами, приводимыми во многих статьях по проблемам переноса программ на 64-битные системы, является некорректное использование функций printf, scanf и их разновидностей.
Пример 1:
const char *invalidFormat = "%u";
size_t value = SIZE_MAX;
printf(invalidFormat, value);
Пример 2:
char buf[9];
sprintf(buf, "%p", pointer);
В первом случае не учитывается, что тип size_t не эквивалентен типу unsigned на 64-битной платформе. Это приведет к выводу на печать некорректного результата в случае, если value > UINT_MAX.
Во втором случае автор кода не учел, что размер указателя в будущем может составить более 32 бит. В результате на 64-битной архитектуре данный код приведет к переполнению буфера.
Некорректное использование функций с перемененным количеством параметров является распространенной ошибкой на всех архитектурах, а не только 64-битных. Это связано с принципиальной опасностью использования данных конструкций языка Си++. Общепринятой практикой является отказ от них и использование безопасных методик программирования. Мы настоятельно рекомендуем модифицировать код и использовать безопасные методы. Например, можно заменить printf на cout, а sprintf на boost::format или std::stringstream.
Данную рекомендацию часто критикуют разработчики под Linux, аргументируя тем, что gcc проверяет соответствие строки форматирования фактическим параметрам, передаваемым в функцию printf. Однако они забывают, что строка форматирования может передаваться из другой части программы, загружаться из ресурсов. Другими словами, в реальной программе строка форматирования редко присутствует в явном виде в коде, и, соответственно, компилятор не может ее проверить. Если же разработчик использует Visual Studio 2005/2008, то он не сможет получить предупреждение на код вида "void *p = 0; printf("%x", p);", даже используя ключи /W4 и /Wall.
Для работы с memsize-типами в функциях вида sscanf, printf имеются спецификаторы размера. Если вы разрабатываете Windows-приложение, то вы можете использовать спецификатор размера "I". Пример использования:
size_t s = 1;
printf("%Iu", s);
Если вы разрабатываете приложение под Linux, то вам будет доступен спецификатор размера "z". Пример использования:
size_t s = 1;
printf("%zu", s);
Спецификаторы хорошо описаны в статье Wikipedia "printf".
Если вы вынуждены поддерживать переносимый код, использующий функции типа sscanf, то в формате управляющих строк можно использовать специальные макросы, раскрывающиеся в необходимые спецификаторы размера. Пример макроса, помогающего создавать переносимый код для разных систем:
// PR_SIZET on Win64 = "I"
// PR_SIZET on Win32 = ""
// PR_SIZET on Linux64 = "z"
// ...
size_t u;
scanf("%" PR_SIZET "u", &u);
Рассмотрим еще один пример. Хотя этот пример выглядит наиболее странно, код, который приведен здесь в упрощенном виде, использовался в реальном приложении в подсистеме UNDO/REDO:
// Здесь указатели сохранялись в виде строки
int *p1, *p2;
....
char str[128];
sprintf(str, "%X %X", p1, p2);
// А в другой функции данная строка
// обрабатывалась следующим образом:
void foo(char *str)
{
int *p1, *p2;
sscanf(str, "%X %X", &p1, &p2);
// Результат - некорректное значение указателей p1 и p2.
...
}
Результатом манипуляций указателями с использованием %X стало некорректное поведение программы на 64-битной системе. Данный пример показывает, как опасны потаенные дебри больших и сложных проектов, которые пишутся многими годами. Если проект достаточно велик и стар, то в нем можно встретить очень интересные фрагменты, подобные этому.
Диагностика
Опасность для функций с переменным количеством аргументов представляют типы, меняющие свой размер на 64-битной системе, то есть memsize типы. Статический анализатор PVS-Studio предупреждает об использовании таких типов диагностическим сообщением V111.
Если типы аргументов не изменили своей разрядности, то код считается корректным и предупреждающих сообщений выдано не будет. Пример корректного кода с точки зрения анализатора:
printf("%d", 10*5);
CString str;
size_t n = sizeof(float);
str.Format(StrFormat, static_cast<int>(n));
Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).
Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com.