Pull to refresh
280.72
PVS-Studio
Static Code Analysis for C, C++, C# and Java

Коллекция примеров 64-битных ошибок в реальных программах — часть 1

Reading time 16 min
Views 4.9K
Эту статью я посвящаю хабрапользователю f0b0s, который постоянно следит за нашей активностью, сопровождая ее тонким юмором, что держит нас в тонусе.

Читатели наших статей, посвященных разработке 64-битных приложений, часто упрекают нас в отсутствии обоснованности описываемых проблем. А именно, что мы не приводим примеры ошибок в реальных приложениях.

Я решил собрать примеры различных типов ошибок, которые мы сами обнаружили в реальных программах, о которых прочитали в интернете или о которых нам сообщили пользователи PVS-Studio. Итак, предлагаю вашему вниманию статью, представляющую собой коллекцию из 30 примеров 64-битных ошибок на языке Си и Си++.

Продолжение статьи >>



Введение


Наша компания ООО «Системы программной верификации» занимается разработкой специализированного статического анализатора Viva64 выявляющего 64-битные ошибки в коде приложений на языке Си/Си++. В ходе этой работы наша коллекция примеров 64-битных дефектов постоянно пополняется, и мы решили собрать в этой статье наиболее интересные на наш взгляд ошибки. В статье приводятся примеры как взятые непосредственно из кода реальных приложений, так и составленные синтетически на основе реального кода, так как в нем они слишком «растянуты».

Статья только демонстрирует различные виды 64-битных ошибок и не описывает методов их обнаружения и профилактики. Вы можете подробно познакомиться с методами диагностики и исправления дефектов в 64-битных программах, обратившись к следующим ресурсам:
  1. Курс по разработке 64-битных приложений на языке Си/Си++ [1];
  2. Что такое size_t и ptrdiff_t [2];
  3. 20 ловушек переноса Си++ — кода на 64-битную платформу [3];
  4. Учебное пособие по PVS-Studio [4];
  5. 64-битный конь, который умеет считать [5].
Также вы можете познакомиться с демонстрационной версией инструмента PVS-Studio, в состав которой входит статический анализатор кода Viva64, выявляющий практически все описанные в статье ошибки. Демонстрационная версия доступна для скачивания по адресу: http://www.viva64.com/ru/pvs-studio/download/.

Пример 1. Переполнение буфера


struct STRUCT_1
{
  int *a;
};

struct STRUCT_2
{
  int x;
};
...
STRUCT_1 Abcd;
STRUCT_2 Qwer;
memset(&Abcd, 0, sizeof(Abcd));
memset(&Qwer, 0, sizeof(Abcd));

В программе объявлены два объекта типа STRUCT_1 и STRUCT_2, которые перед началом использования необходимо очистить (инициализировать все поля нулями). Реализуя инициализацию, программист решил скопировать похожу строчку и заменил в ней "&Abcd" на "&Qwer". Но при этом он забыл заменить «sizeof(Abcd)» на «sizeof(Qwer)».По удачному стечению обстоятельств размер структур STRUCT_1 и STRUCT_2 совпадал в 32-битной системе и код корректно работал долгое время.

При переносе кода на 64-битную систему размер структуры Abcd увеличился и как следствие возникла ошибка переполнения буфера (см. рисунок 1).

Picture 1

Рисунок 1 — Схематичное пояснение примера переполнения буфера

Подобную ошибку может быть сложно выявить, если при этом портятся данные, используемые гораздо позднее.

Пример 2. Лишние приведения типов


char *buffer;
char *curr_pos;
int length;
...
while( (*(curr_pos++) != 0x0a) && 
       ((UINT)curr_pos - (UINT)buffer < (UINT)length) );

Код плох, но это реальный код. Его задача состоит в поиске конца строки, обозначенного символом 0x0A. Код не будет работать со строками длиннее INT_MAX символов, так как переменная length имеет тип int. Однако нас интересует другая ошибка, поэтому будем считать, что программа работает с небольшим буфером и использование типа int корректно.

