Была ли эта страница полезной?
Ваш отзыв об этом контенте важен для нас. Расскажите нам о том, что вы думаете.
Дополнительный отзыв?
1500 символов осталось
Все, что нужно знать, чтобы начать программировать для 64-разрядных версий Windows
Collapse the table of content
Expand the table of content

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

Автор:

  • Мэт Петрек

На этой странице…

Операционная система x64 Операционная система x64
Основные сведения об архитектуре x64 Основные сведения об архитектуре x64
Разработка для x64 с помощью Visual C++ Разработка для x64 с помощью Visual C++
Как сделать код совместимым с Win64 Как сделать код совместимым с Win64
Отладка Отладка
А как быть с управляемым кодом? А как быть с управляемым кодом?
Заключение Заключение

Честно говоря, мне не слишком комфортно работать с новой операционной системой, если я еще не покопался в ее внутреннем устройстве. А покопаться "под капотом" я люблю. Так что, когда на сцену вышли 64-разрядные версии Windows XP и Windows Server 2003, я был в полном восторге.

Приятная особенность Win64 и процессорной архитектуры x64 заключается в том, что они достаточно сильно отличаются от своих предшественников - как раз настолько, чтобы вызывать интерес, в то же время не требуя основательного переобучения. Хотя мы, разработчики, предпочитаем считать, будто при переходе на платформу x64 удастся отделаться простой перекомпиляцией своих программ, реальность такова, что нам все равно придется долго возиться с ними в отладчике. И здесь хорошее понимание новой программно-аппаратной платформы будет важным подспорьем.

В данной статье я поделюсь квинтэссенцией своих знаний в области Win64 и архитектуры x64 - тем минимумом, который необходим опытному Win32-программисту для перехода на платформу x64. При этом я исхожу из того, что вам известны базовые концепции Win32 и платформы x86 и вы понимаете, зачем ваш код должен работать в Win64. Это позволит мне не отвлекаться от основной тематики. В общем, считайте мою статью обзором, где рассматриваются лишь наиболее важные различия архитектур Win64/x64 и Win32/x86.

Системы x64 хороши еще и тем, что они - в отличие от систем на основе процессоров Itanium - позволяют использовать либо Win32, либо Win64 на одном компьютере без серьезных потерь в производительности. И несмотря на некоторые, весьма туманные различия между реализациями x64 от Intel и AMD x64-совместимая версия Windows должна работать с любой из них. Вам не понадобятся отдельные сборки Windows для x64-процессоров AMD и Intel.

Основное внимание я уделю трем областям: деталям реализации ОС, архитектуре x64 (обязательный минимум) и разработке для x64 с помощью Visual C++.

Операционная система x64

В любом обзоре архитектуры Windows я предпочитаю начинать с рассмотрения адресации и адресного пространства. Хотя 64-разрядный процессор теоретически мог бы адресоваться к 16 экзабайтам памяти (264), в настоящее время Win64 поддерживает 16 Тб - значение, которое представлено 44 разрядами. Почему же нельзя задействовать все 64 разряда, чтобы адресоваться к 16 экзабайтам памяти? По целому ряду причин.

Начнем с того, что нынешние процессоры x64 обычно позволяют обращаться лишь к 40-разрядному представлению физической памяти (1 Тб). Сама архитектура (но не современное оборудование) допускает расширение до 52 разрядов (4 петабайтов). Даже если бы это ограничение было снято, размеры таблиц страниц, необходимых для проецирования такого громадного объема памяти, оказались бы просто гигантскими.

Как и в Win32, адресуемая память делится на области пользовательского режима и режима ядра. Каждому процессу выделяется собственное уникальное пространство размером 8 Тб в нижней части памяти, а код режима ядра размещается в верхних 8 Тб и разделяется всеми процессами. У разных версий 64-разрядной Windows разные ограничения на объемы физической памяти (табл. 1 и 2).

Табл. 1. Общие ограничения на память

32-разрядные модели64-разрядные модели
Виртуальное адресное пространство (одного процесса)4 Гб16 Тб
Виртуальное адресное пространство для каждого 32-разрядного процесса2 Гб (3 Гб при запуске системы с ключом /3GB)4 Гб при компиляции с параметром /LARGEADDRESSAWARE (иначе 2 Гб)
Виртуальное адресное пространство для каждого 64-разрядного процессаНеприменимо8 Тб
Пул подкачиваемой памяти режима ядра470 Мб128 Гб
Пул неподкачиваемой памяти режима ядра256 Мб128 Гб
Элемент системной таблицы страниц (Page Table Entry, PTE)660-900 Мб128 Гб

Табл. 2. Ограничения на физическую память в зависимости от процессоров

