Реклама
Спонсорские ссылки
-
Статья
Версия для печати
Обсудить на форуме
20 ловушек переноса Си++ - кода на 64-битную платформу.
Часть 2.

Андрей Карпов
ООО "СиПроВер"
Евгений Рыжков
ООО "СиПроВер"
Март 2007

Публикуется с разрешения автора.
Оригинал статьи находится на сайте www.viva64.com.

Содержание.


(Первая часть статьи.)

11. Битовые поля.

Если вы используете битовые поля, то необходимо учитывать, что использование memsize-типов повлечет изменение размеров структур и выравнивания. Например, приведенная далее структура будет иметь размер 4 байта на 32-битной системе и 8 байт на 64-битной системе:

Код: (C++)
struct MyStruct {
  size_t r : 5;
};

Но на этом ваша внимательность к битовым полям ограничиваться не должна. Рассмотрим тонкий пример:

Код: (C++)
struct BitFieldStruct {
  unsigned short a:15;
  unsigned short b:13;
};
BitFieldStruct obj;
obj.a = 0x4000;
size_t addr = obj.a << 17; //Sign Extension
printf("addr 0x%Ix\n", addr);

//Output on 32-bit system: 0x80000000
//Output on 64-bit system: 0xffffffff80000000

Обратите внимание, если приведенный пример скомпилировать для 64-битной системы, то в выражении "addr = obj.a << 17;" будет присутствовать знаковое расширение, несмотря на то, что обе переменные addr и obj.a являются беззнаковыми. Это знаковое расширение обусловлено правилами приведения типов, которые применяются следующим образом (см. также рисунок 5):
Член структуры obj.a преобразуется из битового поля типа unsigned short в int. Мы получаем тип int, а не unsigned int из-за того, что 15-битное поле помещается в 32-битное знаковое целое.
Выражение "obj.a << 17" имеет тип int, но оно преобразуется в ptrdiff_t и затем в size_t, перед тем, как будет присвоено переменной addr. Знаковое расширение происходит в момент совершения преобразования из int в ptrdiff_t.





Рисунок 5. Вычисление выражения на различных системах.

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

Код: (C++)
...
size_t addr = size_t(obj.a) << 17;
printf("addr 0x%Ix\n", addr);

//Output on 32-bit system: 0x80000000
//Output on 64-bit system: 0x80000000

12. Адресная арифметика с указателями.

Пример первый:

Код: (C++)
unsigned short a16, b16, c16;
char *pointer;

pointer += a16 * b16 * c16;

Данный пример корректно работает с указателями, если значение выражения "a16 * b16 * c16" не превышает UINT_MAX (4Gb). Такой код мог всегда корректно работать на 32-битной платформе, так как программа никогда не выделяла массивов больших размеров. На 64-битной архитектуре размер массива превысил UINT_MAX элементов. Допустим, мы хотим сдвинуть значение указателя на 6.000.000.000 байт, и поэтому переменные a16, b16 и c16 имеют значения 3000, 2000 и 1000 соответственно. При вычислении выражения "a16 * b16 * c16" все переменные, согласно правилам языка Си++, будут приведены к типу int, а уже затем будет произведено их умножение. В ходе выполнения умножения произойдет переполнение. Некорректный результат выражения будет расширен до типа ptrdiff_t и произойдет некорректное вычисление указателя.

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

Код: (C++)
short a16, b16, c16;
char *pointer;

pointer += static_cast<ptrdiff_t>(a16) *
           static_cast<ptrdiff_t>(b16) *
           static_cast<ptrdiff_t>(c16);

Если вы думаете, что злоключения ждут неаккуратные программы только на больших объемах данных, то мы вынуждены вас огорчить. Рассмотрим интересный код для работы с массивом, содержащим всего 5 элементов. Второй пример работоспособен в 32-битном варианте и не работоспособен в 64-битном:

Код: (C++)
int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B); //Invalid pointer value on 64-bit platform
printf("%i\n", *ptr); //Access violation on 64-bit platform

