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

Данная статья ориентирована на программистов, занимающихся разработкой программного обеспечения для 64-разрядной платформы. Мы рассмотрим преимущества этой платформы, недостатки использования 32-разрядного кода на 64-разрядной платформе, основы 64-разрядного программирования и способы перевода приложений Win32 на 64-разрядную платформу. Последние два вопроса освещены в современной прессе и MSDN достаточно слабо. Мы не будем акцентировать внимание на какой-либо конкретной модели (архитектуре) 64-разрядного процессора. Все рассмотренные в статье примеры используют язык С++ и компилируются с помощью 64-разрядного компилятора С++, входящего в состав последних дистрибутивов SDK и DDK.

64-разрядная архитектура

Win64-код объединяет в себе основные возможности 32-разрядного кода, а также включает изменения, связанные с повышением разрядности. В распоряжении программиста оказываются:

  • 64-разрядные указатели;
  • 64-разрядные типы данных;
  • 32-разрядные типы данных;
  • интерфейс Win64 API.

Обратите внимание, что 32-разрядные типы данных не исчезли при повышении разрядности платформы (как было с 16-разрядными типами данных при переходе к Win32). Это связано с тем, что даже в 64-разрядных приложениях в большинстве случаев переменные не требуют объема памяти в 8 байт, поэтому использование 64-разрядных типов в таких случаях оказалось бы крайне неэффективным. Операционной системе пришлось бы дописывать нули в старшие разряды, чтобы увеличить размер данных до 8 байт (такие данные к тому же очень неудобно считывать). Это привело бы к снижению производительности.

Иная участь постигла 32-разрядные указатели: они полностью исчезли. Дело в том, что использование 32-разрядных указателей накладывает ограничение на объем адресуемой памяти. Например, одним из главных преимуществ плоской модели памяти (она является основной для программирования 32-разрядных приложений для платформы NT), использующей 32-разрядные указатели, является возможность создания сегментов объемом до 4 Гбайт. Новые 64-разрядные указатели обеспечивают возможность адресации до 16 Тбайт памяти (1 Тбайт = 1012 Мбайт). Современными бизнес-приложениями этот объем вполне востребован.

Функции в Win64 API претерпели незначительные изменения. Только названия некоторых из них были изменены так, чтобы отразить принадлежность к 64-разрядной платформе. В большинстве случаев изменениям подверглись лишь типы параметров, являющихся аргументами вызова функций. Все остальные преимущества (возможность отказаться от использования файлов подкачки и т. д.) связаны либо с увеличившимся объемом адресации, либо с новыми типами данных.

32-разрядное приложение в 64-разрядной среде

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

Здесь следует остановиться и подробнее рассмотреть вопрос о преобразовании данных. Win64 допускает использование и 32-, и 64-разрядных данных. Поэтому когда операционная система встречает 32-разрядные данные в 32-разрядном приложении, она должна ответить на вопрос: «Преобразовать ли эти данные в 64-разрядные или оставить, как есть?» Делается это для того, чтобы оптимизировать работу приложения. Если операционная система встретит 32-разрядные данные в 64-разрядном приложении, то не обратит на них внимания, так как платформа Win64 допускает использование 32-разрядных типов данных. Преобразование 32-разрядных данных в 64-разрядные осуществляется точно так же, как и преобразование 32-разрядных указателей, — дописыванием нулей в старшие разряды. Очевидно, что на выполнение всех этих операций тратится масса системных ресурсов, и, как результат, производительность снижается. Это особенно заметно при работе 32-разрядных драйверов на 64-разрядной платформе. В связи с тем, что драйверы являются связующим звеном между оборудованием и операционной системой, именно они используются наиболее интенсивно.

Азбука 64-разрядного программирования

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

  • _WIN64 - 64-разрядная платформа;
  • _WIN32 - 32-разрядная платформа (для совместимости с 32-разрядной платформой);
  • _WIN16 - 16-разрядная платформа.

Также компилятор имеет встроенные предопределенные макросы, специфичные для различных архитектур процессоров:

  • _M_IA64 - 64-разрядная архитектура Intel;
  • _M_IX86 - 32-разрядная архитектура Intel;
  • _M_ALPHA_64 - 64-разрядная архитектура Alpha;
  • _M_ALPHA32 - 32-разрядная архитектура Alpha;
  • _M_ALPHA - архитектура Alpha, либо 32-разрядная, либо 64-разрядная.

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

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

#ifdef _WIN32 // Win32 код
...
#else     // А здесь неясно, какой код
 следует далее: 64- или 16-разрядный...
...
#endif
И этот код не является корректным:
#ifdef _WIN16 // Win16 код
...
#else   // То же самое: 32-
 или 64-разрядный код?
...
#endif

Чтобы исправить этот код, нужно после #else добавить макросы _WIN32 или _WIN64. Просто придется привыкнуть к тому, что теперь код бывает трех (а не двух, как раньше) «видов», поэтому использование директивы #else в привычных конструкциях приведет к неоднозначности (хотя 16-разрядных приложений становится все меньше и меньше, поддержка этой платформы в современных компиляторах остается).

