Семь шагов переноса программы в 64-битную систему

ОГЛАВЛЕНИЕ

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

Оглавление

Аннотация
Введение
1. Разбор разных 64-битных режимов
2. Выяснить, нужна ли 64-битная версия продукта

2.1. Длительность жизненного цикла приложений
2.2. Ресурсоемкость приложения
2.3. Разработка библиотек
2.4. Зависимость продукта от сторонних библиотек
2.5. Использование 16-битных приложений
2.6. Код на ассемблере

3. Инструментарий

3.1. 64-битный компилятор
3.2. 64-битные компьютеры под управлением 64-битных операционных систем
3.3. 64-битные версии всех используемых библиотек
3.4. Отсутствие встроенного кода на ассемблере
3.5. Обновление методики тестирования
3.6. Новые данные для тестирования
3.7. 64-битные системы защиты
3.8. Установочный пакет

4. Настройка проекта в Visual Studio 2005/2008
5. Компиляция приложения
6. Выявление скрытых ошибок

6.1. Явное преобразование типа
6.2. Неявное преобразование типа
6.3. Биты и сдвиги
6.4. Магические числа
6.5. Ошибки, касающиеся использования 32-битных переменных в качестве индексов
6.6. Ошибки, касающиеся изменения типов используемых функций
6.7. Выявление скрытых ошибок

7. Обновление процесса тестирования

Аннотация

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

Введение

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

1. Разбор разных 64-битных режимов

В рамках архитектуры компьютера под термином "64-битный" понимаются 64-битные целые числа и иные типы данных размером 64 бит. Под "64-битными" системами могут пониматься 64-битные архитектуры микропроцессора (например, EM64T, IA-64) или 64-битная операционная система (например, Windows XP x64 профессиональная версия).

AMD64 (или x86-64, Intel 64, EM64T, x64) - 64-битная архитектура микропроцессора и соответствующая система команд, разработанные компанией AMD. Эта система команд была лицензирована компанией Intel под именем EM64T (Intel64). Архитектура AMD64 является расширением архитектуры x86 с полной обратной совместимостью. Архитектура стала распространенной в качестве основного компонента персональных компьютеров и рабочих станций.

IA-64 - 64- битная архитектура микропроцессора, разработанная совместно компаниями Intel и Hewlett Packard. Она реализована в микропроцессорах Itanium и Itanium 2. Архитектура в основном используется в многопроцессорных серверах и кластерных системах.

AMD64 и IA-64 – две разные 64-битные архитектуры, несовместимые друг с другом. Поэтому разработчики должны сразу решить, нужна ли им поддержка обеих архитектур или лишь одной из них. Большей частью, если вы не разрабатываете узкоспециализированное программное обеспечение (ПО) для кластерных систем или не реализуете свою собственную высокопроизводительную СУБД, вам понадобиться реализовать поддержку только архитектуры AMD64, гораздо более популярной, чем IA-64. Это особенно касается ПО для рынка ПК, почти на 100% занятого архитектурой AMD64.
Далее в статье рассматривается только архитектура AMD64 (EM64T, x64), поскольку сейчас она наиболее актуальна для разработчиков прикладного ПО.

Говоря о разных архитектурах, надо упомянуть понятие "модель данных". Под моделью данных понимаются соответствия между размерами типов, принятых внутри среды разработки. Может иметься несколько средств разработки, придерживающихся разных типов данных для одной операционной системы. Но обычно преобладает только одна модель, максимально соответствующая аппаратно-программной среде. Таким примером является 64-битная Windows, исходная модель данных которой - LLP64. Но ради совместимости 64-битная Windows поддерживает выполнение 32-битных программ, работающих в режиме модели данных ILP32LL. Таблица 1 дает сведения об основных моделях данных.

Таблица 1. Модели данных.

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


2. Нужна ли 64-битная версия продукта?

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

Ниже перечислены основные факторы, которые помогут принять решение.

2.1. Длительность жизненного цикла приложений

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

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

2.2. Ресурсоемкость приложения

Перекомпиляция программы для 64-битной системы позволяет ей использовать оперативную память большего объема и ускоряет ее работу на 5-15%. Увеличение на 5-10% будет получено из-за использования возможностей 64-битной архитектуры процессора, например, большего количества регистров. Остальное увеличение скорости на 1-5% объясняется отсутствием уровня WOW64, передающего вызовы API между 32-битными приложениями и 64-битной операционной системой.