Операционная система32-разрядные модели64-разрядные модели
Windows XP Professional 4 Гб (1-2 процессора) 128 Гб (1-2 процессора)
Windows Server 2003, Standard Edition 4 Гб (1-4 процессора) 32 Гб (1-4 процессора)
Windows Server 2003, Enterprise Edition 64 Гб (1-8 процессоров) 1 Тб (1-8 процессоров)
Windows Server 2003, Datacenter Edition 64 Гб (8-32 процессора) 1 Тб (8-64 процессора)

Так же, как и в Win32, размер страницы на платформе x64 равен 4 Кб. Первые 64 Кб адресного пространства никогда не проецируются на физическую память, поэтому младший допустимый адрес - 0x10000. В отличие от Win32 системные DLL по умолчанию не загружаются по адресу в верхней части адресного пространства пользовательского режима. Вместо этого они загружаются после 4 Гб, обычно по адресам, близким к 0x7FF00000000.

Приятная особенность процессоров x64 - поддержка битового флага No Execute, который в Windows используется для реализации аппаратной защиты от выполнения данных как кода (Data Execution Protection, DEP). Существование многих вирусов и "багов" на платформе x86 как раз и обусловлено тем, что процессор может выполнять данные так, будто это байты кода. Переполнение буфера (намеренное или случайное) может привести к тому, что процессор будет выполнять содержимое области памяти, где должны храниться данные. Благодаря DEP операционная система гораздо четче разграничивает области памяти, в которых находится код, и становится способной перехватывать попытки выполнения кода, выходящие за эти границы. Это уменьшает уязвимость Windows перед атаками.

Для выявления ошибок компоновщик (linker) на платформе x64 по умолчанию присваивает адресам загрузки исполняемых файлов первое значение, большее 32-разрядного числа (4 Гб). Это помогает быстро находить проблемные места в существующем коде после его переноса на Win64. В частности, если указатель хранится как 32-битное значение (например, как DWORD), то при работе в Win64-версии вашей программы он окажется усеченным и станет недопустимым, тут же вызвав нарушение доступа к памяти (access violation). Такой прием резко упрощает отлов ошибок, связанных с указателями.

Затронутый мной вопрос указателей и DWORD-значений позволяет плавно перейти к системе типов в Win64. Какой размер должен иметь указатель? Как насчет LONG? И описателей (handles) наподобие HWND? К счастью, Microsoft, ведя нас по весьма запутанному пути от Win16 к Win32, заодно создала новые модели типов, легко расширяемые и до 64-разрядных. В общем, если не считать нескольких исключений, все типы, отличные от указателей и size_t, совершенно одинаковы, что в старой Win32, что в новой Win64. То есть у 64-битного указателя размер 8 байтов, а у int, long, DWORD и HANDLE остался прежний размер - 4 байта. Подробнее о типах я расскажу позже, когда речь пойдет о разработке для Win64.

Формат файлов в Win64 называется PE32+. С точки зрения структуры, он почти во всем идентичен формату PE в Win32. Лишь некоторые поля вроде ImageBase в заголовке расширены, одно поле удалено и одно изменено так, чтобы оно отражало новый тип процессоров (табл. 3).

Табл. 3. Изменения в полях заголовков PE-файлов

Поле заголовка Изменение
Magic0x20b вместо 0x10b
BaseOfDataУбрано
ImageBase Расширено до 64 битов
SizeOfStackReserve Расширено
SizeOfStackCommit Расширено
SizeOfHeapReserve Расширено
SizeOfHeapCommit Расширено

Помимо заголовка PE, изменений не так уж много. В некоторых структурах, например IMAGE_LOAD_CONFIG и IMAGE_THUNK_DATA, часть полей просто расширена до 64 битов. Больший интерес представляет введение раздела PDATA, так как он высвечивает одно из основных различий между реализациями Win32 и Win64: концепцию обработки исключений.

На платформе x86 обработка исключений базируется на стеке. Когда Win32-функция содержит код try/catch или try/finally, компилятор генерирует инструкции, создающие небольшие блоки данных в стеке. Каждый блок данных try указывает на предыдущую структуру данных try, образуя связанный список, в котором структуры, добавленные последними, помещаются в начало списка. По мере вызова функций и выхода из них начало связанного списка обновляется. Как только возникает исключение, ОС просматривает связанный список блоков в стеке, отыскивая подходящий обработчик. Все детали этого процесса я изложил в своей статье за январь 1997 г. (microsoft.com/msj/0197/Exception/Exception.aspx), так что здесь я не буду вдаваться в подробности.

