Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|||||||||||||||||||||||||||||||||||||||||||||
Все, что нужно знать, чтобы начать программировать для 64-разрядных версий Windows28.05.2014, 05:07. Показов 45884. Ответов 20
Метки нет Все метки)
(Все, что нужно знать, чтобы начать программировать для 64-разрядных версий Windows Автор: Мэтт Питрек оригинал статьи на английском перевод взят здесь Честно говоря, мне не слишком комфортно работать с новой операционной системой, если я еще не покопался в ее внутреннем устройстве. А покопаться "под капотом" я люблю. Так что, когда на сцену вышли 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=18.446.744.073.709.551.616), в настоящее время Win64 поддерживает 16 Тб — значение, которое представлено 44 разрядами (244=17.592.186.044.416). Почему же нельзя задействовать все 64 разряда, чтобы адресоваться к 16 экзабайтам памяти? По целому ряду причин. Начнем с того, что нынешние процессоры x64 обычно позволяют обращаться лишь к 40-разрядному представлению физической памяти (1 Тб = 240 = 1.099.511.627.776). Сама архитектура (но не современное оборудование) допускает расширение до 52 разрядов (4 петабайтов = 252 = 4.503.599.627.370.496). Даже если бы это ограничение было снято, размеры таблиц страниц, необходимых для проецирования такого громадного объема памяти, оказались бы просто гигантскими. Как и в Win32, адресуемая память делится на области пользовательского режима и режима ядра. Каждому процессу выделяется собственное уникальное пространство размером 8 Тб в нижней части памяти, а код режима ядра размещается в верхних 8 Тб и разделяется всеми процессами. У разных версий 64-разрядной Windows разные ограничения на объемы физической памяти (табл. 1 и 2). Так же, как и в 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). Помимо заголовка 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. Хотя перспективы работы в полностью 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, так как базовые знания набора команд процессора существенно упрощают разработку (особенно отладку).
Кстати, 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-разрядная инструкция:
Основное преимущество всех x64-регистров в том, что компиляторы наконец-то могут генерировать код, в котором большая часть параметров передается через регистры, а не стек. Заталкивая параметры в стек, неизбежно приходится обращаться по адресам памяти. А мы уже давно крепко усвоили, что обращение по адресам памяти, отсутствующим в кэше процессора, занимает несоизмеримо больше времени. При разработке соглашений по вызовам (calling conventions) в архитектуре x64 воспользовались возможностью расчистить все завалы, нагороженные в существующих соглашениях Win32 вроде __stdcall, __cdecl, __fastcall, _thiscall и т. д. В Win64 только одно "родное" соглашение по вызовам, и модификаторы наподобие __cdecl игнорируются компилятором. Такое резкое сокращение числа соглашений — великое благо, в том числе для отладки. Главное, что надо знать о соглашении по вызовам на платформе x64, — оно похоже на x86-соглашение fastcall. В x64-соглашении первые четыре целочисленных аргумента (слева направо) передаются в 64-битных регистрах, предназначенных специально для этой цели:
Хотя аргумент может быть передан в регистре, компилятор все равно резервирует для него место в стеке, уменьшая значение регистра 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. И здесь вам очень пригодится набор макросов препроцессора:
Пытаясь применить макрос препроцессора, хорошенько подумайте о том, чего вы добиваетесь. Например, действительно ли ваш код специфичен только для процессоров x64? Если да, пишите:
Отладка И вот вы наконец добились чистой компиляции Win32- и x64-версий своего кода. Остался последний фрагмент головоломки — выполнение и отладка этого кода. Хотя вы скомпилировали свою x64-версию на компьютере с процессором x64, для отладки в режиме x64 понадобятся средства удаленной отладки, предоставляемые Visual Studio. К счастью, если вы работаете с Visual Studio IDE на 64-разрядной машине, IDE сама позаботится обо всех необходимых операциях. Если по какой-то причине вы не можете использовать удаленную отладку, остается лишь один вариант — взять x64-версию WinDbg (ее можно скачать по ссылке microsoft.com/whdc/devtools/debugging/install64bit.mspx. Однако вы лишитесь многих удобств отладчика Visual Studio. Если вы никогда не пользовались удаленной отладкой, сильно волноваться не стоит. Как только вы ее настроите, она почти ничем не будет отличаться от локальной отладки. Первый шаг — установка на целевой компьютер 64-разрядной MSVSMON. Обычно это делается с помощью программы RdbgSetup, поставляемой с Visual Studio. После запуска MSVSMON зайдите в меню Tools для настройки соответствующих параметров защиты соединения между вашей 32-разрядной Visual Studio и экземпляром MSVSMON. Далее из Visual Studio сконфигурируйте свой проект на применение удаленной отладки x64-кода. И для начала откройте окно свойств проекта (рис. 4). Рис. 4. Выбор платформы сборки Если вы все правильно настроили, можете начинать отладку своего 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/s... fault.aspx появляются x64-версии драйверов устройств и инструментов вроде Process Explorer.to be continued
11
|
28.05.2014, 05:07 | |
Ответы с готовыми решениями:
20
Хочу начать программировать на iOS, нужно ли знать Cи? Что необходимо для того чтобы начать программировать на Java Что надо знать в java, чтобы хорошо программировать под android? |
Ушел с форума
![]() ![]() |
||
28.05.2014, 09:52 | ||
Mikl___, спасибо, замечательная статья !
Вот на таких энтузиастах все и держится. Если позволите, маленькая поправочка:
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
29.05.2014, 05:02 [ТС] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Программные соглашения x64 В данном разделе рассматривается методология соглашения о вызовах Visual C++ x64 для 64-битового расширения в архитектуре x86.Перевод статей MSDN Следующий параметр компилятора позволяет оптимизировать приложение для x64:
Общие сведения о соглашениях о вызовах для архитектуры x64 Двумя важнейшими различиями между архитектурами x86 и x64 является возможность 64-битной адресации и набор из 16 64-битных регистров общего назначения. Предоставляя расширенный набор регистров, x64 использует только соглашение о вызовах __fastcall и модель RISC-архитектуры обработки исключений. Модель соглашения о вызовах __fastcall использует регистры для первых четырех аргументов, а для передачи других параметров используется кадр стека. Типы и хранилище В этом разделе рассматривается перечисление и хранение типов данных архитектуры x64.Скалярные типы Несмотря на то, что обращение к данным возможно при любом выравнивании, в целях повышения производительности рекомендуется использовать выравнивание данных в исходном диапазоне. Перечисления представляют собой константы целого типа и обрабатываются как 32-разрядные целые числа. В следующей таблице приводится определение типа и рекомендуемый для него объем памяти в случае выравнивания с использованием следующих значений:
Статические выражения и объединения К другим типам, таким как массивы, структуры и объединения, предъявляются более строгие требования к выравниванию, обеспечивающие согласованность хранения статистических выражений и объединений и извлечения данных. Далее приведены определения массива, структуры и объединения.
Применяются следующие правила выравнивания статистических выражений:
Примеры выравнивания структуры В каждом из следующих примеров содержится объявление выровненной структуры или объединения. Порядок размещения таких структур или объединений в памяти показан на соответствующих рисунках. Каждый столбец на рисунке соответствует байту в памяти. Номер столбца определяет смещение указанного байта. Имя второго столбца на каждом рисунке соответствует имени переменной в объявлении. Затененные столбцы определяют заполнение, необходимое для указанного типа выравнивания. Пример 1 Пример 2 Пример 3 Пример 4 Разряды Структура битовых полей ограничивается 64 битами и может быть следующих типов: signed int, unsigned int, int64 или unsigned int64. Битовые поля, которые пересекают границу типов, пропустят биты, чтобы выровнять разряды до уровня следующего типа. Например, разряды целого числа (integer) не могут пересечь 32-разрядную границу. Конфликты с компилятором x86 Типы данных, размер которых превышает 4 байта, не выравниваются в стеке автоматически при компиляции приложения с помощью компилятора x86. Поскольку архитектура компилятора x86 представляет собой выровненный 4-байтовый стек, что-либо большее, чем 4 байта, например, 64-разрядное целое число, не может автоматически выравниваться по 8-байтовому адресу. Работа с данными без выравнивания имеет два ограничения.
Использование регистров Архитектура x64 поддерживает 16 регистров общего назначения (в дальнейшем называемых целочисленными регистрами), а также 16 регистров XMM, используемых для операций с плавающей запятой. Временные регистры сбрасываются в процессе выполнения вызова. Постоянные регистры должны сохранять свои значения в процессе выполнения функции и должны сохраняться вызываемым объектом в случае использования. В следующей таблице описываются способы использования каждого регистра в процессе выполнения вызова функции:
Соглашение о вызовах Машинный интерфейс для приложений Application Binary Interface (ABI) x64 — это 4 регистра соглашения о вызовах с возможностью последующего возвращения этих регистров в стек. Существует точное однозначное соответствие между аргументами в функции и регистрами для этих аргументов. Любые аргументы, не равные 8 байтам или 1, 2, 4 или 8 байтам, передаются по ссылке. Попыток разместить один аргумент по нескольким регистрам не происходит. Регистровый стек x87 не используется. Его можно использовать, но он должен быть временным при вызове функций. Все операции с плавающей запятой выполняются с помощью 16 регистров XMM. Аргументы передаются в регистрах RCX, RDX, R8 и R9. Если аргументы являются типами float или double, они передаются в регистрах XMM0L, XMM1L, XMM2L и XMM3L. 16-байтовые аргументы передаются по ссылке. Передача параметров подробно описана в разделе Передача параметров. Кроме того, регистры RAX, R10, R11, XMM4 и XMM5 являются временными. Все остальные регистры не являются временными. Использование регистров подробно описано в разделах Использование регистров и Сохраняемые регистры вызываемого и вызывающего объектов. Вызывающая функция отвечает за выделение пространства для параметров вызываемой функции и должна всегда выделять достаточное пространство для 4 параметров, даже если вызываемая функция не содержит такого количества параметров. Это помогает упростить поддержку функций без прототипов и функций с переменным количеством аргументов (vararg) C/C++. Для функций с переменным количеством аргументов или для функций без прототипов любое значение типа float должно быть продублировано в соответствующем регистре общего назначения. Любые параметры, следующие после первых 4, до вызова должны сохраняться в стеке над резервным хранилищем для первых четырех. Сведения о функции с переменным количеством аргументов представлены в разделе Функции с переменным количеством аргументов (Varargs). Сведения о функции без прототипов представлены в разделе Функции без прототипа. Выравнивание Большинство структур выровнены естественным выравниванием. Главными исключениями являются указатели стека и функции распределения памяти malloc или alloca, которые выровнены на 16 байт для сохранения производительности. Выравнивание свыше 16 байт должно выполняться вручную, но начиная с 16 байт выполняется общее выравнивание размера для операций XMM, которого должно хватать для большей части кода. Дополнительные сведения о структуре и выравнивании см. в разделе Типы и хранилище. Дополнительные сведения о стеке см. в разделе Использование стека. Способность очищаться Все конечные функции (функции, которые никогда не вызывают функцию, а также никогда не выделяют пространство стека) должны дополняться данными (относится к типам данных xdata или ehdata, на которые есть указатель из pdata), которые объясняют ОС, каким образом выполнять их очищение для сохранения неизменяемых регистров. Прологи и эпилоги строго ограничены, следовательно, они могут быть правильно описаны в xdata. Указатель стека должен быть выровнен на 16 байт, за исключением конечных функций, в любой области кода, которая не является частью эпилога или пролога. Дополнительные сведения о структуре функции пролога и эпилога см. в разделе Пролог и эпилог. Соглашение о вызовах Передача параметров Первые четыре целочисленных аргумента передаются через регистры. Целочисленные значения передаются слева направо в регистры RCX, RDX, R8 и R9. Аргументы начиная с пятого и далее передаются через стек. Все 32-разрядные аргументы получают в регистрах знаковое расширение. Это делается для того, чтобы вызываемый мог игнорировать старшие разряды регистра при необходимости и получить доступ только к необходимой части регистра. Вещественные 64-разрядные аргументы передаются в регистры XMM0 – XMM3 (до 4) с помощью гнезда целых чисел (RCX, RDX, R8 и R9), которое обычно и используется, в то время как гнездо кардинальных чисел отклоняется (см. пример) и наоборот. Типы, массивы и строки __m128 никогда не передаются непосредственно, а указатель передается в выделенной памяти вызывающим объектом. Структуры размером в 8, 16, 32 или 64 бита и __m64, передаются так как если бы они были целыми числами того же размера. Структуры или объединения, отличный от этих размеров передаются как указатель на выделенную память вызывающим объектом. Для этих агрегатных типов, передаваемых в качестве указателя (включая __m128), память должна кратна 16. Встроенные функции, которые не выделяют пространства в стеке и не вызывают других функций, могут использовать иные изменяемые регистры для передачи дополнительных аргументов в регистры, так как существует тесная связь между компилятором и реализацией встроенной функции. Это дополнительная возможность увеличения производительности. Тот, кто вызывает функцию, несет ответственность за сбрасывание при необходимости параметров регистра в теневое пространство. В следующей таблице подведены итоги передачи параметров:
Примеры:
Функции с переменным количеством аргументов (Varargs) Если параметры передаются с помощью функции varargs (например, аргументы, задаваемые многоточием), то фактически применяется обычная передача параметра, включая вытеснение пятого и последующих аргументов. Кроме того, вызываемый отвечает за дамп аргументов, которые получают свой адрес. Только для значений с плавающей запятой: целочисленный регистр и регистр с плавающей запятой содержат значение типа float в случае, если вызываемый ожидает значение в целочисленных регистрах. Функции без прототипа Для функций без прототипа вызывающий объект передает целые числа в виде значений типа Integer, а значения с плавающей запятой — в виде чисел двойной точности. (Только для значений с плавающей запятой) Если вызываемый объект предполагает наличие значения в регистре операций с целыми числами, в регистрах операций с целыми числами и числами с плавающей запятой одновременно будут содержаться значения с плавающей запятой.
Возвращаемые значения Возвращаемое значение, которое может быть размещен в 64—разрядном регистре RAX это включает типы __m64, но __m128, __m128i, __m128d, расположенном и типы double возвращаются в XMM0. Если возвращаемое значение пользовательского типа, который нельзя разместить в 64—разрядах и вызывающий объект принимает за выделение и передача указателя для возвращаемого значения в качестве первого аргумента. Последующие аргументы перемещают на один аргумент вправо. Тот же самый указатель возвращается вызываемой стороной в RAX. Пользовательские типы, которые должны возвращать непосредственно от 1, 2, 4, 8, 16, 32 и 64 — в длину. Примеры:
Сохраняемые регистры вызываемого и вызывающего объектов Регистры RAX, RCX, RDX, R8, R9, R10 и R11 считаются временными и должны уничтожаться при вызове функции (если иное не требуется, исходя из соображений безопасности, например в процессе оптимизации программы). Регистры RBX, RBP, RDI, RSI, RSP, R12, R13, R14 и R15 считаются защищенными. Значения этих регистров должны сохраняться и восстанавливаться в использующей их функции. Указатели функций Указатели функций указывают на метку соответствующей функции. Требования к оглавлению для указателей функций не предусмотрены. Использование стека Все адреса памяти, лежащие вне текущего адреса RSP, считаются временными. Во время действия обработчика прерываний или выполнения пользовательского сеанса отладки эти адреса могут перезаписываться с помощью функций операционной системы или отладчика. Таким образом, перед чтением значений из кадра стека или записью в него, необходимо задать RSP. Выделение памяти в стеке Выделение памяти в стеке для локальных переменных, сохраненных регистров, а также параметров стека и регистра осуществляется в прологе функции. Область параметров всегда располагается в нижней части стека (даже если используется функция alloca) и всегда примыкает к адресу, возвращаемому при вызове функции. В области параметров содержится как минимум четыре записи и всегда выделяется объем памяти, достаточный для хранения всех параметров любой вызываемой функции. Обратите внимание, что для параметров регистра память выделяется всегда, даже если они никогда не размещаются в стеке. Также гарантированно выделяется память для всех параметров вызываемого объекта. Чтобы обеспечить выделение непрерывной области памяти при вызове функции, принимающей адрес списка аргументов (va_list) или отдельного аргумента, необходимо предоставить внутренние адреса аргументов регистра. В этой области также хранятся аргументы регистра во время выполнения преобразователя. Кроме того, в ней удобно хранить аргументы регистра, используемые в качестве параметров отладки (например поиск аргументов в процессе отладки является более эффективным, если они хранятся по внутреннему адресу в коде пролога). Даже если вызываемая функция принимает менее четырех параметров, для нее выделяется четыре ячейки стека, которые могут использоваться не только для хранения значений регистра параметров, но и для других целей. Таким образом, во время вызова функции вызывающий объект не может сохранять данные в этой области стека. Если память для функции выделяется динамически (с помощью функции alloca), необходимо использовать защищенный регистр в качестве указателя кадра, с помощью которого помечается основание фиксированной части стека, а также указывается необходимость сохранения и инициализации регистра в прологе. Обратите внимание, что при использовании функции alloca при вызове одного и того же объекта из одного вызывающего объекта для параметров регистра могут использоваться различные внутренние адреса. В стеке всегда используется 16-байтовое выравнивание, за исключением пролога (например при помещении возвращаемого адреса) и других случаев, определенных в разделе Типы функций для конкретных классов или функций кадра. В следующем примере описывается структура стека, в которой функция A вызывает неоконечную функцию B. В прологе функции A уже выделена память в нижней части стека для всех параметров регистра и стека, необходимых для выполнения функции B. В результате вызова в стек помещается возвращаемый адрес, а в прологе функции B выделяется память для ее локальных переменных и защищенных регистров, а также память, необходимая для вызова функций из функции B. Если для функции B используется функция alloca, память выделяется между областью для хранения локальных переменных или защищенного регистра и областью стека параметра. При вызове другой функции из функции B возвращаемый адрес помещается непосредственно под внутренним адресом регистра RCX. Динамическое создание параметра области стека Если используется указатель кадров, то параметр используется для динамического создания параметра области стека. В настоящее время в компиляторе x64 это не выполняется. Типы функций Существует два основных типа функций. Функция, которой требуется кадр стека, называется функцией с кадром. Функция, которая не требует кадр стека, называется конечной функцией. Функция со кадром — это функция, которая распределяет пространство в стеке, вызывает другие функции, сохраняет защищенные регистры или использует обработчик исключений. А также требует наличие записи в таблице функции. Функция с кадром требует наличие пролога и эпилога. Функция с кадром позволяет динамически распределять пространство в стеке и использовать указатель кадра. Функция с кадром имеет все необходимые характеристики для использования вызова этого стандарта. Если функция с кадром не вызывает другую функцию, то выравнивать стеки не нужно (см. раздел Выделение памяти в стеке). Конечная функция является единственной функцией, которая не требует наличия записи в таблице функции. Она не вызывает другие функции, не распределяет пространство и не сохраняет защищенные регистры. Функции позволено не выравнивать стек во время своего выполнения. Выравнивание с помощью функции malloc malloc гарантированно возвращает память, которая подходит для хранения любого объекта с базовым выравниванием, который мог бы поместиться в выделенной памяти. Базовое выравнивание — это выравнивание, которое меньше или равно наибольшему выравниванию, которое поддерживается реализацией без задания выравнивания. (В Visual C++ это основное выравнивание, необходимое для double или 8 байт. В коде, который нацелен на 64-разрядные платформы, это 16 байт.) Например, выделение 4 байт будет выровнено по границе, которая поддерживает все любой четырехбайтовый или меньший объект. Visual C++ допускает типы, имеющие расширенное выравнивание, также известные как сверх-выровненные типы. Например, типы SSE __m128 и __m256, а также типы, объявленные с помощью __declspec(align(n)), где n больше 8, имеют расширенное выравнивание. Выравнивание памяти на границе, которая подходит для объекта, который требует расширенного выравнивания, не гарантированного malloc. Чтобы выделить память для избыточно выровненных типов, используйте _aligned_malloc и соответствующие функции. alloca Функции _alloca требуется выравнивание по 16-байтовой границе и использование указателя кадра стека. Выделяемый стек должен включать расположенное под ним пространство для параметров функций, вызываемых позднее, как описано в разделе Выделение памяти в стеке. Пролог и эпилог Для каждой функции, в которой выделяется память в стеке, вызываются другие функции, сохраняются защищенные регистры или выполняется обработка исключений, необходимо использовать пролог. На адрес пролога накладываются ограничения, описываемые в данных завершения, которые связаны с соответствующей записью таблицы функций. При необходимости в прологе сохраняются регистры аргумента (по внутренним адресам), помещаются в стек защищенные регистры, выделяется фиксированная часть стека для локальных и временных переменных, а также создается указатель кадра. В связанных данных завершения описывается действие пролога, а также предоставляются сведения, используемые для отмены результатов выполнения кода пролога. Если выделяемая фиксированная часть стека занимает более одной страницы (более 4096 байт), выделяемая область стека может располагаться на нескольких страницах виртуальной памяти. В этом случае необходимо проверить выделяемую область перед ее фактическим выделением. Для этих целей используется специальная процедура, которая вызывается из пролога и не уничтожает регистры аргумента. При сохранении защищенных регистров рекомендуется перемещать их в стек до выделения фиксированной части стека. Если выделение фиксированной части стека выполняется до сохранения защищенных регистров, для обращения к области сохраненного регистра в большинстве случаев требуется 32-разрядное смещение. Производительность функций помещения и перемещения регистров примерно одинакова и будет оставаться такой в ближайшем будущем, независимо от предполагаемой зависимости между функциями помещения. Защищенные регистры могут сохраняться в любом порядке. Однако в качестве первой операции с защищенным регистром в прологе необходимо выполнять сохранение регистра. В этом прологе аргумент регистра RCX сохраняется по внутреннему адресу, сохраняются защищенные регистры R13-R15, выделяется кадр фиксированной части кадра стека, а также создается указатель кадра, который указывает на выделенную фиксированную область размером 128 байт. Благодаря использованию смещения обеспечивается обращение к большему числу адресов выделенной фиксированной области с помощью однобайтовых смещений. Если размер фиксированной области памяти превышает размер одной страницы памяти или равен ему, перед изменением RSP следует вызвать вспомогательную функцию. Вызываемая функция __chkstk обеспечивает проверку подлежащей выделению области стека на предмет допустимости расширения стека. В этом случае приведенный выше пример пролога будет выглядеть следующим образом:
Код эпилога существует для каждого выхода в функции. В большинстве случаев используется один пролог, но допускается использование нескольких эпилогов. В коде эпилога выполняется усечение стека до размера фиксированной выделяемой области (при необходимости), отменяется выделение фиксированной части стека, восстанавливаются значения защищенных регистров (посредством извлечения их сохраненных значений из стека), после чего управление возвращается вызывающей функции. В коде эпилога необходимо придерживаться строгого набора правил, применяемых к коду завершения, что позволяет обеспечить безопасное завершение без вызова исключений и прерываний. Это позволяет уменьшить объем используемых данных завершения, поскольку не используются дополнительные данные для описания каждого эпилога. Вместо этого выполнение эпилога определяется в коде завершения посредством прямого просмотра потока кода для идентификации эпилога. Если в функции не используется указатель кадра, в эпилоге сначала отменяется выделение фиксированной части стека, затем извлекаются сохраненные значения защищенных регистров, после чего управление возвращается вызывающей функции. Например:
Обратите внимание, что если указатель кадра не используется, в эпилоге необходимо использовать выражение add RSP,constant для отмены выделения фиксированной части стека. Использование вместо него выражения lea RSP,[RSP+constant] не допускается. Это ограничение позволяет уменьшить число шаблонов, распознаваемых при поиске эпилогов. Если эти правила соблюдаются, в коде завершения определяется выполняемый в данный момент эпилог и имитируется выполнение оставшейся части эпилога, что позволяет воссоздать контекст вызывающей функции. Обработка исключений (x64) В данном разделе рассматриваются структурная обработка исключений и поведение приложений C++ на платформах x64 при обработке исключений. Данные раскрутки для обработки исключений и поддержки отладчика В данном подразделе описываются структуры данных, необходимые для обработки исключений, а также поддержки отладчика. структура RUNTIME_FUNCTION Для табличной обработки исключений требуется запись в таблице для каждой функции, выделяющей место в стеке или вызывающей другую функцию (например, неконечные функции). Записи в таблице функций имеют следующий формат:
структура UNWIND_INFO Информационная структура очищения данных используется для записи эффектов функции на указатель стека и места в стеке, где сохраняются неизменяемые регистры:
структура UNWIND_CODE Массив кода раскрутки используется для записи последовательности операций в прологе, оказывающих влияние на энергонезависимые регистры и RSP. Каждый элемент кода имеет следующий формат:
структуры связанных данных раскрутки Если установлен флаг UNW_FLAG_CHAININFO, то структура информации очистки является вторичной и общее поле обработчика исключений/связанных данных содержит первичную информацию раскрутки. Следующий код извлекает основные сведения очистки, при условии что unwindInfo — структура, имеющая установленный флаг UNW_FLAG_CHAININFO.
Связанные сведения можно также использовать для группировки сохраненных данных энергозависимых регистров. Компилятор может отложить сохранение некоторых энергозависимых регистров до выхода из пролога записи функции. Они могут быть записаны перед группированным кодом посредством использования основных данных раскрутки для части функции и последующей установки связанных данных с ненулевым размером пролога. При этом коды раскрутки в связанных данных будут отражать сохраненные данных энергозависимых регистров. В этом случае все коды раскрутки являются экземплярами UWOP_SAVE_NONVOL. Команда, которая сохраняет слаболетучие регистры с помощью ПРИНУДИТЕЛЬНО ОТПРАВИТЬ или изменить регистр RSP с помощью дополнительного фиксированного выделение стека не поддерживается. Элемент UNWIND_INFO, имеющий набор UNW_FLAG_CHAININFO, может содержать запись RUNTIME_FUNCTION, чей элемент UNWIND_INFO также имеет набор UNW_FLAG_CHAININFO(множественный изолированный код). В конечном счете, указатели связанных данных раскрутки достигнут элемент UNWIND_INFO, для которого флаг UNW_FLAG_CHAININFO не установлен. Этот элемент будет являться основным элементом UNWIND_INFO, указывающим на фактическую точку входа процедуры. Процедура очистки Массив кода очистки сортируется в убывающем порядке. При возникновении исключения полный контекст сохраняется операционной системой в записи контекста. После этого вызывается логика обработки исключений, несколько раз выполняющая следующие операции по поиску обработчика исключения.
Наименьший набор данных очистки имеет размер в 8 байтов. Это позволило бы создать функцию, занимающую в стеке не более 128 байтов, и сэкономить независимый регистр. Это также показывает размер связанной структуры данных очистки для пролога нулевой длины без кодов очистки. Обработчик конкретного языка Относительный адрес обработчика языка присутствует в UNWIND_INFO, когда бы ни были установлены флаги UNW_FLAG_EHANDLER или UNW_FLAG_UHANDLER. Как описано в предыдущем разделе, обработчик языка вызывается как часть поиска обработчика исключения или часть раскрутки. Он имеет следующий прототип:
EstablisherFrame представляет собой адрес базы фиксированного расположения стека для данной функции. ContextRecord указывает на контекст исключения во время его возникновения (в случае если задействован обработчик событий) или текущий контекст "раскрутки" (в случае если задействован обработчик завершения). DispatcherContext указывает на контекст диспетчера для данной функции. Он имеет следующее определение:
ImageBase представляет собой основу образа (адрес загрузки) модуля, содержащего данную функцию, которую необходимо добавить в 32-битные смещения, используемые в записи функции, а также в информации раскрутки для записи относительных адресов. FunctionEntry предоставляет указатель записи функции RUNTIME_FUNCTION, содержащей саму функцию и относительные адреса информации раскрутки основного образа для данной функции. EstablisherFrame представляет собой адрес базы фиксированного расположения стека для данной функции. TargetIp предоставляет адреса выборочных инструкций, указывающие дополнительные адреса раскрутки. Этот адрес пропускается, если не было указано EstablisherFrame. ContextRecord указывает на контекст исключения, используемый кодом диспетчеризации или раскрутки системного исключения. LanguageHandler указывает на подпрограмму вызванного языкового обработчика. HandlerData указывает на данные языкового обработчика для данной функции. Завершение вспомогательных процедур для MASM Для написания правильных подпрограмм ассемблера применяется набор псевдоопераций, которые могут использоваться параллельно с фактическими инструкциями ассемблера для создания соответствующих файлов PDATA и XDATA. Также предусмотрен набор макросов, позволяющих упростить использование псевдоопераций в наиболее распространенных случаях.Необработанные псевдооперации В этом разделе перечислены псевдооперации.
Макросы MASM Для упрощения использования операций, описанных в разделе Необработанные псевдооперации, в файле ksamd64.inc определен набор макросов, которые можно использовать для создания типичных прологов и эпилогов процедур.
Описание раскрутки данных в языке C Далее следует описание раскрутки данных в языке С.
to be continued
3
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|
29.05.2014, 06:16 [ТС] | |
Где взять 64-разрядный компилятор? 64-разрядный ассемблерный транслятор не распространяется отдельно, но ml64.exe и link.exe можно получить бесплатно (пожалуйста, прочитайте и соблюдайте "Лицензионное соглашение" ![]()
После установки WDK ml64.exe и link.exe содержатся в папках C:\WinDDK\7600.16385.1\bin\x86\amd\ После установки SDK ml64.exe и link.exe содержатся в папках C:\Program Files\Microsoft Visual Studio 10.0\VC\bin\x86_amd64\ Обычно используют 64-битный link из папки amd64, а ml64 уже 32-битный из папки x86_amd64. В папке amd64 есть 64-битная версия ml64, но при работе с большим количеством макросов он почему-то зависает или просто медленно работает. В конце концов выбор за Вами ![]() На сайте http://dsmhelp.narod.ru/environment.htm можно найти набор lib- и inc-файлов для создания 64-разрядных приложений на ассемблере, а также кучу полезных макросов to be continued
3
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|||||||||||
08.06.2014, 10:44 [ТС] | |||||||||||
Простейшее приложение для Win64 Итак базовые сведения о Win64 мы получили, теперь напишем простейшее приложение выводящее на экран фразу "Win64 Assembly is Great!" и посмотрим, как это сделать на разных диалектах ассемблераMessageBox на nasm MessageBox на fasm MessageBox на masm MessageBox на JWasm to be continued
3
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
11.06.2014, 10:38 [ТС] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Уменьшаем размер приложения для Win64 Простейшее приложение выводящее на экран фразу "Win64 Assembly is Great!" на диалекте MASMВ результате получаем ехе-файл в 2560 байт. При просмотре внутренностей ехе-файла программой hiew32 видно, что 90% содержимого нули. Возникает законный вопрос, а как уменьшить размер программы, но чтобы при этом не терялась ее функциональность?
при компиляции получаем сообщение LINK : warring LNK4108: /ALIGN specified without /DRIVER; image may not run размер файла msgbox.exe меняется с 2560 на 1536 байт и, не смотря, на предупреждение image may not run файл msgbox.exe благополучно запускается
msgbox.obj : fatal error LNK1164: section 0x1 alignment (16) greater then /ALIGN value похоже, что в этом направлении мы достигли предела, хотя от исходной программы в 2560 байт пришли к программе в 848 байт, а это согласитесь не плохо!Наша программа использует два сегмента, сегмент кода и сегмент данных, посмотрите внимательно через hiew32 ― между этими сегментами прослойка из нулей, от которых мы и пытаемся избавится. А помните во времена DOS'а можно было создавать COM-файлы, которые в единственном сегменте содержал и код, и стек, и данные? А нельзя ли и здесь создать, что-то подобное?
hiew32 показывает, что в хвосте нашего файла, сразу за строкой «kernel32.dll» целых двенадцать байтов содержащих нули. А не удалить ли их нам вручную?
Код нашей программы начинается с 1F8h=504 байта. Всё, что выше ― это заголовок нашего EXE-файла ― вот бы его уменьшить! Заголовок нашего файла состоит из двух частей. От строки «MZ» до строки «PE» DOS-заголовок (DOS-stub) (0C0h=192 байта) и от строки «PE» до 21Fh PE-заголовок (352 байта). Адрес строки «PE» содержится в DOS-заголовке в двойном слове по смещению 3Ch. Начнем с уменьшения DOS-stub'а. Возьмем hiew32.exe и создадим с его помощью вот такой файл:
to be continued
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||
12.06.2014, 12:13 [ТС] | |||||||||||||||||||||||||||||||||||||||||||||||||||||
Уменьшаем размер приложения для Win64 продолжение
3
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
08.07.2014, 11:27 [ТС] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
5.
сокращаем DOS-stub на 16 байтов, смещение e_lfanew оказывается внутри PE-заголовка. Так как у программы со стабом короче 64 байт смещение от начала файла 3Ch (поле e_lfanew) попадает уже внутрь PE-заголовка, то нужно, чтобы он не попал на поле PE-заголовка имеющее критическое значение при загрузке файла. Помещаем указатель на ntHeader в поле Symbol_table_offset. При размещении в нем числа 30h получаем работоспособное приложение.
сокращаем DOS-stub еще на 16 байтов, смещение e_lfanew оказывается в поле Size_of_code. По одним источникам ― это поле используется для первичного отведения памяти под приложение. По другим ― не используются вообще. Если поместить туда число 20h, то практическая проверка показывает, что приложение с таким stub'ом работает нормально.
сокращаем DOS-stub до 16 байт, смещение e_lfanew оказывается в поле base_of_code. При размещении в нем числа 10h получаем работоспособное приложение.
В начале секции импорта убрали 8 байт выравнивания.
количество секций делаем равным 1, удаляем секцию '.rdata' и размещаем импорт в секции кода, помещаю данные за вызовом функции ExitProcess, удаляю 3 нулевых байта перед секцией import.
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||||||||||||||||||||
08.07.2014, 12:51 [ТС] | ||||||||||||||||||||||||
Кликните здесь для просмотра всего текста
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||
09.07.2014, 04:45 [ТС] | ||||||
Шаблон оконного приложения для Win64
Кликните здесь для просмотра всего текста
Почему не используется макрос invoke, а вместо этого вызов WinAPI-функций записан "вручную"? Использование макроса порождает слишком много избыточного кода, например, sub rsp,N перед вызовом WinAPI-функции и add rsp,N после. Хотя это можно сделать только один раз для функции с самым большим количеством параметров.
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||
10.07.2014, 06:59 [ТС] | ||||||
Отрисовка текста в Win64
Кликните здесь для просмотра всего текста
2
|
Модератор
![]() ![]() ![]() 1943 / 719 / 155
Регистрация: 10.06.2009
Сообщений: 2,845
|
||||||
25.03.2015, 05:53 | ||||||
Пришлось побаловаться немного, но я добился своего!
Вот консольный вариант на тему FASM64. Размер ехе-файла 320 байт. Hellow World Windows CUI
1
|
Модератор
![]() ![]() ![]() 1943 / 719 / 155
Регистрация: 10.06.2009
Сообщений: 2,845
|
||||||
06.04.2015, 19:14 | ||||||
Я думал что меньше некуда.
Итог готовый бинарник размером 313байт Hellow World Windows CUI WriteFile
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||
07.04.2015, 06:05 [ТС] | ||||||
NoNaMe,
вот вариант "окна с меню", который я написал на FASM,
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|||||||
08.04.2015, 10:05 [ТС] | |||||||
Кликните здесь для просмотра всего текста
0
|
286 / 192 / 56
Регистрация: 25.12.2012
Сообщений: 640
|
||||
09.04.2015, 01:20 | ||||
Ещё здесь не написали что с появление x64, код стал по настоящему база-независим.
2
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
||||||
08.06.2015, 11:56 [ТС] | ||||||
Минимальный по размеру ЕХЕ-файл для х64 Размер ЕХЕ-файла 268 байт, меньше система не дает создать, пришлось в конце файла добавлять нули. DOS-stub =4 байта, section_alignment = file_alignment = 4, содержит секцию импорта, поэтому будет запускаться на любой х64 Windows ![]() Кликните здесь для просмотра всего текста
1
|
Ушел с форума
![]() ![]() 16355 / 7671 / 1078
Регистрация: 11.11.2010
Сообщений: 13,734
|
|
18.08.2015, 13:44 [ТС] | |
George_,
то же, что и о работе с 10 байтовым числом в 8087, ничего не изменилось...
1
|
18.08.2015, 13:44 | |
Помогаю со студенческими работами здесь
20
Что скачать и установить, чтобы начать программировать? Какие темы нужно знать, чтобы начать работать с DirectX Java для фриланса - что нужно знать чтобы потянуть на Junior'a? Искать еще темы с ответами Или воспользуйтесь поиском по форуму: |
|
Опции темы | |
|
Новые блоги и статьи
![]() |
||||
Batch Transform и Batch Gizmo Drawing API в Unity
GameUnited 20.04.2025
В мире разработки игр и приложений на Unity производительность всегда была критическим фактором успеха. Создатели игр постоянно балансируют между визуальной привлекательностью и плавностью работы. . .
|
Звук в Unity: Рандомизация с Audio Random Container
GameUnited 20.04.2025
В современных играх звуковое оформление часто становится элементом, который либо полностью погружает игрока в виртуальный мир, либо разрушает атмосферу за считанные минуты. Представьте: вы исследуете. . .
|
Максимальная производительность C#: Советы, тестирование и заключение
stackOverflow 20.04.2025
Погружение в мир микрооптимизаций C# открывает перед разработчиком целый арсенал мощных техник. Но как определить, где и когда их применять? Ответ начинается с точных измерений и профилирования.
. . .
|
Максимальная производительность C#: Предсказание ветвлений
stackOverflow 20.04.2025
Третий ключевой аспект низкоуровневой оптимизации — предсказание ветвлений. Эта тема менее известна среди разработчиков, но её влияние на производительность может быть колоссальным. Чтобы понять. . .
|
Максимальная производительность C#: Векторизация (SIMD)
stackOverflow 20.04.2025
Помимо работы с кэшем, другим ключевым аспектом низкоуровневой оптимизации является векторизация вычислений. SIMD (Single Instruction, Multiple Data) позволяет обрабатывать несколько элементов данных. . .
|
Максимальная производительность C#: Процессорный кэш
stackOverflow 20.04.2025
Знакомство с внутренним устройством процессорного кэша — ключевой шаг в написании по-настоящему быстрого кода на C#. Этот слой архитектуры компьютера часто ускользает от внимания разработчиков, но. . .
|
Максимальная производительность C#: Введение в микрооптимизации
stackOverflow 20.04.2025
В мире разработки на C# многие привыкли полагаться на . NET Runtime, который "магическим образом" сам оптимизирует код. И часто это работает - современные JIT-компиляторы творят чудеса. Но когда речь. . .
|
MVC фреймворк в PHP
Jason-Webb 19.04.2025
Архитектурный паттерн Model-View-Controller (MVC) – это не просто модный термин из мира веб-разработки. Для PHP-программистов это фундаментальный подход к организации кода, который радикально меняет. . .
|
Dictionary Comprehensions в Python
py-thonny 19.04.2025
Python славится своей выразительностью и лаконичностью, что позволяет писать чистый и понятный код. Среди множества синтаксических конструкций языка особое место занимают словарные включения. . .
|
Шаблоны и протоколы для создания устойчивых микросервисов
ArchitectMsa 19.04.2025
Микросервисы — архитектурный подход, разбивающий сложные приложения на небольшие, независимые компоненты. Вместо монолитного гиганта, система превращается в созвездие небольших взаимодействующих. . .
|