Если ваша программа не работает с большими объемами данных (более 2Гб) и скорость ее работы не критична, перенос в 64-битную систему не срочен в ближайшее время.

Кстати, даже простые 32-битные приложения могут получить преимущество при запуске в 64-битной среде. Программа, собранная с ключом /LARGEADDRESSAWARE:YES, может выделять до 3 Гб памяти, если 32-битная Windows запущена с ключом /3gb. Эта же 32-битная программа, запущенная в 64-битной системе, может выделить около 4 Гб памяти (на практике около 3.5 Гб).

2.3. Разработка библиотек

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

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

2.4. Зависимость продукта от сторонних библиотек

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

2.5. Использование 16-битных приложений

Если ваши решения все еще используют 16-битные единицы, пора избавиться от них. 16-битные приложения не поддерживаются в 64-битных версиях Windows.

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

2.6. Код на ассемблере

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

Обдумав все перечисленные факторы и взвесив все плюсы и минусы, решите, надо ли вам переносить свой проект на 64-битные системы. Если да, идем дальше.


3. Инструментарий

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

Отсутствие 64-битного компилятора является простейшей, но самой непреодолимой проблемой. Статья пишется в 2009, когда по-прежнему нет 64-битного компилятора C++ Builder от Codegear. Его выпуск ожидается к концу текущего года. Эту проблему невозможно обойти, только если переписать весь проект с использованием, например, Visual Studio. Но если все ясно с отсутствием 64-битного компилятора, другие подобные проблемы могут быть менее явными и возникать лишь на этапе переноса проекта на новую архитектуру. Оттого следует заранее выяснить, имеются ли все нужные компоненты, требуемые для реализации 64-битной версии вашего продукта, иначе  вы можете столкнуться с неприятными неожиданностями.

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

3.1. 64-битный компилятор

Иметь 64-битный компилятор надо обязательно.

Если вы планируете разрабатывать 64-битные приложения с использованием последней (на момент написания статьи) версии Visual Studio 2008, следующая таблица 2 поможет понять, какой из выпусков Visual Studio вам нужен.

Таблица 2. Возможности разных выпусков Visual Studio 2008.

3.2. 64-битные компьютеры под управлением 64-битных операционных систем

Можно использовать виртуальные машины для запуска 64-битных приложений на 32-битных компьютерах, но это крайне неудобно и не обеспечивает необходимый уровень тестирования. Желательно, чтобы машины имели не менее 4-8 Гб оперативной памяти.

3.3. 64-битные версии всех используемых библиотек

Если библиотеки представлены в исходных кодах, то должна иметься 64-битная конфигурация проекта. Самостоятельно обновление библиотеки для 64-битной системы являются трудной задачей, и результат может быть ненадежным и содержать ошибки. Также вы можете нарушить лицензионные соглашения такими действиями. Если вы используете библиотеки в виде двоичных единиц, то вы также должны выяснить, существуют ли 64-битные единицы. Нельзя использовать 32-битную DLL (динамически подключаемая библиотека) в 64-битном приложении. Можно создать специальную связь с помощью COM (компонентная модель объектов), но это будет отдельной большой и тяжелой задачей. Так вам может понадобиться потратить лишние деньги на покупку 64-битной версии библиотеки.

3.4. Отсутствие встроенного кода на ассемблере

Visual C++ не поддерживает 64-битный встроенный ассемблер. Вы должны использовать внешний 64-битный ассемблер (например, MASM) или иметь реализацию с такой же функциональностью на C/C++.

3.5. Обновление методики тестирования

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

3.6. Новые данные для тестирования

Если вы разрабатываете ресурсоемкие приложения, использующие большие объемы оперативной памяти, вы должны пополнить базу входных данных для испытаний. При нагрузочном тестировании 64-битных приложений желательно превышать пределы 4 Гб используемой памяти. Многие ошибки происходят именно( лишь) в таких условиях.

3.7. 64-битные системы защиты

Используемая система защиты должна полностью поддерживать 64-битные системы. Например, компания Aladdin довольно быстро выпустила 64-битные драйверы для поддержки аппаратных ключей Hasp. Но долго не было никакой системы автоматической защиты 64-битных двоичных файлов (программа обертка Hasp). Следовательно, механизм защиты приходилось реализовывать вручную в программном коде, что было трудной задачей, требующей профессионализма и времени. Не забывайте о таких вещах, касающихся защиты, системы обновления и т.д.