В Win64 (в версиях для x64 и Itanium) применяется табличная обработка исключений. Никакого связанного списка блоков данных try в стеке не создается. Вместо этого каждый исполняемый файл в Win64 содержит таблицу функций периода выполнения (runtime function table). В каждой записи этой таблицы хранятся начальный и конечный адреса функции, а также местонахождение большого набора данных о коде, обрабатывающем исключения в данной функции, и структура ее фрейма стека. Детальное содержимое этих структур см. в определении IMAGE_RUNTIME_FUNCTION_ENTRY в файле WINNT.H и в x64 SDK.

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

А как быть, если вы сгенерировали код непосредственно в памяти, не используя нижележащий модуль формата PE32+? В Win64 имеется API-функция RtlAddFunctionTable, позволяющая сообщить ОС о динамически генерируемом коде.

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

Не забывайте, что я даю лишь краткое введение, а не полное описание обработки исключений на платформе x64. Более глубокий обзор x64-модели исключений читайте в онлайновом дневнике Кевина Фрая (Kevin Frei) по ссылке blogs.msdn.com/509372.aspx.

В x64-совместимых версиях Windows не появилось слишком уж много новых API-функций - большинство таковых в Win64 добавлено в выпуски Windows для процессоров Itanium. Две наиболее важные API-функции - IsWow64Process и GetNativeSystemInfo - позволяют Win32-приложениям определять, выполняются ли они в Win64, и, если да, выяснять реальные возможности данной системы. Если же 32-разрядный процесс обращается к GetSystemInfo, он видит лишь те возможности, которые свойственны обычной 32-разрядной системе. Так, GetSystemInfo способна сообщать о диапазонах адресов лишь 32-разрядных процессов. В табл. 4 перечислены API-функции для платформы x64, которых не было на платформе x86.

Табл. 4. Изменения в полях заголовков PE-файлов

Функциональность API-функции
Обработка исключений

RtlAddFunctionTable
RtlDeleteFunctionTable
RtlRestoreContext
RtlLookupFunctionEntry
RtlInstallFunctionTableCallback

Реестр

RegDeleteKeyEx
RegGetValue
RegQueryReflectionKey

NUMA (Non-Uniform Memory Access)

GetNumaAvailableMemoryNode
GetNumaHighestNodeNumber
GetNumaNodeProcessorMask
GetNumaProcessorNode

Перенаправление WOW64

Wow64DisableWow64FsRedirection
Wow64RevertWow64FsRedirection
RegDisableReflectionKey
RegEnableReflectionKey

Разное

GetLogicalProcessorInformation
QueryWorkingSetEx
SetThreadStackGuarantee
GetSystemFileCacheSize
SetSystemFileCacheSize
EnumSystemFirmwareTables
GetSystemFirmwareTable

Хотя перспективы работы в полностью 64-разрядной Windows-системе выглядят очень радужно, реалии таковы, что какое-то время вам скорее всего понадобится запускать в ней Win32-код. В связи с этим в x64-версиях Windows предусмотрена подсистема WOW64, позволяющая сосуществовать Win32- и Win64-процессам в одной системе. Однако загрузка вашей 32-разрядной DLL в 64-разрядный процесс (или наоборот) не поддерживается. (Поверьте мне, это хорошо.) И наконец-то можно окончательно распрощаться с устаревшим 16-разрядным кодом!

В x64-версиях Windows процесс, запускаемый из 64-разрядного исполняемого файла, например из Explorer.exe, может загружать только Win64 DLL-библиотеки, а процесс, запускаемый из 32-разрядного исполняемого файла, - только Win32 DLL-библиотеки. Когда Win32-процесс вызывает какую-то функцию режима ядра (скажем, для чтения файла), WOW64 автоматически перехватывает этот вызов и переадресует его эквивалентному x64-коду.

Конечно, процессам разных "весовых категорий" (32- и 64-разрядным) нужно как-то взаимодействовать между собой. К счастью, все известные и любимые вами Win32-механизмы межпроцессного взаимодействия работают и в Win64; это относится, в том числе, к разделяемой памяти (shared memory), именованным каналам (named pipes) и именованным синхронизирующим объектам.

Возможно, вас интересует: "А как насчет системного каталога? Не могут же в одном каталоге храниться 32- и 64-разрядные версии системных DLL вроде KERNEL32 или USER32, верно?". WOW64 сама заботится об этом и осуществляет избирательное перенаправление в файловой системе. Файловые операции, выполняемые из Win32-процесса, которые в обычных условиях адресовались бы к каталогу System32, перенаправляются в каталог SysWow64. На внутреннем уровне WOW64 "молча" модифицирует соответствующие запросы так, чтобы они были направлены каталогу SysWow64. По сути, в Win64-системе существуют два каталога \Windows\System32: один - для двоичных файлов x64, а другой - для Win32-эквивалентов.

