SlideShare uma empresa Scribd logo
1 de 50
Baixar para ler offline
Коллекция примеров 64-битных
ошибок в реальных программах
Автор: Андрей Карпов

Дата: 29.06.2010


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


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




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

Ошибка неприятна тем, что код долгое время может корректно работать, пока память под буфер
будет выделяться в младших четырех гигабайтах адресного пространства. Исправление ошибки
заключается в удалении совершенно ненужных явных приведений типов:
while(curr_pos - buffer < length && *curr_pos != 'n')

  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
Рисунок 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
Рисунок 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).
Рисунок 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).




Рисунок 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).
Рисунок 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. Останки динозавров в больших и старых программах
Большие старые программные системы, развивающиеся десятилетиями, изобилуют
разнообразнейшими атавизмами и просто участками кода, написанными с использованием
популярных парадигм и стилей разнообразных лет. В таких системах можно наблюдать эволюцию
развития языков программирования, когда наиболее старые части написаны в стиле языка Си, а в
наиболее свежих можно встретить сложные шаблоны в стиле Александреску.




Рисунок 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,%dn",

          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).
Рисунок 9 – Ошибка, связанная с виртуальными функциями


Пример 10. Магические числа в качестве параметров
Магические числа, содержащиеся в теле программ, являются плохим стилем и провоцируют
возникновение ошибок. В качестве примера магических чисел можно привести 1024 и 768, жестко
обозначающие размер разрешения экрана. В рамках этой статьи нам интересны те магические
числа, которые могут привести к проблемам в 64-битном приложении. Наиболее
распространенные числа, опасные для 64-битных программ, представлены в таблице на рисунке
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 всегда равен четырем байтам.
Рисунок 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', но в реальном
приложении переполнение буфера может возникнуть и без магических чисел.
Рисунок 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).




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


Пример 16. Адресная арифметика. A + B != A - (-B)
Адресная арифметика (address arithmetic) - это способ вычисления адреса какого-либо объекта
при помощи арифметических операций над указателями, а также использование указателей в
операциях сравнения. Адресную арифметику также называют арифметикой над указателями
(pointer arithmetic).

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

Рассмотрим первую из ошибок данного типа:

char *A = "123456789";

unsigned B = 1;

char *X = A + B;

char *Y = A - (-B);

if (X != Y)

  cout << "Error" << endl;

Причина, по которой в Win32 программе A + B == A - (-B), показана на рисунке 14.




Рисунок 14 – Win32: A + B == A - (-B)
Причина, по которой в Win64 программе A + B != A - (-B), показана на рисунке 15.




Рисунок 15 – Win64: A + B != A - (-B)

Ошибка будет устранена, если использовать подходящий memsize-тип. В данном случае
используется тип ptrdfiff_t:

char *A = "123456789";

ptrdiff_t B = 1;

char *X = A + B;

char *Y = A - (-B);


Пример 17. Адресная арифметика. Знаковые и беззнаковые типы.
Рассмотрим еще один вариант ошибки, связанный с использованием знаковых и беззнаковых
типов. В этот раз ошибка приведет не к неверному сравнению, а сразу к падению приложения.

LONG p1[100];

ULONG x = 5;

LONG y = -1;

LONG *p2 = p1 + 50;

p2 = p2 + x * y;

*p2 = 1; // Access violation

Выражение "x * y" имеет значение 0xFFFFFFFB и имеет тип unsigned. Данный код, собранный в
32-битном варианте работоспособен, так как сложение указателя с 0xFFFFFFFB эквивалентно его
уменьшению на 5. В 64-битной указатель после прибавления 0xFFFFFFFB начнет указывать далеко
за пределы массива p1 (смотри рисунок 16).
Рисунок 16 – Выход за границы массива
Исправление заключается в использовании memsize-типов и аккуратной работе со знаковыми и
беззнаковыми типами:

LONG p1[100];

LONG_PTR x = 5;

LONG_PTR y = -1;

LONG *p2 = p1 + 50;

p2 = p2 + x * y;

*p2 = 1; // OK


Пример 18. Адресная арифметика. Переполнения.
class Region {

    float *array;

    int Width, Height, Depth;

    float Region::GetCell(int x, int y, int z) const;

    ...

};



float Region::GetCell(int x, int y, int z) const {

    return array[x + y * Width + z * Width * Height];

}

Код взят из реальной программы математического моделирования, в которой важным ресурсом
является объем оперативной памяти, и возможность на 64-битной архитектуре использовать
более 4 гигабайт памяти существенно увеличивает вычислительные возможности. В программах
данного класса для экономии памяти часто используют одномерные массивы, осуществляя работу
с ними как с трехмерными массивами. Для этого существуют функции, аналогичные GetCell,
обеспечивающие доступ к необходимым элементам.

Приведенный код корректно работает с указателями, если значение выражения " x + y * Width + z
* Width * Height" не превышает INT_MAX (2147483647). В противном случае произойдет
переполнение, что приведет к неопределенному поведению в программы.

Такой код мог всегда корректно работать на 32-битной платформе. В рамках 32-битной
архитектуры программе недоступен объем памяти для создания массива подобного размеров. На
64-битной архитектуре это ограничение снято, и размер массива легко может превысить INT_MAX
элементов.

Программисты часто допускают ошибку, пытаясь исправить код следующим образом:
float Region::GetCell(int x, int y, int z) const {

    return array[static_cast<ptrdiff_t>(x) + y * Width +

           z * Width * Height];

}

Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь тип
ptrdiff_t и надеются за счет этого избежать переполнения. Но переполнение может произойти
внутри подвыражения "y * Width" или "z * Width * Height", так как для их вычисления по-
прежнему используется тип int.

Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы
можете явно привести каждое подвыражение к типу ptrdiff_t:

float Region::GetCell(int x, int y, int z) const {

     return array[ptrdiff_t(x) +

                         ptrdiff_t(y) * Width +

                         ptrdiff_t(z) * Width * Height];

}

Другое, более верное решение - изменить типы переменных:

typedef ptrdiff_t TCoord;

class Region {

     float *array;

     TCoord Width, Height, Depth;

     float Region::GetCell(TCoord x, TCoord y, TCoord z) const;

     ...

};



float Region::GetCell(TCoord x, TCoord y, TCoord z) const {

     return array[x + y * Width + z * Width * Height];

}


Пример 19. Изменение типа массива
Иногда в программах для удобства изменяют тип массива при его обработке. Опасное и
безопасное приведение типов представлено в следующем коде:

int array[4] = { 1, 2, 3, 4 };
enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };



//safe cast (for MSVC)

ENumbers *enumPtr = (ENumbers *)(array);

cout << enumPtr[1] << " ";



//unsafe cast

size_t *sizetPtr = (size_t *)(array);

cout << sizetPtr[1] << endl;



//Output on 32-bit system: 2 2

//Output on 64-bit system: 2 17179869187

Как видите, результат вывода программы отличается в 32-битном и 64-битном варианте. На 32-
битной системе доступ к элементам массива осуществляется корректно, так как размеры типов
size_t и int совпадают, и мы видим вывод "2 2".

На 64-битной системе мы получили в выводе "2 17179869187", так как именно значение
17179869187 находится в 1-ом элементе массива sizetPtr (см. рисунок 17). В некоторых случаях
именно такое поведение и бывает нужно, но обычно это является ошибкой.
Рисунок 17 – Представление элементов массивов в памяти

Примечание. Тип enum в компиляторе Visual C++ по умолчанию совпадает размером с типом int,
то есть является 32-битным типом. Использование enum другого размера возможно только с
помощью расширения, считающимся нестандартным в Visual C++. Поэтому приведенный
пример корректен в Visual C++, но с точки зрения других компиляторов приведение указателя
на элементы int к указателю на элементы enum может быть также некорректным.


Пример 20. Упаковка указателя в 32-битный тип
Иногда в программах указатели сохраняют в целочисленных типах. Обычно для этого
используется такой тип, как int. Это, пожалуй, одна из самых распространенных 64-битных
ошибок.

char *ptr = ...;

int n = (int) ptr;

...

ptr = (char *) n;

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




Рисунок 18 – Помещение указателя в переменную типа int

Если все же необходимо поместить указатель в переменную целочисленного типа, то следует
использовать такие типы как intptr_t, uintptr_t, ptrdiff_t и size_t.
Пример 21. Memsize-типы в объединениях
Когда возникает необходимость работать с указателем как с целым числом, иногда удобно
воспользоваться объединением, как показано в примере, и работать с числовым представлением
типа без использования явных приведений:

union PtrNumUnion {

  char *m_p;

  unsigned m_n;

} u;



u.m_p = str;

u.m_n += delta;

Данный код корректен на 32-битных системах и некорректен на 64-битных. Изменяя член m_n на
64-битной системе, мы работаем только с частью указателя m_p (смотри рисунок 19).




Рисунок 19 – Представление объединения в памяти на 32-битной и 64-битной системе.

Следует использовать тип, который будет соответствовать размеру указателя:

union PtrNumUnion {

  char *m_p;

  uintptr_t m_n; //type fixed

} u;


Пример 22. Вечный цикл
Смешанное использование 32-битных и 64-битных типов неожиданно может привести к
возникновению вечных циклов. Рассмотрим синтетический пример, иллюстрирующий целый
класс подобных дефектов:

size_t Count = BigValue;

for (unsigned Index = 0; Index != Count; Index++)
{ ... }