Проблема в том, что в 64-битной системе указатели buffer и curr_pos могут лежать за пределами первых 4 гигабайт адресного пространства. В этом случае явное приведение указателей к типу UINT отбросит значащие биты, и работа алгоритма будет нарушена (см. рисунок 2).


Picture 2


Рисунок 2 — Некорректны вычисления при поиске терминального символа

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

while(curr_pos - buffer < length && *curr_pos != '\r')
  curr_pos++;

Пример 3. Некорректные #ifdef


Часто в программах с длинной историей можно встретить участки кода, обернутые в конструкции #ifdef — -#else — #endif. При переносе программ на новую архитектуру, некорректно написанные условия могут привести к компиляции не тех фрагментов кода, как это планировалось разработчиками в прошлом (см. рисунок 3). Пример:
#ifdef _WIN32 // Win32 code
  cout << "This is Win32" << endl;
#else         // Win16 code
  cout << "This is Win16" << endl;
#endif

//Альтернативный некорректный вариант:
#ifdef _WIN16 // Win16 code
  cout << "This is Win16" << endl;
#else         // Win32 code
  cout << "This is Win32" << endl;
#endif


Picture 3


Рисунок 3 — Два варианта — это слишком мало

Полагаться на вариант #else в подобных ситуациях опасно. Лучше явно рассмотреть поведение для каждого случая (см. рисунок 4), а в ветку #else поместить сообщение об ошибке компиляции:

#if   defined _M_X64 // Win64 code (Intel 64)
  cout << "This is Win64" << endl;
#elif defined _WIN32 // Win32 code
  cout << "This is Win32" << endl;
#elif defined _WIN16 // Win16 code
  cout << "This is Win16" << endl;
#else
  static_assert(false, "Неизвестная платформа");
#endif


Picture 4


Рисунок 4 — Проверяются все возможные пути компиляции

Пример 4. Путаница с int и int*


В старых программах, особенно на Си, не редки фрагменты кода, где указатель хранят в типе int. Однако иногда это делается не умышленно, а скорее по невнимательности. Рассмотрим пример, содержащий путаницу, возникшую с использованием типа int и указателем на тип int:

int GlobalInt = 1;

void GetValue(int **x)
{
  *x = &GlobalInt;
}

void SetValue(int *x)
{
  GlobalInt = *x;
}

...
int XX;
GetValue((int **)&XX);
SetValue((int *)XX); 

В данном примере переменная XX используется в качестве буфера для хранения указателя. Этот код будет корректно работать в тех 32-битных системах, где размер указателя совпадает с размером типа int. В 64-битном системе этот код некорректен и вызов

GetValue((int **)&XX);

приведет к порче 4 байт памяти рядом с переменной XX (см. рисунок 5).
Picture 5

Рисунок 5 — Порча памяти рядом с переменной XX

Приведенный код писался или новичком, или в спешке. Причем явные приведения типа свидетельствует, что компилятор до последнего сопротивлялся, намекая разработчику что указатель и int, это разные сущности. Однако победила грубая сила.

Исправление ошибки элементарно и заключается в выборе правильного типа для переменной XX. При этом перестает быть необходимым явное приведение типа:

int *XX;
GetValue(&XX);
SetValue(XX);

Пример 5. Использование устаревших функций


Ряд API-функций, хотя и оставлен для совместимости, представляет собой опасность при разработке 64-битных приложений. Классическим примером является использование таких функций как SetWindowLong и GetWindowLong. В программах можно встретить код, подобный следующему:

SetWindowLong(window, 0, (LONG)this);
...
Win32Window* this_window = (Win32Window*)GetWindowLong(window, 0);

Программиста, некогда написавшего этот код, не в чем упрекнуть. В ходе разработки, лет 5-10 назад, программист, опираясь на свой опыт и MSDN, составил код совершенно корректный с точки зрения 32-битвной системы Windows. Прототип этих функций выглядит следующим образом:

LONG WINAPI SetWindowLong(HWND hWnd, int nIndex, LONG dwNewLong);
LONG WINAPI GetWindowLong(HWND hWnd, int nIndex);

То, что указатель явно приводится к типу LONG также оправдано, поскольку размер указателя и типа LONG совпадают в Win32 системах. Но думаю понятно, что при перекомпиляции программы в 64-битном варианте, данные приведения типа могут послужить причиной падения или неверной работы приложения.

Неприятность ошибки заключается в ее нерегулярном или даже крайне редком проявлении. Произойдет ошибка или нет, зависит от того, в какой области памяти создан объект, на который указывает указатель «this». Если объект создается в младших 4 гигабайтах адресного пространства, то 64-битная программа может корректно функционировать. Ошибка неожиданно может проявить себя через большой промежуток времени, когда из-за выделения памяти, объекты начнут создаваться за пределами первых четырех гигабайт.

В 64-битной системе использовать функции SetWindowLong/GetWindowLong можно только в том случае, если программа действительно сохраняет некие значения типа LONG, int, bool и подобные им. Если необходимо работать с указателями, то следует использовать расширенные варианты функций: SetWindowLongPtr/GetWindowLongPtr. Хотя, пожалуй, следует порекомендовать в любом случае использовать новые функции, чтобы не спровоцировать в будущем новых ошибок.

Примеры с функциями SetWindowLong и GetWindowLong являются классическими и приводятся практически во всех статьях посвященных разработке 64-битных приложений. Однако следует учесть, что этими функциями дело не ограничивается. Обратите внимания на: SetClassLong, GetClassLong, GetFileSize, EnumProcessModules, GlobalMemoryStatus (см. рисунок 6).

Picture 6
Рисунок 6 — Таблица с именами некоторых устаревших и современных функций

Пример 6. Обрезание значений при неявном приведении типов


Неявное приведение типа size_t к типу unsigned и аналогичные приведения хорошо диагностируются предупреждениями компилятора. Однако в больших программах, подобные предупреждения легко могут затеряться. Рассмотрим пример схожий с реальным кодом, где предупреждение было проигнорировано, так как казалось, что ничего плохого при работе с короткими строками произойти не может.

bool Find(const ArrayOfStrings &arrStr)
{
  ArrayOfStrings::const_iterator it;
  for (it = arrStr.begin(); it != arrStr.end(); ++it)
  {
    unsigned n = it->find("ABC"); // Truncation
    if (n != string::npos)
      return true;
  }
  return false;
};

Приведенная функция ищет текст «ABC» в массиве строк и возвращает true, в случае если хотя бы одна строка содержит последовательность «ABC». При компиляции 64-битной версии кода, эта функция всегда будет возвращать true.

Константа «string::npos» в 64-битной системе имеет значение 0xFFFFFFFFFFFFFFFF типа size_t. При помещение этого значения в переменную «n» типа unsigned, происходит его обрезание до 0xFFFFFFFF. В результате условие " n != string::npos" всегда истинно, так как 0xFFFFFFFFFFFFFFFF не равно 0xFFFFFFFF (см. рисунок 7).


Picture 7


Рисунок 7 — Схематичное пояснение ошибки обрезания значения

Исправление элементарно, достаточно прислушаться к предупреждениям компилятора:

for (auto it = arrStr.begin(); it != arrStr.end(); ++it)
{
  auto n = it->find("ABC");
  if (n != string::npos)
    return true;
}
return false;

Пример 7. Необъявленные функции в Си


Несмотря на годы, программы или части программ, написанные на языке Си, остаются живее всех живых. Код этих программ гораздо более предрасположен к 64-битным ошибкам из-за менее строгих правил контроля типов в языке Си.

В языке Си можно использовать функции без их предварительного объявления. Проанализируем связанный с этим интересный пример 64-битной ошибки. Для начала рассмотрим корректный вариант кода, в котором происходит выделение и использование трех массивов размером по гигабайту каждый:
#include <stdlib.h>