Как бы гладко ни выглядело это на бумаге, на самом деле легко запутаться. Например, в одном из участков своего кода я использовал окно 32-разрядной командной строки. Выполняя команду DIR применительно к Kernel32.dll в каталоге System32, я получал ровно тот же результат, что и при обращении к каталогу SysWow64. И мне пришлось долго чесать в затылке, пока я не сообразил, что перенаправление в файловой системе работает именно так, как и должно. То есть, даже когда я думал, будто работаю с каталогом \Windows\System32, WOW64 переадресовывала вызовы в каталог SysWow64. Кстати, если вам вдруг понадобится доступ к 32-разрядному каталогу \Windows\System32 из x64-приложения, правильный путь укажет API-функция GetSystemWow64Directory. Только сначала почитайте документацию MSDN, чтобы не упустить какие-нибудь детали.

WOW64 выполняет перенаправление не только в файловой системе, но и в реестре. Вспомните, что я недавно говорил: Win32 DLL нельзя загрузить в Win64-процесс. А теперь подумайте о COM и о том, как она использует реестр для загрузки DLL-библиотек внутреннего (внутрипроцессного) сервера (in-process server). Что будет, если 64-разрядное приложение вызовет CoCreateInstance для создания объекта, реализованного в Win32 DLL? Ведь эту DLL нельзя загрузить, верно? WOW64 вновь спасает положение, перенаправляя запросы из 32-разрядных приложений в \Software\Classes и связанные с ними узлы реестра. В конечном счете Win32-приложения получают отличное от x64-приложений (по большей части параллельное) представление реестра. Как и следовало ожидать, ОС предоставляет возможность 32-разрядным приложениям считывать реальный 64-разрядный параметр из реестра, указывая новые флаговые значения при вызове RegOpenKey и родственных ей функций.

Последние несколько различий в ОС, близкие и дорогие моему сердцу, касаются данных, локальных для потока. В x86-версиях Windows на области памяти, локальные для потоков, в том числе на "последнюю ошибку" и Thread Local Storage (GetLastError и TlsGetValue соответственно), указывал регистр FS. В x64-версиях Windows регистр FS заменен регистром GS. В остальном все работает почти аналогично.

Хотя в этой статье x64 в основном рассматривается с точки зрения пользовательского режима, не могу не обратить ваше внимание на одно важное изменение в архитектуре режима ядра. В Windows для x64 появилась новая технология под названием PatchGuard, предназначенная для повышения как безопасности, так и отказоустойчивости. Если в двух словах, то программы или драйверы пользовательского режима, изменяющие содержимое ключевых структур данных вроде таблиц syscall и таблицы диспетчеризации прерываний (interrupt dispatch table, IDT), создают дыры в защите и потенциальную угрозу стабильности. При разработке архитектуры x64-версий Windows было решено, что допускать модификацию памяти режима ядра такими недокументированными способами больше нельзя. Противодействовать им и призвана технология PatchGuard. Она использует поток режима ядра для мониторинга изменений в критически важных областях памяти режима ядра. Если такое изменение обнаруживается, система останавливается через bugcheck.

Итак, если вы хорошо знакомы с архитектурой Win32 и знаете, как писать "родной" для нее код, то при переходе на Win64 вас ждет не так уж много сюрпризов. Считайте, что по большей части Win64 - просто более просторная среда.

Основные сведения об архитектуре x64

Теперь рассмотрим архитектуру самих процессоров x64, так как базовые знания набора команд процессора существенно упрощают разработку (особенно отладку). Первое, что вы заметите в x64-коде, сгенерированном компилятором, - насколько он похож на известный и любимый вами x86-код. Это вам не код для Intel IA64!

Второе, на что вы обратите внимание, - имена регистров слегка отличаются от привычных и их гораздо больше. Имена регистров общего назначения в процессорах x64 начинаются с буквы R, например RAX, RBX и др. Это развитие старой схемы именования на основе префикса E для 32-битных регистров x86. В далеком прошлом, уже подернутом дымкой забвения, 16-битный регистр AX стал 32-битным EAX, 16-битный BX - 32-битным EBX и т. д. Таким образом, при переходе с 32-разрядных версий на 64-разрядные префикс E во всех именах регистров теперь заменяется префиксом R (скажем, RAX - наследник EAX, RBX продолжает дело EBX, RSI заменяет ESI и т. п.). Кроме того, появилось восемь новых регистров общего назначения (R8-R15). Список основных 64-разрядных регистров общего назначения приведен в табл. 5.

Табл. 5. Список основных 64-разрядных регистров общего назначения

RAX R8
RBX R9
RCX R10
RDX R11
RSI R12
RDI R13
RSP R14
RBP R15

Кстати, 32-битный регистр EIP стал регистром RIP. Конечно, 32-разрядные инструкции по-прежнему должны выполняться, поэтому доступны и все остальные версии регистров (EAX, AX, AL, AH и др.).

