Классическими примерами, приводимыми во многих статьях по проблемам переноса программ на 64-битные системы, является некорректное использование функций printf, scanf и их разновидностей.
Урок 10. Паттерн 2. Функции с переменным количеством аргументов
1. Урок 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;
2. 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.
...
3. }
Результатом манипуляций указателями с использованием %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.
Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800.