![]() |
64-разрядная архитектура ворвалась в нашу жизнь как торнадо, затягивая в себя всё больше инженеров и прикладных программистов. Новые термины стали появляться как грибы после дождя, а давать им толкового объяснения никто не торопился. В данной статье рассматриваются фундаментальные основы кодинга на платформах Win64, с реальными примерами в отладчике.
Оглавление: 1. Вводная часть; 2. Организация памяти в режиме “Long-mode” х64; 3. Доп.регистры GPR и префикс REX; 4. RIP-относительная адресация; 5. Соглашение “Fast-Call” – быстрый вызов функций; 6. Заключение. 1. Вводная часть С обзором архитектур связано и без того множество нестыковок, а выход на сцену в 2003 году 64-битных ОС ещё больше усугубил ситуацию. Более того, в результате модификаций одних и тех-же инженерных решений, в литературе встречаются и синонимы, которые сбивают логическую нить. Так-что сразу обозначим здесь основные моменты.. 1.1. Первое, на что стоит обратить внимание – это названия архитектур процессоров, согласно их разрядности. Так, на данный момент 64-битная от AMD называется AMD64, а в закрытых офисах Intel отдали предпочтение имени Intel-64. И тут нас ожидает мозговой штурм! Дело в том, что на заре 64-бит эры Intel окрестила свою архитектуру как IA-32e, всего-то добавив к привычной аббревиатуре литер(е) “Extended”, расширенный. Но на презентации критики посчитали такое название не отражающим разрядность в 64-бит, и буквально спустя год монархи переименовали её в EM64T – теперь это звучит громко и подразумевает «Extended Memory 64 Technology». Такой зоопарк имён привёл к тому, что имеем шесть разных названий одной и той-же архитектуры: х86-64, IA-32e, EM64T, Intel-64, AMD64, AA64 (последнее от «Amd Architecture»). А вот что не вписывается ни в какие ворота, это когда Intel-64 обзывают как IA-64. Предположение в корне не верно, т.к. под IA-64 кроется «Itanium Architecture», которая никак не пересекается с Intel-64. Процессоры «Intel Itanium» имели ядро RISC (когда одну команду исполняют множество простых инструкций), а процессоры IA-32 построены уже на ядре CISC (подмножество простых инструкций собираются в одну сложную). Написанные для процессоров Itanium приложения не будут работать на Intel-64, и наоборот. 1.2. Второй момент – это режимы работы 64-битных процессоров, с которыми мы не раз столкнёмся в данном треде. Согласно докам их всего три, и программное обеспечение ОС переключает эти режимы при помощи битов в модельно-специфичных регистрах MSR. От младшего к старшему следуют они в таком порядке: • Legacy-mode. Унаследованный от древних 16/32-разрядных процессоров х86. Режим впитал в себя четыре моды: RM, SMM, VM, PM (Real, Sys-Management, Virtual, и Protected соответственно). Он был родным для отправленных на свалку истории Win2k и Windows-XP. • Compatibility-mode. Режим совместимости х64 с х32. Процессор переключается в этот режим, когда на 64-бит платформе запускаются 32-бит приложения. Чтобы поддерживать совместимость со-старым софтом, инженерам пришлось заключить с ним мезальянс и тащить на себе всё это бремя вплоть до наших времён. Режим не поддерживает Virtual-моду унаследованного, поэтому софт реального режима RM в нём не работает. Со стороны ОС поддержка осуществляется технологией WOW64 (Windows on Windows). • Long-mode. Родной для х64 режим с плоской организацией памяти Flat. Сегментация в привычном виде отсутствует. К восьми имеющимся добавлены ещё 8 регистров R8-R15 общего назначения GPR (General Purpose Registers), и разрядность всех 16-ти расширена с 32 до 64-бит. Написанный для х32 код должен быть перекомпилирован с учётом 64-битной архитектуры. Вот несколько ссылок по теме на документацию Intel и AMD в формате *.pdf Лично на мой взгляд доки от AMD предпочтительней, хотя выводы можно сделать только в сравнении: - Большая библиотека документов Intel и AMD (SDM = Software Developer Manual, APM = AMD Programming Manual); - Intel volume(1) – описание архитектуры Intel-64 & IA-32; - Intel volume(3) – руководство по программированию для разработчиков; - AMD volume(1) – руководство по программированию процессоров AMD; - AMD volume(2) – описание архитектуры AMD64. 2. Организация памяти в режиме “Long-mode” х64 Нельзя назвать плавным переход на х64 – в архитектуру было внесено множество новых решений, и в большей степени это коснулось организации вирт.памяти. В связи с тем, что стало доступным огромное пространство, инженеры без зазрения совести просто удалили львиную долу системных механизмов. А ведь в этом есть смысл.. Вспомним, как на программном уровне представляется сегментная модель вирт.памяти в архитектуре IA-32. Значит имеем общее пространство размером 4GB, которое делится на несколько сегментов – кода, данных, стека. Свойства их определяют 8-байтные дескрипторы, которые собраны в глобальную таблицу GDT – Global Descriptor Table. В самих 16-битных сегментных регистрах CS,DS,ES,SS,FS,GS лежат селекторы с полем индекса (порядкового номера) соответствующих дескрипторов в таблице GDT. При этом 3-младших бита в селекторах обнуляются (биты RPL и TI), так-что индекс всегда кратен 8-ми, по размеру дескриптора. Непосредственно в дескрипторах указывается уже тип сегмента, его базовый адрес, размер (лимит), и атрибуты защиты. https://forum.antichat.xyz/attachmen...201b4f36db.png Теперь начнём с того, что расширенные до 64-бит регистры позволяют адресовать линейную память размером: 2^64=16 эксаБайт, или 16 млн.тераБайт. Если даже взять самое тяжеловесное в природе приложение, в таком объёме оно тупо потеряется и будет занимать порядка 0.001%. Зато системные расходы на её поддержку выйдут за рамки разумного: как-минимум потребуется таблица страниц «PageTable» исполинских размеров, и соответственно на поиск адреса в ней уйдёт больше времени. Поэтому инженеры ограничили указатель на вирт.память 48-битами, в результате чего аппетиты системы поубавились до 256 тераБайт, хотя моя Win7-х64 использует только 16 из них (8 приложениям, 8 ядру), а Win10 юзает уже на полную катушку 256. В то-же время, во-внешней шине памяти под адрес отводится итого меньше: всего 40-бит, что влечёт за собой поддержку мат.платами физической DDR общим размером в 1 тераБайт. Сомневаюсь, что у большинства из нас установлено хотя-бы 64 гига из них, но видимо у инженеров далеко идущие планы. В итоге имеем макс.256ТБ виртуальной, и до 1ТБ физ.памяти. На этом фоне потолок IA-32 в 4ГБ кажется ничтожно малым. https://forum.antichat.xyz/attachmen...ccf6f4b046.png Такой расклад навёл инженеров на мысль, что пришло время вообще избавиться от устаревшей сегментной модели, ведь памяти теперь предостаточно и располагай хоть 1000 программных секций следом, одна за другой. Они приняли идею с энтузиазмом, и наконец затенили три сегментных регистра DS,ES,SS. Три оставшихся CS,FS,GS на костылях всё-же функционируют, но и в их дескрипторах игнорируются все поля, кроме атрибутов. Отменим, что полностью изкоренить сегментную модель нельзя – она жёстко прошита в CPU на аппаратном уровне. Придётся в корне менять микро-архитектуру CPU, что приведёт к краху пока ещё держащихся на плаву 32-бит приложений. Может на следующем витке эволюции, но не на этом точно. В доках AMD имеется фрагмент ниже, где серым обозначены игнорируемые биты. Здесь видно, что размер дескрипторов как и прежде остался 8-байт, а база, лимит и некоторые атрибуты, отправлены инженерами в утиль. Как только процессор переходит в режим Long-mode, база во-всех сегментных регистрах выставляется в нуль, а лимит на 48-битный максимум. О том, что это дескриптор 64-битного сегмента, свидетельствует взведённый бит(21) под ником(L). Если-же процессор находится в режиме совместимости с х32, бит(L) будет сброшен, все затенённые поля авто-активируются, и мы получаем обратно сегментную модель IA-32. Цитата:
А вот с дескрипторами сегментных регистров FS и GS дела обстоят немного иначе. Инженеры оставили их, чтобы использовать в качестве доп.базовых регистров при вычислении адреса. Базы этих сегментов могут иметь отличные от нуля значения, что облегчает доступ к определенным структурам системы. К примеру по адресу Код:
[GS:60h]Поле базы FS и GS расширено до 64 бит, и хранится теперь не в самом дескрипторе (в нём уже нет места, см.скрин выше), а в MSR-регистрах Код:
IA32_FS_BASEКод:
IA32_GS_BASEКод:
CPUID.8000_0001Код:
rdfs(gs)baseКод:
wrfs(gs)baseC-подобный: Код:
mov eaxНе нужно думать, что в модели Flat отсутствуют атрибуты защиты-доступа. Конечно-же они есть, только зарыты не в дескрипторах как прежде, а на более высоком уровне, в 64-битных записях РТЕ «Page Table Entry» системной таблицы-страниц. Если учесть, что большие сегменты всегда чекаются на мелкие 4К-страницы (привет своп вирт.памяти), то атрибуты этих страниц имеют приоритет над атрибутами в дескрипторах сегментов, поэтому механизм функционирует исправно. Надеюсь скрин прояснит выше-сказанное: https://forum.antichat.xyz/attachmen...b4bbb7b485.png 3. Расширенные регистрыGPR и префиксREX Теперь про регистры и логику обращения к ним.. Выше упоминалось, что инженеры не только расширили имеющиеся 32-битные регистры до 64-бит, но и удвоили их кол-во. Такой размах нужен для того, чтобы минимизировать обращения к сравнительно медленной памяти ОЗУ. Раньше, при отсутствии свободных регистров мы сохраняли данные в стек, для чего контролёру нужно было сначала дождаться освобождения внешней шины, и лишь потом за’PUSH’ить содержимое регистра в стековую память. Именно поэтому в процессор был добавлен кэш, куда сбрасывались все последние обращения к ОЗУ. То-есть следующий PUSH уже не запрашивал шину, а обращался к 64-байтной линейке кэш. И регистры CPU, и кэш построен на одном типе памяти – статической SRAM (на триггерах, а не как DRAM на конденсаторах), поэтому никаких задержек при обмене данными ядра с кэшем не возникает. В таблице ниже видно, что инженеры породили 8 новых векторных AVX-регистров: YMM8-YMM15 (Advanced Vector Extensions), такое-же число потоковых SSE: XMM8-XMM15 (Streaming SIMD Extensions), и бонусом 8 регистров общего назначения GPR: R8-R15 (General Purpose Registers). В последних процессорах регистры AVX расширены до 512-бит (AVX512) и ожидается увеличение их разрядности вплоть до 1024-бит. Для поддержки таких монстров кол-во регистров AVX было увеличено с 16 до 32-х, и называются они теперь ZMM0-ZMM31. В столбце «Бит» данной таблицы указана разрядность одного регистра: https://forum.antichat.xyz/attachmen...38dd98d592.png Обратите внимание, что регистры MMX являются частью 80-битных регистров сопроцессора FPU, занимая в них младшие 64-бита мантиссы так, что для инструкций MMX остаётся исключительно целочисленная арифметика, без дробной части. При таком раскладе, в любой момент времени (внутри одного блока кода) мы можем использовать или регистры FPU, или MMX, но не оба сразу – в противном случае получим винегрет из данных. Чтобы решить эту проблему, инженеры ввели независимый от FPU пул 128-битных регистров SSE, который позже был расширен до 256-бит AVX. Здесь наблюдаем аналогичную картину, когда SSE живут внутри AVX. Соответственно их инструкции так-же нельзя комбинировать. Отличительной особенностью SSE\AVX является доступность им операций как со-скалярными числами (тогда инструкции заканчиваются на «ss», scalar), так и с упакованными (окончание «ps», packet) – это значительно расширяет область их применения. А вообще в эти регистры можно пихать любые типы данных: https://forum.antichat.xyz/attachmen...369897eca9.png Обратим свой взор на упакованный формат. Это четыре отдельных 32-битных числа с плавающей запятой (одинарной точности), над которыми за теоретический такт процессора можно произвести независимые вычисления. На аналогичную операцию сопр FPU потратил-бы в 4 раза больше времени! Основным требованием здесь является «правильная» организация массива данных (в виде непрерывного потока Stream), и обязательное выравнивание массива на 8-байтную границу (для скалярных типов это не принципиально). Типичный пример использования фишки – 3D графика. Инструкции AVX пошли ещё дальше, и могут иметь уже не два, а три\четыре операнда для тех случаев, когда значения операндов-источников нужно сохранить для последующих вычислений. К таким инструкциям привязывается не рассматриваемый здесь префикс «VEX». А вот как выглядят «мемберы» новых регистров общего назначения GPR. Четыре первых регистра RAX..RDX как и прежде позволяют обращаться к любым своим частям, в том числе и к старшим байтам 16-битных слов AH..DH. Как видно из рисунка, в регистрах R8..R15 это поле уже отсутствует, и мы можем использовать лишь мл.байты слов R8b..R15b. Непонятно зачем были добавлены и мл.байты индексных + базовых регистров под никами SIL..SPL, которых не было в IA-32. Видимо просто в поле «Opcode» инструкций оставались свободные биты. Указатель RIP остался в девственном виде, с мин.значением в 16-бит. Интересным моментом является то, что запись 32-битных значений в 64-бит регистры всегда обнуляет старшую их часть: https://forum.antichat.xyz/attachmen...5890f0024b.png 3.1. Назначение префикса REX Теперь спустимся в тёмные подвалы микро-архитектуры и рассмотрим, как кодируются инструкции в 64-битном коде. Для этого откроем доку Intel том(2) «Instruction Set Reference», где на стр.35 представлен формат ниже. Значит макс.размер инструкции составляет 15-байт, из которых обязательно лишь поле «Opcode». Этот код операции ограничен разрядность 24-бита (три байта), а если их не хватает, то инструкция «отжимает» биты[5:3] у следующего поля «Mod R/M». Итого получаем 27-битный опкод, в которых можно закодировать 134 лярда теоретических инструкций. Впечатляет.. Далее идёт байт «SIB» – он кодирует регистры при относительной адресации, например конструкцию с базой и индексом Код:
[EBX+ESI]Код:
[EBP+8]Код:
[EBX+4*ESI+2]Последний дворд «Immediate» выделяется для непосредственных значений, типа число в арифметике, адрес вызываемой функции, и т.д. Обратите внимание, что два этих поля в режиме х64 не расширяются до 64-бит: они по-прежнему ограничены 32-битным числом со-знаком. Однако поддерживаются некоторые 64-бит смещения, и непосредственные формы инструкции MOV – в этом случае два этих поля объединяются. Инструкции опционально могут иметь префиксы, а при обращении к 64-битным регистрам префикс «REX» является вообще обязательным. Он всегда занимает позицию после унаследованного Legacy-префикса, чтобы в режиме совместимости с IA-32 им можно было принебречь. Старшая тетрада 1-байтного REX жёстко прошита значением 0100=4, а потому диапазон его значений лежит в пределах 40..4Fh. Младшая-же тетрада является флагами расширений так, что если в ней взведён старший бит(W), то операндом инструкции считается весь 64-бит регистр, а если он сброшен, только 8/16/32-битная его часть. Таким макаром, при обращении к регистрам RAX-R15 значения этого префикса будет больше\равно 48h, иначе меньше. Оставшаяся триада с битами(RXB) конфискует перечисленные ниже поля в байтах «Mod R/M» и «SIB», чтобы в них можно было закодировать 64-битные регистры: https://forum.antichat.xyz/attachmen...26d5c8ab75.png Посмотрим в отладчике х64Dbg на код, в котором я собрал обращения к разным частям 64-бит регистров: Byte, Word, Dword, Qword. Как видно из скрина, отладчик разделяет опкод от его префикса двоеточием. Первые 4 инструкции предваряются префиксом Код:
REX=48hКод:
83.EC.08Код:
F3.48https://forum.antichat.xyz/attachmen...a7478814ea.png [FONT="verdana"]Начиная с адреса 0х00401020 видим запись значения(5) в младшие части 64-битного регистра R8. Здесь уже бит REX.W сброшен, и его значение равно 41h mov dword [ osVer ] , sizeof . OSVERSIONINFOA invoke GetVersionEx , osVer invoke GlobalMemoryStatusEx , ms ; //------------- Версия Windows. ; // printf() поместит в RCX первый свой аргумент в виде строки спецификаторов, ; // поэтому начинаем передавать аргументы со-второго RDX mov edx , [ osVer . dwMajorVersion ] ; // запись 32-бит обнуляет старшую часть 64-бит регистра mov r8d , [ osVer . dwMinorVersion ] mov r9d , [ osVer . dwBuildNumber ] mov r10d , osVer . szCSDVersion cinvoke printf , , \ rdx , r8 , r9 , r10 ; //------------- Физической памяти mov rdx , [ ms . dwTotalPhys ] ; // всего shr rdx , 20 ; // ..(в Мбайтах) mov r8 , [ ms . dwAvailPhys ] ; // свободно shr r8 , 20 ; // ..(в Мбайтах) mov r9 , 100 ; // ...и в процентах sub r9d , [ ms . dwMemoryLoad ] cinvoke printf , , rdx , r8 , r9 ; //------------- Файл подкачки movsd xmm0 , [ ms . dwTotalPageFile ] divsd xmm0 , [ gb ] ; // в гигабайтах movsd [ doubleA ] , xmm0 movsd xmm0 , [ ms . dwAvailPageFile ] divsd xmm0 , [ gb ] movsd [ doubleB ] , xmm0 mov rdx , [ doubleA ] mov r8 , [ doubleB ] cinvoke printf , , rdx , r8 ; //------------- Виртуальная movsd xmm0 , [ ms . dwTotalVirtual ] divsd xmm0 , [ gb ] movsd [ doubleA ] , xmm0 movsd xmm0 , [ ms . dwAvailVirtual ] divsd xmm0 , [ gb ] movsd [ doubleB ] , xmm0 mov rdx , [ doubleA ] mov r8 , [ doubleB ] cinvoke printf , , rdx , r8 endf ; //------------- Вызов своей процедуры с 8 аргументами fastcall Convert , szHelp , szCapt , rax , rbx , \ r10 , [ printf ] , [ SetConsoleTitle ] , [ GlobalMemoryStatusEx ] cinvoke getch cinvoke exit , 0 ; //----------------------------- proc Convert a , b , c , d , e , f , h , g mov [ a ] , rcx mov [ b ] , rdx mov [ c ] , r8 mov [ d ] , r9 cinvoke printf , %s' , 10 , ' ---> %s' , \ 10 , ' RAX: 0x%016I64x' , \ 10 , ' RBX: 0x%016I64x' , \ 10 , ' R10: 0x%016I64x' , 10 , \ 10 , ' 0x%016I64x , \ [ a ] , [ b ] , [ c ] , [ d ] , [ e ] , [ f ] , [ h ] , [ g ] ret endp ; //----------------- section '.idata' import data readable library kernel32 , 'kernel32.dll' , msvcrt , 'msvcrt.dll' import kernel32 , GlobalMemoryStatusEx , 'GlobalMemoryStatusEx' , \ SetConsoleTitle , 'SetConsoleTitleA' , GetVersionEx , 'GetVersionExA' import msvcrt , printf , 'printf' , getch , '_getch' , exit , 'exit' [/CODE] https://forum.antichat.xyz/attachmen...6933185fa8.png Посмотрим на выхлоп функции GlobalMemoryStatus(). Моя тестовая машина с 64-битной семёркой на борту вполне себе сносно функционирует даже при 1.5 ГБ физ.памяти. И это при том, что запущен браузер Хром с несколькими вкладками, тотал, офис и фотошоп. В файле-подкачки свободен гиг, а виртуальной 8 тераБайт для приложений, и столько-же для ядра. А вот логи десятки.. Из четырёх физических доступен всего один, но и этого ей недостаточно, т.к. шесть из десяти в подкачке занято. Зато виртуальной хоть отбавляй, аж 128 ТБайт, только юзать их (кроме игр) как-правило некому. Все системные либы грузятся в верхнюю область пользовательского 48-бит пространства, хотя на семёрке Kernel32.dll тусуется где-то между небом и землёй, по середине. https://forum.antichat.xyz/attachmen...030f0d9041.png 6. Заключение Что-то букаф в треде получилось много, видимо пора заканчивать.. В скрепку кладу исполняемый *.exe для тестов, и файл «fasmw.ini» для тех, кто хочет поменять визуальную тему FASM’a на Black. У меня он установлен по-пути D:\Install\FASM, а вам нужно открыть его в блокноте, и указать свой путь до инклуд. Сделайте бэкап оригинального *.ini на случай, если что-то пойдёт не так. Всем удачи, пока! |
Тимур, огромное спасибо за статью!
|
| Время: 11:58 |