void test()
{
  const size_t Gbyte = 1024 * 1024 * 1024;
  size_t i;
  char *Pointers[3];

  // Allocate
  for (i = 0; i != 3; ++i)
    Pointers[i] = (char *)malloc(Gbyte);

  // Use
  for (i = 0; i != 3; ++i)
    Pointers[i][0] = 1;

  // Free
  for (i = 0; i != 3; ++i)
    free(Pointers[i]);
}

Данный код корректно выделит память, запишет в первый элемент каждого массива по единице и освободит занятую память. Код совершенно корректно работает на 64-битной системе.

Теперь удалим или закомментируем строчку "#include <stdlib.h>". Код по-прежнему будет собираться, но при запуске программы произойдет ее аварийное завершение. Если заголовочный файл «stdlib.h» не подключен, компилятор языка Си считает, что функция malloc вернет тип int. Первые два выделения памяти, скорее всего, пройдут успешно. При третьем обращении функция malloc вернет адрес массива за пределами первых 2-х гигабайт. Поскольку компилятор считает, что результат работы функции имеет тип int, он неверно интерпретирует результат и сохраняет в массиве Pointers некорректное значение указателя.

Рассмотрим ассемблерный код, генерируемый компилятором Visual C++ для 64-битной Debug версии. Вначале приводится корректный код, который будет сгенерирован, когда присутствует объявление функции malloc (подключен файл «stdlib.h»):

Pointers[i] = (char *)malloc(Gbyte);
mov   rcx,qword ptr [Gbyte]
call  qword ptr [__imp_malloc (14000A518h)]
mov    rcx,qword ptr [i]
mov    qword ptr Pointers[rcx*8],rax


Теперь рассмотрим вариант некорректного кода, когда отсутствует объявление функции malloc:

Pointers[i] = (char *)malloc(Gbyte);
mov    rcx,qword ptr [Gbyte]
call   malloc (1400011A6h)
cdqe
mov    rcx,qword ptr [i]
mov    qword ptr Pointers[rcx*8],rax

Обратите внимание на наличие инструкции CDQE (Convert doubleword to quadword). Компилятор посчитал, что результат содержится в регистре eax и расширил его до 64-битного значения, чтобы записать в массив Pointers. Соответственно старшие биты регистра rax будут потеряны. Если даже адрес выделенной памяти лежит в пределах первых четырех гигабайт, в случае, когда старший бит регистра eax равен 1 мы все равно получим некорректный результат. Например, адрес 0x81000000 превратится в 0xFFFFFFFF81000000.

Пример 8. Останки динозавров в больших и старых программах


Большие старые программные системы, развивающиеся десятилетиями, изобилуют разнообразнейшими атавизмами и просто участками кода, написанными с использованием популярных парадигм и стилей разнообразных лет. В таких системах можно наблюдать эволюцию развития языков программирования, когда наиболее старые части написаны в стиле языка Си, а в наиболее свежих можно встретить сложные шаблоны в стиле Александреску.

Picture 1
Рисунок 8 — Раскопки динозавра

Есть атавизмы связанные и с 64-битностью. Вернее атавизмы, препятствующие работе современного 64-битного кода. Рассмотрим пример:

// beyond this, assume a programming error
#define MAX_ALLOCATION 0xc0000000 

void *malloc_zone_calloc(malloc_zone_t *zone,
  size_t num_items, size_t size)
{
  void *ptr;
  ...

  if (((unsigned)num_items >= MAX_ALLOCATION) ||
      ((unsigned)size >= MAX_ALLOCATION) ||
      ((long long)size * num_items >=
       (long long) MAX_ALLOCATION))
  {  
    fprintf(stderr,
      "*** malloc_zone_calloc[%d]: arguments too large: %d,%d\n",
      getpid(), (unsigned)num_items, (unsigned)size);
    return NULL;
  }
  ptr = zone->calloc(zone, num_items, size);
  ...
  return ptr;
}