Это цикл никогда не прекратится, если значение Count > UINT_MAX. Предположим, что на 32-
битных системах этот код работал с количеством итераций менее значения UINT_MAX. Но 64-
битный вариант программы может обрабатывать больше данных, и ему может потребоваться
большее количество итераций. Поскольку значения переменной Index лежат в диапазоне
[0..UINT_MAX], то условие "Index != Count" никогда не выполнится, что и приводит к бесконечному
циклу (см. рисунок 20).
Рисунок 20 – Механизм возникновения вечного цикла


Пример 23. Работа с битами и операция NOT
Работа с битовыми операциями требует особой аккуратности от программиста при разработке
кроссплатформенных приложений, в которых типы данных могут иметь различные размеры.
Поскольку перенос программы на 64-битную платформу также ведет к изменению размерности
некоторых типов, то высока вероятность возникновения ошибок в участках кода, работающих с
отдельными битами. Чаще всего это происходит из-за смешенной работы с 32-битными и 64-
битными типами данных. Рассмотрим ошибку, возникшую в коде из-за некорректного
применения операции NOT:

UINT_PTR a = ~UINT_PTR(0);

ULONG b = 0x10;

UINT_PTR c = a & ~(b - 1);

c = c | 0xFu;
if (a != c)

  cout << "Error" << endl;

Ошибка заключается в том, что маска, заданная выражением "~(b - 1)", имеет тип ULONG. Это
приводит к обнулению старших разрядов переменной "a", ходя должны были обнулиться только
младшие четыре бита (см. рисунок 21).
Рисунок 21 – Ошибка из-за обнуления старших бит

Исправленный вариант кода может выглядеть следующим образом:

UINT_PTR c = a & ~(UINT_PTR(b) - 1);

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


Пример 24. Работа с битами, сдвиги
ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {

    ptrdiff_t mask = 1 << bitNum;

    return value | mask;

}
Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит с
номерами от 0 до 31 в единицу. После переноса программы на 64-битную платформу возникает
необходимость выставлять биты с номерами от 0 до 63. Однако данный код неспособен
выставить старшие биты, с номерами 32-63. Обратите внимание, что числовой литерал "1" имеет
тип int, и при сдвиге на 32 позиции произойдет переполнение, как показано на рисунке 22.
Получим мы в результате 0 (рисунок 22-B) или 1 (рисунок 22-C) - зависит от реализации
компилятора.




Рисунок 22 – a) корректная установка 31-ого бита в 32-битном коде (биты считаются от 0); b,c) -
Ошибка установки 32-ого бита на 64-битной системе (два варианта поведения, зависящих от
компилятора)

Для исправления кода необходимо сделать константу "1" того же типа, что и переменная mask:
ptrdiff_t mask = static_cast<ptrdiff_t>(1) << bitNum;

Заметим также, что неисправленный код приведет еще к одной интересной ошибке. При
выставлении 31 бита на 64-битной системе результатом работы функции будет значение
0xffffffff80000000 (см. рисунок 23). Результатом выражения 1 << 31 является отрицательное число -
2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000.




Рисунок 23 – Ошибка установки 31-ого бита на 64-битной системе


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

struct BitFieldStruct {

  unsigned short a:15;

  unsigned short b:13;

};



BitFieldStruct obj;

obj.a = 0x4000;

size_t x = obj.a << 17; //Sign Extension

printf("x 0x%Ixn", x);

//Output on 32-bit system: 0x80000000

//Output on 64-bit system: 0xffffffff80000000

В 32-битной среде порядок вычисления выражения будет выглядеть, как показано на рисунке 24.
Рисунок 24 - Вычисление выражения в 32-битном коде

Обратим внимание, что при вычислении выражения "obj.a << 17" происходит знаковое
расширение типа unsigned short до типа int. Более наглядно, это может продемонстрировать
следующий код:

#include <stdio.h>



template <typename T> void PrintType(T)

{

    printf("type is %s %d-bitn",

             (T)-1 < 0 ? "signed" : "unsigned", sizeof(T)*8);

}



struct BitFieldStruct {

    unsigned short a:15;

    unsigned short b:13;

};



int main(void)

{

    BitFieldStruct bf;

    PrintType( bf.a );

    PrintType( bf.a << 2);

    return 0;
}



Result:

type is unsigned 16-bit

type is signed 32-bit

Теперь посмотрим, к чему приводит наличие знакового расширения в 64-битном коде.
Последовательность вычисления выражения показана на рисунке 25.




Рисунок 25 - Вычисление выражения в 64-битном коде

Член структуры obj.a преобразуется из битового поля типа unsigned short в int. Выражение "obj.a
<< 17" имеет тип int, но оно преобразуется в ptrdiff_t и затем в size_t, перед тем как будет
присвоено переменной addr. В результате мы получим число значение 0xffffffff80000000, вместо
ожидаемого значения 0x0000000080000000.

Будьте внимательны при работе с битовыми полями. Для предотвращения описанной ситуации в
нашем примере достаточно явно привести obj.a к типу size_t.

...

size_t x = static_cast<size_t>(obj.a) << 17; // OK

printf("x 0x%Ixn", x);

//Output on 32-bit system: 0x80000000

//Output on 64-bit system: 0x80000000


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

В основном, ошибки данного рода заключаются в сериализации memsize-типов и операциях
обмена данными с их использованием:

size_t PixelsCount;

fread(&PixelsCount, sizeof(PixelsCount), 1, inFile);

Недопустимо использование типов, которые меняют свой размер в зависимости от среды
разработки, в бинарных интерфейсах обмена данными. В языке Си++ большинство типов не
имеют четкого размера и, следовательно, их все невозможно использовать для этих целей.
Поэтому создатели средств разработки и сами программисты создают типы данных, имеющие
строгий размер, такие как __int8, __int16, INT32, word64 и так далее.

Даже после внесения исправлений, касающихся размеров типа, вы можете столкнуться с
несовместимостью бинарных форматов. Причина кроется в ином представлении данных.
Наиболее часто это связано с другой последовательностью байт.

Порядок байт - метод записи байтов многобайтовых чисел (см. рисунок 26). Порядок от младшего
к старшему (англ. little-endian) - запись начинается с младшего и заканчивается старшим. Этот
порядок записи принят в памяти персональных компьютеров с x86 и x86-64-процессорами.
Порядок от старшего к младшему (англ. big-endian) - запись начинается со старшего и
заканчивается младшим. Этот порядок является стандартным для протоколов TCP/IP. Поэтому,
порядок байтов от старшего к младшему часто называют сетевым порядком байтов (англ. network
byte order). Этот порядок байт используется процессорами Motorola 68000, SPARC.

Кстати, некоторые процессоры могут работать и в порядке от младшего к старшему, и наоборот. К
их числу относится, например IA-64.
Рисунок 26 - Порядок байт в 64-битном типе на little-endian и big-endian системах

Разрабатывая бинарный интерфейс или формат данных, следует помнить о последовательности
байт. А если 64-битная система, на которую Вы переносите 32-битное приложение, имеет иную
последовательность байт, то вы просто будете вынуждены учесть это в своем коде. Для
преобразования между сетевым порядком байт (big-endian) и порядком байт (little-endian), можно
использовать функции htonl(), htons(), bswap_64, и так далее.


Пример 27. Изменение выравнивания типов
Помимо изменения размеров некоторых типов данных, ошибки могут возникать и из-за
изменения правил их выравнивания в 64-битной системе (см. рисунок 27).
Рисунок 27 – Размеры типы и границы их выравнивания (значения точны для Win32/Win64, но
могут варьироваться в "Unix-мире" и приводятся просто в качестве примера)

Рассмотрим пример описания проблемы, найденного в одном из форумов:

Столкнулся сегодня с одной проблемой в Linux. Есть структура данных, состоящая из
нескольких полей: 64-битный double, потом 8 unsigned char и один 32-битный int. Итого
получается 20 байт (8 + 8*1 + 4). Под 32-битными системами sizeof равен 20 и всё работает
нормально. А под 64-битным Linux'ом sizeof возвращает 24. Т.е. идёт выравнивание по границе
64 бит.

Далее в форуме идут рассуждения о совместимости данных и просьба совета, как упаковать
данные в структуре. Но не это сейчас интересно. Интереснее то, что здесь наблюдается очередной
тип ошибки, который может возникнуть при переносе приложений на 64-битную систему.
Когда меняются размеры полей в структуре и из-за этого меняется сам размер структуры это
понятно и привычно. Здесь другая ситуация. Размер полей остался прежний, но из-за иных правил
выравнивания размер структуры все равно изменится (см. рисунок 28). Такое поведение может
привести к разнообразным ошибкам, например в несовместимости форматов сохраняемых
данных.
Рисунок 28 – Схематическое изображение структур и правил выравнивания типов


Пример 28. Выравнивания типов и почему нельзя писать sizeof(x) +
sizeof(y)
Иногда программисты используют структуры, в конце которых расположен массив переменного
размера. Эта структура и выделение памяти для нее может выглядеть следующим образом:

struct MyPointersArray {

  DWORD m_n;

  PVOID m_arr[1];

} object;

...

malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );

...

Этот код будет корректно работать в 32-битном варианте, но его 64-битный вариант даст сбой.