Давайте проследим, как происходит вычисление выражения "ptr + (A + B)":
  • Согласно правилам языка Си++, переменная A типа int приводится к типу unsigned.
  • Происходит сложение A и B. В результате мы получаем значение 0xFFFFFFFF типа unsigned.
Затем происходит вычисление выражения "ptr + 0xFFFFFFFFu", но что из этого выйдет, будет зависеть от размера указателя на данной архитектуре. Если сложение будет происходить в 32-битной программе, то данное выражение будет эквивалентно "ptr - 1", и мы успешно распечатаем число 3.
В 64-битной программе к указателю честным образом прибавится значение 0xFFFFFFFFu, в результате чего указатель окажется далеко за пределами массива. И при доступе к элементу по данному указателю нас ждут неприятности.
Для предотвращения показанной ситуации, как и в первом случае, рекомендуем использовать в арифметике с указателями только memsize-типы. Два варианта исправления кода:

Код: (C++)
ptr = ptr + (ptrdiff_t(A) + ptrdiff_t(B));

ptrdiff_t A = -2;
size_t B = 1;
...
ptr = ptr + (A + B);

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

Код: (C++)
int A = -2;
int B = 1;
...
ptr = ptr + (A + B);

Да, такой код будет работать, но он плох по ряду причин:
  • Он будет приучать к неаккуратной работе с указателями. Через некоторое время вы можете забыть нюансы и по ошибке вновь сделать одну из переменных типа unsigned.
  • Использование не memsize-типов совместно с указателями потенциально опасно. Допустим, что в выражении с указателем участвует переменная Delta типа int. И это выражение совершенно корректно. Но ошибка может укрыться в вычислении самой переменной Delta, так как 32-бит может не хватить для необходимых вычислений при работе с большими массивами данных. Использование memsize-типа для переменной Delta автоматически устраняет такую опасность.

13. Индексация массивов.

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

Код: (C++)
unsigned Index = 0;
while (MyBigNumberField[Index] != id)
  Index++;

Но время идет, и все меняется. И вот теперь пришло время сказать: "Больше так не делайте! Используйте для индексации (больших) массивов только memsize-типы."
Приведенный код не сможет обработать в 64-битной программе массив, содержащий более UINT_MAX элементов. После доступа к элементу с индексом UINT_MAX произойдет переполнение переменной Index, и мы получим вечный цикл.
Чтобы окончательно убедить вас в необходимости использования только memsize-типов для индексации и в выражениях адресной арифметики, приведем последний пример.

Код: (C++)
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, обеспечивающие доступ к необходимым элементам. Но приведенный код будет корректно работать только с массивами, содержащими менее INT_MAX элементов. Причина - использование 32-битных типов int для вычисления индекса элемента.
Программисты часто допускают ошибку, пытаясь исправить код следующим образом:

Код: (C++)
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.
Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы можете явно привести каждую переменную к memsize-типу:

Код: (C++)
float Region::GetCell(int x, int y, int z) const {
  return array[ptrdiff_t(x) +
               ptrdiff_t(y) * ptrdiff_t(Width) +
               ptrdiff_t(z) * ptrdiff_t(Width) *
               ptrdiff_t(Height)];
}

Другое решение - изменить типы переменных на memsize-тип:

Код: (C++)
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];
}

14. Смешанное использование простых целочисленных типов и memsize-типов.

Смешанное использование memsize- и не memsize-типов в выражениях может приводить к некорректным результатам на 64-битных системах и быть связано с изменением диапазона входных значений. Рассмотрим ряд примеров:

Код: (C++)
size_t Count = BigValue;
for (unsigned Index = 0; Index != Count; ++Index)
{ ... }  

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

Код: (C++)
int x, y, z;
intptr_t SizeValue = x * y * z;