Во-первых, код функции содержит проверку на допустимые размеры выделяемой памяти, являющиеся странными для 64-битной системы. А во-вторых, выдаваемое диагностическое сообщение будет некорректно, поскольку если мы попросим выделить память под 4 400 000 000 элементов, из-за явного приведения типа к unsigned, нам будет выдано странное сообщение о невозможности выделения памяти всего лишь для 105 032 704 элементов.

Пример 9. Виртуальные функции


Одним из красивых примеров 64-битных ошибок является использование неверных типов аргументов в объявлениях виртуальных функций. Причем обычно это не чья-то неаккуратность, а просто «несчастный случай», где нет виноватых, но есть ошибка. Рассмотрим следующую ситуацию.

С незапамятных времен в библиотеке MFC есть класс CWinApp, в котором имеется функция WinHelp:

class CWinApp {
  ...
  virtual void WinHelp(DWORD dwData, UINT nCmd);
};

Для показа собственной справки в пользовательском приложении необходимо было эту функцию перекрыть:

class CSampleApp : public CWinApp {
  ...
  virtual void WinHelp(DWORD dwData, UINT nCmd);
};

И все было прекрасно до тех пор, пока не появились 64-битные системы. Разработчикам MFC пришлось поменять интерфейс функции WinHelp (и некоторых других функций) так:

class CWinApp {
  ...
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};

В 32-битном режиме типы DWORD_PTR и DWORD совпадали, а вот в 64-битном нет. Естественно разработчики пользовательского приложения также должны сменить тип на DWORD_PTR, но чтобы это сделать, про это необходимо в начале узнать. В результате в 64-битной программе возникает ошибка, так как функция WinHelp в пользовательском классе не вызывается (см. рисунок 9).

Picture 9

Рисунок 9 — Ошибка, связанная с виртуальными функциями

Пример 10. Магические числа в качестве параметров


Магические числа, содержащиеся в теле программ, являются плохим стилем и провоцируют возникновение ошибок. В качестве примера магических чисел можно привести 1024 и 768, жестко обозначающие размер разрешения экрана. В рамках этой статьи нам интересны те магические числа, которые могут привести к проблемам в 64-битном приложении. Наиболее распространенные числа, опасные для 64-битных программ, представлены в таблице на рисунке 10.

Picture 10
Рисунок 10 — Магические числа опасные для 64-битных программ

Продемонстрируем пример работы с функцией CreateFileMapping, встретившийся в одной из CAD-систем:
HANDLE hFileMapping = CreateFileMapping(
  (HANDLE) 0xFFFFFFFF,
  NULL,
  PAGE_READWRITE,
  dwMaximumSizeHigh,
  dwMaximumSizeLow,
  name);

Вместо корректной зарезервированной константы INVALID_HANDLE_VALUE используется число 0xFFFFFFFF. Это некорректно в Win64 программе, где константа INVALID_HANDLE_VALUE принимает значение 0xFFFFFFFFFFFFFFFF. Правильным вариантом вызова функции будет:

HANDLE hFileMapping = CreateFileMapping(
  INVALID_HANDLE_VALUE,
  NULL,
  PAGE_READWRITE,
  dwMaximumSizeHigh,
  dwMaximumSizeLow,
  name);

Примечание. Некоторые считают, что значение 0xFFFFFFFF при расширении до указателя превращается в 0xFFFFFFFFFFFFFFFF. Это не так. Согласно правилам языка Си/Си++ значение 0xFFFFFFFF имеет тип «unsigned int», так как не может быть представлено типом «int». Соответственно, расширяясь до 64-битного типа, значение 0xFFFFFFFFu превращается в 0x00000000FFFFFFFFu. А вот если написать так (size_t)(-1), то мы получим ожидаемое 0xFFFFFFFFFFFFFFFF. Здесь «int» вначале расширяется до «ptrdiff_t», а затем превращается в «size_t».

