Прогресс идет семимильными шагами. Технологии развиваются, и вместе с ними меняется наша жизнь. Еще совсем недавно 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

Жизнь без антивируса. Как побороть малварь голыми руками и обезопасить себя на будущее

На вопрос «Какой антивирус вы используете на своей виндовой машине?» многие безопасники (в…