Ранее уже рассматривались подобные примеры, когда при вычислении значений с использованием не memsize-типов происходило арифметическое переполнение. И конечный результат был некорректен. Поиск и исправление приведенного кода осложняется тем, что компиляторы, как правило, не выдают на него никаких предупреждений. С точки зрения языка Си++, это совершенно корректная конструкция. Происходит умножение нескольких переменных типа int, после чего результат неявно расширяется до типа intptr_t и происходит присваивание.
Приведем небольшой код, показывающий опасность неаккуратных выражений со смешанными типами (результаты получены с использованием Microsoft Visual C++ 2005, 64-битный режим компиляции):

Код: (C++)
int x = 100000;
int y = 100000;
int z = 100000;
intptr_t size = 1;                  // Result:
intptr_t v1 = x * y * z;            // -1530494976
intptr_t v2 = intptr_t(x) * y * z;  // 1000000000000000
intptr_t v3 = x * y * intptr_t(z);  // 141006540800000
intptr_t v4 = size * x * y * z;     // 1000000000000000
intptr_t v5 = x * y * z * size;     // -1530494976
intptr_t v6 = size * (x * y * z);   // -1530494976
intptr_t v7 = size * (x * y) * z;   // 141006540800000
intptr_t v8 = ((size * x) * y) * z; // 1000000000000000
intptr_t v9 = size * (x * (y * z)); // -1530494976

Необходимо, чтобы все операнды в подобных выражениях были заранее приведены к типу большей разрядности. Помните, что выражение вида
Код: (C++)
intptr_t v2 = intptr_t(x) * y * z;
вовсе не гарантирует правильный результат. Оно гарантирует только то, что выражение "intptr_t(x) * y * z" будет иметь тип intptr_t. Правильный результат, показанный этим выражением в примере, не более чем везение, обусловленное конкретной версией компилятора и фазой Луны.

Порядок вычисления выражения с операторами одинакового приоритета не определен. Точнее, компилятор волен вычислять подвыражения в том порядке, который он считает более эффективным, даже если подвыражения вызывают побочные эффекты. Порядок возникновения побочных эффектов не определен. Выражения, включающие в себя коммутативные и ассоциативные операции (*, +, &, |, ^), могут быть реорганизованы произвольным образом даже при наличии скобок. Для задания определенного порядка вычисления выражения необходимо использовать явную временную переменную.

Следовательно, если результатом выражения должен являться memsize-тип, то в выражении должны участвовать только memsize-типы. Или элементы, приведенные к memsize-типам. Правильный вариант:

Код: (C++)
intptr_t v2 = intptr_t(x) * intptr_t(y) * intptr_t(z); // OK!

ПРИМЕЧАНИЕ.
Примечание. Если у вас много целочисленных вычислений и контроль над переполнениями для Вас является важной задачей, то мы предлагаем обратить Ваше внимание на класс SafeInt, реализацию и описание которого можно найти в MSDN.

Смешанное использование типов может проявляться и в изменении программной логики:

Код: (C++)
ptrdiff_t val_1 = -1;
unsigned int val_2 = 1;
if (val_1 > val_2)
  printf ("val_1 is greater than val_2\n");
else
  printf ("val_1 is not greater than val_2\n");

//Output on 32-bit system: "val_1 is greater than val_2"
//Output on 64-bit system: "val_1 is not greater than val_2"

На 32-битной системе переменная val_1, согласно правилам языка Си++, расширялась до типа unsigned int и становилась значением 0xFFFFFFFFu. В результате условие "0xFFFFFFFFu > 1" выполнялось. На 64-битной системе, наоборот, расширяется переменная val_2 до типа ptrdiff_t. В этом случае уже проверяется выражение "-1 > 1". На рисунке 6 схематично отображены происходящие преобразования.





Рисунок 6. Преобразования, происходящие в выражении.

Если вам необходимо вернуть прежнее поведение кода, следует изменить тип переменной val_2:

Код: (C++)
ptrdiff_t val_1 = -1;
size_t val_2 = 1;
if (val_1 > val_2)
  printf ("val_1 is greater than val_2\n");