При выделении памяти, необходимой для хранения объекта типа MyPointersArray, содержащего 5
указателей, необходимо учесть, что начало массива m_arr будет выровнено по границе 8 байт.
Расположение данных в памяти на разных системах (Win32/Win64) показано на рисунке 29.
Рисунок 29 – Расположение данных в памяти в 32-битной и 64-битной системе

Корректный расчет размера должен выглядеть следующим образом:

struct MyPointersArray {

  DWORD m_n;

  PVOID m_arr[1];

} object;

...

malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) +

          5 * sizeof(PVOID) );

...

В приведенном коде мы узнаем смещение последнего члена структуры и суммируем это
смещение с его размером. Смещение члена структуры или класса можно узнать с использованием
макроса offsetof или FIELD_OFFSET. Всегда используйте эти макросы для получения смещения в
структуре, не опираясь на свои предположения о размерах типов и правилах их выравнивания.


Пример 29. Перегруженные функции
При перекомпиляции программы может начать выбираться другая перегруженная функция (см.
рисунок 30).
Рисунок 30 – Выбор перегруженной функции в 32-битной и 64-битной системе

Пример проблемы:

class MyStack {

...

public:

  void Push(__int32 &);

  void Push(__int64 &);

  void Pop(__int32 &);

  void Pop(__int64 &);

} stack;



ptrdiff_t value_1;

stack.Push(value_1);

...

int value_2;

stack.Pop(value_2);

Неаккуратный программист помещал и затем выбирал из стека значения различных типов
(ptrdiff_t и int). На 32-битной системе их размеры совпадали, все замечательно работало. Когда в
64-битной программе изменился размер типа ptrdiff_t, то в стек стало попадать больше байт, чем
затем извлекаться.


Пример 30. Ошибки в 32-битных модулях, работающих в WoW64
Последний пример посвящен ошибкам в 32-битных программах, которые возникают при их
выполнении в 64-битной среде. В состав 64-битных программных комплексов еще долго будут
входить 32-битные модули, а следовательно необходимо обеспечить их корректную работу в 64-
битной среде. Подсистема WoW64 очень хорошо справляется со своей задачей, изолируя 32-
битное приложение, и практически все 32-битные приложения функционирует корректно. Однако
иногда ошибки все же встречаются и в основном они связаны с механизмом перенаправления
при работе с файлами и реестром Windows.

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

lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE,

  "SOFTWAREODBCODBC.INIODBC Data Sources", 0,

  KEY_QUERY_VALUE,        &hKey);

Чтобы подружить эту программу с другими 64-битными частями, необходимо вписать ключ
KEY_WOW64_64KEY:

lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE,

  "SOFTWAREODBCODBC.INIODBC Data Sources", 0,

  KEY_QUERY_VALUE | KEY_WOW64_64KEY,               &hKey);


Заключение
Наилучший результат при поиске описанных в статье ошибок дает методика статического анализа
кода. В качестве примера инструмента осуществляющего такой анализ, можно назвать
разрабатываемый нами инструмент Viva64, входящий в состав PVS-Studio.

Методы статического поиска дефектов позволяют обнаруживать дефекты по исходному коду
программы. При этом поведение программы оценивается на всех путях исполнения
одновременно. За счет этого, возможно обнаружение дефектов, проявляющихся лишь на
необычных путях исполнения при редких входных данных. Эта особенность позволяет дополнить
методы тестирования, повышая надежность программ. Системы статического анализа могут
использоваться при проведении аудита исходного кода, для систематического устранения
дефектов в существующих программах, и встраиваться в цикл разработки, автоматически
обнаруживая дефекты в создаваемом коде.


Библиографический список
   1. Андрей Карпов, Евгений Рыжков. Уроки разработки 64-битных приложений на языке
      Си/Си++. http://www.viva64.com/ru/articles/x64-lessons/
   2. Андрей Карпов. Что такое size_t и ptrdiff_t. http://www.viva64.com/art-1-1-72510946.html
   3. Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ - кода на 64-битную
      платформу. http://www.viva64.com/art-1-1-1958348565.html
   4. Евгений Рыжков. Учебное пособие по PVS-Studio. http://www.viva64.com/art-4-1-
      1796251700.html
   5. Андрей Карпов. 64-битный конь, который умеет считать. http://www.viva64.com/art-1-1-
      1064884779.html

Mais conteúdo relacionado

Mais procurados

20 ловушек переноса Си++ - кода на 64-битную платформу
20 ловушек переноса Си++ - кода на 64-битную платформу20 ловушек переноса Си++ - кода на 64-битную платформу
20 ловушек переноса Си++ - кода на 64-битную платформуTatyanazaxarova
 
64-битная версия Loki
64-битная версия Loki64-битная версия Loki
64-битная версия LokiTatyanazaxarova
 
О сложностях программирования, или C# нас не спасет?
О сложностях программирования, или C# нас не спасет?О сложностях программирования, или C# нас не спасет?
О сложностях программирования, или C# нас не спасет?Tatyanazaxarova
 
PVS-Studio, решение для разработки современных ресурсоемких приложений
PVS-Studio, решение для разработки современных ресурсоемких приложенийPVS-Studio, решение для разработки современных ресурсоемких приложений
PVS-Studio, решение для разработки современных ресурсоемких приложенийOOO "Program Verification Systems"
 
Поиск явного приведения указателя к 32-битному типу
Поиск явного приведения указателя к 32-битному типуПоиск явного приведения указателя к 32-битному типу
Поиск явного приведения указателя к 32-битному типуTatyanazaxarova
 
Статический анализ кода для верификации 64-битных приложений
Статический анализ кода для верификации 64-битных приложенийСтатический анализ кода для верификации 64-битных приложений
Статический анализ кода для верификации 64-битных приложенийTatyanazaxarova
 
Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...
Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...
Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...Tatyanazaxarova
 
Что такое "Parallel Lint"?
Что такое "Parallel Lint"?Что такое "Parallel Lint"?
Что такое "Parallel Lint"?Tatyanazaxarova
 
Большой брат помогает тебе
Большой брат помогает тебеБольшой брат помогает тебе
Большой брат помогает тебеTatyanazaxarova
 
Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...
Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...
Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...Sergey Platonov
 
Акторы на C++: стоило ли оно того?
Акторы на C++: стоило ли оно того?Акторы на C++: стоило ли оно того?
Акторы на C++: стоило ли оно того?Yauheni Akhotnikau
 
Статический анализ и регулярные выражения
Статический анализ и регулярные выраженияСтатический анализ и регулярные выражения
Статический анализ и регулярные выраженияTatyanazaxarova
 
Что такое size_t и ptrdiff_t
Что такое size_t и ptrdiff_tЧто такое size_t и ptrdiff_t
Что такое size_t и ptrdiff_tTatyanazaxarova
 
Обработка ошибок — общие соображения и грязные подробности
Обработка ошибок — общие соображения и грязные подробностиОбработка ошибок — общие соображения и грязные подробности
Обработка ошибок — общие соображения и грязные подробностиsi14
 
Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...
Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...
Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...Yauheni Akhotnikau
 
Intel IPP Samples for Windows - работа над ошибками
Intel IPP Samples for Windows - работа над ошибкамиIntel IPP Samples for Windows - работа над ошибками
Intel IPP Samples for Windows - работа над ошибкамиTatyanazaxarova
 
Правила статического анализа кода для диагностики потенциально опасных констр...
Правила статического анализа кода для диагностики потенциально опасных констр...Правила статического анализа кода для диагностики потенциально опасных констр...
Правила статического анализа кода для диагностики потенциально опасных констр...Sergey Vasilyev
 
Юнит-тестирование и Google Mock. Влад Лосев, Google
Юнит-тестирование и Google Mock. Влад Лосев, GoogleЮнит-тестирование и Google Mock. Влад Лосев, Google
Юнит-тестирование и Google Mock. Влад Лосев, Googleyaevents
 
основы ооп на языке C#. часть 1. введение в программирование
основы ооп на языке C#. часть 1. введение в программированиеосновы ооп на языке C#. часть 1. введение в программирование
основы ооп на языке C#. часть 1. введение в программированиеYakubovichDA
 

Mais procurados (20)

20 ловушек переноса Си++ - кода на 64-битную платформу
20 ловушек переноса Си++ - кода на 64-битную платформу20 ловушек переноса Си++ - кода на 64-битную платформу
20 ловушек переноса Си++ - кода на 64-битную платформу
 
PVS-Studio vs Chromium
PVS-Studio vs ChromiumPVS-Studio vs Chromium
PVS-Studio vs Chromium
 
64-битная версия Loki
64-битная версия Loki64-битная версия Loki
64-битная версия Loki
 
О сложностях программирования, или C# нас не спасет?
О сложностях программирования, или C# нас не спасет?О сложностях программирования, или C# нас не спасет?
О сложностях программирования, или C# нас не спасет?
 
PVS-Studio, решение для разработки современных ресурсоемких приложений
PVS-Studio, решение для разработки современных ресурсоемких приложенийPVS-Studio, решение для разработки современных ресурсоемких приложений
PVS-Studio, решение для разработки современных ресурсоемких приложений
 
Поиск явного приведения указателя к 32-битному типу
Поиск явного приведения указателя к 32-битному типуПоиск явного приведения указателя к 32-битному типу
Поиск явного приведения указателя к 32-битному типу
 