3.8. Установочный пакет

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


4. Настройка проекта в Visual Studio 2005/2008

Создание 64-битной конфигурации проекта в Visual Studio 2005/2008 выглядит весьма просто. Трудности начинаются на этапе сборки новой конфигурации и поиске ошибок в ней. Для создания 64-битной конфигурации надо выполнить следующие 4 шага:

Запустите диспетчер конфигураций, как показано на рисунке 1:

Рисунок 1. Запуск диспетчера конфигураций

В диспетчере конфигураций выберите поддержку новой платформы (рисунок 2):

Рисунок 2. Создание новой конфигурации

Выберите 64-битную платформу (x64) и в качестве основы – настройки из 32-битной версии (рисунок 3). Настройки, влияющие на режим сборки, будут автоматически исправлены Visual Studio.

 

Рисунок 3. Выберите x64 в качестве платформы и используйте конфигурацию Win32 в качестве основы

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

Рисунок 4. Теперь доступны 32-битная и 64-битная конфигурации

Если вам повезет, то вам не понадобится дополнительно настраивать 64-битный проект. Но это сильно зависит от проекта, его сложности и числа используемых библиотек. Вы должны изменить лишь размер стека. Если размер стека в вашем проекте задан по умолчанию, то есть 1 Мб, вы должны определить его как 2 Мб для 64-битной версии. Это не обязательно, но лучше подстраховаться заранее. Если вы используете размер, отличный от стандартного, то следует увеличить его вдвое для 64-битной версии. Чтобы сделать это, найдите и измените параметры «Размер резерва стека» и «Размер фиксации стека» в настройках проекта.


5. Компиляция приложения

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

Следующий список ссылок на ресурсы, посвященные разработке 64-битных приложений, может отчасти помочь вам: http://www.viva64.com/links/64-bit-development/. Список непрерывно дополняется, и автор будет рад, если читатели отправят ему ссылки на достойные внимания ресурсы.

Ниже описаны типы, представляющие интерес для разработчиков при переносе приложений. Эти типы показаны в таблице 3. Большинство ошибок перекомпиляции связано с использованием данных типов.

Тип

Размер типа на платформе x32 / x64

Примечание

int

32 / 32

Базовыйтип. В 64-битных системах остается 32-битным.

long

32 / 32

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

size_t

32 / 64

Базовыйбеззнаковыйтип. Размер типа выбирается так, чтобы в него можно было записать массив теоретически возможного максимального размера. Можно безопасно поместить указатель в тип size_t (исключая указатели на функции класса, являющиеся особым случаем).

ptrdiff_t

32 / 64

Знаковый тип, похожий на size_t. Результат выражения, в котором один указатель вычитается из другого (ptr1-ptr2), имеет тип ptrdiff_t.

Указатель

32 / 64

Размер указателя прямо зависит от размера платформы. Будьте внимательны при преобразовании указателей в другие типы.

__int64

64 / 64

Знаковый 64-битный тип.

DWORD

32 / 32

32-битныйбеззнаковыйтип. В WinDef.h определенкак:typedef unsigned(беззнаковый) long(длинный) DWORD;

DWORDLONG

64 / 64

64-битныйбеззнаковыйтип. В WinNT.h определенкак:typedef ULONGLONG DWORDLONG;

DWORD_PTR

32 / 64

Беззнаковый тип, в который можно поместить указатель. В BaseTsd.hопределен как: typedefULONG_PTRDWORD_PTR;

DWORD32

32 / 32

32-битныйбеззнаковыйтип. В BaseTsd.h определенкак:typedef unsigned int(целый) DWORD32;

DWORD64

64 / 64

64-битныйбеззнаковыйтип. В BaseTsd.h определенкак:typedef unsigned __int64 DWORD64;

HALF_PTR

16 / 32

Половинауказателя. В Basetsd.h определенкак:#ifdef _WIN64 typedef int HALF_PTR;#else typedef short HALF_PTR;#endif

INT_PTR

32 / 64

Знаковый тип, в который можно поместить указатель. В BaseTsd.h определенкак:#if defined(_WIN64) typedef __int64 INT_PTR; #else typedef int INT_PTR;#endif