else
  printf ("val_1 is not greater than val_2\n");

15. Неявные приведения типов при использовании функций.

Рассматривая предыдущий класс ошибок, связанный со смешиванием простых целочисленных типов и memsize-типов, мы рассматривали только простые выражения. Но аналогичные проблемы могут проявиться и при использовании других конструкций языка Си++:

Код: (C++)
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
  return x + y * Width + z * Width * Height;
}
...
MyArray[GetIndex(x, y, z)] = 0.0f;

В случае работы с большими массивами (более INT_MAX элементов) данный код будет вести себя некорректно, и мы будем адресоваться не к тем элементам массива MyArray, к которым рассчитываем. Несмотря на то, что мы возвращаем значение типа size_t, выражение "x + y * Width + z * Width * Height" вычисляется с использованием типа int. Мы думаем, вы уже догадались, что исправленный код будет выглядеть следующим образом:

Код: (C++)
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
  return (size_t)(x) +
         (size_t)(y) * (size_t)(Width) +
         (size_t)(z) * (size_t)(Width) * (size_t)(Height);
}

В следующем примере у нас вновь смешивается memsize-тип (указатель) и простой тип unsigned:

Код: (C++)
extern char *begin, *end;
unsigned GetSize() {
  return end - begin;
}

Результат выражения "end - begin" имеет тип ptrdiff_t. Поскольку функция возвращает тип unsigned, то происходит неявное приведение типа, при котором старшие биты результата теряются. Таким образом, если указатели begin и end ссылаются на начало и конец массива, по размеру большего UINT_MAX (4Gb), то функция вернет некорректное значение.
И еще один пример. На этот раз рассмотрим не возвращаемое значение, а формальный аргумент функции:

Код: (C++)
void foo(ptrdiff_t delta);
int i = -2;
unsigned k = 1;
foo(i + k);

Этот код не напоминает вам пример с некорректной арифметикой указателей, рассмотренный ранее? Да, здесь происходит то же самое. Некорректный результат возникает при неявном расширении фактического аргумента, имеющего значение 0xFFFFFFFF и тип unsigned, до типа ptrdiff_t.

16. Перегруженные функции.

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

Код: (C++)
static size_t GetBitCount(const unsigned __int32 &) {
  return 32;
}
static size_t GetBitCount(const unsigned __int64 &) {
  return 64;
}
size_t a;
size_t bitCount = GetBitCount(a);

Но такое изменение логики хранит в себе опасность. Представьте себе программу, где для каких-то целей используется класс для организации стека. Особенность этого класса в том, что он позволяет хранить значение различных типов:

Код: (C++)
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, то в стек стало попадать больше байт, чем затем извлекаться.
Думаем, что вам понятен данный класс ошибок, и как внимательно следует относиться к вызову перегруженных функций, передавая фактические аргументы типа memsize.

17. Выравнивание данных.

Процессоры работают эффективнее, когда имеют дело с правильно выровненными данными. Как правило, 32-битный элемент данных должен быть выровнен по границе, кратной 4 байт, а 64-битный элемент - по границе 8 байт. Попытка работать с не выровненными данными на процессорах IA-64 (Itanium), как показано в следующем примере, приведет к возникновению исключения:

Код: (C++)
#pragma pack (1) // Also set by key /Zp in MSVC
struct AlignSample {
  unsigned size;
  void *pointer;
} object;
void foo(void *p) {
  object.pointer = p; // Alignment fault
}

Если вы вынуждены работать с невыровненными данными на Itanium, то следует явно указать это компилятору. Например, воспользоваться специальным макросом UNALIGNED:

Код: (C++)
#pragma pack (1) // Also set by key /Zp in MSVC
struct AlignSample {
  unsigned size;
  void *pointer;
} object;
void foo(void *p) {
  *(UNALIGNED void *)&object.pointer = p; //Very slow
}