Чтобы гуру программирования графических и научных приложений не почувствовали себя забытыми, в процессорах x64 также есть 16 128-битных SSE2-регистров с именами от XMM0 до XMM15. Полный набор x64-регистров, поддерживаемых Windows, вы найдете в структуре _CONTEXT, определенной в файле WINNT.H.

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

Если вы знакомы со схемами кодирования процессорных операций, то вспомните, что пространство для новых инструкций быстро исчезало и что введение восьми новых регистров - задача не из простых. Один из путей ее решения - исключение некоторых редко применяемых инструкций. Пока что я заметил отсутствие 64-разрядных версий PUSHAD и POPAD, которые соответственно сохраняли все регистры общего назначения в стеке и восстанавливали их из него. Другой способ - полное избавление от сегментов в 64-разрядном режиме. Так что жизнь регистров CS, DS, ES, SS, FS и GS подходит к концу. Но об этом мало кто пожалеет.

Теперь, когда адреса стали 64-битными, вероятно, вас интересует, насколько увеличится размер кода. Вот, например, распространенная 32-разрядная инструкция:

CALL DWORD PTR [XXXXXXXX]

Здесь "иксы" представляют 32-битный адрес. Станет ли он в 64-разрядном режиме 64-битным и превратится ли таким образом из 5-байтовой инструкции в 9-байтовую? К счастью, нет. Эта инструкция останется прежней длины. В 64-разрядном режиме 32-битная часть операнда инструкции интерпретируется как смещение данных относительно текущей инструкции. Небольшой пример поможет прояснить сказанное. Вот что представляет собой инструкция для вызова по 32-битному значению указателя, хранящемуся по адресу 00020000h в 32-разрядном режиме:

00401000: CALL DWORD PTR [00020000h]

В 64-разрядном режиме те же байты кода операции (opcodebytes) делают вызов по 64-битному значению указателя, хранящемуся по адресу 00421000h (4010000h + 20000h). Немного поразмыслив, вы поймете, что этот режим относительной адресации влечет за собой важные последствия в том случае, если вы сами генерируете код. Нельзя просто задать 8-байтовое значение указателя в инструкции. Вместо этого вы должны указать 32-битный относительный адрес участка памяти, в котором хранится реальный 64-битный целевой адрес. То есть предполагается, что 64-битный целевой указатель находится в пределах 2 Гб от использующей его инструкции. Для большинства это не имеет особого значения, но только не для тех, кто динамически генерирует код или модифицирует существующий код в памяти!

Основное преимущество всех x64-регистров в том, что компиляторы наконец-то могут генерировать код, в котором большая часть параметров передается через регистры, а не стек. Заталкивая параметры в стек, неизбежно приходится обращаться по адресам памяти. А мы уже давно крепко усвоили, что обращение по адресам памяти, отсутствующим в кэше процессора, занимает несоизмеримо больше времени.

При разработке соглашений по вызовам (calling conventions) в архитектуре x64 воспользовались возможностью расчистить все завалы, нагороженные в существующих соглашениях Win32 вроде __stdcall, __cdecl, __fastcall, _thiscall и т. д. В Win64 только одно "родное" соглашение по вызовам, и модификаторы наподобие __cdecl игнорируются компилятором. Такое резкое сокращение числа соглашений - великое благо, в том числе для отладки.

Главное, что надо знать о соглашении по вызовам на платформе x64, - оно похоже на x86-соглашение fastcall. В x64-соглашении первые четыре целочисленных аргумента (слева направо) передаются в 64-битных регистрах, предназначенных специально для этой цели:

RCX: 1-й целочисленный аргумент
RDX: 2-й целочисленный аргумент
R8: 3-й целочисленный аргумент
R9: 4-й целочисленный аргумент

Остальные целочисленные аргументы передаются через стек. Указатель this считается целочисленным аргументом, поэтому он всегда помещается в регистр RCX. Что касается параметров со значениями с плавающей точкой, то первые четыре из них передаются в регистрах XMM0-XMM3, а последующие - через стек потока.

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

Рассмотрим пример. Допустим, некая функция передает два целочисленных параметра дочерней функции. Компилятор не только запихнет эти значения в регистры RCX и RDX, но и вычтет 32 байта из регистра RSP (указателя стека). Вызываемая функция может обратиться к параметрам через регистры RCX и RDX. Если же коду этой функции данные регистры понадобятся для какой-то иной цели, он сможет скопировать их содержимое в зарезервированное пространство стека размером 32 байта. На рис. 1 показаны регистры и стек после передачи шести целочисленных параметров.

Передача целочисленных аргументов Рис. 1. Передача целочисленных аргументов