Теперь рассмотрим новые типы данных. Их можно условно разделить на три группы (см. Таблицу 1):

  • целочисленные типы явного представления (fixed-precision integer types);
  • целочисленные типы, представленные указателями (pointer-precision integer types);
  • типы специальных указателей (specific-precision pointer types).

Описания этих типов находятся в файле basetsd.h (входящем в состав DDK/SDK) и приведены в Таблице 1.

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

...
#ifdef _WIN32 // Win32 код
...

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

Второй вид (целочисленные типы, представленные указателями) имеет свою особенность: размер данных каждого такого типа зависит от платформы, на которой исполняется приложение. Именно поэтому многие из этих типов названы интегральными. Они сочетают в себе свойства 32- и 64-разрядных типов. Эти типы можно сравнить с виртуальными функциями и поздним связыванием из объектно-ориентированного программирования, так как в обоих случаях среда исполнения определяет конкретные свойства элементов только во время исполнения приложения, а не компиляции (в этом и заключается позднее связывание). Эти типы данных позволяют создавать приложения, одинаково хорошо работающие на 32- и 64-разрядной платформах.

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

Следующие функции Win64 (Helper Functions) отвечают за преобразование одного типа в другой (эти inline-функции определены в Basetsd.h). Смысл многих из них понятен из определения прототипов, к остальным требуются объяснения:

  • unsigned long HandleToUlong( const void *h );
  • long HandleToLong( const void *h );
  • void *LongToHandle( const long h );
  • unsigned long PtrToUlong( const void *p );
  • unsigned int PtrToUint( const void *p );
  • unsigned short PtrToUshort( const void *p );
  • long PtrToLong( const void *p );
  • int PtrToInt( const void *p );
  • short PtrToShort( const void *p );
  • void * IntToPtr( const int i ) расширяет знаком (sign-extends) значение типа int;
  • void * UIntToPtr( const unsigned int ui) расширяет нулем (zero-extends) значение типа unsigned int;
  • void * LongToPtr( const long l ) расширяет знаком (sign-extends) значение типа long;
  • void * ULongToPtr( const unsigned long ul) расширяет нулем (zero-extends) значение типа unsigned long.

Теперь рассмотрим изменения в Win64 API. Как уже отмечалось, лишь некоторые функции Win64 API были изменены. Для того чтобы перевести 32-разрядный код в 64-разрядный, следует использовать новые функции оконного класса (Window class). Если в private-данных окна или класса есть указатели, необходимо задействовать следующие новые функции: GetClassLongPtr, GetWindowLongPtr, SetClassLongPtr, SetWindowLongPtr. Эти функции могут работать и на 32- и на 64-разрядной платформе, но для компиляции требуют 64-разрядного компилятора.

Также необходимо, чтобы указатели и дескрипторы (handles), входящие в private-данные класса, могли использовать новые 64-разрядные функции. Нужно иметь в виду, что следующие элементы в Winuser.h во время 64-разрядной компиляции не определены:

GWL_WNDPROC, GWL_HINSTANCE,
GWL_HWDPARENT, GWL_USERDATA.

Вместо них в Winuser.h определены следующие новые элементы:

GWLP_WNDPROC, GWLP_HINSTANCE,
 GWLP_HWNDPARENT, GWLP_USERDATA, GWLP_ID.

Например, следующий код вызовет ошибку компиляции:

SetWindowLong(hWnd,
 GWL_WNDPROC, (LONG)MyWndProc);

Его нужно изменить так:

SetWindowLongPtr(hWnd, GWLP_WNDPROC,
 (LONG_PTR)MyWndProc);

Обращаю внимание читателей на следующий момент. Прежде чем делать cbWndExtra членом структуры WNDCLASS, необходимо убедиться, что для указателя имеется достаточно памяти. Например, если зарезервировано sizeof(DWORD) байт для адресуемой переменной, следует зарезервировать sizeof(DWORD_PTR) байт.

Теперь вернемся к новым 64-разрядным API-функциям. Все они объявлены в Winuser.h, включены в Windows.h и используют User32.lib.

1. GetClassLongPtr. GetClassLongPtr возвращает значение из структуры WNDCLASSEX, которое связано с определенным окном. Если вы возвращаете указатель или дескриптор окна (handle), эта функция в точности повторяет GetClassLong. Но чтобы написать код, работающий под двумя платформами (32- и 64-разрядной), нужно использовать только GetClassLongPtr. Вот ее определение:

ULONG_PTR GetClassLongPtr( HWND hWnd,
 // указатель на окно
int nIndex
 // смещение возвращаемого значения).

Было бы нецелесообразно описывать все аргументы, так как они в точности повторяют параметры 32-разрядной функции GetClassLong (и, следовательно, доступны в любой справке по Win32 API).

2. GetWindowLongPtr. Здесь та же картина, что и в предыдущем случае. Эта функция является заменой «старой» функции GetWindowLong и служит лишь для совместимости двух платформ (а значит, и для создания кросс-платформенных приложений). Функция GetWindowLongPtr возвращает дескриптор окна и значение из экстрапамяти окна по указанному смещению. Вот ее определение:

LONG_PTR GetWindowLongPtr(
HWND hWnd,
 // указатель на окно
int nIndex
 // смещение возвращаемого значения).

3. SetClassLongPtr. Функция SetClassLong заменяет определенные значения по заданным смещениям в экстрапамяти класса или WNDCLASSEX-структуры на значения того класса, к которому принадлежит данное окно. Во многом эта функция похожа на SetClassLong, но она является межплатформенной.

ULONG_PTR SetClassLongPtr(
HWND hWnd,
 // указатель на окно
int nIndex,
 // указатель на значение, которое надо поменять
LONG_PTR dwNewLong
 // новое значение);

4. SetWindowLongPtr. Функция SetWindowLongPtr меняет атрибуты окон. Она также записывает определенные значения (по определенным смещениям) в экстрапамять окна. Эта функция является интегральным (в смысле объединения двух платформ) аналогом SetWindowLong.

LONG_PTR SetWindowLongPtr(
HWND hWnd,
 // указатель на окно
int nIndex,
 // смещение, куда записывать значение
LONG_PTR dwNewLong
 // новое значение).

Перевод 32 в 64

Итак, я рассказал обо всех программных средствах, предоставляемых Win64. Пора разобраться, как применить эти знания. В общем виде алгоритм перевода кода Win32 на 64-разрядную платформу выглядит так:

  • замена "старых" типов новыми в тех случаях, когда это необходимо;
  • замена всех 32-разрядных указателей на 64-разрядные;
  • замена всех API-функций Win32 их 64-разрядными эквивалентами.

Для создания кросс-платформенных приложений (это предпочтительней первого варианта) необходимо:

  • воспользоваться макросами, определяющими платформу;
  • заменить все 32-разрядные типы данных их интегральными эквивалентами;
  • заменить все указатели на 64-разрядные;
  • заменить API-функции Win32 их интегральными эквивалентами.

К сожалению, пока не существует программ, которые могли бы помочь это сделать. Поэтому все изменения нужно делать самим. Единственным помощником в данном случае является 64-разрядный компилятор: в частности, его режим предупреждений (warnings), касающийся 64-разрядного кода. Для того чтобы включить эти предупреждения, нужно воспользоваться параметром компилятора-Wp64-W3. Он сделает активными следующие предупреждения:

  • C4305 - предупреждение о преобразовании типов. Например, "return": truncation from "unsigned int64" to "long";
  • C4311 - предупреждение о преобразовании типов. Например, "type cast": pointer truncation from "int*_ptr64" to "int";
  • C4312 - преобразование до большего размера (bigger-size). Например, "type cast": conversion from "int" to "int*_ptr64" of greater size;
  • C4318 - использование нулевой длины (Passing zero length). Например, passing constant zero as the length to the memset function;
  • C4319 - нет оператора (Not operator). Например, "~": zero extending "unsigned long" to "unsigned _int64" of greater size;
  • C4313 - вызов функций, входящих в printf-семейство, с конфликтным преобразованием типов в спецификаторах и аргументах. Например, "printf": "%p" in format string conflicts with argument 2 of type "_int64." Или, например, вызов функции printf("%x", pointer_value) потребует преобразования верхних 32 разрядов. Правильный вызов: printf("%p", pointer_value);
  • C4244 - то же, что и C4242. Например, "return": conversion from "_int64" to "unsigned int," possible loss of data.

Для преобразования кода в 64-разрядный нужно исправить все строки кода, на которые укажет компилятор. Некоторые советы приведены во врезке «Портируем 32-разрядный код»

В заключение

Мы постарались по возможности рассмотреть все значимые аспекты перевода 32-разрядного кода на 64-разрядную платформу. Следует заметить, что портирование драйверов происходит точно по такому же принципу. Хотя при этом обнаруживается достаточно много подводных камней, каждый из которых проявляется в своем конкретном случае. Составить общее представление об этом вопросе позволяет MSDN.

Как сообщают представители Microsoft, средства для работы с 64-разрядным кодом (новые типы данных, указатели, компилятор, библиотеки...) будут включены в новую версию MS Visual C++. Пока что в нашем распоряжении есть DDK/SDK, и это уже немало. С их помощью уже сейчас можно создавать кросс-платформенные приложения и портировать 32-разрядный код.

АЛЕКСЕЙ ДОЛЯ — технический писатель. С ним можно связаться по адресу: TanaT@hotmail.ru.


Портируем 32-разрядный код

При портировании 32-разрядного кода на 64-разрядную платформу следует учитывать следующие моменты:

  • следите за разрядностью указателей и адресуемых ими данных. Если разрядности не совпадут, то либо приложение потеряет в производительности (операционная система сама будет расширять указатели), либо будет утрачена часть данных (они могут быть просто затерты);
  • используйте интегральные типы данных и функции Win64. Это позволит избежать множества конфликтных ситуаций;
  • делайте приложения кросс-платформенными. Это залог стабильности и высокой производительности приложения (гарантия корректной работы);
  • старайтесь исправлять код так, чтобы он не вызывал ни одного предупреждения 64-разрядного компилятора. Это позволит оптимизировать код и устранить риск, связанный со скрытыми в нем ошибками.