Такое решение неэффективно, так как доступ к невыровненным данным будет происходить в несколько раз медленнее. Лучшего результата можно достичь, располагая в 64-битные элементы данных до 32,16 и 8-битных элементов.
На архитектуре x64 при обращении к невыровненным данным исключения не возникает, но их также следует избегать. Во-первых, из-за существенного замедления скорости доступа к таким данным, а во-вторых, из-за высокой вероятности переноса программы в будущем на платформу IA-64.
Рассмотрим еще один пример кода, не учитывающий выравнивание данных:

Код: (C++)
struct MyPointersArray {
  DWORD m_n;
  PVOID m_arr[1];
} object;
...
malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );
...

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



Рисунок 7. Выравнивание данных в памяти на системах Win32 и Win64.

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

Код: (C++)
struct MyPointersArray {
  DWORD m_n;
  PVOID m_arr[1];
} object;
...
malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) +
        5 * sizeof(PVOID) );
...

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

Код: (C++)
struct TFoo {
  DWORD_PTR whatever;
  int value;
} object;
int *valuePtr =
  (int *)((size_t)(&object) + offsetof(TFoo, value)); // OK

18. Исключения.

Генерирование и обработка исключений с участием целочисленных типов не является хорошей практикой программирования на языке Си++. Для этих целей следует использовать более информативные типы, например, классы, производные от классов std::exception. Но иногда все-таки приходится работать с менее качественным кодом, таким, как показано ниже:

Код: (C++)
char *ptr1;
char *ptr2;
try {
  try {
    throw ptr2 - ptr1;
  }
  catch (int) {
    std::cout << "catch 1: on x86" << std::endl;
  }
}
catch (ptrdiff_t) {
  std::cout << "catch 2: on x64" << std::endl;
}

Следует тщательно избегать генерирования или обработки исключений с использованием memsize-типов, так как это чревато изменением логики работы программы. Исправление данного кода может заключаться в замене "catch (int)" на "catch (ptrdiff_t)". А более правильным решением будет использование специального класса для передачи информации о возникшей ошибке.

19. Использование устаревших функций и предопределенных констант.

Разрабатывая 64-битное приложение, помните об изменениях среды, в которой оно теперь будет выполняться. Часть функций станут устаревшими, их будет необходимо изменить на обновленные варианты. Примером такой функции в ОС Windows будет GetWindowLong. Обратите внимание на константы, относящиеся к взаимодействию со средой, в которой выполняется программа. В Windows подозрительными будут являться строки, содержащие "system32" или "Program Files".

20. Явные приведения типов.

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

Диагностика ошибок.

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

Юнит-тестирование.

Юнит-тестирование (англ. unit test) давно завоевало заслуженное уважение среди программистов. Юнит-тесты помогут проверить корректность программы после переноса на новую платформу. Но тут есть одна тонкость, о которой вы должны помнить.
Юнит-тестирование может не позволить вам проверить новые диапазоны входных значений, которые становятся доступны на 64-битных системах. Юнит-тесты классически разрабатываются таким образом, чтобы по возможности проходить за минимальное время. И та функция, которая обычно работает с массивом размером в десятки мегабайт, в юнит-тестах, скорее всего, будет обрабатывать десятки килобайт. Это обоснованно, так как эта функция в тестах может вызваться много раз с различными наборами входных значений. Но вот перед нами 64-битный вариант программы. И рассматриваемая функция теперь обрабатывает уже более 4 гигабайт данных. Соответственно, возникает необходимость увеличения входного размера массива и в тестах до размеров более 4 гигабайт. Проблема в том, что время прохождения тестов в таком случае увеличится на несколько порядков.
Поэтому, модифицируя наборы тестов, помните о компромиссе между скоростью выполнения юнит-тестов и полнотой проверок. К счастью, убедиться в работоспособности ваших приложений могут помочь другие методики.

Просмотр кода.

