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

Для начала давай определимся, что же такое эта загадочная 64-битность. Существуют две наиболее известные 64-битные архитектуры – это IA64 и Intel 64 (или AMD64/x86-64/x64). Первая является совместной разработкой Intel и Hewlett Packard и реализована в микропроцессорах Itanium и Itanium 2. Вторая же представляет собой расширение архитектуры x86 с полной обратной совместимостью. Благодаря этой совместимости x86-64 пользуется большей популярностью, чем IA64. Основными достоинствами x64 является 64-битное адресное пространство, расширенный набор регистров и набор команд, привычный для разработчиков, а также возможность запуска 32-битных ОС и приложений.

Практически все современные ОС поддерживают 64 бита, а Microsoft проводила такие эксперименты еще в годы расцвета Windows XP. Благодаря специальному режиму WoW64 (Windows-on-Windows 64), который транслирует вызовы 32-битных приложений к ресурсам 64-битной операционной системы, в винде могут корректно работать как 64-битные приложения, так и приложения, заточенные под архитектуру x86.

Стоит немного рассказать и об адресном пространстве в Intel 64, поскольку именно с ним связанно большинство типичных ошибок при переходе от 32-битного программирования к 64-битному. Процессоры x64 теоретически могут адресовать до 16 экзобайт памяти, но на практике это число оказывается гораздо меньше – 16 терабайт. Более того, в связи с маркетинговыми соображениями, максимальный предел оперативки изменяется в зависимости от версии винды. Так, например Windows 7 Home Basic может адресовать до 8 Гб памяти, а Windows 7 Ultimate – до 192 Гб.

Теперь, когда мы имеем определенное представление о 64-битности, пора рассмотреть основные ошибки, совершаемые неопытными в x64-программировании кодерами.

 

Виртуальные функции

Пусть у нас есть код, который разрабатывался в Visual Studio 6. В этом коде мы определили класс CSampleApp, который наследуется от CWinApp. В дочернем классе мы перекрываем функцию WinHelp, которая определена как виртуальная. Выглядит это примерно так:

Виртуальная функция WinHelp

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

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

Затем проект перекочевывает в Visual Studio 2005, где прототип функции WinHelp в базовом классе CWinApp изменился. Первый параметр метода теперь имеет тип DWORD_PTR, меж тем как в дочернем классе он так и остался DWORD.

Проблема с прототипами функции

class CWinApp
{
// тип первого параметра изменился
virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
}

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

Этот код будет прекрасно работать, будучи скомпилированным под 32-архитектуру, но если мы попробуем собрать программу для x64-системы, то получим странный эффект – две разные функции WinHelp. Связано это с тем, что в 64-битном кодинге тип DWORD не равен типу DWORD_PTR, и компилятор в дочернем классе генерирует две копии одной и той же функции, а не перекрывает ее, как ожидается.

Такой тип ошибок нельзя предвидеть заранее, ведь подобное недоразумение может случиться не только при использовании классов MFC, но и вообще с любым кодом. Единственный способ избежать неприятностей – это внимательно анализировать код.

 

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

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

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

static void NumOfBits(const unsigned __int32 &)
{
printf(“32-битный параметр”);
}
static void NumOfBits(const unsigned __int64 &)
{
printf(“64-битный параметр”);
}

Код, собранный для x86 процессора, будет вызывать первую версию функции, а для x64 – вторую. В некоторых ситуациях такое поведение может быть очень удобным. Но тут есть и свои опасности.

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

Класс стека и его неправильное использование

class MyStack {
...
public:
void Push(__int32 &);
void Push(__int64 &);
void Pop(__int32 &);
void Pop(__int64 &);
}

MyStack stack;
// в x64 системе кладем в стек 8 байт
ptrdiff_t value1;
stack.Push(value1);

// а достаем 4!!!
int value2;
stack.Pop(value2);