Очистка параметров в стеке несколько необычна в x64-системах. С технической точки зрения, за очистку стека отвечает вызвавшая функция, а не вызываемая. Но, кроме как в прологе и эпилоге, вы редко где увидите изменение регистра RSP. В отличие от компилятора x86, который явно добавляет параметры в стек и удаляет их из него с помощью инструкций PUSH и POP соответственно, компилятор x64 резервирует в стеке пространство, достаточное для вызова функции с самым длинным списком параметров. А потом он снова и снова использует это пространство для задания параметров при вызовах дочерних функций.

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

Вновь обратимся к примеру. Возьмем x64-функцию, вызывающую три другие функции. Первая из них принимает четыре параметра (0x20 байтов), вторая - 12 (0x60 байтов), а третья - восемь (0x40 байтов). В прологе сгенерированный код просто резервирует 0x60 байтов в стеке и копирует в соответствующие участки выделенного пространства значения параметров, так что места хватит и для вызова функции с самым длинным списком параметров.

Хорошее и более глубокое описание соглашения по вызовам на платформе x64 можно найти в комментарии Рэймонда Чена (Raymond Chen) по ссылке blogs.msdn.com/58579.aspx. Я не буду тратить драгоценное место в статье на все детали, а лишь подчеркну несколько важных нюансов. Во-первых, целочисленные параметры с размером менее 64 битов дополняются знаком, а затем все равно передаются через соответствующие регистры (если они относятся к первым четырем целочисленным параметрам). Во-вторых, ни при каких условиях ни один параметр не должен оказаться по такому адресу стека, который не кратен 8 байтам; иначе будет нарушено 64-битное выравнивание. Любые аргументы, не равные 1, 2, 4 или 8 байтам (в том числе структуры), передаются по ссылке. И наконец, структуры (struct) и объединения (union) размером 8, 16, 32 или 64 бита передаются так, будто это целые значения того же размера.

Возвращаемое значение функции помещается в регистр RAX. Исключение делается для арифметических типов с плавающей точкой, которые возвращаются в XMM0. Между вызовами нужно сохранять содержимое регистров RBX, RBP, RDI, RSI и R12-R15. Значения в регистрах RAX, RCX, RDX и R8-R11 можно изменять и удалять.

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

Принудительное введение стандарта на создание фрейма стека для функции - один из способов, благодаря которым ОС гарантирует (теоретически), что стек всегда можно будет просмотреть без особых ухищрений. Помимо стандартизированных прологов, компилятор и компоновщик должны создавать соответствующие записи в таблице функций. Для интересующихся сообщу, что эта таблица представляет собой массив элементов IMAGE_FUNCTION_ENTRY64, определенных в winnt.h. Как ее найти? На нее указывает элемент IMAGE_DIRECTORY_ENTRY_EXCEPTION в поле DataDirectory заголовка PE-файла.

Итак, я рассмотрел основные особенности архитектуры, не вдаваясь в детали. Усвоив одни лишь эти основы и зная язык ассемблера на 32-разрядной платформе, вы сможете быстро понять смысл x64-инструкции в отладчике. Ну а мастерство, как всегда, придет с опытом.

Разработка для x64 с помощью Visual C++

Хотя x64-код можно было писать в Microsoft C++ до появления Visual Studio 2005, это было весьма неудобно. Поэтому здесь я исхожу из того, что вы работаете в Visual Studio 2005 и что вы выбрали инструментарий для платформы x64, который по умолчанию не устанавливается. Я также предполагаю, что у вас уже есть какой-то Win32-проект (пользовательского режима) на C++, который вы хотите компилировать для обеих платформ - как x86, так и x64.

Первый шаг в компиляции программы для x64 - создание конфигурации 64-разрядной сборки. Как пользователь Visual Studio, вы прекрасно знаете, что по умолчанию у ваших проектов две конфигурации сборки: Debug и Retail. Поэтому остается создать еще две конфигурации: Debug и Retail для x64.

Начните с загрузки существующего проекта/решения. В меню Build выберите Configuration Manager. В диалоговом окне Configuration Manager в раскрывающемся списке Active Solution Platform выберите New (рис. 2). После этого вы должны увидеть диалог New Solution Platform.

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

Выберите x64 в качестве новой платформы (рис. 3), прочие параметры оставьте в состоянии по умолчанию и щелкните OK. Вот и все! Теперь у вас должно быть четыре конфигурации сборки: Win32 Debug, Win32 Retail, x64 Debug и x64 Retail. Переключаться между ними вы будете через Configuration Manager.

Выбор платформы сборки Рис. 3. Выбор платформы сборки

