64-разрядная архитектура ворвалась в нашу жизнь как торнадо, затягивая в себя всё больше инженеров и прикладных программистов. Новые термины стали появляться как грибы после дождя, а давать им толкового объяснения никто не торопился. В данной статье рассматриваются фундаментальные основы кодинга на платформах Win64, с реальными примерами в отладчике.
Оглавление:
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. От младшего к старшему следуют они в таком порядке:
Вот несколько ссылок по теме на документацию Intel и AMD в формате *.pdf
Лично на мой взгляд доки от AMD предпочтительней, хотя выводы можно сделать только в сравнении:
-
-
-
-
-
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-ми, по размеру дескриптора. Непосредственно в дескрипторах указывается уже тип сегмента, его базовый адрес, размер (лимит), и атрибуты защиты.
Теперь начнём с того, что расширенные до 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ГБ кажется ничтожно малым.
Такой расклад навёл инженеров на мысль, что пришло время вообще избавиться от устаревшей сегментной модели, ведь памяти теперь предостаточно и располагай хоть 1000 программных секций следом, одна за другой. Они приняли идею с энтузиазмом, и наконец затенили три сегментных регистра DS,ES,SS. Три оставшихся CS,FS,GS на костылях всё-же функционируют, но и в их дескрипторах игнорируются все поля, кроме атрибутов. Отменим, что полностью изкоренить сегментную модель нельзя – она жёстко прошита в CPU на аппаратном уровне. Придётся в корне менять микро-архитектуру CPU, что приведёт к краху пока ещё держащихся на плаву 32-бит приложений. Может на следующем витке эволюции, но не на этом точно.
В доках AMD имеется фрагмент ниже, где серым обозначены игнорируемые биты. Здесь видно, что размер дескрипторов как и прежде остался 8-байт, а база, лимит и некоторые атрибуты, отправлены инженерами в утиль. Как только процессор переходит в режим Long-mode, база во-всех сегментных регистрах выставляется в нуль, а лимит на 48-битный максимум. О том, что это дескриптор 64-битного сегмента, свидетельствует взведённый бит(21) под ником(L). Если-же процессор находится в режиме совместимости с х32, бит(L) будет сброшен, все затенённые поля авто-активируются, и мы получаем обратно сегментную модель IA-32.
А вот с дескрипторами сегментных регистров FS и GS дела обстоят немного иначе.
Инженеры оставили их, чтобы использовать в качестве доп.базовых регистров при вычислении адреса. Базы этих сегментов могут иметь отличные от нуля значения, что облегчает доступ к определенным структурам системы. К примеру по адресу
Поле базы FS и GS расширено до 64 бит, и хранится теперь не в самом дескрипторе (в нём уже нет места, см.скрин выше), а в MSR-регистрах
Модель плоской Flat памяти – это простейшая форма сегментации. Здесь базовые адреса всех сегментов имеют значение нуль, а лимиты выставлены на макс (хотя и в сегментной модели IA-32 мастдая можем наблюдать такую-же картину). Сброс базы ниже плинтуса фактически отключает трансляцию сегментов, и он охватывает всё пространство вирт.адресов – дескрипторы ссылаются теперь на этот единственный плоский сегмент.
Не нужно думать, что в модели Flat отсутствуют атрибуты защиты-доступа. Конечно-же они есть, только зарыты не в дескрипторах как прежде, а на более высоком уровне, в 64-битных записях РТЕ «Page Table Entry» системной таблицы-страниц. Если учесть, что большие сегменты всегда чекаются на мелкие 4К-страницы (привет своп вирт.памяти), то атрибуты этих страниц имеют приоритет над атрибутами в дескрипторах сегментов, поэтому механизм функционирует исправно. Надеюсь скрин прояснит выше-сказанное:
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. В столбце «Бит» данной таблицы указана разрядность одного регистра:
Обратите внимание, что регистры MMX являются частью 80-битных регистров сопроцессора FPU, занимая в них младшие 64-бита мантиссы так, что для инструкций MMX остаётся исключительно целочисленная арифметика, без дробной части. При таком раскладе, в любой момент времени (внутри одного блока кода) мы можем использовать или регистры FPU, или MMX, но не оба сразу – в противном случае получим винегрет из данных.
Чтобы решить эту проблему, инженеры ввели независимый от FPU пул 128-битных регистров SSE, который позже был расширен до 256-бит AVX. Здесь наблюдаем аналогичную картину, когда SSE живут внутри AVX. Соответственно их инструкции так-же нельзя комбинировать. Отличительной особенностью SSE\AVX является доступность им операций как со-скалярными числами (тогда инструкции заканчиваются на «ss», scalar), так и с упакованными (окончание «ps», packet) – это значительно расширяет область их применения. А вообще в эти регистры можно пихать любые типы данных:
Обратим свой взор на упакованный формат. Это четыре отдельных 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-бит регистры всегда обнуляет старшую их часть:
3.1. Назначение префикса REX
Теперь спустимся в тёмные подвалы микро-архитектуры и рассмотрим, как кодируются инструкции в 64-битном коде. Для этого откроем доку Intel том(2) «Instruction Set Reference», где на стр.35 представлен формат ниже.
Значит макс.размер инструкции составляет 15-байт, из которых обязательно лишь поле «Opcode». Этот код операции ограничен разрядность 24-бита (три байта), а если их не хватает, то инструкция «отжимает» биты[5:3] у следующего поля «Mod R/M». Итого получаем 27-битный опкод, в которых можно закодировать 134 лярда теоретических инструкций. Впечатляет..
Далее идёт байт «SIB» – он кодирует регистры при относительной адресации, например конструкцию с базой и индексом
Последний дворд «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-битные регистры:
Посмотрим в отладчике х64Dbg на код, в котором я собрал обращения к разным частям 64-бит регистров: Byte, Word, Dword, Qword. Как видно из скрина, отладчик разделяет опкод от его префикса двоеточием. Первые 4 инструкции предваряются префиксом
Начиная с адреса 0х00401020 видим запись значения(5) в младшие части 64-битного регистра R8.
Здесь уже бит REX.W сброшен, и его значение равно 41h < 48h. Соответственно инструкции оперируют байтом, вордом, и двордом, после чего REX опять перемещается в верхнюю половину тетрады 49-48h, и жизнь налаживается.
А вот инструкции SSE с регистрами XMM0-XMM15 вообще не используют префикс REX. Коды их операций занимают всё 3-байтное поле «Opcode», и как правило первым идёт байт 0Fh, в качестве управляющего Escape. Этот байт является флагом декодеру инструкций, что пора выходить из привычной зоны комфорта, и переключаться на регистры SSE\AVX. Опкод 0Fh как псевдо-префикс используют и многие другие инструкции, например
4. RIP-относительная адресация
Выше упоминалось, что поля «Displacement & Immediate» в Long расширяются до 64-бит только в исключительных случаях, придерживаясь старой политики x32. Тогда как процессору удаётся охватить всё плоское пространство в 256 тераБайт, ведь 32-бит указатель способен адресовать всего 4 гига?
Здесь инженеры нашли решение, которое заключается в неявной адресации памяти, относительно указателя на сл.инструкцию RIP. Эффективный адрес получается суммированием 32-битного операнда (число со-знаком, играет роль смещения), и регистра RIP (база с текущим адресом). Результатом будет обращение по указателю RIP+2Gb вперёд, или назад. То-есть имеем уже знакомую нам схему: Scale+Index+Base.
Отшлифованная годами практика доказала, что чем дальше адрес отстоит от текущего значения RIP, тем меньше вероятность его применения. Это позволяет в 2-раза сократить разрядность адресов в командах. В случае, когда 32-битного смещения недостаточно, компилятор формирует полный 64-бит адрес, после чего опять возвращается к схеме с 32-бит. Такой подход практически ликвидировал главный недостаток кода х64 – большой размер исполняемых файлов.
В отличии от х64, архитектура х32 полностью ограничивала доступ к регистру EIP. Его нельзя было использовать в качестве базы при обращениях к памяти, нельзя запушить в стек, и вообще исключены любые операции с ним. Правда имелась возможность фиктивно запихать EIP в стек, и снять от туда в произвольный регистр инструкциями CALL\POP, но это хак, и юзался он в основном малварью для вычисления дельты в шелл-кодах. С переходом на х64 инженеры сняли запрет лишь на RIP-адресацию, а остальные замки так и остались висеть закрытыми. Вот несколько примеров доступных операций в 64-бит режиме (RIP всегда указывает на сл.инструкцию):
5. Соглашение о вызове “Fast-Call” – быстрый вызов функций
Узким местом в архитектуре была и остаётся оперативная память ОЗУ. При любых обращениях к ней CPU тратит огромное кол-во своих тактов отстукивая в холостую, пока запрашиваемые данные не скопируются из ОЗУ в кэш. Все контролёры памяти имеют параметр «Burst-Length». Он определяет кол-во повторений чтения, при открытой DRAM-строке. Как правило BL=8 и разрядность шины = 64-бит, или 8-байт. Поэтому контролёр читает память в пакетном режиме сразу минимум по: 8х8=64 байт, которые принимает кэш в виде одной «Cache-Line». Размер кэш-линейки своего процессора можете подсмотреть в программе CPU-Z:
Теперь как нам предлагают вызывать функции WinAPI 32-битные системы?
Они используют соглашение _stdcall (standart), когда все аргументы функции передаются через стек, справа-налево. После того-как функция отработает, на выходе она сама должна очистить свой стековый фрейм от аргументов. Системные WinAPI делаю это инструкцией
Основным недостатком данного соглашения является активное использование стека, ведь PUSH это реальный запрос к внешней памяти ОЗУ, что влечёт за собой падение производительности. С учётом этих проблем, в 64-битных системах Win применяется другой тип вызова _fastcall (быстрый). Здесь 4 первых аргумента передаются через регистры RCX,RDX,R8,R9, а если функция ожидает больше аргументов, то остальные как и прежде – через стек. Использование регистров для засылки аргументов полностью ликвидирует связанные в памятью задержки, а потому вызов осуществляется намного быстрее. Листинг запроса той-же функции представлен ниже:
С быстрым вызовом связана пара нюансов:
В данном случае функция имеет 4 аргумента, поэтому в резерв уходят 4х8=32-байта, или 20h. Если у функции аргументов больше (например 7), то оставшиеся три помещаются в стек. Здесь компилятор зарезервирует уже 7х8=56, плюс 8-байт под адрес-возврата, итого 64=40h. Причём резерв\освобождение фреймов нам нужно предусматривать перед вызовом каждой функцией, что сильно напрягает. По сути это то, что называется «стек очищает вызывающий», в отличии от stdcall, где очисткой фрейма от оставшегося мусора озадачена сама функция. Вот пример последовательного вызова MessageBox() и CreateFile() с семью аргументами:
В компиляторе FASM имеется огромное кол-во умных макросов, среди которых есть и парочка frame\endf. Они специально были написаны разработчиком продукта Томашом Грыштаром, чтобы привести бинарник в более приглядный вид, удалив из него этот бессмысленный резерв фреймов на входе и выходе из каждой функции. По мнению автора лучше один раз выделить большой фрейм, чем несколько раз мелкие.
Fasm относится к многопроходным компиляторам. На первом проходе транслятор вычисляет в исходнике API-функцию с наибольшим числом аргументов, и по их кол-ву сразу выделяет соответствующий фрейм. Теперь все функции, которые находятся внутри макросов frame\endf будут использовать для своих нужд этот общий фрейм. Такая конструкция выглядит более логичной, чем неоптимизированная в дефолте. Вот пример и то, что получим в результате:
Значит первой строкой
А в остальном программирование в х64 ничем не отличается от х32. Радует то, что наделённые разумом макросы делают за нас всю черновую работу, если только код наш не маргинальный, с стиле проприетарного
Под занавес приведу небольшой пример, в котором соберу всё выше-сказанное. Вот его алго:
• GetVersionEx() в структуру «OSVERSIONINFO» возвращает версию системы:
• GlobalMemoryStatusEx() возвращает основную инфу о системной памяти.
Функция с одним аргументом, и ожидает указатель на структуру «MEMORYSTATUSEX»:
• Использование инструкций SSE с регистрами XMM, для перевода величин в формат с плавающей точкой.
• printf() из msvcrt.dll для вывода данных на консоль.
• В качестве примера – оформление и вызов собственных процедур с 8 аргументами, по соглашению «fastcall».
Выше говорилось, что инструкцией
Посмотрим на выхлоп функции GlobalMemoryStatus().
Моя тестовая машина с 64-битной семёркой на борту вполне себе сносно функционирует даже при 1.5 ГБ физ.памяти. И это при том, что запущен браузер Хром с несколькими вкладками, тотал, офис и фотошоп. В файле-подкачки свободен гиг, а виртуальной 8 тераБайт для приложений, и столько-же для ядра.
А вот логи десятки..
Из четырёх физических доступен всего один, но и этого ей недостаточно, т.к. шесть из десяти в подкачке занято. Зато виртуальной хоть отбавляй, аж 128 ТБайт, только юзать их (кроме игр) как-правило некому. Все системные либы грузятся в верхнюю область пользовательского 48-бит пространства, хотя на семёрке Kernel32.dll тусуется где-то между небом и землёй, по середине.
6. Заключение
Что-то букаф в треде получилось много, видимо пора заканчивать..
В скрепку кладу исполняемый *.exe для тестов, и файл «fasmw.ini» для тех, кто хочет поменять визуальную тему FASM’a на Black. У меня он установлен по-пути D:\Install\FASM, а вам нужно открыть его в блокноте, и указать свой путь до инклуд. Сделайте бэкап оригинального *.ini на случай, если что-то пойдёт не так. Всем удачи, пока!
Оглавление:
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-64 & IA-32;-
Ссылка скрыта от гостей
– руководство по программированию для разработчиков;-
Ссылка скрыта от гостей
– руководство по программированию процессоров AMD;-
Ссылка скрыта от гостей
– описание архитектуры 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-ми, по размеру дескриптора. Непосредственно в дескрипторах указывается уже тип сегмента, его базовый адрес, размер (лимит), и атрибуты защиты.
Теперь начнём с того, что расширенные до 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ГБ кажется ничтожно малым.
Такой расклад навёл инженеров на мысль, что пришло время вообще избавиться от устаревшей сегментной модели, ведь памяти теперь предостаточно и располагай хоть 1000 программных секций следом, одна за другой. Они приняли идею с энтузиазмом, и наконец затенили три сегментных регистра DS,ES,SS. Три оставшихся CS,FS,GS на костылях всё-же функционируют, но и в их дескрипторах игнорируются все поля, кроме атрибутов. Отменим, что полностью изкоренить сегментную модель нельзя – она жёстко прошита в CPU на аппаратном уровне. Придётся в корне менять микро-архитектуру CPU, что приведёт к краху пока ещё держащихся на плаву 32-бит приложений. Может на следующем витке эволюции, но не на этом точно.
В доках AMD имеется фрагмент ниже, где серым обозначены игнорируемые биты. Здесь видно, что размер дескрипторов как и прежде остался 8-байт, а база, лимит и некоторые атрибуты, отправлены инженерами в утиль. Как только процессор переходит в режим Long-mode, база во-всех сегментных регистрах выставляется в нуль, а лимит на 48-битный максимум. О том, что это дескриптор 64-битного сегмента, свидетельствует взведённый бит(21) под ником(L). Если-же процессор находится в режиме совместимости с х32, бит(L) будет сброшен, все затенённые поля авто-активируются, и мы получаем обратно сегментную модель IA-32.
• Бит(D) – Default-Operand Size (размер операндов: 0=32, 1=64 бит);
• Бит(L) – Long mode (1=режим х64, 0=совместимости);
• Бит(P) – Present (1=сегмент загружен в память);
• Бит(DPL) – Descriptor Privilege Level (уровень кольца защиты 0-3);
• Бит(C) – Conforming (соответствие привилегий: 0=доступ напрямую, 1=через системный шлюз).
А вот с дескрипторами сегментных регистров FS и GS дела обстоят немного иначе.
Инженеры оставили их, чтобы использовать в качестве доп.базовых регистров при вычислении адреса. Базы этих сегментов могут иметь отличные от нуля значения, что облегчает доступ к определенным структурам системы. К примеру по адресу
[GS:60h]
можно найти указатель на инфо-структуру TЕВ 64-битного потока (Thread Environment Block), под которую выделяется отдельный сегмент.Поле базы FS и GS расширено до 64 бит, и хранится теперь не в самом дескрипторе (в нём уже нет места, см.скрин выше), а в MSR-регистрах
IA32_FS_BASE
и IA32_GS_BASE
(см.доки Intel том.4). Проверить, что базы реально прописаны в MSR (а не в дескрипторах) можно при помощи CPUID.8000_0001
– запрос должен вернуть взведённый бит(29) в регистре EDX. Для чтения\записи этих значений с уровня драйверов, были введены новые инструкции: rdfs(gs)base
и wrfs(gs)base
.
C-подобный:
mov eax,0x80000001 ;// IA32_FS_BASE support
cpuid ;// Requst..
bt edx,29 ;// Bit-Test
jnc @f ;// Jump no Carry flag
rdfsbase rax ;// Else: save FS-base to RAX
rdgsbase rbx ;// save GS-base to RBX
@@: nop
Модель плоской Flat памяти – это простейшая форма сегментации. Здесь базовые адреса всех сегментов имеют значение нуль, а лимиты выставлены на макс (хотя и в сегментной модели IA-32 мастдая можем наблюдать такую-же картину). Сброс базы ниже плинтуса фактически отключает трансляцию сегментов, и он охватывает всё пространство вирт.адресов – дескрипторы ссылаются теперь на этот единственный плоский сегмент.
Не нужно думать, что в модели Flat отсутствуют атрибуты защиты-доступа. Конечно-же они есть, только зарыты не в дескрипторах как прежде, а на более высоком уровне, в 64-битных записях РТЕ «Page Table Entry» системной таблицы-страниц. Если учесть, что большие сегменты всегда чекаются на мелкие 4К-страницы (привет своп вирт.памяти), то атрибуты этих страниц имеют приоритет над атрибутами в дескрипторах сегментов, поэтому механизм функционирует исправно. Надеюсь скрин прояснит выше-сказанное:
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. В столбце «Бит» данной таблицы указана разрядность одного регистра:
Обратите внимание, что регистры MMX являются частью 80-битных регистров сопроцессора FPU, занимая в них младшие 64-бита мантиссы так, что для инструкций MMX остаётся исключительно целочисленная арифметика, без дробной части. При таком раскладе, в любой момент времени (внутри одного блока кода) мы можем использовать или регистры FPU, или MMX, но не оба сразу – в противном случае получим винегрет из данных.
Чтобы решить эту проблему, инженеры ввели независимый от FPU пул 128-битных регистров SSE, который позже был расширен до 256-бит AVX. Здесь наблюдаем аналогичную картину, когда SSE живут внутри AVX. Соответственно их инструкции так-же нельзя комбинировать. Отличительной особенностью SSE\AVX является доступность им операций как со-скалярными числами (тогда инструкции заканчиваются на «ss», scalar), так и с упакованными (окончание «ps», packet) – это значительно расширяет область их применения. А вообще в эти регистры можно пихать любые типы данных:
Обратим свой взор на упакованный формат. Это четыре отдельных 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-бит регистры всегда обнуляет старшую их часть:
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]
. В последнем случае значение(8) считается отклонением и хранится в поле «Displacement». Адрес может вычисляться и в более извращённой форме [EBX+4*ESI+2]
. Тогда значение(4) будет определять масштаб, под который отводятся 2-бита «Scale» байта SIB.Последний дворд «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-битные регистры:
Посмотрим в отладчике х64Dbg на код, в котором я собрал обращения к разным частям 64-бит регистров: Byte, Word, Dword, Qword. Как видно из скрина, отладчик разделяет опкод от его префикса двоеточием. Первые 4 инструкции предваряются префиксом
REX=48h
, значит их операндом является весь 64-бит регистр. Три байта 83.EC.08
первой строки занимают в инструкции поля «Opcode, ModR/M, Immediate» соответственно. Обратите внимание, что второй операнд(8) инструкции SUB не расширяется до 64-бит, хотя и был отправлен в регистр RSP. Инструкция по адресу 0х00401019 имеет уже 2-байтый префикс F3.48
, где Legacy(F3) олицетворяет повторение, а А5h – это опкод:Начиная с адреса 0х00401020 видим запись значения(5) в младшие части 64-битного регистра R8.
Здесь уже бит REX.W сброшен, и его значение равно 41h < 48h. Соответственно инструкции оперируют байтом, вордом, и двордом, после чего REX опять перемещается в верхнюю половину тетрады 49-48h, и жизнь налаживается.
А вот инструкции SSE с регистрами XMM0-XMM15 вообще не используют префикс REX. Коды их операций занимают всё 3-байтное поле «Opcode», и как правило первым идёт байт 0Fh, в качестве управляющего Escape. Этот байт является флагом декодеру инструкций, что пора выходить из привычной зоны комфорта, и переключаться на регистры SSE\AVX. Опкод 0Fh как псевдо-префикс используют и многие другие инструкции, например
LFENCE=0F.AE.E8
, RDTSC=0F.31
, и прочие.4. RIP-относительная адресация
Выше упоминалось, что поля «Displacement & Immediate» в Long расширяются до 64-бит только в исключительных случаях, придерживаясь старой политики x32. Тогда как процессору удаётся охватить всё плоское пространство в 256 тераБайт, ведь 32-бит указатель способен адресовать всего 4 гига?
Здесь инженеры нашли решение, которое заключается в неявной адресации памяти, относительно указателя на сл.инструкцию RIP. Эффективный адрес получается суммированием 32-битного операнда (число со-знаком, играет роль смещения), и регистра RIP (база с текущим адресом). Результатом будет обращение по указателю RIP+2Gb вперёд, или назад. То-есть имеем уже знакомую нам схему: Scale+Index+Base.
Отшлифованная годами практика доказала, что чем дальше адрес отстоит от текущего значения RIP, тем меньше вероятность его применения. Это позволяет в 2-раза сократить разрядность адресов в командах. В случае, когда 32-битного смещения недостаточно, компилятор формирует полный 64-бит адрес, после чего опять возвращается к схеме с 32-бит. Такой подход практически ликвидировал главный недостаток кода х64 – большой размер исполняемых файлов.
В отличии от х64, архитектура х32 полностью ограничивала доступ к регистру EIP. Его нельзя было использовать в качестве базы при обращениях к памяти, нельзя запушить в стек, и вообще исключены любые операции с ним. Правда имелась возможность фиктивно запихать EIP в стек, и снять от туда в произвольный регистр инструкциями CALL\POP, но это хак, и юзался он в основном малварью для вычисления дельты в шелл-кодах. С переходом на х64 инженеры сняли запрет лишь на RIP-адресацию, а остальные замки так и остались висеть закрытыми. Вот несколько примеров доступных операций в 64-бит режиме (RIP всегда указывает на сл.инструкцию):
C-подобный:
.code
start: push rip ;// Ошибка! Invalid Operand.
mov rax,rip ;// Ошибка! ^^^^
mov rax,[rip+rbp] ;// Ошибка! ^^^^
jmp rip ;// Ошибка! ^^^^
jmp qword[rip] ;// ОК! Перейти по адресу в RIP
dq $+8 ;// ....(адрес перехода, нацелен на call)
call @f ;// ОК! Адрес возврата в стеке -------------+
@@: pop rax ;// ОК! RAX = текущее значение RIP <--------+
mov rbx,$ ;// ОК! RBX = текущее значение RIP
mov rcx,[rip] ;// ОК! RCX = qword с текущего адреса RIP
mov rdx,[rip+100h] ;// ОК! RDX = RIP-относительное чтение адреса
5. Соглашение о вызове “Fast-Call” – быстрый вызов функций
Узким местом в архитектуре была и остаётся оперативная память ОЗУ. При любых обращениях к ней CPU тратит огромное кол-во своих тактов отстукивая в холостую, пока запрашиваемые данные не скопируются из ОЗУ в кэш. Все контролёры памяти имеют параметр «Burst-Length». Он определяет кол-во повторений чтения, при открытой DRAM-строке. Как правило BL=8 и разрядность шины = 64-бит, или 8-байт. Поэтому контролёр читает память в пакетном режиме сразу минимум по: 8х8=64 байт, которые принимает кэш в виде одной «Cache-Line». Размер кэш-линейки своего процессора можете подсмотреть в программе CPU-Z:
Теперь как нам предлагают вызывать функции WinAPI 32-битные системы?
Они используют соглашение _stdcall (standart), когда все аргументы функции передаются через стек, справа-налево. После того-как функция отработает, на выходе она сама должна очистить свой стековый фрейм от аргументов. Системные WinAPI делаю это инструкцией
RET 4*(кол-во аргументов)
. Вот пример:Основным недостатком данного соглашения является активное использование стека, ведь PUSH это реальный запрос к внешней памяти ОЗУ, что влечёт за собой падение производительности. С учётом этих проблем, в 64-битных системах Win применяется другой тип вызова _fastcall (быстрый). Здесь 4 первых аргумента передаются через регистры RCX,RDX,R8,R9, а если функция ожидает больше аргументов, то остальные как и прежде – через стек. Использование регистров для засылки аргументов полностью ликвидирует связанные в памятью задержки, а потому вызов осуществляется намного быстрее. Листинг запроса той-же функции представлен ниже:
С быстрым вызовом связана пара нюансов:
• Аргументы передаются в обратном порядке слева-направо. Если на скринах выше сравнить их в последнем столбце то видно, что порядок их обратный.
• Обратите внимание на конструкцию
sub\add rsp,20
, которая берёт в контейнер непосредственный вызов функции. Так индусы из Microsoft по-старинке резервируют место в стеке для четырёх аргументов не смотря на то, что и находятся они теперь в регистрах RCX,RDX,R8,R9.В данном случае функция имеет 4 аргумента, поэтому в резерв уходят 4х8=32-байта, или 20h. Если у функции аргументов больше (например 7), то оставшиеся три помещаются в стек. Здесь компилятор зарезервирует уже 7х8=56, плюс 8-байт под адрес-возврата, итого 64=40h. Причём резерв\освобождение фреймов нам нужно предусматривать перед вызовом каждой функцией, что сильно напрягает. По сути это то, что называется «стек очищает вызывающий», в отличии от stdcall, где очисткой фрейма от оставшегося мусора озадачена сама функция. Вот пример последовательного вызова MessageBox() и CreateFile() с семью аргументами:
В компиляторе FASM имеется огромное кол-во умных макросов, среди которых есть и парочка frame\endf. Они специально были написаны разработчиком продукта Томашом Грыштаром, чтобы привести бинарник в более приглядный вид, удалив из него этот бессмысленный резерв фреймов на входе и выходе из каждой функции. По мнению автора лучше один раз выделить большой фрейм, чем несколько раз мелкие.
Fasm относится к многопроходным компиляторам. На первом проходе транслятор вычисляет в исходнике API-функцию с наибольшим числом аргументов, и по их кол-ву сразу выделяет соответствующий фрейм. Теперь все функции, которые находятся внутри макросов frame\endf будут использовать для своих нужд этот общий фрейм. Такая конструкция выглядит более логичной, чем неоптимизированная в дефолте. Вот пример и то, что получим в результате:
C-подобный:
.code
start:
nop
frame
invoke MessageBox,0,szHello,szCapt,0
nop
invoke CreateFile,path, GENERIC_READ + GENERIC_WRITE,\
FILE_SHARE_READ + FILE_SHARE_WRITE,\
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0
endf
nop
Значит первой строкой
sub rsp,8
компиль выравнивает стек на 16-байтную границу, что является обязательным для кода х64. Причём делает он это на автомате (без нашего участия), при помощи макросов из инклуда «win64ax.inc». Далее идёт NOP и следом резервируется стековый фрейм сразу для обоих API-функций, по кол-ву аргументов CreateFile(). Как видим, после MessageBox() фрейм уже не возвращается системе, и остаётся активным до конца. Макросами frame\endf
можно оборачивать весь код, или отдельные его блоки.А в остальном программирование в х64 ничем не отличается от х32. Радует то, что наделённые разумом макросы делают за нас всю черновую работу, если только код наш не маргинальный, с стиле проприетарного
call
в место invoke
. Тогда можно запросто нарваться на повсюду разбросанные шипы, то как выравнивание стека, последовательность аргументов и многое другое. Просто подключайте инклуд «win64ax.inc», и всё будет пучком.Под занавес приведу небольшой пример, в котором соберу всё выше-сказанное. Вот его алго:
• GetVersionEx() в структуру «OSVERSIONINFO» возвращает версию системы:
C-подобный:
struct OSVERSIONINFOA
dwOSVersionInfoSize dd sizeof.OSVERSIONINFOA
dwMajorVersion dd 0
dwMinorVersion dd 0
dwBuildNumber dd 0
dwPlatformId dd 0
szCSDVersion rb 128
ends
• GlobalMemoryStatusEx() возвращает основную инфу о системной памяти.
Функция с одним аргументом, и ожидает указатель на структуру «MEMORYSTATUSEX»:
C-подобный:
struct MEMORYSTATUSEX
dwLength dd sizeof.MEMORYSTATUSEX
dwMemoryLoad dd 0 ;// занятая физ.память в процентах
dwTotalPhys dq 0 ;// всего физической
dwAvailPhys dq 0 ;// ..(свободно)
dwTotalPageFile dq 0 ;// размер файла-подкачки
dwAvailPageFile dq 0 ;// ..(свободно)
dwTotalVirtual dq 0 ;// всего виртуальной
dwAvailVirtual dq 0 ;// ..(свободно)
dwAvailExtVirtual dq 0 ;// резерв.
ends
• Использование инструкций SSE с регистрами XMM, для перевода величин в формат с плавающей точкой.
• printf() из msvcrt.dll для вывода данных на консоль.
• В качестве примера – оформление и вызов собственных процедур с 8 аргументами, по соглашению «fastcall».
Выше говорилось, что инструкцией
sub rsp,20h
компилятор уже выделяет фрейм в стеке, как-минимум для четырёх аргументов. Поэтому если мы планируем вызов системных API из своей процедуры, то должны заранее сохранить в этот выделенный фрейм регистры RCX,RDX,R8,R9, где им собственно и место. Иначе вызываемая API затерёт их под себя, и наши аргументы отправятся к праотцам:
C-подобный:
format pe64 console
include 'win64ax.inc'
entry start
;//-----------------
section '.data' data readable writeable
osVer OSVERSIONINFOA
ms MEMORYSTATUSEX
szHelp db 'This is Long-mode "_fastcall" proc example.',0
szCapt db 'CPU registers dump & Function virtual address.',10,0
gb dq 1024 shl 20 ;//1024 x 1024 x 1024
doubleA dq 0
doubleB dq 0
buff dd 0
;//-----------------
section '.code' code readable executable
start:
frame ;//<---- Волшебный макрос
invoke SetConsoleTitle,<'System memory info',0>
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,<10,' Microsoft Windows [Version: %d.%d.%d. %s]',10,0>,\
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, <10,\
' Physical memory',10,\
' -------------------------',10,\
' Total : %5d Mb',10,\
' Free : %5d Mb = %d%%',10,10,0>,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, <' PageFile',10,\
' -------------------------',10,\
' Total : %5.2f Gb',10,\
' Free : %5.2f Gb',10,10,0>,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, <' Virtual memory',10,\
' -------------------------',10,\
' Total : %.2f Gb',10,\
' Free : %.2f Gb',10,0>,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,<10,10,' ---> %s',10,' ---> %s',\
10,' RAX: 0x%016I64x',\
10,' RBX: 0x%016I64x',\
10,' R10: 0x%016I64x',10,\
10,' 0x%016I64x <-- printf() address',\
10,' 0x%016I64x <-- SetCosoleTitle() address',\
10,' 0x%016I64x <-- GlobalMemoryStatusEx() address',10,0>,\
[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'
Посмотрим на выхлоп функции GlobalMemoryStatus().
Моя тестовая машина с 64-битной семёркой на борту вполне себе сносно функционирует даже при 1.5 ГБ физ.памяти. И это при том, что запущен браузер Хром с несколькими вкладками, тотал, офис и фотошоп. В файле-подкачки свободен гиг, а виртуальной 8 тераБайт для приложений, и столько-же для ядра.
А вот логи десятки..
Из четырёх физических доступен всего один, но и этого ей недостаточно, т.к. шесть из десяти в подкачке занято. Зато виртуальной хоть отбавляй, аж 128 ТБайт, только юзать их (кроме игр) как-правило некому. Все системные либы грузятся в верхнюю область пользовательского 48-бит пространства, хотя на семёрке Kernel32.dll тусуется где-то между небом и землёй, по середине.
6. Заключение
Что-то букаф в треде получилось много, видимо пора заканчивать..
В скрепку кладу исполняемый *.exe для тестов, и файл «fasmw.ini» для тех, кто хочет поменять визуальную тему FASM’a на Black. У меня он установлен по-пути D:\Install\FASM, а вам нужно открыть его в блокноте, и указать свой путь до инклуд. Сделайте бэкап оригинального *.ini на случай, если что-то пойдёт не так. Всем удачи, пока!