Тут мы кладем в стек значение типа ptrdiff_t, а достаем уже int. Под x86 величина этих типов одинакова, но в архитектуре Intel 64 ptrdiff_t ровно в два раза больше старого-доброго int. Таким образом, мало того, что мы будем получать из стека неправильные значения, так еще и организуем себе утечки памяти. Поэтому надо внимательнее относиться к перегруженным функциям, передавая им аргументы правильных типов.

 

Константы

За много лет программирования под 32-битные платформы кодеры привыкли использовать константы, которые участвуют в операциях вычисления адреса, размера объектов или в битовых операциях. Эти константы могут сильно усложнить жизнь при переносе проекта на x64-систему. Вот несколько примеров, которые прекрасно работают в x86-операционках, но приведут к плачевным результатам на 64-битной машине.

Константы, которые ломают 64-битные программы

// Пример 1
size_t ArraySize = N * 4;
intptr_t *Array = (intptr_t *)malloc(ArraySize);

// Пример 2
size_t values[ARRAY_SIZE];
memset(values, 0, ARRAY_SIZE * 4);

// Пример 3
size_t n, r;
n = n >> (32 - r);

Код первого примера выделяет память под массив указателей, считая, что размер каждого элемента массива равен 4 байтам. Все будет прекрасно работать под 32 битами, но вот в Intel 64 мы получим «out of memory». Во втором примере мы тоже считаем размер size_t равным 4 байтам, но размерность этого типа зависит от количества бит в системе. Ну, а третий пример демонстрирует ошибку, связанную с числом бит, содержащихся в size_t.

Исправить эти баги можно, внимательно проанализировав код и заменив подобные константы на sizeof() и специальные значения из <limits.h>, <inttypes.h> и т.д.

Исправляем ошибки с размерностью

// Пример 1
size_t ArraySize = N * sizeof(intptr_t);
intptr_t *Array = (intptr_t *)malloc(ArraySize);

// Пример 2
size_t values[ARRAY_SIZE];
memset(values, 0, ARRAY_SIZE * sizeof(size_t));

// Пример 3
size_t n, r;
n = n >> (CHAR_BIT * sizeof(n) - r);

Еще один способ потратить долгие часы на отладку кода в 64-битных системах – это использовать константу вида const size_t M = 0xFFFFFFF0u. Обычный 32-битный кодер, записывая такое выражение, предполагает, что создает константу, все биты которой, кроме четырех последних, равны единице. К сожалению, под x64 значение M будет совсем другое – 0x00000000FFFFFFF0u. Исправить это можно, используя либо #ifdef, либо специальный макрос.

Решаем проблему с константой 0xFFFFFFF0u

#ifdef _WIN64
#define CONST3264(a) (a##i64)
#else
#define CONST3264(a) (a)
#endif

const size_t M = ~CONST3264(0xFu);

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

Возвращаем код ошибки

#define INVALID_RESULT (0xFFFFFFFFu)
size_t UserStrLen(const char *str)
{
if (str == NULL)
return INVALID_RESULT;
...
return n;
}

size_t len = UserStrLen(str);

// в 64-битной системе обработка ошибки никогда не будет выполнена
// даже если ошибка действительно имела место
if (len == (size_t)(-1))
// обработка ошибки

Тело if выполнится только в 32-битной среде, а в x64 сравнение в условии оператора будет ложно. Если архитектура приложения не позволяет отказаться от использования кодов ошибок, то корректная запись INVALID_RESULT, которая будет правильно работать и в 32-, и в 64-битном окружениях, будет выглядеть примерно так: #define INVALID_RESULT (size_t(-1)).

 

Заключение

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

 

Тип ptrdiff_t

ptrdiff_t – базовый знаковый целочисленный тип языка C/C++. Размер типа выбирается таким образом, что на 32-битной системе он равен 4 байтам, а на 64-битной – 8. То есть, в переменную этого типа может быть безопасно помещен практически любой указатель. ptrdiff_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики.

Оставить мнение

Check Also

LUKS container vs Border Patrol Agent. Как уберечь свои данные, пересекая границу

Не секрет, что если ты собрался посетить такие страны как США или Великобританию то, прежд…