Теперь посмотрим, насколько совместим с x64 ваш код. Создайте конфигурацию x64 Debug по умолчанию и соберите проект. Если его код не тривиален, все шансы за то, что вы получите при компиляции ошибки, не встречавшиеся в Win32-конфигурации. Но справиться с этими проблемами и сделать код действительно совместимым как с Win32, так и с x64 сравнительно легко, если только вы не нарушали все принципы написания портируемого C++-кода. И не потребуются тонны директив условной компиляции.

Как сделать код совместимым с Win64

Вероятно, при преобразовании Win32-кода в x64-код больше всего усилий понадобится для того, чтобы сохранить корректность ваших определений типов. Помните, что я говорил о системе типов в Win64? Используя Windows-типы, определенные через typedef в заголовочных файлах Windows, а не "родные" для компилятора C++ типы (int, long и др.), вы упростите себе написание чистого Win32-кода, способного работать на платформе x64. Например, если Windows передает вам HWND, не сохраняйте его в FARPROC просто потому, что это легко и удобно.

Вероятно, наиболее частая и легко устранимая ошибка, которую я встречал при переносе кода, вызвана предположением о том, что значение указателя может быть сохранено или перенесено в 32-разрядном типе вроде int, long или даже DWORD. Но вся штука в том, что указатели в Win32 и Win64 имеют разные размеры, а целочисленные типы остались прежней длины.

Здесь помогают типы _PTR, определенные в заголовочных файлах Windows. Такие типы, как DWORD_PTR, INT_PTR и LONG_PTR, позволяют объявлять переменные целочисленного типа, которые всегда имеют достаточный размер для хранения указателя на целевой платформе. Например, переменная, определенная как тип DWORD_PTR, является 32-битной целой при компиляции для Win32 и 64-битной при компиляции для Win64. Немного практики, и использование таких переменных станет вашей второй натурой: объявляя какой-либо тип, вы всегда будете спрашивать себя, нужен здесь DWORD или DWORD_PTR?

Возможны ситуации, где надо точно указывать, сколько именно байтов следует отвести под целый тип. На такие случаи в тех же заголовочных файлах (Basetsd.h и др.), где определяются DWORD_PTR и прочие типы, предлагаются определения целых специфической длины, например INT32, INT64, INT16, UINT32 и DWORD64.

Еще одна проблема, связанная с различиями в размерах типов, относится к форматированию вывода printf и sprintf. Вот я раньше часто грешил конструкциями %X или %08X при форматировании значений указателей и был строго наказан за это при запуске подобного кода в x64-системе. Правильный способ - использование %p, при котором автоматически учитывается размер указателя на целевой платформе. Кроме того, printf и sprintf поддерживают префикс I для типов, размер которых зависит от платформы. Скажем, для вывода значения переменной UINT_PTR можно было бы использовать %Iu. Если же вы точно знаете, что переменная всегда будет 64-битной знаковой, то могли бы указать %I64d.

Вычистив ошибки, вызванные определениями типов, неподходящими для Win64, вы все равно можете остаться с кодом, который работает только на платформе x86. Тогда, вероятно, лучше пойти по простейшему пути и написать две версии функции: одну - для Win32, другую - для x64. И здесь вам очень пригодится набор макросов препроцессора:

_M_IX86
_M_AMD64
_WIN64

Правильное использование макросов препроцессора очень важно для написания корректного кросс-платформенного кода. Макросы _M_IX86 и _M_AMD64 применяются только при компиляции под определенную платформу, а _WIN64 - при компиляции для любой 64-разрядной версии Windows, в том числе выпуска для процессоров Itanium.

Пытаясь применить макрос препроцессора, хорошенько подумайте о том, чего вы добиваетесь. Например, действительно ли ваш код специфичен только для процессоров x64? Если да, пишите:

#ifdef _M_AMD64

С другой стороны, если тот же код мог бы работать и на x64, и на Itanium, лучше сделать так:

#ifdef _WIN64

Для себя я принял полезное правило при использовании любого из этих макросов: всегда создавать явные варианты #else (подчеркиваю, явные!), чтобы можно было быстро понять, не пропущено ли что-то. Для примера возьмем плохо написанный код:

#ifdef _M_AMD64
// Здесь находится x64-код
#else
// Здесь находится x86-код
#endif

Что будет, если я теперь скомпилирую его для третьей процессорной архитектуры? Сам того не желая, я скомпилирую x86-код. Гораздо лучше переделать предыдущий код примерно так:

#ifdef _M_AMD64
// Здесь находится x64-код
#elif defined (_M_IX86)
// Здесь находится x86-код
#else
#error !!! Нужно написать код для этой архитектуры
#endif