LONG

32 / 32

Знаковыйтип, оставшийся 32-битным. Оттого теперь зачастую должен использоваться LONG_PTR. В WinNT.h определен как:typedef long LONG;

LONG_PTR

32 / 64

Знаковый тип, в который можно поместить указатель. В BaseTsd.h определенкак:#if defined(_WIN64) typedef __int64 LONG_PTR; #else typedef long LONG_PTR;#endif

LPARAM

32 / 64

Параметрдляотправкисообщений. В WinNT.h определенкак:typedef LONG_PTR LPARAM;

SIZE_T

32 / 64

Аналогтипа size_t. В BaseTsd.h определенкак:typedef ULONG_PTR SIZE_T;

SSIZE_T

32 / 64

Аналогтипа ptrdiff_t. В BaseTsd.h определенкак: typedef LONG_PTR SSIZE_T;

ULONG_PTR

32 / 64

Беззнаковый тип, в который можно поместить указатель. В BaseTsd.h определенкак:#if defined(_WIN64) typedef unsigned __int64 ULONG_PTR;#else typedef unsigned long ULONG_PTR;#endif

WORD

16 / 16

Беззнаковый 16-битныйтип. In WinDef.h определенкак: typedef unsigned short(короткий) WORD;

WPARAM

32 / 64

Параметрдляотправкисообщений. В WinDef.h определенкак: typedef UINT_PTR WPARAM;

Таблица 3. Типы, на которые надо обращать внимание при переносе 32-битных программ на 64-битные системы Windows.


6. Выявление скрытых ошибок

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

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

Рассмотрим несколько примеров скрытых ошибок.

6.1. Явное преобразование типа

Простейший, но отнюдь не самый легкий для выявления класс ошибок связан с явными преобразованиями типов, когда обрезаются значимые биты. Известный пример – преобразование указателей в 32-битные типы при передаче их в такие функции, как SendMessage(послать сообщение):

MyObj* pObj = ...
::SendMessage(hwnd, msg, (WORD)x, (DWORD)pObj);

Здесь явное преобразование типа используется для преобразования указателя в числовой тип. Для 32-битной архитектуры этот пример правилен, так как последний параметр функции SendMessage имеет тип LPARAM, совпадающий с DWORD в 32-битной архитектуре. Для 64-битной архитектуры DWORD неправилен и должен быть заменен на LPARAM. Тип LPARAM имеет размеры 32 или 64 бита в зависимости от архитектуры.

Это простой случай, но преобразование типа часто кажется более сложным, и его нельзя обнаружить с помощью предупреждений компилятора или поиска по тексту программы. Явные преобразования типов подавляют выявление ошибок компилятором, так как они предназначены для этой самой цели – сообщать компилятору, что преобразование типа верное, и что программист отвечает за безопасность кода. Явный поиск также не поможет. Типы могут иметь нестандартные имена (определенные программистом с помощью typedef), и велико количество методов, выполняющих явное преобразование типов. Для надежного обнаружения таких ошибок используйте только специальный инструментарий, такой как анализаторы Viva64 или PC-Lint.

6.2. Неявное преобразование типа

Следующий пример касается неявного преобразования типа, когда значимые биты также теряются. Код функции fread производит чтение из файла, но работает неверно, когда пытается считать более 2 ГБ в 64-битной системе.

size_t __fread(void * __restrict buf, 
    size_t size, size_t count, FILE * __restrict fp);

size_t fread(void * __restrict buf, size_t size, size_t count,
    FILE * __restrict fp)
{
    int ret;
    FLOCKFILE(fp);
    ret = __fread(buf, size, count, fp);
    FUNLOCKFILE(fp);
    return (ret);
}

Функция __fread возвращает тип size_t, но тип int используется для хранения количества считанных байтов. В результате при больших размерах считанных данных функция может вернуть неверное количество байтов.

Можно сказать, что это безграмотный код для начинающих, что компилятор сообщил о таком преобразовании типа, и что на самом деле данный код легко найти и исправить. Это в теории. На практике в крупных проектах все иначе. Этот пример взят из исходного кода FreeBSD. Ошибка была исправлена лишь в декабре 2008! Заметьте, что первая (пробная) 64-битная версия FreeBSD была выпущена в июне 2003.

Исходный код до исправления:

http://www.freebsd.org/cgi/cvsweb.cgi/src/lib/libc/stdio/fread.c?rev=1.14

Исправленный вариант кода (декабрь 2008):

http://www.freebsd.org/cgi/cvsweb.cgi/src/lib/libc/stdio/fread.c?rev=1.15

6.3. Биты и сдвиги

Легко сделать ошибку в коде при работе с отдельными битами. Следующий тип ошибки относится к операциям сдвига.

Пример ниже:

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 разряда, происходит переполнение, как показано на рисунке 5. Будет ли получен 0 (рисунок 5-B) или 1 (рисунок 5-C) в результате –  зависит от реализации компилятора.

 

Рисунок 5. A – Правильная установка 32-го бита в 32-битном коде; B,C – ошибка установки 32-го бита в 64-битной системе (два способа поведения)

Чтобы исправить код, надо сделать константу "1" такого же типа, что и переменная mask(маска):

ptrdiff_t mask = ptrdiff_t(1) << bitNum;

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

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

6.4. Магические числа

Магические константы, т.е. числа, с помощью которых определяется размер того или иного типа, могут вызывать много затруднений. Правильное решение – использовать операторы sizeof() для этих целей, но в большой программе может быть скрыт старый участок кода, где, как программисты считают, размер указателя равен 4 байтам, а в size_t всегда входит 32 бита. Обычно такие ошибки выглядят так:

size_t ArraySize = N * 4;
size_t *Array = (size_t *)malloc(ArraySize);

Рисунок 4 показывает основные числа, с которыми надо работать осторожно при переносе на 64-битную платформу.

Таблица 4. Основные магические значения, представляющие опасность при переносе приложений с 32-битной платформы на 64-битную

6.5. Ошибки, касающиеся использования 32-битных переменных в качестве индексов

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

const size_t size = ...;
char *array = ...;
char *end = array + size;
for (unsigned i = 0; i != size; ++i)
{
  const int one = 1;
  end[-i - one] = 0;
}

Первая ошибка заключается в том, что если объем обрабатываемых данных превышает 4 GB (0xFFFFFFFF), может возникнуть бесконечный цикл, так как переменная 'i' имеет беззнаковый тип и никогда не достигнет значения 0xFFFFFFFF. Это может произойти, но не обязательно. Это зависит от того, какой код собирает компилятор. Например, в режиме отладки бесконечный цикл будет иметься, а в коде выпуска не будет цикла, поскольку компилятор решит оптимизировать код с помощью 64-битного регистра для счетчика, и цикл будет правильным. Все это создает путаницу, и код, работавший вчера, может не работать сегодня.

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

Согласно правилам C++ выражение <-i - единица> в 32-битной системе вычисляется следующим образом: (на первом шаге i = 0):
•    выражение '-i' имеет беззнаковый тип и значение 0x00000000u.
•    переменная 'one' будет увеличена с типа 'int' до беззнакового типа и будет равняться 0x00000001u. Примечание: тип int увеличивается (согласно стандарту C++) до беззнакового типа, если участвует в операции, где второй аргумент имеет беззнаковый тип.

Совершается операция вычитания, в которой участвуют два значения беззнакового типа, и результат операции равен 0x00000000u - 0x00000001u = 0xFFFFFFFFu. Результат имеет беззнаковый тип.

В 32-битной системе обращение к массиву по индексу 0xFFFFFFFFu тождественно использованию индекса -1. То есть конец[0xFFFFFFFFu] является аналогом конец[-1]. В результате элементы массива обрабатываются правильно.
В 64-битной системе ситуация будет отличаться в последнем пункте. Беззнаковый тип будет увеличен до знакового типа ptfdiff_t, и индекс массива будет равняться 0x00000000FFFFFFFFi64. В результате произойдет переполнение.
Чтобы исправить код, надо использовать типы ptrdiff_t и size_t.

6.6. Ошибки, касающиеся изменения типов используемых функций

Есть ошибки, в которых никто не виноват, но это все же ошибки. Допустим, давно в Visual Studio 6.0 был разработан проект, содержавший класс CSampleApp - потомок CWinApp. В базовом классе есть виртуальная функция WinHelp. Потомок перекрывает эту функцию и выполняет все необходимые действия. Данный процесс показан на рисунке 7.

Рисунок 7. Эффективный правильный код, созданный в Visual Studio 6.0.