Пример 11. Магические константы, обозначающие размер


Другой частой ошибкой является использование магических чисел для задания размера объекта. Рассмотрим пример выделения и обнуления буфера:

size_t count = 500;
size_t *values = new size_t[count];
// Будет заполнена только часть буфера
memset(values, 0, count * 4);

В данном случае в 64-битной системе выделяется больше памяти, чем затем заполняется нулевыми значениями (см. рисунок 11). Ошибка заключается в предположении, что размер типа size_t всегда равен четырем байтам.

Picture 11

Рисунок 11 — Заполнение только части массива

Корректный вариант:
size_t count = 500;
size_t *values = new size_t[count];
memset(values, 0, count * sizeof(values[0]));

Схожие ошибки можно встретить при вычислении размеров выделяемой памяти или сериализации данных.

Пример 12. Переполнение стека


Во многих случаях 64-битная программа потребляет больше памяти и стека. Выделение большего количества памяти в куче опасности не представляет, так как этого вида памяти 64-битной программе доступно во много раз больше, чем 32-битной. А вот увеличение используемой стековой памяти может привести к его неожиданному переполнению (stack overflow).

Механизм использования стека отличается в различных операционных системах и компиляторах. Мы рассмотрим особенность использования стека в коде Win64 приложений, построенных компилятором Visual C++.

При разработке соглашений по вызовам (calling conventions) в Win64 системах решили положить конец существованию различных вариантов вызова функций. В Win32 существовал целый ряд соглашений о вызове: stdcall, cdecl, fastcall, thiscall и так далее. В Win64 только одно «родное» соглашение по вызовам. Модификаторы подобные __cdecl компилятором игнорируются.

Соглашение по вызовам на платформе x86-64 похоже на соглашение fastcall, существующее в x86. В x64-соглашении первые четыре целочисленных аргумента (слева направо) передаются в 64-битных регистрах, выбранных специально для этой цели:

RCX: 1-й целочисленный аргумент
RDX: 2-й целочисленный аргумент
R8: 3-й целочисленный аргумент
R9: 4-й целочисленный аргумент

Остальные целочисленные аргументы передаются через стек. Указатель «this» считается целочисленным аргументом, поэтому он всегда помещается в регистр RCX. Если передаются значения с плавающей точкой, то первые четыре из них передаются в регистрах XMM0-XMM3, а последующие — через стек.

Хотя аргументы могут быть переданы в регистрах, компилятор все равно резервирует для них место в стеке, уменьшая значение регистра RSP (указателя стека). Как минимум, каждая функция должна резервировать в стеке 32 байта (четыре 64-битных значения, соответствующие регистрам RCX, RDX, R8, R9). Это пространство в стеке позволяет легко сохранить содержимое переданных в функцию регистров в стеке. От вызываемой функции не требуется сбрасывать в стек входные параметры, переданные через регистры, но резервирование места в стеке при необходимости позволяет это сделать. Если передается более четырех целочисленных параметров, в стеке резервируется соответствующее дополнительное пространство.

Описанная особенность приводит к существенному возрастанию скорости поглощения стека. Даже если функция не имеет параметров, то от стека все равно будет «откушено» 32 байта, которые затем никак не используются. Смысл использования такого неэкономного механизма связан в унификации и упрощение отладки.

Обратим внимание еще на один момент. Указатель стека RSP должен перед очередным вызовом функции быть выровнен по границе 16 байт. Таким образом, суммарный размер используемого стека при вызове в 64-битном коде функции без параметров составляет 48 байт: 8 (адрес возврата) + 8 (выравнивание) + 32 (резерв для аргументов).

Неужели все так плохо? Нет. Не следует забывать, что большее количество регистров имеющихся в распоряжении 64-битного компилятора, позволяют построить более эффективный код и не резервировать в стеке память под некоторые локальные переменные функций. Таким образом, в ряде случаев 64-битный вариант функции использует меньше стека, чем 32-битный вариант. Более подробно этот вопрос и различные примеры рассматриваются в статье "Причины, по которым 64-битные программы требуют больше стековой памяти".