Одна из частей моего Win32-кода, которую удалось перенести на платформу x64 лишь с большим трудом, - подставляемый (inline) ассемблерный код, не поддерживаемый Visual C++ для x64. Но не бойтесь, любители ассемблера. Существует 64-разрядный MASM (ML64.exe); его вместе с документацией можно получить через MSDN. ML64.exe и другие инструменты для x64 (в том числе CL.EXE и LINK.EXE) доступны из командной строки. Чтобы настроить нужные переменные окружения, достаточно запустить файл VCVARS64.BAT.

Отладка

И вот вы наконец добились чистой компиляции Win32- и x64-версий своего кода. Остался последний фрагмент головоломки - выполнение и отладка этого кода. Хотя вы скомпилировали свою x64-версию на компьютере с процессором x64, для отладки в режиме x64 понадобятся средства удаленной отладки, предоставляемые Visual Studio. К счастью, если вы работаете с Visual Studio IDE на 64-разрядной машине, IDE сама позаботится обо всех необходимых операциях. Если по какой-то причине вы не можете использовать удаленную отладку, остается лишь один вариант - взять x64-версию WinDbg (ее можно скачать по ссылке microsoft.com/whdc/devtools/debugging/install64bit.mspx (EN). Однако вы лишитесь многих удобств отладчика Visual Studio.

Если вы никогда не пользовались удаленной отладкой, сильно волноваться не стоит. Как только вы ее настроите, она почти ничем не будет отличаться от локальной отладки.

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

Далее из Visual Studio сконфигурируйте свой проект на применение удаленной отладки x64-кода. И для начала откройте окно свойств проекта (рис. 4).

Выбор платформы сборки Рис. 4. Выбор платформы сборки

Убедитесь, что текущей является ваша 64-разрядная конфигурация, и выберите Debugging в разделе Configuration Properties. Ближе к верхней части вы увидите раскрывающийся список Debugger to launch. Его изначальное значение - Local Windows Debugger. Смените его на Remote Windows Debugger. Под этим списком вы можете задать удаленную команду (например, имя программы), которая должна выполняться в момент начала отладки, а также указать имя удаленного компьютера и тип соединения.

Если вы все правильно настроили, можете начинать отладку своего x64-приложения точно так же, как и Win32-программы. О соединении с MSVSMON свидетельствует строка "connected", которая появляется в трассировочном окне этой программы при каждом успешном подключении отладчика. С этого момента большинство операций выполняется так же, как и в хорошо известном вам отладчике Visual Studio. Не забудьте открыть окно регистров, чтобы увидеть все 64-битные регистры, а также заглянуть в окно дизассемблированного кода, чтобы посмотреть на такой знакомый, но все же слегка другой ассемблерный x64-код.

Заметьте, что 64-разрядный минидамп в отличие от 32-разрядного нельзя напрямую загрузить в Visual Studio. Вместо этого нужно использовать Remote Debugging. Кроме того, в настоящий момент Visual Studio 2005 не поддерживает отладку interop-вызовов между управляемым и неуправляемым 64-разрядным кодом.

А как быть с управляемым кодом?

Одно из преимуществ кодирования с применением Microsoft .NET Framework заключается в том, что универсальный код абстрагируется от большей части нижележащей операционной системы. Более того, формат IL-инструкций независим от конкретной процессорной архитектуры. А значит, теоретически двоичный файл .NET-программы, скомпилированной в Win32-системе, можно запускать без модификации в x64-системе. Но на практике все не так гладко.

.NET Framework 2.0 выпускается и в x64-версии. После ее установки на свой компьютер с x64-системой я смог запускать те же исполняемые .NET-файлы, что и в предыдущей Win32-системе. Конечно, нет никакой гарантии, что любая .NET-программа будет одинаково хорошо работать и в Win32, и в x64 без перекомпиляции, но все же мои файлы выполнялись без ошибок.

Если ваш управляемый код явно вызывает неуправляемый (например, через P/Invoke на C# или Visual Basic), вас почти наверняка ждут проблемы при попытке его запуска в среде 64-разрядной CLR. Однако ключ /platform компилятора позволяет указать, на какой платформе должен выполняться ваш код. Так, вы могли бы разрешить выполнение вашего управляемого кода только в WOW64 несмотря на наличие 64-разрядной CLR.

Заключение

В общем, для меня переход на x64-версию Windows был сравнительно безболезненным. Как только вы усвоите основные различия (а их не так уж и много) в архитектурах операционных систем и инструментальных средств, вам будет достаточно легко поддерживать единую кодовую базу, способную работать на обеих платформах. Существенную помощь окажет Visual Studio 2005. К тому же чуть ли не каждый день на сайте http://technet.microsoft.com/sysinternals/default.aspx появляются x64-версии драйверов устройств и инструментов вроде Process Explorer.


К началу страницы К началу страницы

Показ:
© 2015 Microsoft