Затем проект переносится на Visual Studio 2005, где прототип функции WinHelp изменился, но этого никто не заметит, так как в 32-битном режиме типы DWORD и DWORD_PTR совпадают, и программа продолжает работать правильно (рисунок 8).

Рисунок 8. Неправильный, но эффективный 32-битный код.

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

Рисунок 9. Ошибка возникает в 64-битном коде

6.7. Выявление скрытых ошибок

Есть много примеров таких 64-битных ошибок. Те, кто интересуется данной темой и хочет узнать больше о таких ошибках, смотрите статью (<) “20 проблем переноса кода C++ на 64-битную платформу”.

Этап поиска скрытых ошибок – весьма непростая задача, и многие из них возникают нерегулярно и лишь при больших входных данных. Статические анализаторы кода годятся для выявления таких ошибок, так как они могут проверить весь код приложения независимо от входных данных и частоту выполнения его участков в реальных условиях. Есть смысл в использовании статического анализа на этапе переноса приложения на 64-битные платформы для обнаружения большинства ошибок в самом начале и при дальнейшей разработке 64-битных решений. Статический анализ предупреждает и учит программиста лучше понимать особенности ошибок, относящихся к 64-битной архитектуре, и писать более эффективный код. Автор статьи является разработчиком одного из таких специализированных анализаторов кода под названием Viva64. Чтобы узнать больше об инструменте и скачать демоверсию, посетите сайт компании OOO "Program Verification Systems".

Справедливости ради надо сказать, что анализаторы кода Gimpel PC-Lint и Parasoft C++Test имеют наборы правил для диагностики 64-битных ошибок. Но, во-первых, это универсальные анализаторы, и правила диагностики 64-битных ошибок в них неполноценные. Во-вторых, они рассчитаны преимущественно на модель данных LP64, применяемую в семействе операционной системы Linux, и оттого они не особо полезны для программ Windows, где применяется модель данных LLP64.


7. Обновление процесса тестирования

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

Если ваша 64-битная программа обрабатывает более крупные объемы данных, чем 32-битная версия, вы должны расширить тесты, чтобы включить в них обработку данных объемом более 4 Гб. Это граница, за которой начинают проявляться многие 64-битные ошибки. Такие тесты могут занять гораздо больше времени, и вы должны быть готовы к этому. Обычно тесты пишутся так, чтобы каждый тест мог обрабатывать малое число элементов и тем самым позволять выполнять все внутренние тесты модулей за несколько минут, а автоматические тесты (например, с помощью AutomatedQA TestComplete) – за несколько часов. Почти несомненно, что функция сортировки, сортирующая 100 элементов, будет вести себя правильно на 100000 элементах в 32-битной системе. Но эта же функция может сбоить в 64-битной системе при попытке обработать 5 миллиардов элементов. Скорость выполнения теста модулей может упасть в миллион раз. Не забывайте о стоимости переделки тестов при овладении 64-битными системами. Удачным решением является разделение тестов модулей на быстрые (работающие с малыми объемами памяти) и медленные, обрабатывающие гигабайты, и проводимые, например, ночью. Автоматизированное тестирование ресурсоемких 64-битных программ можно выстроить на основе распределенных вычислений.

Есть еще один неприятный факт. Вы едва ли преуспеете в использовании инструментов, таких как BoundsChecker, для поиска ошибок в ресурсоемких 64-битных программах, потребляющих большой объем памяти. Причина – сильное замедление тестируемых программ, что делает такой подход крайне неудобным. В режиме выявления всех ошибок, относящихся к работе памяти, инструмент Parallel Inspector, входящий в Intel Parallel Studio, в среднем замедляет выполнение приложения в 100 раз (рисунок 10). Скорее всего, тестируемый алгоритм, работающий 10 минут, придется оставлять на ночь. И все же Parallel Inspector является одним из самых полезных и удобных инструментов для поиска ошибок работы памяти. Надо быть готовым изменить приемы диагностики ошибок и учитывать их при планировании овладения 64-битными системами.

Рисунок 10. Окно настроек программы Parallel Inspector перед запуском приложения.

И последнее. Не забудьте добавить тесты, проверяющие совместимость форматов данных между 32-битной и 64-битной версиями. Совместимость данных часто нарушается при переносе по причине записи в файлы таких типов, как size_t или long (в системах Linux).