Предсказать, будет потреблять 64-битная программа больше стека или меньше невозможно. В силу того, что Win64-программа может использовать в 2-3 раза больше стековой памяти, необходимо подстраховаться и изменить настройку проекта, отвечающую за размер резервируемого стека. Выберите в настройках проекта параметр Stack Reserve Size (ключ /STACK:reserve) и увеличьте размер резервируемого стека в три раза. По умолчанию этот размер составляет 1 мегабайт.

Пример 13. Функция с переменным количеством аргументов и переполнение буфера


Хотя использование функций с переменным количеством аргументов, таких как printf, scanf считается в Си++ плохим стилем, они по прежнему широко используются. Эти функции создают множество проблем при переносе приложений на другие системы, в том числе и на 64-битные системы. Рассмотрим пример:

int x;
char buf[9];

sprintf(buf, "%p", &x);
Автор кода не учел, что размер указателя в будущем может составить более 32 бит. В результате на 64-битной архитектуре данный код приведет к переполнению буфера (см. рисунок 12). Эту ошибку вполне можно отнести к использованию магического числа '9', но в реальном приложении переполнение буфера может возникнуть и без магических чисел.

Picture 12

Рисунок 12 — Переполнение буфера при работе с функцией sprintf

Варианты исправления данного кода различны. Рациональнее всего провести рефакторинг кода с целью избавиться от использования опасных функций. Например, можно заменить printf на cout, а sprintf на boost::format или std::stringstream.

Примечание. Эту рекомендацию часто критикуют разработчики под Linux, аргументируя тем, что gcc проверяет соответствие строки форматирования фактическим параметрам, передаваемым, например, в функцию printf. И, следовательно, использование printf безопасно. Однако они забывают, что строка форматирования может передаваться из другой части программы, загружаться из ресурсов. Другими словами, в реальной программе строка форматирования редко присутствует в явном виде в коде, и, соответственно, компилятор не может ее проверить. Если же разработчик использует Visual Studio 2005/2008/2010, то он не сможет получить предупреждение на код вида void *p = 0; printf("%x", p); даже используя ключи /W4 и /Wall.

Пример 14. Функция с переменным количеством аргументов и неверный формат


Часто в программах можно встретить некорректные строки форматирования при работе с функцией printf и другими схожими функциями. Из-за этого будут выведены неверные значения, что хотя и не приведет к аварийному завершению программы, но, конечно же, является ошибкой:

const char *invalidFormat = "%u";
size_t value = SIZE_MAX;

// Будет распечатано неверное значение
printf(invalidFormat, value);

В других случаях ошибка в строке форматирования будет критична. Рассмотрим пример, основанный на реализации подсистемы 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-битных систем. В 32-битных системах он вполне работоспособен, хотя и не красив.

Пример 15. Хранение в double целочисленных значений


Нам не приходилось самим встречать подобную ошибку. Вероятно, это ошибка редка, но вполне реальна.
Тип double, имеет размер 64-бита, и совместим со стандартом IEEE-754 на 32-битных и 64-битных системах. Некоторые программисты используют тип double для хранения и работы с целочисленными типами:
size_t a = size_t(-1);
double b = a;
--a;
--b;
size_t c = b; // x86: a == c
              // x64: a != c

Данный пример еще можно пытаться оправдывать на 32-битной системе, так как тип double имеет 52 значащих бита и способен без потерь хранить 32-битное целое значение. Но при попытке сохранить в double 64-битное целое число точное значение может быть потеряно (см. рисунок 13).

Picture 13
Рисунок 13 — Количество значащих битов в типах size_t и double

Вторая часть статьи.
Tags:
Hubs:
+136
Comments 62
Comments Comments 62

Articles

Information

Website
pvs-studio.com
Registered
Founded
2008
Employees
31–50 employees