Статья FASM. Особенности кодинга на платформе Win-64

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-ми, по размеру дескриптора. Непосредственно в дескрипторах указывается уже тип сегмента, его базовый адрес, размер (лимит), и атрибуты защиты.


Selector.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ГБ кажется ничтожно малым.


MS_limit.png


Такой расклад навёл инженеров на мысль, что пришло время вообще избавиться от устаревшей сегментной модели, ведь памяти теперь предостаточно и располагай хоть 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=через системный шлюз).

Seg_Reg.png


А вот с дескрипторами сегментных регистров 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К-страницы (привет своп вирт.памяти), то атрибуты этих страниц имеют приоритет над атрибутами в дескрипторах сегментов, поэтому механизм функционирует исправно. Надеюсь скрин прояснит выше-сказанное:


Flat_Mem.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. В столбце «Бит» данной таблицы указана разрядность одного регистра:


Reg_table.png


Обратите внимание, что регистры MMX являются частью 80-битных регистров сопроцессора FPU, занимая в них младшие 64-бита мантиссы так, что для инструкций MMX остаётся исключительно целочисленная арифметика, без дробной части. При таком раскладе, в любой момент времени (внутри одного блока кода) мы можем использовать или регистры FPU, или MMX, но не оба сразу – в противном случае получим винегрет из данных.

Чтобы решить эту проблему, инженеры ввели независимый от FPU пул 128-битных регистров SSE, который позже был расширен до 256-бит AVX. Здесь наблюдаем аналогичную картину, когда SSE живут внутри AVX. Соответственно их инструкции так-же нельзя комбинировать. Отличительной особенностью SSE\AVX является доступность им операций как со-скалярными числами (тогда инструкции заканчиваются на «ss», scalar), так и с упакованными (окончание «ps», packet) – это значительно расширяет область их применения. А вообще в эти регистры можно пихать любые типы данных:


SSE_type.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-бит регистры всегда обнуляет старшую их часть:


x64_Reg.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]. В последнем случае значение(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-битные регистры:


REX.png


Посмотрим в отладчике х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 – это опкод:

Opcode_prefix.png


Начиная с адреса 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:


CPUZ.png


Теперь как нам предлагают вызывать функции WinAPI 32-битные системы?
Они используют соглашение _stdcall (standart), когда все аргументы функции передаются через стек, справа-налево. После того-как функция отработает, на выходе она сама должна очистить свой стековый фрейм от аргументов. Системные WinAPI делаю это инструкцией RET 4*(кол-во аргументов). Вот пример:


stdcall.png


Основным недостатком данного соглашения является активное использование стека, ведь PUSH это реальный запрос к внешней памяти ОЗУ, что влечёт за собой падение производительности. С учётом этих проблем, в 64-битных системах Win применяется другой тип вызова _fastcall (быстрый). Здесь 4 первых аргумента передаются через регистры RCX,RDX,R8,R9, а если функция ожидает больше аргументов, то остальные как и прежде – через стек. Использование регистров для засылки аргументов полностью ликвидирует связанные в памятью задержки, а потому вызов осуществляется намного быстрее. Листинг запроса той-же функции представлен ниже:

fastcall.png


С быстрым вызовом связана пара нюансов:

• Аргументы передаются в обратном порядке слева-направо. Если на скринах выше сравнить их в последнем столбце то видно, что порядок их обратный.
• Обратите внимание на конструкцию sub\add rsp,20, которая берёт в контейнер непосредственный вызов функции. Так индусы из Microsoft по-старинке резервируют место в стеке для четырёх аргументов не смотря на то, что и находятся они теперь в регистрах RCX,RDX,R8,R9.

В данном случае функция имеет 4 аргумента, поэтому в резерв уходят 4х8=32-байта, или 20h. Если у функции аргументов больше (например 7), то оставшиеся три помещаются в стек. Здесь компилятор зарезервирует уже 7х8=56, плюс 8-байт под адрес-возврата, итого 64=40h. Причём резерв\освобождение фреймов нам нужно предусматривать перед вызовом каждой функцией, что сильно напрягает. По сути это то, что называется «стек очищает вызывающий», в отличии от stdcall, где очисткой фрейма от оставшегося мусора озадачена сама функция. Вот пример последовательного вызова MessageBox() и CreateFile() с семью аргументами:

sub_rsp.png


В компиляторе 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

frame_marco.png


Значит первой строкой 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'

Win7_result.png


Посмотрим на выхлоп функции GlobalMemoryStatus().
Моя тестовая машина с 64-битной семёркой на борту вполне себе сносно функционирует даже при 1.5 ГБ физ.памяти. И это при том, что запущен браузер Хром с несколькими вкладками, тотал, офис и фотошоп. В файле-подкачки свободен гиг, а виртуальной 8 тераБайт для приложений, и столько-же для ядра.

А вот логи десятки..
Из четырёх физических доступен всего один, но и этого ей недостаточно, т.к. шесть из десяти в подкачке занято. Зато виртуальной хоть отбавляй, аж 128 ТБайт, только юзать их (кроме игр) как-правило некому. Все системные либы грузятся в верхнюю область пользовательского 48-бит пространства, хотя на семёрке Kernel32.dll тусуется где-то между небом и землёй, по середине.


Win10_result.png



6. Заключение

Что-то букаф в треде получилось много, видимо пора заканчивать..
В скрепку кладу исполняемый *.exe для тестов, и файл «fasmw.ini» для тех, кто хочет поменять визуальную тему FASM’a на Black. У меня он установлен по-пути D:\Install\FASM, а вам нужно открыть его в блокноте, и указать свой путь до инклуд. Сделайте бэкап оригинального *.ini на случай, если что-то пойдёт не так. Всем удачи, пока!
 

Вложения

  • MemStatus.zip
    1,5 КБ · Просмотры: 63
Мы в соцсетях:

1 августа стартует курс «Основы программирования на Python» от команды The Codeby

Курс будет начинаться с полного нуля, то есть начальные знания по Python не нужны. Длительность обучения 2 месяца. Учащиеся получат методички, видео лекции и домашние задания. Много практики. Постоянная обратная связь с кураторами, которые помогут с решением возникших проблем.

Запись на курс до 10 августа. Подробнее ...