Статический анализ кода для верификации 64-битных приложений
Статический анализ кода для верификации 64-битных приложенийСтатический анализ кода для верификации 64-битных приложений
Статический анализ кода для верификации 64-битных приложений
 
Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...
Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...
Сравнение статического анализа общего назначения из Visual Studio 2010 и PVS-...
 
Что такое "Parallel Lint"?
Что такое "Parallel Lint"?Что такое "Parallel Lint"?
Что такое "Parallel Lint"?
 
Большой брат помогает тебе
Большой брат помогает тебеБольшой брат помогает тебе
Большой брат помогает тебе
 
Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...
Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...
Александр Тарасенко, Использование python для автоматизации отладки С/C++ код...
 
Акторы на C++: стоило ли оно того?
Акторы на C++: стоило ли оно того?Акторы на C++: стоило ли оно того?
Акторы на C++: стоило ли оно того?
 
Статический анализ и регулярные выражения
Статический анализ и регулярные выраженияСтатический анализ и регулярные выражения
Статический анализ и регулярные выражения
 
Что такое size_t и ptrdiff_t
Что такое size_t и ptrdiff_tЧто такое size_t и ptrdiff_t
Что такое size_t и ptrdiff_t
 
Обработка ошибок — общие соображения и грязные подробности
Обработка ошибок — общие соображения и грязные подробностиОбработка ошибок — общие соображения и грязные подробности
Обработка ошибок — общие соображения и грязные подробности
 
Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...
Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...
Акторы в C++: взгляд старого практикующего актородела (St. Petersburg C++ Use...
 
Intel IPP Samples for Windows - работа над ошибками
Intel IPP Samples for Windows - работа над ошибкамиIntel IPP Samples for Windows - работа над ошибками
Intel IPP Samples for Windows - работа над ошибками
 
Правила статического анализа кода для диагностики потенциально опасных констр...
Правила статического анализа кода для диагностики потенциально опасных констр...Правила статического анализа кода для диагностики потенциально опасных констр...
Правила статического анализа кода для диагностики потенциально опасных констр...
 
Юнит-тестирование и Google Mock. Влад Лосев, Google
Юнит-тестирование и Google Mock. Влад Лосев, GoogleЮнит-тестирование и Google Mock. Влад Лосев, Google
Юнит-тестирование и Google Mock. Влад Лосев, Google
 
основы ооп на языке C#. часть 1. введение в программирование
основы ооп на языке C#. часть 1. введение в программированиеосновы ооп на языке C#. часть 1. введение в программирование
основы ооп на языке C#. часть 1. введение в программирование
 

Semelhante a Коллекция примеров 64-битных ошибок в реальных программах

Урок 15. Паттерн 7. Упаковка указателей
Урок 15. Паттерн 7. Упаковка указателейУрок 15. Паттерн 7. Упаковка указателей
Урок 15. Паттерн 7. Упаковка указателейTatyanazaxarova
 
Безопасность 64-битного кода
Безопасность 64-битного кодаБезопасность 64-битного кода
Безопасность 64-битного кодаTatyanazaxarova
 
Разработка статического анализатора кода для обнаружения ошибок переноса прог...
Разработка статического анализатора кода для обнаружения ошибок переноса прог...Разработка статического анализатора кода для обнаружения ошибок переноса прог...
Разработка статического анализатора кода для обнаружения ошибок переноса прог...Tatyanazaxarova
 
Реклама PVS-Studio - статический анализ кода на языке Си и Си++
Реклама PVS-Studio - статический анализ кода на языке Си и Си++Реклама PVS-Studio - статический анализ кода на языке Си и Си++
Реклама PVS-Studio - статический анализ кода на языке Си и Си++Andrey Karpov
 
Урок 5. Сборка 64-битного приложения
Урок 5. Сборка 64-битного приложенияУрок 5. Сборка 64-битного приложения
Урок 5. Сборка 64-битного приложенияTatyanazaxarova
 
Урок 9. Паттерн 1. Магические числа
Урок 9. Паттерн 1. Магические числаУрок 9. Паттерн 1. Магические числа
Урок 9. Паттерн 1. Магические числаTatyanazaxarova
 
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#Andrey Karpov
 
64-битный конь, который умеет считать
64-битный конь, который умеет считать64-битный конь, который умеет считать
64-битный конь, который умеет считатьTatyanazaxarova
 
Алексей Андросов - Debugger: Отладка кода
Алексей Андросов - Debugger: Отладка кодаАлексей Андросов - Debugger: Отладка кода
Алексей Андросов - Debugger: Отладка кодаYandex
 
Особенности разработки 64-битных приложений
Особенности разработки 64-битных приложенийОсобенности разработки 64-битных приложений
Особенности разработки 64-битных приложенийTatyanazaxarova
 
Забытые проблемы разработки 64-битных программ
Забытые проблемы разработки 64-битных программЗабытые проблемы разработки 64-битных программ
Забытые проблемы разработки 64-битных программTatyanazaxarova
 
Регулярное использование статического анализа кода в командной разработке
Регулярное использование статического анализа кода в командной разработкеРегулярное использование статического анализа кода в командной разработке
Регулярное использование статического анализа кода в командной разработкеTatyanazaxarova
 
Как стандарт C++0x поможет в борьбе с 64-битными ошибками
Как стандарт C++0x поможет в борьбе с 64-битными ошибкамиКак стандарт C++0x поможет в борьбе с 64-битными ошибками
Как стандарт C++0x поможет в борьбе с 64-битными ошибкамиTatyanazaxarova
 
Цена ошибки
Цена ошибкиЦена ошибки
Цена ошибкиAndrey Karpov
 
Урок 26. Оптимизация 64-битных программ
Урок 26. Оптимизация 64-битных программУрок 26. Оптимизация 64-битных программ
Урок 26. Оптимизация 64-битных программTatyanazaxarova
 
SAST и Application Security: как бороться с уязвимостями в коде
SAST и Application Security: как бороться с уязвимостями в кодеSAST и Application Security: как бороться с уязвимостями в коде
SAST и Application Security: как бороться с уязвимостями в кодеAndrey Karpov
 
Урок 8. Статический анализ для выявления 64-битных ошибок
Урок 8. Статический анализ для выявления 64-битных ошибокУрок 8. Статический анализ для выявления 64-битных ошибок
Урок 8. Статический анализ для выявления 64-битных ошибокTatyanazaxarova
 
Урок 21. Паттерн 13. Выравнивание данных
Урок 21. Паттерн 13. Выравнивание данныхУрок 21. Паттерн 13. Выравнивание данных
Урок 21. Паттерн 13. Выравнивание данныхTatyanazaxarova
 

Semelhante a Коллекция примеров 64-битных ошибок в реальных программах (19)

Урок 15. Паттерн 7. Упаковка указателей
Урок 15. Паттерн 7. Упаковка указателейУрок 15. Паттерн 7. Упаковка указателей
Урок 15. Паттерн 7. Упаковка указателей
 
Безопасность 64-битного кода
Безопасность 64-битного кодаБезопасность 64-битного кода
Безопасность 64-битного кода
 
Разработка статического анализатора кода для обнаружения ошибок переноса прог...
Разработка статического анализатора кода для обнаружения ошибок переноса прог...Разработка статического анализатора кода для обнаружения ошибок переноса прог...
Разработка статического анализатора кода для обнаружения ошибок переноса прог...
 
Реклама PVS-Studio - статический анализ кода на языке Си и Си++
Реклама PVS-Studio - статический анализ кода на языке Си и Си++Реклама PVS-Studio - статический анализ кода на языке Си и Си++
Реклама PVS-Studio - статический анализ кода на языке Си и Си++
 
Урок 5. Сборка 64-битного приложения
Урок 5. Сборка 64-битного приложенияУрок 5. Сборка 64-битного приложения
Урок 5. Сборка 64-битного приложения
 
Урок 9. Паттерн 1. Магические числа
Урок 9. Паттерн 1. Магические числаУрок 9. Паттерн 1. Магические числа
Урок 9. Паттерн 1. Магические числа
 
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
 
64-битный конь, который умеет считать
64-битный конь, который умеет считать64-битный конь, который умеет считать
64-битный конь, который умеет считать
 
Алексей Андросов - Debugger: Отладка кода
Алексей Андросов - Debugger: Отладка кодаАлексей Андросов - Debugger: Отладка кода
Алексей Андросов - Debugger: Отладка кода
 
Особенности разработки 64-битных приложений
Особенности разработки 64-битных приложенийОсобенности разработки 64-битных приложений
Особенности разработки 64-битных приложений
 
Забытые проблемы разработки 64-битных программ
Забытые проблемы разработки 64-битных программЗабытые проблемы разработки 64-битных программ
Забытые проблемы разработки 64-битных программ
 
Регулярное использование статического анализа кода в командной разработке
Регулярное использование статического анализа кода в командной разработкеРегулярное использование статического анализа кода в командной разработке
Регулярное использование статического анализа кода в командной разработке
 
Как стандарт C++0x поможет в борьбе с 64-битными ошибками
Как стандарт C++0x поможет в борьбе с 64-битными ошибкамиКак стандарт C++0x поможет в борьбе с 64-битными ошибками
Как стандарт C++0x поможет в борьбе с 64-битными ошибками
 
Цена ошибки
Цена ошибкиЦена ошибки
Цена ошибки
 
Цена ошибки
Цена ошибкиЦена ошибки
Цена ошибки
 
Урок 26. Оптимизация 64-битных программ
Урок 26. Оптимизация 64-битных программУрок 26. Оптимизация 64-битных программ
Урок 26. Оптимизация 64-битных программ
 
SAST и Application Security: как бороться с уязвимостями в коде
SAST и Application Security: как бороться с уязвимостями в кодеSAST и Application Security: как бороться с уязвимостями в коде
SAST и Application Security: как бороться с уязвимостями в коде
 
Урок 8. Статический анализ для выявления 64-битных ошибок
Урок 8. Статический анализ для выявления 64-битных ошибокУрок 8. Статический анализ для выявления 64-битных ошибок
Урок 8. Статический анализ для выявления 64-битных ошибок
 
Урок 21. Паттерн 13. Выравнивание данных
Урок 21. Паттерн 13. Выравнивание данныхУрок 21. Паттерн 13. Выравнивание данных
Урок 21. Паттерн 13. Выравнивание данных
 

Mais de Tatyanazaxarova

Урок 27. Особенности создания инсталляторов для 64-битного окружения
Урок 27. Особенности создания инсталляторов для 64-битного окруженияУрок 27. Особенности создания инсталляторов для 64-битного окружения
Урок 27. Особенности создания инсталляторов для 64-битного окруженияTatyanazaxarova
 
Урок 25. Практическое знакомство с паттернами 64-битных ошибок
Урок 25. Практическое знакомство с паттернами 64-битных ошибокУрок 25. Практическое знакомство с паттернами 64-битных ошибок
Урок 25. Практическое знакомство с паттернами 64-битных ошибокTatyanazaxarova
 
Урок 24. Фантомные ошибки
Урок 24. Фантомные ошибкиУрок 24. Фантомные ошибки
Урок 24. Фантомные ошибкиTatyanazaxarova
 
Урок 23. Паттерн 15. Рост размеров структур
Урок 23. Паттерн 15. Рост размеров структурУрок 23. Паттерн 15. Рост размеров структур
Урок 23. Паттерн 15. Рост размеров структурTatyanazaxarova
 
Урок 20. Паттерн 12. Исключения
Урок 20. Паттерн 12. ИсключенияУрок 20. Паттерн 12. Исключения
Урок 20. Паттерн 12. ИсключенияTatyanazaxarova
 
Урок 19. Паттерн 11. Сериализация и обмен данными
Урок 19. Паттерн 11. Сериализация и обмен даннымиУрок 19. Паттерн 11. Сериализация и обмен данными
Урок 19. Паттерн 11. Сериализация и обмен даннымиTatyanazaxarova
 
Урок 17. Паттерн 9. Смешанная арифметика
Урок 17. Паттерн 9. Смешанная арифметикаУрок 17. Паттерн 9. Смешанная арифметика
Урок 17. Паттерн 9. Смешанная арифметикаTatyanazaxarova
 
Урок 16. Паттерн 8. Memsize-типы в объединениях
Урок 16. Паттерн 8. Memsize-типы в объединенияхУрок 16. Паттерн 8. Memsize-типы в объединениях
Урок 16. Паттерн 8. Memsize-типы в объединенияхTatyanazaxarova
 
Урок 13. Паттерн 5. Адресная арифметика
Урок 13. Паттерн 5. Адресная арифметикаУрок 13. Паттерн 5. Адресная арифметика
Урок 13. Паттерн 5. Адресная арифметикаTatyanazaxarova
 
Урок 11. Паттерн 3. Операции сдвига
Урок 11. Паттерн 3. Операции сдвигаУрок 11. Паттерн 3. Операции сдвига
Урок 11. Паттерн 3. Операции сдвигаTatyanazaxarova
 
Урок 10. Паттерн 2. Функции с переменным количеством аргументов
Урок 10. Паттерн 2. Функции с переменным количеством аргументовУрок 10. Паттерн 2. Функции с переменным количеством аргументов
Урок 10. Паттерн 2. Функции с переменным количеством аргументовTatyanazaxarova
 
Урок 7. Проблемы выявления 64-битных ошибок
Урок 7. Проблемы выявления 64-битных ошибокУрок 7. Проблемы выявления 64-битных ошибок
Урок 7. Проблемы выявления 64-битных ошибокTatyanazaxarova
 
Урок 4. Создание 64-битной конфигурации
Урок 4. Создание 64-битной конфигурацииУрок 4. Создание 64-битной конфигурации
Урок 4. Создание 64-битной конфигурацииTatyanazaxarova
 
Статический анализ Си++ кода
Статический анализ Си++ кодаСтатический анализ Си++ кода
Статический анализ Си++ кодаTatyanazaxarova
 
PVS-Studio научился следить за тем, как вы программируете
PVS-Studio научился следить за тем, как вы программируетеPVS-Studio научился следить за тем, как вы программируете
PVS-Studio научился следить за тем, как вы программируетеTatyanazaxarova
 
Пояснения к статье про Copy-Paste
Пояснения к статье про Copy-PasteПояснения к статье про Copy-Paste
Пояснения к статье про Copy-PasteTatyanazaxarova
 
Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...
Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...
Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...Tatyanazaxarova
 
Статический анализ и ROI
Статический анализ и ROIСтатический анализ и ROI
Статический анализ и ROITatyanazaxarova
 
Вечный вопрос измерения времени
Вечный вопрос измерения времениВечный вопрос измерения времени
Вечный вопрос измерения времениTatyanazaxarova
 

Mais de Tatyanazaxarova (20)

Урок 27. Особенности создания инсталляторов для 64-битного окружения
Урок 27. Особенности создания инсталляторов для 64-битного окруженияУрок 27. Особенности создания инсталляторов для 64-битного окружения
Урок 27. Особенности создания инсталляторов для 64-битного окружения
 
Урок 25. Практическое знакомство с паттернами 64-битных ошибок
Урок 25. Практическое знакомство с паттернами 64-битных ошибокУрок 25. Практическое знакомство с паттернами 64-битных ошибок
Урок 25. Практическое знакомство с паттернами 64-битных ошибок
 
Урок 24. Фантомные ошибки
Урок 24. Фантомные ошибкиУрок 24. Фантомные ошибки
Урок 24. Фантомные ошибки
 
Урок 23. Паттерн 15. Рост размеров структур
Урок 23. Паттерн 15. Рост размеров структурУрок 23. Паттерн 15. Рост размеров структур
Урок 23. Паттерн 15. Рост размеров структур
 
Урок 20. Паттерн 12. Исключения
Урок 20. Паттерн 12. ИсключенияУрок 20. Паттерн 12. Исключения
Урок 20. Паттерн 12. Исключения
 
Урок 19. Паттерн 11. Сериализация и обмен данными
Урок 19. Паттерн 11. Сериализация и обмен даннымиУрок 19. Паттерн 11. Сериализация и обмен данными
Урок 19. Паттерн 11. Сериализация и обмен данными
 
Урок 17. Паттерн 9. Смешанная арифметика
Урок 17. Паттерн 9. Смешанная арифметикаУрок 17. Паттерн 9. Смешанная арифметика
Урок 17. Паттерн 9. Смешанная арифметика
 
Урок 16. Паттерн 8. Memsize-типы в объединениях
Урок 16. Паттерн 8. Memsize-типы в объединенияхУрок 16. Паттерн 8. Memsize-типы в объединениях
Урок 16. Паттерн 8. Memsize-типы в объединениях
 
Урок 13. Паттерн 5. Адресная арифметика
Урок 13. Паттерн 5. Адресная арифметикаУрок 13. Паттерн 5. Адресная арифметика
Урок 13. Паттерн 5. Адресная арифметика
 
Урок 11. Паттерн 3. Операции сдвига
Урок 11. Паттерн 3. Операции сдвигаУрок 11. Паттерн 3. Операции сдвига
Урок 11. Паттерн 3. Операции сдвига
 
Урок 10. Паттерн 2. Функции с переменным количеством аргументов
Урок 10. Паттерн 2. Функции с переменным количеством аргументовУрок 10. Паттерн 2. Функции с переменным количеством аргументов
Урок 10. Паттерн 2. Функции с переменным количеством аргументов
 
Урок 7. Проблемы выявления 64-битных ошибок
Урок 7. Проблемы выявления 64-битных ошибокУрок 7. Проблемы выявления 64-битных ошибок
Урок 7. Проблемы выявления 64-битных ошибок
 
Урок 4. Создание 64-битной конфигурации
Урок 4. Создание 64-битной конфигурацииУрок 4. Создание 64-битной конфигурации
Урок 4. Создание 64-битной конфигурации
 
Статический анализ Си++ кода
Статический анализ Си++ кодаСтатический анализ Си++ кода
Статический анализ Си++ кода
 
PVS-Studio
PVS-Studio PVS-Studio
PVS-Studio
 
PVS-Studio научился следить за тем, как вы программируете
PVS-Studio научился следить за тем, как вы программируетеPVS-Studio научился следить за тем, как вы программируете
PVS-Studio научился следить за тем, как вы программируете
 
Пояснения к статье про Copy-Paste
Пояснения к статье про Copy-PasteПояснения к статье про Copy-Paste
Пояснения к статье про Copy-Paste
 
Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...
Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...
Использование анализатора PVS-Studio в процессе инкрементальной сборки в Micr...
 
Статический анализ и ROI
Статический анализ и ROIСтатический анализ и ROI
Статический анализ и ROI
 
Вечный вопрос измерения времени
Вечный вопрос измерения времениВечный вопрос измерения времени
Вечный вопрос измерения времени
 

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

  • 1. Коллекция примеров 64-битных ошибок в реальных программах Автор: Андрей Карпов Дата: 29.06.2010 Аннотация Статья представляет собой наиболее полную коллекцию примеров 64-битных ошибок на языках Си и Си++. Статья ориентирована на разработчиков Windows-приложений, использующих Visual C++, но будет полезна и более широкой аудитории. Введение Наша компания ООО "Системы программной верификации" занимается разработкой специализированного статического анализатора 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;
  • 2. }; 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).
  • 3. Рисунок 1 – Схематичное пояснение примера переполнения буфера Подобную ошибку может быть сложно выявить, если при этом портятся данные, используемые гораздо позднее. Пример 2. Лишние приведения типов char *buffer; char *curr_pos; int length; ... while( (*(curr_pos++) != 0x0a) && ((UINT)curr_pos - (UINT)buffer < (UINT)length) ); Код плох, но это реальный код. Его задача состоит в поиске конца строки, обозначенного символом 0x0A. Код не будет работать со строками длиннее INT_MAX символов, так как
  • 4. переменная length имеет тип int. Однако нас интересует другая ошибка, поэтому будем считать, что программа работает с небольшим буфером и использование типа int корректно. Проблема в том, что в 64-битной системе указатели buffer и curr_pos могут лежать за пределами первых 4 гигабайт адресного пространства. В этом случае явное приведение указателей к типу UINT отбросит значащие биты, и работа алгоритма будет нарушена (см. рисунок 2). Рисунок 2 – Некорректны вычисления при поиске терминального символа Ошибка неприятна тем, что код долгое время может корректно работать, пока память под буфер будет выделяться в младших четырех гигабайтах адресного пространства. Исправление ошибки заключается в удалении совершенно ненужных явных приведений типов:
  • 5. while(curr_pos - buffer < length && *curr_pos != 'n') 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
  • 6.
  • 7. Рисунок 3 – Два варианта - это слишком мало Полагаться на вариант #else в подобных ситуациях опасно. Лучше явно рассмотреть поведение для каждого случая (см. рисунок 4), а в ветку #else поместить сообщение об ошибке компиляции: #if defined _M_X64 // Win64 code (Intel 64) cout << "This is Win64" << endl; #elif defined _WIN32 // Win32 code
  • 8. cout << "This is Win32" << endl; #elif defined _WIN16 // Win16 code cout << "This is Win16" << endl; #else static_assert(false, "Неизвестная платформа"); #endif
  • 9. Рисунок 4 – Проверяются все возможные пути компиляции Пример 4. Путаница с int и int* В старых программах, особенно на Си, не редки фрагменты кода, где указатель хранят в типе int. Однако иногда это делается не умышленно, а скорее по невнимательности. Рассмотрим пример, содержащий путаницу, возникшую с использованием типа int и указателем на тип int: int GlobalInt = 1; void GetValue(int **x) {
  • 10. *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).
  • 11. Рисунок 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);
  • 12. Программиста, некогда написавшего этот код, не в чем упрекнуть. В ходе разработки, лет 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). Рисунок 6 – Таблица с именами некоторых устаревших и современных функций Пример 6. Обрезание значений при неявном приведении типов Неявное приведение типа size_t к типу unsigned и аналогичные приведения хорошо диагностируются предупреждениями компилятора. Однако в больших программах, подобные предупреждения легко могут затеряться. Рассмотрим пример схожий с реальным кодом, где
  • 13. предупреждение было проигнорировано, так как казалось, что ничего плохого при работе с короткими строками произойти не может. 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).
  • 14. Рисунок 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-битной ошибки. Для начала рассмотрим корректный вариант кода, в котором происходит выделение и использование трех массивов размером по гигабайту каждый:
  • 15. #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
  • 16. 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. Останки динозавров в больших и старых программах Большие старые программные системы, развивающиеся десятилетиями, изобилуют разнообразнейшими атавизмами и просто участками кода, написанными с использованием популярных парадигм и стилей разнообразных лет. В таких системах можно наблюдать эволюцию развития языков программирования, когда наиболее старые части написаны в стиле языка Си, а в наиболее свежих можно встретить сложные шаблоны в стиле Александреску. Рисунок 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; ...
  • 17. 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,%dn", 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 {
  • 18. ... 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).
  • 19. Рисунок 9 – Ошибка, связанная с виртуальными функциями Пример 10. Магические числа в качестве параметров Магические числа, содержащиеся в теле программ, являются плохим стилем и провоцируют возникновение ошибок. В качестве примера магических чисел можно привести 1024 и 768, жестко обозначающие размер разрешения экрана. В рамках этой статьи нам интересны те магические числа, которые могут привести к проблемам в 64-битном приложении. Наиболее распространенные числа, опасные для 64-битных программ, представлены в таблице на рисунке 10. Рисунок 10 – Магические числа опасные для 64-битных программ
  • 20. Продемонстрируем пример работы с функцией 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);
  • 21. В данном случае в 64-битной системе выделяется больше памяти, чем затем заполняется нулевыми значениями (см. рисунок 11) . Ошибка заключается в предположении, что размер типа size_t всегда равен четырем байтам.
  • 22. Рисунок 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). Это пространство в стеке позволяет легко сохранить содержимое переданных в функцию регистров в стеке. От вызываемой функции не требуется сбрасывать в стек входные параметры, переданные через регистры, но резервирование места в стеке при необходимости
  • 23. позволяет это сделать. Если передается более четырех целочисленных параметров, в стеке резервируется соответствующее дополнительное пространство. Описанная особенность приводит к существенному возрастанию скорости поглощения стека. Даже если функция не имеет параметров, то от стека все равно будет "откушено" 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', но в реальном приложении переполнение буфера может возникнуть и без магических чисел.
  • 24. Рисунок 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;
  • 25. // Будет распечатано неверное значение 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;
  • 26. size_t c = b; // x86: a == c // x64: a != c Данный пример еще можно пытаться оправдывать на 32-битной системе, так как тип double имеет 52 значащих бита и способен без потерь хранить 32-битное целое значение. Но при попытке сохранить в double 64-битное целое число точное значение может быть потеряно (см. рисунок 13). Рисунок 13 – Количество значащих битов в типах size_t и double Пример 16. Адресная арифметика. A + B != A - (-B) Адресная арифметика (address arithmetic) - это способ вычисления адреса какого-либо объекта при помощи арифметических операций над указателями, а также использование указателей в операциях сравнения. Адресную арифметику также называют арифметикой над указателями (pointer arithmetic). Большой процент 64-битных ошибок связан именно с адресной арифметикой. Часто ошибки возникают в тех выражениях, где совместно используются указатели и 32-битные переменные. Рассмотрим первую из ошибок данного типа: char *A = "123456789"; unsigned B = 1; char *X = A + B; char *Y = A - (-B); if (X != Y) cout << "Error" << endl; Причина, по которой в Win32 программе A + B == A - (-B), показана на рисунке 14. Рисунок 14 – Win32: A + B == A - (-B)
  • 27. Причина, по которой в Win64 программе A + B != A - (-B), показана на рисунке 15. Рисунок 15 – Win64: A + B != A - (-B) Ошибка будет устранена, если использовать подходящий memsize-тип. В данном случае используется тип ptrdfiff_t: char *A = "123456789"; ptrdiff_t B = 1; char *X = A + B; char *Y = A - (-B); Пример 17. Адресная арифметика. Знаковые и беззнаковые типы. Рассмотрим еще один вариант ошибки, связанный с использованием знаковых и беззнаковых типов. В этот раз ошибка приведет не к неверному сравнению, а сразу к падению приложения. LONG p1[100]; ULONG x = 5; LONG y = -1; LONG *p2 = p1 + 50; p2 = p2 + x * y; *p2 = 1; // Access violation Выражение "x * y" имеет значение 0xFFFFFFFB и имеет тип unsigned. Данный код, собранный в 32-битном варианте работоспособен, так как сложение указателя с 0xFFFFFFFB эквивалентно его уменьшению на 5. В 64-битной указатель после прибавления 0xFFFFFFFB начнет указывать далеко за пределы массива p1 (смотри рисунок 16).
  • 28. Рисунок 16 – Выход за границы массива
  • 29. Исправление заключается в использовании memsize-типов и аккуратной работе со знаковыми и беззнаковыми типами: LONG p1[100]; LONG_PTR x = 5; LONG_PTR y = -1; LONG *p2 = p1 + 50; p2 = p2 + x * y; *p2 = 1; // OK Пример 18. Адресная арифметика. Переполнения. class Region { float *array; int Width, Height, Depth; float Region::GetCell(int x, int y, int z) const; ... }; float Region::GetCell(int x, int y, int z) const { return array[x + y * Width + z * Width * Height]; } Код взят из реальной программы математического моделирования, в которой важным ресурсом является объем оперативной памяти, и возможность на 64-битной архитектуре использовать более 4 гигабайт памяти существенно увеличивает вычислительные возможности. В программах данного класса для экономии памяти часто используют одномерные массивы, осуществляя работу с ними как с трехмерными массивами. Для этого существуют функции, аналогичные GetCell, обеспечивающие доступ к необходимым элементам. Приведенный код корректно работает с указателями, если значение выражения " x + y * Width + z * Width * Height" не превышает INT_MAX (2147483647). В противном случае произойдет переполнение, что приведет к неопределенному поведению в программы. Такой код мог всегда корректно работать на 32-битной платформе. В рамках 32-битной архитектуры программе недоступен объем памяти для создания массива подобного размеров. На 64-битной архитектуре это ограничение снято, и размер массива легко может превысить INT_MAX элементов. Программисты часто допускают ошибку, пытаясь исправить код следующим образом:
  • 30. float Region::GetCell(int x, int y, int z) const { return array[static_cast<ptrdiff_t>(x) + y * Width + z * Width * Height]; } Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь тип ptrdiff_t и надеются за счет этого избежать переполнения. Но переполнение может произойти внутри подвыражения "y * Width" или "z * Width * Height", так как для их вычисления по- прежнему используется тип int. Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы можете явно привести каждое подвыражение к типу ptrdiff_t: float Region::GetCell(int x, int y, int z) const { return array[ptrdiff_t(x) + ptrdiff_t(y) * Width + ptrdiff_t(z) * Width * Height]; } Другое, более верное решение - изменить типы переменных: typedef ptrdiff_t TCoord; class Region { float *array; TCoord Width, Height, Depth; float Region::GetCell(TCoord x, TCoord y, TCoord z) const; ... }; float Region::GetCell(TCoord x, TCoord y, TCoord z) const { return array[x + y * Width + z * Width * Height]; } Пример 19. Изменение типа массива Иногда в программах для удобства изменяют тип массива при его обработке. Опасное и безопасное приведение типов представлено в следующем коде: int array[4] = { 1, 2, 3, 4 };
  • 31. enum ENumbers { ZERO, ONE, TWO, THREE, FOUR }; //safe cast (for MSVC) ENumbers *enumPtr = (ENumbers *)(array); cout << enumPtr[1] << " "; //unsafe cast size_t *sizetPtr = (size_t *)(array); cout << sizetPtr[1] << endl; //Output on 32-bit system: 2 2 //Output on 64-bit system: 2 17179869187 Как видите, результат вывода программы отличается в 32-битном и 64-битном варианте. На 32- битной системе доступ к элементам массива осуществляется корректно, так как размеры типов size_t и int совпадают, и мы видим вывод "2 2". На 64-битной системе мы получили в выводе "2 17179869187", так как именно значение 17179869187 находится в 1-ом элементе массива sizetPtr (см. рисунок 17). В некоторых случаях именно такое поведение и бывает нужно, но обычно это является ошибкой.
  • 32. Рисунок 17 – Представление элементов массивов в памяти Примечание. Тип enum в компиляторе Visual C++ по умолчанию совпадает размером с типом int, то есть является 32-битным типом. Использование enum другого размера возможно только с помощью расширения, считающимся нестандартным в Visual C++. Поэтому приведенный пример корректен в Visual C++, но с точки зрения других компиляторов приведение указателя на элементы int к указателю на элементы enum может быть также некорректным. Пример 20. Упаковка указателя в 32-битный тип Иногда в программах указатели сохраняют в целочисленных типах. Обычно для этого используется такой тип, как int. Это, пожалуй, одна из самых распространенных 64-битных ошибок. char *ptr = ...; int n = (int) ptr; ... ptr = (char *) n; В 64-битной программе это некорректно, поскольку тип int остался 32-битным и не может хранить в себе 64-битный указатель. Часто это не удается заметить сразу. Благодаря стечению обстоятельств при тестировании указатель может всегда ссылаться на объекты, расположенные в младших 4 гигабайтах адресного пространства. В этом случае 64-битная программа будет удачно
  • 33. работать, и может неожиданно отказать только спустя большой промежуток времени (см. рисунок 18). Рисунок 18 – Помещение указателя в переменную типа int Если все же необходимо поместить указатель в переменную целочисленного типа, то следует использовать такие типы как intptr_t, uintptr_t, ptrdiff_t и size_t.
  • 34. Пример 21. Memsize-типы в объединениях Когда возникает необходимость работать с указателем как с целым числом, иногда удобно воспользоваться объединением, как показано в примере, и работать с числовым представлением типа без использования явных приведений: union PtrNumUnion { char *m_p; unsigned m_n; } u; u.m_p = str; u.m_n += delta; Данный код корректен на 32-битных системах и некорректен на 64-битных. Изменяя член m_n на 64-битной системе, мы работаем только с частью указателя m_p (смотри рисунок 19). Рисунок 19 – Представление объединения в памяти на 32-битной и 64-битной системе. Следует использовать тип, который будет соответствовать размеру указателя: union PtrNumUnion { char *m_p; uintptr_t m_n; //type fixed } u; Пример 22. Вечный цикл Смешанное использование 32-битных и 64-битных типов неожиданно может привести к возникновению вечных циклов. Рассмотрим синтетический пример, иллюстрирующий целый класс подобных дефектов: size_t Count = BigValue; for (unsigned Index = 0; Index != Count; Index++)
  • 35. { ... } Это цикл никогда не прекратится, если значение Count > UINT_MAX. Предположим, что на 32- битных системах этот код работал с количеством итераций менее значения UINT_MAX. Но 64- битный вариант программы может обрабатывать больше данных, и ему может потребоваться большее количество итераций. Поскольку значения переменной Index лежат в диапазоне [0..UINT_MAX], то условие "Index != Count" никогда не выполнится, что и приводит к бесконечному циклу (см. рисунок 20).
  • 36. Рисунок 20 – Механизм возникновения вечного цикла Пример 23. Работа с битами и операция NOT Работа с битовыми операциями требует особой аккуратности от программиста при разработке кроссплатформенных приложений, в которых типы данных могут иметь различные размеры. Поскольку перенос программы на 64-битную платформу также ведет к изменению размерности некоторых типов, то высока вероятность возникновения ошибок в участках кода, работающих с отдельными битами. Чаще всего это происходит из-за смешенной работы с 32-битными и 64- битными типами данных. Рассмотрим ошибку, возникшую в коде из-за некорректного применения операции NOT: UINT_PTR a = ~UINT_PTR(0); ULONG b = 0x10; UINT_PTR c = a & ~(b - 1); c = c | 0xFu;
  • 37. if (a != c) cout << "Error" << endl; Ошибка заключается в том, что маска, заданная выражением "~(b - 1)", имеет тип ULONG. Это приводит к обнулению старших разрядов переменной "a", ходя должны были обнулиться только младшие четыре бита (см. рисунок 21).
  • 38. Рисунок 21 – Ошибка из-за обнуления старших бит Исправленный вариант кода может выглядеть следующим образом: UINT_PTR c = a & ~(UINT_PTR(b) - 1); Приведенный пример крайне прост, но хорошо демонстрирует класс ошибок, которые могут возникать при активной работе с битовыми операциями. Пример 24. Работа с битами, сдвиги ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) { ptrdiff_t mask = 1 << bitNum; return value | mask; }
  • 39. Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит с номерами от 0 до 31 в единицу. После переноса программы на 64-битную платформу возникает необходимость выставлять биты с номерами от 0 до 63. Однако данный код неспособен выставить старшие биты, с номерами 32-63. Обратите внимание, что числовой литерал "1" имеет тип int, и при сдвиге на 32 позиции произойдет переполнение, как показано на рисунке 22. Получим мы в результате 0 (рисунок 22-B) или 1 (рисунок 22-C) - зависит от реализации компилятора. Рисунок 22 – a) корректная установка 31-ого бита в 32-битном коде (биты считаются от 0); b,c) - Ошибка установки 32-ого бита на 64-битной системе (два варианта поведения, зависящих от компилятора) Для исправления кода необходимо сделать константу "1" того же типа, что и переменная mask:
  • 40. ptrdiff_t mask = static_cast<ptrdiff_t>(1) << bitNum; Заметим также, что неисправленный код приведет еще к одной интересной ошибке. При выставлении 31 бита на 64-битной системе результатом работы функции будет значение 0xffffffff80000000 (см. рисунок 23). Результатом выражения 1 << 31 является отрицательное число - 2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000. Рисунок 23 – Ошибка установки 31-ого бита на 64-битной системе Пример 25. Работа с битами и знаковое расширение Приведенная далее ошибка редка, но, к сожалению, достаточно сложна в понимании. Поэтому остановимся на ней чуть подробнее. struct BitFieldStruct { unsigned short a:15; unsigned short b:13; }; BitFieldStruct obj; obj.a = 0x4000; size_t x = obj.a << 17; //Sign Extension printf("x 0x%Ixn", x); //Output on 32-bit system: 0x80000000 //Output on 64-bit system: 0xffffffff80000000 В 32-битной среде порядок вычисления выражения будет выглядеть, как показано на рисунке 24.
  • 41. Рисунок 24 - Вычисление выражения в 32-битном коде Обратим внимание, что при вычислении выражения "obj.a << 17" происходит знаковое расширение типа unsigned short до типа int. Более наглядно, это может продемонстрировать следующий код: #include <stdio.h> template <typename T> void PrintType(T) { printf("type is %s %d-bitn", (T)-1 < 0 ? "signed" : "unsigned", sizeof(T)*8); } struct BitFieldStruct { unsigned short a:15; unsigned short b:13; }; int main(void) { BitFieldStruct bf; PrintType( bf.a ); PrintType( bf.a << 2); return 0;
  • 42. } Result: type is unsigned 16-bit type is signed 32-bit Теперь посмотрим, к чему приводит наличие знакового расширения в 64-битном коде. Последовательность вычисления выражения показана на рисунке 25. Рисунок 25 - Вычисление выражения в 64-битном коде Член структуры obj.a преобразуется из битового поля типа unsigned short в int. Выражение "obj.a << 17" имеет тип int, но оно преобразуется в ptrdiff_t и затем в size_t, перед тем как будет присвоено переменной addr. В результате мы получим число значение 0xffffffff80000000, вместо ожидаемого значения 0x0000000080000000. Будьте внимательны при работе с битовыми полями. Для предотвращения описанной ситуации в нашем примере достаточно явно привести obj.a к типу size_t. ... size_t x = static_cast<size_t>(obj.a) << 17; // OK printf("x 0x%Ixn", x); //Output on 32-bit system: 0x80000000 //Output on 64-bit system: 0x80000000 Пример 26. Сериализация и обмен данными Важным элементом переноса программного решения на новую платформу является преемственность к существующим протоколам обмена данными. Необходимо обеспечить чтение
  • 43. существующих форматов проектов, осуществлять обмен данными между 32-битными и 64- битными процессами и так далее. В основном, ошибки данного рода заключаются в сериализации memsize-типов и операциях обмена данными с их использованием: size_t PixelsCount; fread(&PixelsCount, sizeof(PixelsCount), 1, inFile); Недопустимо использование типов, которые меняют свой размер в зависимости от среды разработки, в бинарных интерфейсах обмена данными. В языке Си++ большинство типов не имеют четкого размера и, следовательно, их все невозможно использовать для этих целей. Поэтому создатели средств разработки и сами программисты создают типы данных, имеющие строгий размер, такие как __int8, __int16, INT32, word64 и так далее. Даже после внесения исправлений, касающихся размеров типа, вы можете столкнуться с несовместимостью бинарных форматов. Причина кроется в ином представлении данных. Наиболее часто это связано с другой последовательностью байт. Порядок байт - метод записи байтов многобайтовых чисел (см. рисунок 26). Порядок от младшего к старшему (англ. little-endian) - запись начинается с младшего и заканчивается старшим. Этот порядок записи принят в памяти персональных компьютеров с x86 и x86-64-процессорами. Порядок от старшего к младшему (англ. big-endian) - запись начинается со старшего и заканчивается младшим. Этот порядок является стандартным для протоколов TCP/IP. Поэтому, порядок байтов от старшего к младшему часто называют сетевым порядком байтов (англ. network byte order). Этот порядок байт используется процессорами Motorola 68000, SPARC. Кстати, некоторые процессоры могут работать и в порядке от младшего к старшему, и наоборот. К их числу относится, например IA-64.
  • 44. Рисунок 26 - Порядок байт в 64-битном типе на little-endian и big-endian системах Разрабатывая бинарный интерфейс или формат данных, следует помнить о последовательности байт. А если 64-битная система, на которую Вы переносите 32-битное приложение, имеет иную последовательность байт, то вы просто будете вынуждены учесть это в своем коде. Для преобразования между сетевым порядком байт (big-endian) и порядком байт (little-endian), можно использовать функции htonl(), htons(), bswap_64, и так далее. Пример 27. Изменение выравнивания типов Помимо изменения размеров некоторых типов данных, ошибки могут возникать и из-за изменения правил их выравнивания в 64-битной системе (см. рисунок 27).
  • 45. Рисунок 27 – Размеры типы и границы их выравнивания (значения точны для Win32/Win64, но могут варьироваться в "Unix-мире" и приводятся просто в качестве примера) Рассмотрим пример описания проблемы, найденного в одном из форумов: Столкнулся сегодня с одной проблемой в Linux. Есть структура данных, состоящая из нескольких полей: 64-битный double, потом 8 unsigned char и один 32-битный int. Итого получается 20 байт (8 + 8*1 + 4). Под 32-битными системами sizeof равен 20 и всё работает нормально. А под 64-битным Linux'ом sizeof возвращает 24. Т.е. идёт выравнивание по границе 64 бит. Далее в форуме идут рассуждения о совместимости данных и просьба совета, как упаковать данные в структуре. Но не это сейчас интересно. Интереснее то, что здесь наблюдается очередной тип ошибки, который может возникнуть при переносе приложений на 64-битную систему.
  • 46. Когда меняются размеры полей в структуре и из-за этого меняется сам размер структуры это понятно и привычно. Здесь другая ситуация. Размер полей остался прежний, но из-за иных правил выравнивания размер структуры все равно изменится (см. рисунок 28). Такое поведение может привести к разнообразным ошибкам, например в несовместимости форматов сохраняемых данных.
  • 47. Рисунок 28 – Схематическое изображение структур и правил выравнивания типов Пример 28. Выравнивания типов и почему нельзя писать sizeof(x) + sizeof(y) Иногда программисты используют структуры, в конце которых расположен массив переменного размера. Эта структура и выделение памяти для нее может выглядеть следующим образом: struct MyPointersArray { DWORD m_n; PVOID m_arr[1]; } object; ... malloc( sizeof(DWORD) + 5 * sizeof(PVOID) ); ... Этот код будет корректно работать в 32-битном варианте, но его 64-битный вариант даст сбой. При выделении памяти, необходимой для хранения объекта типа MyPointersArray, содержащего 5 указателей, необходимо учесть, что начало массива m_arr будет выровнено по границе 8 байт. Расположение данных в памяти на разных системах (Win32/Win64) показано на рисунке 29.
  • 48. Рисунок 29 – Расположение данных в памяти в 32-битной и 64-битной системе Корректный расчет размера должен выглядеть следующим образом: struct MyPointersArray { DWORD m_n; PVOID m_arr[1]; } object; ... malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) + 5 * sizeof(PVOID) ); ... В приведенном коде мы узнаем смещение последнего члена структуры и суммируем это смещение с его размером. Смещение члена структуры или класса можно узнать с использованием макроса offsetof или FIELD_OFFSET. Всегда используйте эти макросы для получения смещения в структуре, не опираясь на свои предположения о размерах типов и правилах их выравнивания. Пример 29. Перегруженные функции При перекомпиляции программы может начать выбираться другая перегруженная функция (см. рисунок 30).
  • 49. Рисунок 30 – Выбор перегруженной функции в 32-битной и 64-битной системе Пример проблемы: class MyStack { ... public: void Push(__int32 &); void Push(__int64 &); void Pop(__int32 &); void Pop(__int64 &); } stack; ptrdiff_t value_1; stack.Push(value_1); ... int value_2; stack.Pop(value_2); Неаккуратный программист помещал и затем выбирал из стека значения различных типов (ptrdiff_t и int). На 32-битной системе их размеры совпадали, все замечательно работало. Когда в 64-битной программе изменился размер типа ptrdiff_t, то в стек стало попадать больше байт, чем затем извлекаться. Пример 30. Ошибки в 32-битных модулях, работающих в WoW64 Последний пример посвящен ошибкам в 32-битных программах, которые возникают при их выполнении в 64-битной среде. В состав 64-битных программных комплексов еще долго будут входить 32-битные модули, а следовательно необходимо обеспечить их корректную работу в 64-
  • 50. битной среде. Подсистема WoW64 очень хорошо справляется со своей задачей, изолируя 32- битное приложение, и практически все 32-битные приложения функционирует корректно. Однако иногда ошибки все же встречаются и в основном они связаны с механизмом перенаправления при работе с файлами и реестром Windows. Например, в комплексе, состоящим из 32-битных и 64-битных модулей, при их взаимодействии следует учитывать, что они используют различные представления реестра. Таким образом, в одной из программ следующая строчка в 32-битном модуле стала неработоспособной: lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWAREODBCODBC.INIODBC Data Sources", 0, KEY_QUERY_VALUE, &hKey); Чтобы подружить эту программу с другими 64-битными частями, необходимо вписать ключ KEY_WOW64_64KEY: lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWAREODBCODBC.INIODBC Data Sources", 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &hKey); Заключение Наилучший результат при поиске описанных в статье ошибок дает методика статического анализа кода. В качестве примера инструмента осуществляющего такой анализ, можно назвать разрабатываемый нами инструмент Viva64, входящий в состав PVS-Studio. Методы статического поиска дефектов позволяют обнаруживать дефекты по исходному коду программы. При этом поведение программы оценивается на всех путях исполнения одновременно. За счет этого, возможно обнаружение дефектов, проявляющихся лишь на необычных путях исполнения при редких входных данных. Эта особенность позволяет дополнить методы тестирования, повышая надежность программ. Системы статического анализа могут использоваться при проведении аудита исходного кода, для систематического устранения дефектов в существующих программах, и встраиваться в цикл разработки, автоматически обнаруживая дефекты в создаваемом коде. Библиографический список 1. Андрей Карпов, Евгений Рыжков. Уроки разработки 64-битных приложений на языке Си/Си++. http://www.viva64.com/ru/articles/x64-lessons/ 2. Андрей Карпов. Что такое size_t и ptrdiff_t. http://www.viva64.com/art-1-1-72510946.html 3. Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ - кода на 64-битную платформу. http://www.viva64.com/art-1-1-1958348565.html 4. Евгений Рыжков. Учебное пособие по PVS-Studio. http://www.viva64.com/art-4-1- 1796251700.html 5. Андрей Карпов. 64-битный конь, который умеет считать. http://www.viva64.com/art-1-1- 1064884779.html