Просмотр кода (англ. code review) - самая лучшая методика поиска ошибок и улучшения кода. Совместный тщательный просмотр кода может полностью избавить программу от ошибок, связанных с особенностями разработки 64-битных приложений. Естественно, сначала следует узнать, какие именно ошибки следует искать, иначе просмотр может не дать положительных результатов. Для этого необходимо заранее ознакомиться с этой и другими статьями, посвященными переносу программ с 32-битных систем на 64-битные. Ряд интересных ссылок по данной тематике вы можете найти в конце статьи.
Но у этого подхода к анализу исходного кода есть один существенный недостаток. Он требует очень большого количества времени, из-за чего практически неприменим на больших проектах.
Компромиссом является использование статических анализаторов. Статический анализатор можно рассматривать как автоматизированную систему просмотра кода, где для программиста создается выборка потенциально опасных мест для проведения им дальнейшего анализа.
Но в любом случае, желательно провести несколько просмотров кода с целью совместного обучения команды поиску новых разновидностей ошибок, проявляющих себя на 64-битных системах.

Встроенные средства компиляторов.

Часть задач с поиском дефектного кода позволяют решать компиляторы. В них часто бывают встроены различные механизмы для диагностики рассматриваемых нами ошибок. Например, в Microsoft Visual C++ 2005 вам могут быть полезны следующие ключи: /Wp64, /Wall, а в SunStudio C++ ключ -xport64.
К сожалению, предоставляемые ими возможности часто недостаточны, и не стоит полагаться только на них. Но в любом случае, крайне рекомендуется включить соответствующие опции компилятора для диагностики ошибок в 64-битном коде.

Статические анализаторы.

Статические анализаторы - прекрасное средство повышения качества и надежности программного кода. Основная сложность, связанная с использованием статических анализаторов, заключается в том, что они генерируют довольно много ложных сообщений о потенциальных ошибках. Программисты, будучи по натуре ленивыми, используют этот аргумент, чтобы так или иначе не заниматься исправлением найденных ошибок. В Microsoft эта проблема решается безусловным внесением обнаруженных ошибок в bug tracking систему. Тем самым у программиста не остается выбора между исправлением кода и попытками избежать этого.
Мы считаем, что такие жесткие правила оправданы. Выигрыш от качественного кода существенно покрывает издержки времени на статический анализ и соответствующую модификацию кода. Выигрыш достигается за счет облегчения поддержки кода и уменьшения сроков отладки и тестирования.
Статические анализаторы могут с успехом использоваться для диагностики многих из рассмотренных в статье классов ошибок.
Авторам известны 3 статических анализатора, которые заявляют о наличии средств диагностирования ошибок, связанных с переносом программ на 64-битные системы. Хотим сразу предупредить, что мы можем заблуждаться по поводу возможностей, которыми они обладают, тем более что это развивающиеся продукты и новые версии могут иметь большую функциональность.
  • Gimpel Software PC-Lint (http://www.gimpel.com). Данный анализатор обладает широким списком поддерживаемых платформ и является статическим анализатором общего назначения. Он позволяет выявлять ошибки при переносе программ на архитектуру с моделью данных LP64. Преимуществом является возможность построения жесткого контроля над преобразованиями типов. К недостаткам можно отнести отсутствие среды, но это можно исправить, используя стороннюю оболочку  Riverblade Visual Lint.
  • Parasoft C++test (http://www.parasoft.com/). Другой известный статический анализатор общего назначения. Также существует под большое количество аппаратных и программных платформ. Имеет встроенную среду, существенно облегчающую работу и настройку правил анализа. Как и PC-Lint, он рассчитан на модель данных LP64.
  • Viva64 (http://www.viva64.com). В отличие от других анализаторов, рассчитан на модель данных Windows (LLP64). Интегрируется в среду разработки Visual Studio 2005. Предназначен только для диагностики проблем, связанных с переносом программ на 64-битные системы, что существенно упрощает его настройку.

Заключение.

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

Библиографический список.

Версия для печати
Обсудить на форуме