Глава 1 Введение

Содержание
Глава 1 Введение

Эта глава содержит всю важнейшую информацию, которая понадобится вам, чтобы начать использовать flat assembler. Если у вас уже есть опыт программирования на ассемблере, вам достаточно прочитать лишь первую главу перед использованием этого компилятора.


1.1 Обзор компилятора

Flat assembler (далее Fasm ) - это быстрый компилятор языка ассемблера для процессоров архитектуры x86, который делает несколько проходов, чтобы оптимизировать размер сгенерированного машинного кода. Это само компилируемый и версии для разных операционных систем предоставляются. Они предназначены использоваться из командной строки системы и в обращении с ними нет разницы.

Этот документ описывает также версию IDE, разработанную для системы Windows, которая использует графический интерфейс вместо консоли и имеет встроенный редактор. Но с точки зрения компиляции он обладает точно такой же функциональностью, что и все консольные версии, и поэтому более поздние части (начиная с 1.2) этого документа являются общими с другими выпусками. Исполняемый файл версии IDE называется fasmw.exe, а fasm.exe - версия командной строки.


1.1.1 Системные требования

Для всех версий требуется 32-битный процессор архитектуры x86 (не менее 80386), хотя они также могут создавать программы для 16-битных процессоров архитектуры x86. Для консольной версии Windows требуется любая операционная система Win32, а для версии Windows GUI требуется система Win32 GUI версии 4.0 или выше, поэтому она должна работать на всех системах, совместимых с Windows 95.

Для примеров кода, поставляемых с этой версией, требуется, чтобы у переменной среды INCLUDE был установлен путь к каталогу include, который является частью пакета Fasm. Если такая переменная уже существует в вашей системе и содержит пути, используемые какой-либо другой программой, достаточно добавить к ней новый путь (различные пути должны быть разделены точками с запятой). Если вы не хотите определять такую ??переменную в системе или не знаете, как это сделать, вы можете установить ее для Fasm IDE только путем редактирования файла fasmw.ini в его каталоге (этот файл создается fasmw.exe, когда он выполняется, но вы также можете создать его самостоятельно). В этом случае вам следует добавить значение Include в раздел Environment. Например, когда вы распаковали файлы Fasm в каталог c:\fasmw, вы должны поместить в файл c:\fasmw\fasmw.ini следующие две строки:

[Environment]
Include = c:\fasmw\include

Если вы не определите переменную среды INCLUDE должным образом, вам придется вручную указать полный путь к папке include Win32 в каждую программу, которую вы хотите скомпилировать.


1.1.2 Использование компилятора

Чтобы начать работать с Fasm, просто дважды щелкните значок файла fasmw.exe или перетащите на него значок исходного файла. Вы также можете позже открыть новые исходные файлы с помощью команды Open из меню File или перетащив файлы в окно редактора. Вы можете открыть несколько исходных файлов одновременно, каждый из которых представлен одной вкладкой в нижней части окна редактора. Чтобы выбрать файл для редактирования, щелкните по соответствующей вкладке левой кнопкой мыши. Компилятор по умолчанию работает с файлом, который вы сейчас редактируете, но вы можете заставить его всегда работать с каким-то конкретным файлом, щелкнув правой кнопкой мыши на соответствующей вкладке и выбрав команду Assign . Только один файл может быть назначен компилятору одновременно.

Когда ваш исходный файл готов, вы можете выполнить компиляцию с помощью команды Compile из меню Run. Когда компиляция будет успешной, компилятор отобразит сводную информацию о процессе компиляции; в противном случае будет отображаться информация об ошибке, которая произошла. Сводная информация о компиляции включает в себя информацию о том, сколько проходов было сделано, сколько времени это заняло и сколько байтов было записано в целевой файл. Он также содержит текстовое поле с именем Display , в котором будут появляться любые сообщения из директивы display в исходнике (см. 2.2.5). Сводка ошибок состоит как минимум из сообщения об ошибке и текстового поля Display, которое имеет ту же цель, что и выше. Если ошибка связана с какой-либо конкретной строкой исходного кода, сводка также содержит текстовое поле Instruction, которое содержит предварительно обработанную форму инструкции, которая вызвала ошибку, если ошибка произошла после стадии препроцессора (в противном случае она пуста), и список Source , где показано расположение всех исходных строк, связанных с этой ошибкой, при выборе строки из этого списка она будет одновременно выбрана в окне редактора (если файл, содержащий эту строку, не загружен, он будет автоматически добавлен).

Команда Run также выполняет компиляцию, и в случае успешной компиляции она запускает скомпилированную программу, если только это один из форматов, которые можно запустить в среде Windows, в противном случае вы получите сообщение о том, что файл такого типа не может быть выполнен. Если возникает ошибка, компилятор отображает информацию о ней в той же форме, как если бы использовалась команда Compile .

Если у компилятора заканчивается память, вы можете увеличить выделение памяти в диалоговом окне Compiler setup, которое можно запустить из меню Options. Вы можете указать количество килобайт, которое должен использовать компилятор, а также приоритет потока компилятора.

Если вы хотите, чтобы работал только один экземпляр программы, добавьте параметр OneInstanceOnly=1 в раздел Options файла fasmw.ini.


1.1.3 Клавиатурные команды в редакторе

В этом разделе перечислены все команды клавиатуры, доступные при работе с редактором. За исключением клавиш, перечисленных как специальные, они являются общими с DOS IDE для Fasm.

Перемещение

Left arrow | перейти на один символ влево
Right arrow | перейти на один символ вправо
Up arrow | перейти на одну строку вверх
Down arrow | перейти на одну строку вниз
Ctrl+Left arrow | перейти на одно слово влево
Ctrl+Right arrow | перейти на одно слово вправо
Home | перейти к началу строки
End | перейти к концу строки
PageUp | перейти на одну страницу вверх
PageDown | перейти на одну страницу вниз
Ctrl+Home | перейти к первой строке страницы
Ctrl+End | перейти к последней строке страницы
Ctrl+PageUp | перейти к первой строке текста
Ctrl+PageDown | перейти к последней строке текста

Каждая из клавиш перемещения, нажимаемых с помощью Shift , выделяет текст.

Редактирование:

Insert | переключить режим вставки/перезаписи
Alt+Insert | переключить горизонтальное/вертикальное выделение блоков
Delete | удалить текущий символ
Backspace | удалить предыдущий символ
Ctrl+Backspace | удалить предыдущее слово
Alt+Backspace | отменить предыдущую операцию (также Ctrl+Z)
Alt+Shift+Backspace | повторить ранее отмененную операцию (также Ctrl+Shift+Z)
Ctrl+Y | удалить текущую строку
F6 | дублировать текущую строку

Операции с блоком:

Ctrl+Insert | скопировать блок в буфер обмена (также Ctrl+C)
Shift+Insert | вставить блок из буфера обмена (также Ctrl+V)
Ctrl+Delete | удалить блок
Shift+Delete | вырезать блок в буфер обмена (также Ctrl+X)
Ctrl+A | выделить весь текст

Поиск:

F5 | перейти в указанную позицию (также Ctrl+G)
F7 | найти (также Ctrl+F)
Shift+F7 | найти следующее (также F3)
Ctrl+F7 | заменить (также Ctrl+H)

Компиляция:

F9 | скомпилировать и запустить
Ctrl+F9 | только скомпилировать
Shift+F9 | назначить текущий файл в качестве основного для компиляции
Ctrl+F8 | скомпилировать и создавать символы информации

Другие клавиши:

F2 | сохранить текущий файл
Shift+F2 | сохранить файл под новым именем
F4 | открыть файл
Ctrl+N | создать новый файл
Ctrl+Tab | перейти к следующему файлу
Ctrl+Shift+Tab | перейти к предыдущему файлу
Alt+[1-9] | переключиться на файл с указанным номером
Esc | закрыть текущий файл
Alt+X | закрыть все файлы и выйти
Ctrl+F6 | калькулятор
Alt+Left arrow | прокрутить влево
Alt+Right arrow | прокрутить вправо
Alt+Up arrow | прокрутить вверх
Alt+Down arrow | прокрутить вниз

Специальные клавиши:

F1 | поиск по ключевому слову в выбранном файле справки
Alt+F1 | содержимое выбранного файла справки


1.1.4 Параметры редактора

В меню Options также находится список параметров редактора, которые могут быть включены или отключены и влиять на поведение редактора. В этом разделе описываются эти параметры.

Secure selection – когда вы включаете эту опцию, выделенный блок никогда не удаляется, если вы начинаете печатать. Когда вы выполняете какую-либо операцию по изменению текста, выделение отменяется, и никак не влияет на выделенный текст, а затем выполняется команда. Когда эта опция выключена, и вы начинаете печатать, текущий выбор отменяется, также клавиша Del просто удаляет выбранный блок (когда Secure selection включен, вы должны использовать Ctrl+Del ).

Automatic brackets – при вводе любой из открывающих скобок закрывающая автоматически ставится сразу после каретки.

Automatic indents – когда вы нажимаете Enter, чтобы начать новую строку, каретка перемещается в новую строку в той же позиции, где в предыдущей строке помещается первый непустой символ. Если вы разрываете строку, и после каретки при нажатии клавиши Enter было несколько непустых символов, они перемещаются в новую строку в позиции отступа, все пустые символы, которые были между кареткой и ими, игнорируются.

Smart tabulation – когда вы нажимаете клавишу Tab , она перемещает вас на позицию чуть ниже следующей последовательности непустых символов в строке выше, начиная с позиции чуть выше того места, где вы были. Если в приведенной выше строке такой последовательности не найдено, используется стандартный размер табуляции 8 символов.

Optimal ?ll on saving – если этот параметр включен, при сохранении файла все пустые области заполняются оптимальной комбинацией табов и пробелов для получения меньшего размера файла. Если этот параметр выключен, пустые области сохраняются заполненные пробелами (но пробелы в концах строк не сохраняются).

Revive dead keys – когда эта опция включена, в редакторе отключаются так называемые мертвые клавиши (клавиши, которые не сразу генерируют символ, но ждут следующей клавиши, чтобы решить, какой символ поставить - обычно вы вводите символ мертвой клавиши, нажав клавишу пробела после нее). Это может быть полезно, если ключ для ввода некоторых символов, которые вам часто нужно вводить в исходный код сборки, является мертвым ключом и вам не нужны эти функции для написания программ.

Time scrolling – с этой опцией можно использовать колесо мыши для прокрутки пространства отмены / повтора, пока нажаты клавиши AltGr или Ctrl+Alt .


1.1.5 Компилятоция из командной строки

Чтобы выполнить компиляцию из командной строки, вам нужно выполнить исполняемый файл fasm.exe , предоставив два параметра: первый должен быть именем исходного файла, второй должен быть именем выходного файла. Если второй параметр не указан, имя для выходного файла будет создано автоматически. После отображения краткой информации о названии и версии программы компилятор прочитает данные из исходного файла и скомпилирует их. Когда компиляция будет успешной, компилятор запишет сгенерированный код в целевой файл и отобразит сводную информацию о процессе компиляции; в противном случае будет отображаться информация об ошибке, которая произошла.

Исходный файл должен быть текстовым файлом и может быть создан в любом текстовом редакторе. Разрывы строк допускаются как в стандартах DOS, так и в стандартах Unix, табуляторы рассматриваются как пробелы.

В командной строке вы также можете включить опцию -m, за которой следует число, указывающее, сколько килобайт памяти Fasm должен максимально использовать. В случае версии DOS эта опция ограничивается только использованием расширенной памяти. Параметр -p, за которым следует число, можно использовать для указания предела количества проходов, которые выполняет ассемблер. Если код не может быть сгенерирован в течение заданного количества проходов, сборка будет прервана с сообщением об ошибке. Максимальное значение этого параметра составляет 65536, в то время как ограничение по умолчанию, используемое, когда такая опция не включена в командную строку, равно 100. Можно также ограничить число проходов, которые выполняет ассемблер, с параметром -p , за которым следует параметр число, указывающее максимальное количество проходов.

Не существует опций командной строки, которые влияли бы на вывод компилятора, Fasm требуется только исходный код для включения информации, которая ему действительно нужна. Например, чтобы указать формат вывода, вы указываете его с помощью директивы format в начале исходного кода.


1.1.6 Сообщения компилятора командной строки

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

flat assembler version 1.72 (16384 kilobytes memory)
38 passes, 5.3 seconds, 77824 bytes.

В случае ошибки во время процесса компиляции, программа отобразит сообщение об ошибке. Например, когда компилятор не может найти входной файл, он отобразит следующее сообщение:

flat assembler version 1.72 (16384 kilobytes memory)
error: source file not found.

Если ошибка связана с определенной частью исходного кода, также будет отображена строка с исходным кодом, которая вызвала ошибку. Также приведено размещение этой строки в источнике, чтобы помочь вам найти эту ошибку, например:

flat assembler version 1.72 (16384 kilobytes memory)
example.asm [3]:
    mob ax,1
error: illegal instruction.

Это означает, что в третьей строке файла example.asm компилятор обнаружил неопознанную инструкцию. Когда строка, вызвавшая ошибку, содержит макроинструкцию, также отображается строка в определении макроинструкции, которая сгенерировала ошибочную инструкцию:

flat assembler version 1.72 (16384 kilobytes memory)
example.asm [6]:
    stoschar 7
example.asm [3] stoschar [1]:
    mob al,char
error: illegal instruction.

Это означает, что макрос в шестой строке файла example.asm сгенерировал нераспознанную инструкцию в первой строке своего определения.


1.1.7 Форматы вывода

По умолчанию, когда в исходном файле нет директивы format, Fasm просто помещает сгенерированные коды команд в вывод, создавая таким образом простой двоичный файл. По умолчанию он генерирует 16-битный код, но вы всегда можете перевести его в 16-битный или 32-битный режим, используя директиву use16 или use32 . Некоторые из выходных форматов переключаются в 32-битный режим, если выбрана эта опция - больше информации о форматах, которые вы можете выбрать, можно найти в 2.4.

Расширение файла назначения выбирается компилятором автоматически в зависимости от выбранного формата вывода.

Весь сгенерированный код в файле-адресате всегда идет в том же порядке, что и написанный в исходнике.


1.2 Синтаксис ассемблера

Информация, представленная ниже, предназначена главным образом для программистов, которые ранее использовали некоторые другие компиляторы ассемблера. Если вы новичок, вам следует поискать учебники по программированию на ассемблере.

Fasm по умолчанию использует синтаксис Intel для ассемблерных инструкций, хотя вы можете настроить его, используя возможности препроцессора (макроинструкции и символические константы). Он также имеет свой собственный набор директив - инструкции для компилятора.

Все символы, определенные внутри исходников, чувствительны к регистру.


1.2.1 Синтаксис инструкций

Инструкции на языке ассемблера разделяются разрывами строк, и одна инструкция должна располагаться на одной строке текста. Если строка содержит точку с запятой, за исключением точек с запятой внутри строк в кавычках, остальная часть этой строки является комментарием и компилятор игнорирует ее. Если строка заканчивается символом \ (в конце концов точка с запятой и комментарий могут следовать за ней), в этой точке присоединяется следующая строка.

Каждая строка в источнике - это последовательность элементов, которая может быть одного из трех типов. Один тип - это знаки символов, которыми являются специальные знаки, которые являются отдельными элементами, даже если они не отделены пробелами от других символов. Любой из + - * / = < > ( ) [ ] { } : , | & ~ # ` являются знаком символа. Последовательность других знаков, отделенных от других элементов или пробелами или знаками символов, это символ. Если первый знак символа является одинарной или двойной кавычкой, он объединяет любую последовательность символов, следующих за ним, даже специальные, в строку, которая должна заканчиваться тем же символом, с которого она начиналась (одинарная или двойная). Однако если встречаются две кавычки подряд (без знаков между ними), они также включаются в строку и она продолжается. Символы, отличные от знаков символов и строк, заключенных в кавычки, могут использоваться как имена, поэтому они также называются символами имен.

Каждая инструкция состоит из мнемоники и различного числа операндов, разделенных запятыми. Операндом может быть регистр, непосредственное значение или данные, размещенные в памяти, ему также может предшествовать оператор размера для определения или переопределения его размера (таблица 1.1). Названия доступных регистров вы можете найти в таблице 1.2, их размеры нельзя переопределить. Непосредственное значение может быть указано любым числовым выражением.

Когда операнд представляет собой данные в памяти, адрес этих данных (также любое числовое выражение, но оно может содержать регистры) должен быть заключен в квадратные скобки или предварен оператором ptr. Например, инструкция mov eax, 3 поместит непосредственное значение 3 в регистр eax, инструкция mov eax, [7] поместит 32-битное значение с адреса 7 в eax, а инструкция mov byte [7], 3 поместит непосредственное значение 3 в байт по адресу 7, также может быть записано как mov byte ptr 7,3 . Для того чтобы установить какой сегментный регистр будет использоваться для адресации, нужно поставить его название с двоеточием перед адресом внутри квадратных скобок или после оператора ptr .

Таблица 1.1 Размеры операторов
Operator | Bits | Bytes
byte | 8 | 1
word | 16 | 2
dword | 32 | 4
fword | 48 | 6
pword | 48 | 6
qword | 64 | 8
tbyte | 80 | 10
tword | 80 | 10
dqword | 128 | 16
xword | 128 | 16
qqword | 256 | 32
yword | 256 | 32
dqqword | 512 | 64
zword | 512 | 64

Таблица 1.2 Регистры
Type Bits
General

8
16
32

 al  cl  dl  bl  ah  ch  dh  bh
 ax  cx  dx  bx  sp  bp  si  di
 eax ecx edx ebx esp ebp esi edi
Segment

16

 es cs ss ds fs gs
Control

32

 cr0 cr2 cr3 cr4
Debug

32

 dr0 dr1 dr2 dr3 dr6 dr7
FPU

80

 st0 st1 st2 st3 st4 st5 st6 st7
MMX

64

 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7
SSE

128

 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7
AVX

256

 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7
AVX-512

512

 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7
Opmask

64

 k0 k1 k2 k3 k4 k5 k6 k7
Bounds

128

 bnd0 bnd1 bnd2 bnd3


1.2.2 Определение данных

Чтобы описать данные или зарезервировать для них место, используйте одну из директив, перечисленных в таблице 1.3. За директивой описания данных должно следовать одно или несколько числовых значений, разделенных запятыми. Эти выражения определяют значения для простейших элементов данных, размер которых зависит от того, какая директива используется. Например db 1,2,3 описывает три байта со значениями 1, 2 и 3 соответственно.

Директивы db и du также принимают строки любой длины, которые будут преобразованы в цепочку байтов при использовании db и в цепочку слов с обнуленным старшим байтом при использовании du. Например, db ’abc’ определит три байта значений 61, 62 и 63.

Директива dp или её синоним df допускают, чтобы значения состояли из двух числовых выражений, разделенных двоеточием, где первое значение - это верхнее слово, а второе - это нижнее двойное слово значения дальнего указателя. Также dd допускает такие указатели, состоящие из двух слов, разделенных двоеточием, и dt допускает слово и четверное слово, разделенные двоеточием, четверное слово запоминается первым. Директива dt с одним параметром допускает только значения с плавающей точкой и создает данные в FPU-формате двойной расширенной точности.

Любая из приведенных выше директив позволяет использовать специальный оператор dup для создания нескольких копий заданных значений. Этому оператору должно предшествовать число дубликатов, а за ним должно следовать значение для дублирования - это может быть даже цепочка значений, разделенных запятыми, но такой набор значений должен быть заключен в круглые скобки, как db 5 dup (1,2) , который определяет пять копий данной двухбайтовой последовательности.

file - это специальная директива и её синтаксис может быть различным. Эта директива включает цепь байтов из файла. В качестве параметра за ней должно идти в кавычках имя файла, далее, опционально, двоеточие и числовое выражение, указывающее начало цепочки байтов, далее, также опционально, запятая и числовое выражение, определяющее количество байтов в этой цепочке (если этот параметр не определен, то будут включены все данные до конца файла). Например, file 'data.bin' включит весь файл как двоичные данные, а file 'data.bin':10h,4 включит только четыре байта, начиная со смещения 10h.

Таблица 1.3 Директивы данных

Size (bytes)

De?ne data

Reserve data

1

db
file

rb

2

dw
du

rw

4

dd

rd

6

dp
df

rp
rf

8

dq

rq

10

dt

rt

За директивой резервирования данных должно следовать одно числовое выражение, значение которого определяет количество резервируемых ячеек установленного размера. Все директивы описания данных также поддерживают значение ?, которое значит, что этой ячейке не должно быть присвоено какое-то значение. Эффект от этой директивы такой же, как от директивы резервирования данных. Неинициализированные данные не могут быть включены в файл вывода, и, таким образом, их значения всегда будут считаться неизвестными.


1.2.3 Константы и метки

В числовых выражениях вы также можете использовать константы или метки вместо чисел. Чтобы определить константу или метку, вы должны использовать специальные директивы. Каждый метка может быть определен только один раз, и она доступна из любого источника (даже до того, как она была определена). Константа может быть переопределена много раз, но в этом случае она доступна только после того, как она была определена, и всегда равна значению от последнего определения до места, где она использовалась. Когда константа определяется только один раз в источнике, она - как метка - доступна из любого места.

Определение константы состоит из имени константы, за которым следует символ = и числовое выражение, которое после расчета станет значением константы. Это значение всегда рассчитывается во время определения константы. Например, вы можете определить константу count, используя директиву count = 17, а затем использовать ее в инструкциях ассемблера, таких как mov cx, count - которые станут mov cx, 17 во время компиляции.

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

Метка может быть обработана как константа со значением, равным смещению помеченного кода или данных. Например, если вы определяете данные, используя помеченную директиву char db 224, для того, чтобы поместить адрес начала этих данных в регистр bx, вам нужно использовать инструкцию mov bx,char, а для того, чтобы поместить в регистр dl значение байта, на который ссылается char, нужно использовать mov dl,[char] (или mov dl,ptr char ). Если вы попытаетесь ассемблировать mov ax,[char], Fasm выдаст ошибку, так как он сравнивает размеры операндов, которые должны быть равны. Вы можете принудительно проассемблировать эту инструкцию, изменяя размер операнда: mov ax, word [char], но помните, что эта инструкция прочитает два байта, начинающихся с адреса char, тогда как он был определен как один байт.

Последний и самый гибкий способ определения меток - это использование директивы label. За этой директивой должно следовать имя метки, затем, необязательно, оператор размера, а затем - также необязательно, оператор at и числовое выражение, определяющее адрес, по которому эта метка должна быть определена. Например, label wchar word at char будет определять новую метку для 16-битных данных по адресу char. Теперь инструкция mov ax, [wchar] будет после компиляции такой же, как mov ax, word [char]. Если адрес не указан, директива label будет ссылаться на текущий адрес. Таким образом, mov [wchar], 57568 скопирует два байта, а mov [char], 224 скопирует один байт на тот же адрес.

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

@@ обозначает анонимную метку, вы можете определить её множество раз. Символ @b (или эквивалент @r) ссылается на ближайшую предшествующую анонимную метку, а символ @f ссылается на ближайшую после неё анонимною метку. Эти специальные символы нечувствительны к регистру.


1.2.4 Числовые выражения

В приведенных выше примерах все числовые выражения были простыми числами, константы или метки. Но они могут быть более сложными, используя арифметические или логические операторы для расчетов во время компиляции. Все эти операторы с их значениями приоритета перечислены в таблице 1.4. Операции с высшим значение приоритета будет рассчитано в первую очередь, вы, конечно, можете изменить это поведение, поместив некоторые части выражения в скобках. +, -, * и / являются тандартными арифметическими операциями,mod вычисляет остаток от деления. and, or, xor, shl, shr, bsf, bsr и not совершаюте же логические операции, что и инструкции ассемблера с такими же имена. rva и plt - специальные унарные операторы, которые выполняют преобразования между различными типами адресов, они могут быть использованы только с некоторыми выходными форматами и их значение могут отличаться (см. 2.4).

The arithmetical and bit–logical calculations are usually processed as if they operated on in?nite precision 2–adic numbers, and assembler signalizes an over?ow error if because of its limitations it is not table to perform the required calculation, or if the result is too large number to ?t in either signed or unsigned range for the destination unit size.

Числа в выражениях по умолчанию обрабатываются как десятичные, двоичные числа должны иметь b в конце, восьмеричные числа должны заканчиваться на букву o, шестнадцатеричные цифры должны начинаться символами 0x (как в языке C), или символом $ (как в языке Pascal) или должны заканчиваться буквой h . Также заключенная в кавычки строка при включении в выражение будет конвертирована в число - первый символ станет минимальным значащим байтом числа.

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

Также есть несколько специальных символов, которые могут быть использованы в числовом выражении. Первое - это $ , которое всегда равно значению текущего смещения, тогда как $$ равно базовому адресу текущего диапазона адресов. Следующий символ - % - это номер текущего повтора в частях кода, которые повторяются, благодаря использованию некоторых специальных директив (смотрите 2.2). Также существует символ %t , который всегда равен текущей отметке времени.

Любое численное выражение также может состоять из одного значения с плавающей точкой (Fasm не может производить во время компиляции операции с плавающей точкой) в научной записи. Для распознания компилятором, эти значения должны содержать в конце букву f, либо включать в себя по крайней мере один символ "." или E. Так, 1.0, 1E0 и 1f определяют одно и то же значение с плавающей точкой, когда как просто "1" определяет целочисленное значение.

Таблица 1.4 Арифметические и логические операторы в порядке приоритета
Приоритет | Операторы
0 | + -
1 | * /
2 | mod
3 | and or xor
4 | shl shr
5 | not
6 | bsf bsr
7 | rva plt


1.2.5 Переходы и вызовы

Операнд любого перехода или инструкция вызова может предваряться не только операторами размера, но также одним из операторов, определяющих тип перехода: short, near или far . Например, если ассемблер в 16-битном режиме, инструкция jmp dword [0] станет далеким переходом, а если ассемблер в 32-битном режиме, она станет близким переходом. Чтобы заставить эту инструкцию обрабатываться по-разному, используйте формы jmp near dword [0] или jmp far dword [0] .

Если операнд близкого перехода это немедленное значение, ассемблер, если возможно, сгенерирует кратчайший вариант этой инструкции перехода (но не будет создавать 32-битную инструкцию в 16-битном режиме или 16-битную инструкцию в 32-битном режиме, если оператор размера точно её не определит). Заданием оператора размера вы можете заставить ассемблер всегда генерировать длинный вариант (например, jmp near 0) или всегда создавать короткий вариант и завершаться с ошибкой, когда это невозможно (например jmp short 0 ).


1.2.6 Установки размера

Если инструкция использует некоторую адресацию в памяти, по умолчанию будет генерироваться кратчайшая 8-битная форма, если значение адреса попадает в нужный диапазон, но он может быть изменен с помощью операторов word и dword перед адресом в квадратных скобках (или после оператора ptr ), которые делают длинное смещение соответствующего размера. Такое размещение оператора размера также может быть использовано для установки размера адреса, отличного от размера, установленного в данном режиме по умолчанию.

Инструкции adc, add, and, cmp, or, sbb, sub и xor с первым 16-ти или 32-битным операндом по умолчанию генерируются в укороченной 8-битной форме, если второй операнд - это непосредственное значение, применимое для предписанных 8-битных значений. Она также может быть изменена операторами word и dword перед такими значениями. Сходные правила применимы к инструкции imul с непосредственным значениям в качестве последнего операнда.

Непосредственное значение как операнд для инструкции push без оператора размера, по умолчанию обрабатывается как слово, если ассемблер в 16-битном режиме, и как двойное слово, если Fasm в 32-битном режиме. Короткая 8-битная форма используется по возможности, операторы размера word и dword могут заставить инструкцию push быть сгенерированной в более длинной форме. Мнемоники pushw и pushd указывают ассемблеру сгенерировать 16-битный или 32-битный код без принуждения его использовать длинную форму инструкции.


Глава 2 Описание инструкций

Содержание
Глава 2 Описание инструкций

Эта глава обеспечивает детальную информацию о командах и директивах, которые поддерживает Fasm. Директивы для того, чтобы определять константы и метки были уже обсуждены в 1.2.3 все другие директивы будут описаны позже в этой главе.


2.1 Инструкции архитектуры x86

В этом параграфе вы найдете всю информацию о синтаксисе и назначении инструкций ассемблера.Если вам нужно больше технической информации, смотрите Intel Architecture Software Developer’s Manual.

Инструкции ассемблера состоят из мнемоника (имени инструкции) и нескольких операндов (от нуля до трех). Если операндов два или три, то обычно первым идет адресат, а вторым источник. Операндом может быть регистр, память или непосредственное значение (подробнее о синтаксисе операндов смотрите в 1.2). После описания каждой инструкции ниже будут примеры разных комбинаций операндов, если, конечно, она содержит операнды.

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


2.1.1 Инструкции перемещения данных

mov переносит байт, слово или двойное слово из операнда-источника в операнд-адресат. Этот мнемоник может передавать данные между регистрами общего назначения, из этих регистров в память, обратно, но не может перемещать данные из памяти в память. Он также может передавать непосредственное значение в регистр общего назначения или в память, сегментный регистр в регистр общего назначения или в память, регистр общего назначения в сегментный регистр или в память, контрольный или отладочный регистр в регистр общего назначения и назад. mov может быть ассемблирована только если размер операнда-источника и размер операнда-адресата совпадают. Ниже приведены примеры каждой из перечисленных комбинаций:

mov bx,ax                ; из регистра общего назначения в регистр общего назначения
mov [char],al            ; из регистра общего назначения в память
mov bl,[char]            ; из памяти в регистр общего назначения
mov dl,32                ; епосредственное значение в регистр общего назначения
mov [char],32            ; непосредственное значение в память
mov ax,ds                ; из сегментного регистра в регистр общего назначения
mov [bx],ds              ; из сегментного регистра в память
mov ds,ax                ; из регистра общего назначения в сегментный регистр
mov ds,[bx]              ; из памяти в сегментный регистр
mov eax,cr0              ; из контрольного регистра в регистр общего назначения
mov cr3,ebx              ; из регистра общего назначения в контрольный регистр

xchg меняет местами значения двух операндов. Инструкция может поменять два байтовых операнда, операнды размером в слово и размером в двойное слово. Порядок операндов не важен. В их роли могут выступать два регистра общего назначения либо регистр общего назначения и адрес в памяти. Например:

xchg ax,bx                 ; меняет местами два регистра общего назначения
xchg al,[char]             ; регистр общего назначения и память

push уменьшает значение указателя стекового фрейма (регистр esp), потом переводит операнд на верх стека, на который указывает esp. Операндом может быть память, регистр общего назначения, сегментный регистр или непосредственное значение размером в слово или двойное слово. Если операнд - это непосредственное значение и его размер не определен, то в 16-битном режиме по умолчанию он обрабатывается как слово, а в 32-битном режиме как двойное слово. Мнемоники pushw и pushd - это варианты этой инструкции, которые сохраняют соответственно слова и двойные слова. Если на одной строке содержится несколько операндов (разделенных пробелами, а не запятыми), компилятор проассемблирует цепь инструкций push с этими операндами. Вот примеры с одиночными операндами:

push ax                 ; сохраняет регистр общего назначения
push es                 ; сохраняет сегментный регистр
pushw [bx]              ; сохраняет память
push 1000h              ; сохраняет непосредственное значение

pusha сохраняет в стек содержимое восьми регистров общего назначения. У неё нет операндов. Существует две версии этой инструкции: 16-битная и 32-битная. Ассемблер автоматически генерирует версию, соответствующую текущему режиму, но, используя мнемоники pushaw или pushad, это можно изменить для того, чтобы всегда получать, соответственно, 16- или 32-битную версию. 16-битная версия этой инструкции сохраняет регистры общего назначения в таком порядке: ax, cx, dx, bx, значение регистра sp перед тем, как был сохранен ax, далее bp, si и di . 32-битная версия сохраняет эквивалентные 32-битные регистры в том же порядке.

pop переводит слово или двойное слово из текущей верхушки стека в операнд-адресат и после уменьшает esp на указатель на новую верхушку стека. Операндом может служить память, регистр общего назначения или сегментный регистр. Мнемоники popw и popd - это варианты этой инструкции, восстанавливающие соответственно слова и двойные слова. Если на одной строке содержится несколько операндов, разделенных пробелами, компилятор ассемблирует цепочку инструкций с этими операндами.

pop bx                    ; восстанавливает регистр общего назначения
pop ds                    ; восстанавливает сегментный регистр
popw [si]                 ; восстанавливает память

popa восстанавливает регистры, сохраненные в стек инструкцией pusha, кроме сохраненного значения sp (или esp), который будет проигнорирован. У этой инструкции нет операндов. Чтобы ассемблировать 16 или 32-битную версию этой инструкции, используйте мнемоники popaw или popad .


2.1.2 Инструкции преобразования типов

Инструкции преобразования типов конвертируют байты в слова, слова в двойные слова и двойные слова в четверные слова. Эти преобразования можно совершить, используя знаковое или нулевое расширение. Знаковое расширение заполняют дополнительные биты большего операнда значением бита знака меньшего операнда, нулевое расширение просто забивает их нулями.

cwd и cdq удваивают размер регистра ax или eax соответственно и сохраняет дополнительные биты в регистр dx или edx . Преобразование делается, используя знаковое расширение. Эти инструкции не имеют операндов.

cbw растягивает знак байта al по регистру ax, а cwde растягивает знак слова ax на eax . Эти инструкции также не имеют операндов.

movsx преобразует байт в слово или в двойное слово и слово в двойное слово, используя знаковое расширение. movzx делает то же самое, но используя нулевое расширение. Операндом-источником может быть регистр общего назначения или память, тогда как операндом-адресатом должен быть регистр общего назначения. Например:

movsx ax,al                 ; байт в слово
movsx edx,dl                ; байт в двойное слово
movsx eax,ax                ; слово в двойное слово
movsx ax,byte [bx]          ; байт памяти в слово
movsx edx,byte [bx]         ; байт памяти в двойное слово
movsx eax,word [bx]         ; слово памяти в двойное слово


2.1.3 Двоичные арифметические инструкции

add заменяет операнд-адресат суммой операнда-источника и адресата и ставит CF , если было переполнение. Операндами могут байты, слова или двойные слова. Адресатом может быть регистр общего назначения или память, источником регистр общего назначени или непосредственное значение. Также это может быть память, если адресат - это регистр.

add ax,bx               ; прибавляет регистр к регистру
add ax,[si]             ; прибавляет память к регистру
add [di],al             ; прибавляет регистр к памяти
add al,48               ; прибавляет непосредственное значение к регистру
add [char],48           ; прибавляет непосредственное значение к памяти

adc суммирует операнды, прибавляет единицу, если стоит CF и заменяет адресат результатом. Правила для операндов такие же как с инструкцией add. add со следующими за ней несколькими инструкциями adc может быть использована для сложения чисел длиннее, чем 32 бита.

inc прибавляет к операнду единицу, он не может изменить CF . Операндом может быть регистр общего назначения или память, размером он может быть в байт, слово или двойное слово.

inc ax               ; прибавляет единицу к регистру
inc byte [bx]        ; увеличивает единицу к памяти

sub вычитает операнд-источник от операнда адресата и заменяет адресат результатом. Если требуется отрицательный перенос, устанавливается CF. Правила для операндов такие же, как с инструкцией add .

sbb вычитает источник из адресата, отнимает единицу, если установлен CF и заменяет адресат результатом. Правила для операндов такие же, как с инструкцией add. sub со следующими за ней несколькими инструкциями sbb может быть использована для вычитания чисел длиннее, чем 32 бита.

dec вычитает из операнда единицу, не может изменить CF. Правила для операнда такие же, как с инструкцией inc .

cmp вычитает операнд-источник из оператора-адресата. Эта инструкция может устанавливать флаги, как и sub, но не вносит изменения в операнды. Правила для операндов такие же, как с инструкцией sub .

neg отнимает от нуля целочисленный операнд с знаком. Эффект от этой инструкции - это смена знака операнда с положительного на отрицательный или с отрицательного на положительный. Правила для операндов такие же, как с инструкцией inc .

xadd меняет местами операнд-адресат и операнд-источник, потом загружает сумму двух значений в операнд-адресат. Операндом-адресатом может быть общий регистр или память, операндом-источником должен быть общий регистр.

Все вышеперечисленные инструкции изменяют флаги SF, ZF, PF и OF. SF всегда принимает значение, равное биту знака результата, ZF устанавливается, если результат равен нулю, PF устанавливается, если восемь битов нижнего разряда содержат четное число единиц, OF устанавливается, если результат слишком большой для положительного числа или слишком маленький для отрицательного (исключая бит знака) для того, чтобы уместиться в операнде-адресате.

mul выполняет беззнаковое перемножение операнда и аккумулятора. Если операнд - байт, процессор умножает его на содержимое al и возвращает 16-битный результат в ah и al. Если операнд - слово, процессор умножает его на содержимое ax и возврщает 32-битный результат в dx и ax. Если же операнд - это двойное слово, процессор умножает его на содержимое eax и возвращает 64-битный результат в edx и eax. mul устанавливает CF и OF, если верхняя половина результата ненулевая, иначе они очищаются. Правила для операндов такие же, как с инструкцией inc .

imul выполняет знаковое перемножение операндов. У этой инструкции есть три вариации. Первая имеет один операнд и работает так же, как инструкция mul. Вторая имеет два операнда, и здесь операнд-адресат умножается на операнд-источник и результат заменяет операнд-адресат. Этоим операндом может быть регистр общего назначения, память или непосредственное значение. Третья форма инструкции имеет три операнда, операндом-адресатом должен быть регистр общего назначения, длиной в слово или в двойное слово, операндом-источником может быть регистр общего назначения или память, третьим операндом должно быть непосредственное значение. Источник умножается на непосредственное значение и результат помещается в регистр-адресат. Все три формы вычисляют результат размером в два раза больше размера операндов и ставят CF и OF , если верхняя часть результата ненулевая, но вторая и третья формы усекают результат до размера операндов. Так, их можно использовать для беззнаковых операндов, потому что нижняя половина результата одна и та же для знаковых и беззнаковых операндов. Ниже вы видите примеры всех трех форм:

imul bl                  ; умножение аккумулятора на регистр
imul word [si]           ; умножение аккумулятора на память
imul bx,cx               ; умножение регистра на регистр
imul bx,[si]             ; умножение регистра на память
imul bx,10               ; умножение регистра на непосредственное значение
imul ax,bx,10            ; регистр, умноженный на непосредственное значение, в регистр
imul ax,[si],10          ; память, умноженная на непосредственное значение, в регистр

div производит беззнаковое деление аккумулятора на операнд. Делимое (аккумулятор) размером в два раза больше делителя (операнда), частное и остаток такого же размера , как и делитель. Если делитель - байт, делимое берется из регистра ax, частное сохраняется в al, а остаток - в ah. Если делитель - слово, верхняя половина делимого берется из dx, а нижняя - из ax, частное сохраняется в ax, а остаток - в dx. Если делитель - двойное слово, верхняя половина делимого берется из edx, а нижняя - из eax, частное сохраняется в eax, а остаток - в edx. Правила для операндов такие же, как с инструкцией mul .

idiv выполняет знаковое деление аккумулятора на операнд. Инструкция использует те же регистры, что и div , правила для операнда тоже такие - же.


2.1.4 Десятичные арифметические инструкции

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

daa корректирует результат сложения двух допустимых упакованных десятичных числа к al. daa всегда должна следовать за суммированием двух пар упакованных десятичных цифр (один знак в каждой половине байта), чтобы получить как результат пару допустимых упакованных десятичных символов. Если потребуется перенос, будет установлен флаг переноса. У этой инструкции нет операндов.

das корректирует результат вычитания двух допустимых упакованных десятичных числа к al. das всегда должна следовать за вычитанием одной пары упакованных десятичных цифр (один знак в каждой половине байта) из другой, чтобы получить как результат пару допустимых сжатых десятичных символов. Если потребуется отрицательный перенос, будет установлен флаг переноса. У этой инструкции нет операндов.

aaa изменяет содержимое регистра al на допустимое неупакованное десятичное число и обнуляет верхние четыре бита. aaa всегда должна следовать за сложением двух неупакованных десятичных операндов в al. Если необходим перенос, устанавливается флаг переноса и увеличивается на единицу ah . У этой инструкции нет операндов.

aas изменяет содержимое регистра al на допустимое неупакованное десятичное число и обнуляет верхние четыре бита. aas всегда должна следовать за вычитанием одного неупакованного десятичного операнда из другого в al. Если необходим перенос, устанавливается флаг переноса и уменьшается на единицу ah. У этой инструкции нет операндов.

aam корректирует результат умножения двух допустимых неупакованных десятичных чисел. Для создания правильного десятичного результата инструкция должна всегда следовать за умножением двух десятичных чисел. Цифра верхнего регистра передается в ah, а нижнего - в al. Обобщенная версия этой инструкции делает возможной подгонку содержимого ax для создания двух неупакованных цифр с любым основанием. Стандартная версия этой инструкции не имеет операндов, у обобщенной версии есть один операнд - непосредственное значение, определяющее основание создаваемых чисел.

aad модифицирует делимое в ah и al, чтобы подготовится к делению двух допустимых неупакованных десятичных операндов так, чтобы частное стало допустимым неупакованным десятичным числом. ah должен содержать цифру верхнего регистра, а al - цифру нижнего регистра. Эта инструкция корректирует значение и помещает результат в al, тогда как ah будет содержать ноль. Обобщенная версия этой инструкции делает возможной подгонку двух неупакованных цифр с любым основанием. Правила для операнда такие же, как с инструкцией aam.


2.1.5 Логические инструкции

not инвертирует биты в заданном операнде к форме обратного кода операнда. Не оказывает влияния на флаги. Правила для операнда таки же, как с инструкцией inc .

and, or и xor производят стандартные логические операции. Они изменяют флаги SF, ZF и PF. Правила для операнда таки же, как с инструкцией add .

bt, bts, btr и btc оперируют с единичным битом, который может быть в памяти или регистре общего назначения. Расположения бита определяется как смещение от конца нижнего регистра операнда. Значение смещения берется из второго операнда, это может быть либо регистр общего назначения, либо байт. Эти инструкции первым делом присваивают флагу CF значение выбранного байта. bt больше ничего не делает, bts присваивает выбранному биту значение 1, btr сбрасывает его на 0, btc изменяет значение бита на его дополнение. Первый операнд может быть словом или двойным словом.

bt ax,15              ; тестирует бит в регистре
bts word [bx],15      ; тестирует и ставит бит в памяти
btr ax,cx             ; тестирует и сбрасывает бит в регистре
btc word [bx],cx      ; тестирует и дополняет бит в памяти

Инструкции bsf и bsr ищут в слове или двойном слове первый установленный бит и сохраняют индекс этого бита в операнд-адресат, которым должен быть регистр общего назначения. Сканируемая строка битов определяется операндом-источником, им может быть либо регистр общего назначения, либо память. Если срока нулевая (ни одного единичного бита), то устанавливается флаг ZF, иначе он очищается. Если не найдено ни одного установленного бита, значение операнда адресата не определено. bsf сканирует от нижнего регистра к верхнему (начиная с бита с индексом ноль). bsr сканирует от верхнего регистра к нижнему (начиная с бита с индексом 15 в слове или с индекса 31 в двойном слове).

bsf ax,bx            ; сканирование регистра по возрастанию
bsr ax,[si]          ; сканирование памяти в обратном порядке

shl сдвигает операнд-адресат влево на определенное вторым операндом количество битов. Операндом-адресатом может быть регистр общего назначения или память размером в байт, слово или двойное слово. Вторым операндом может быть непосредственное значение или регистр cl. Процессор "задвигает" нули справа (с нижнего регистра), и биты "выходят" слева. Последний "вышедший" бит запоминается в CF. sal - это синоним shl.

shl al,1               ; сдвиг регистра влево на один бит
shl byte [bx],1        ; сдвиг памяти влево на один бит
shl ax,cl              ; сдвиг регистра влево на количество из CL
shl word [bx],cl       ; сдвиг памяти влево на количество из CL

shr и sar сдвигают операнд-адресат вправо на число битов, определенное во втором операнде. Правила для операндов такие же, как с инструкцией shl. shr "задвигает" нули с левой стороны операнда-адресата, биты "выходят" справа. Последний "вышедший" бит запоминается в CF. sar сохраняет знак операнда, "забивая" слева нулями, если значение положительное, и единицами, если значение отрицательное.

shld сдвигает биты операнда-адресата влево за заданное в третьем операнде число битов, в то время как справа "задвигаются" биты верхних регистров операнда-источника. Операнд-источник не изменяется. Операндом-адресатом может быть регистр общего назначения или память размером в слово или двойное слово, операндом-источником должен быть регистр общего назначения, третьим операндом может быть непосредственное значение либо cl.

shld ax,bx,1         ; сдвиг регистра влево на один бит
shld [di],bx,1       ; сдвиг памяти влево на один бит
shld ax,bx,cl        ; сдвиг регистра влево на количество из CL
shld [di],bx,cl      ; сдвиг памяти влево на количество из CL

shrd cдвигает биты операнда-адресата вправо, в то время как слева "задвигаются" биты нижних регистров операнда-источника. Операнд-источник остается неизменным. Правила для операндов такие же, как с инструкцией shld .

rol и rcl циклически сдвигают байт, слово или двойное слово влево на число битов, заданное во втором операнде. Для каждой заданной ротации старший бит, выходящий слева, появляется справа и становится самым младшим битом. rcl дополнительно помещает в CF каждый бит высшего регистра, выходящий слева, перед тем, как он возвратится в операнд как младший бит во время следующего цикла ротации. Правила для операндов такие же, как с инструкцией shl .

ror и rcr циклически сдвигают байт, слово или двойное слово вправо на число битов, заданное во втором операнде. Для каждой заданной ротации младший бит, выходящий справа, появляется слева и становится самым старшим битом. rcr дополнительно помещает в CF каждый бит низшего регистра, выходящий слева, перед тем, как он возвратится в операнд как старший бит во время следующего цикла ротации. Правила для операндов такие же, как с инструкцией shl .

test производит такое же действие, как инструкция and, но не изменяет операнд-адресат, только обновляет флаги. Правила для операндов такие же, как с инструкцией and .

bswap переворачивает порядок битов в 32-битном регистре общего назначения: биты от 0 до 7 меняются местами с битами от 24 до 31, а биты от 8 до 15 меняются с битами от 16 до 23. Эта инструкция предусмотрена для преобразования значений с прямым порядком байтов к формату с обратномым порядком и наоборот.

bswap edx     ; перестановка байтов в регистре


2.1.6 Инструкции передачи управления

jmp безоговорочно передает управление на заданное место. Адрес назначения может быть определен непосредственно в инструкции или косвенно через регистр или память, допустимый размер адреса зависит от того, какой переход, близкий или дальний (это может быть предварительно указано перед операндом оператором near или far), а также от того, какая инструкция, 16-битная или 32-битная. Операнд для близкого перехода должен быть размером word для 16-битной инструкции и размером dword для 32-битной инструкции. Операнд для дальнего перехода должен быть размером dword для 16-битной инструкции и размером pword для 32-битной инструкции. Прямая инструкция jmp содержит адрес назначения как часть инструкции (и может предшествоваться операторами short, near или far), операндом, определяющим этот адрес, должно быть числовое выражение для близкого перехода и два числа, разделенные двоеточием, для дальнего перехода, первое определяет номер сегмента, второе - смещение внутри сегмента. Оператор pword может использоваться для принудительного 32-битного дальнего вызова, и dword, чтобы использовать 16-битный дальний вызов. Непрямая инструция jmp получает адрес назначения через регистр или переменную-указатель, операндом должен быть регистр общего назначения или память. Для более подробной информации смотрите также 1.2.5.

jmp 100h             ; прямой близкий переход
jmp 0FFFFh:0         ; прямой дальний переход
jmp ax               ; непрямой близкий переход
jmp pword [ebx]      ; непрямой дальний переход

call передает управление процедуре, сохраняя в стеке адрес инструкции, следующей за call , для дальнейшего возвращения к ней инструкцией ret . Правила для операндов такие же, что с инструкцией jmp , но call не имеет короткого варианта в виде прямой инструкции, и поэтому не оптимизирована.

ret, retn и retf прекращают выполнение процедуры передают управление назад программе, которая изначально вызвала эту процедуру, используя адрес, который был сохранен в стеке инструкцией call. ret это эквивалент retn , которая возвращает из процедуры, которая была вызвана с использованием близкого перехода, тогда как retf возвращает из процедуры, которая была вызвана с использованием дальнего перехода. Эти инструкции подразумевают размер адреса, соответствующий текущей установке кода, но этот размер может быть изменен на 16-битный с помощью мнемоников retw, retnw и retfw и на 32-битный с помощью retd, retnd и retfd . Все эти инструкции могут опционально предписывать непосредственный операнд, если добавить константу к указателю стека, они эффективно удаляют любые аргументы, которые взывающая программа положила в стек перед выполнением инструкции call.

iret возвращает управление прерванной процедуре. Эта инструкция отличается от ret в том, что она также выводит из стека флаги в регистр флагов. Флаги сохраняются в стек механизмом прерывания. Инструкция подразумевает размер адреса, соответствующий текущей установке кода, но этот размер может быть изменен на 16-битный или на 32-битный с помощью мнемоников iretw или iretd.

Условные инструкции перехода jcc осуществляют или не осуществляют передачу управления в зависимости от состояния флагов CPU во время вызова этих инструкций. Мнемоники условных переходов могут быть получены добавлением условных мнемоников, смотри таблицу 2.1., к символу j , например инструкция jc осуществляет передачу управления, если установлен флаг CF. Логические условия "больше" и "меньше" относятся к сравнениям целочисленных значений со знаком, а "выше и "ниже" - к сравнениям целочисленных значений без знака. Условные переходы могут быть только близкие и прямые, и могут быть оптимизированы (смотри 1.2.5), операндом должно быть число, определяющее адрес назначения.

Таблица 2.1 Условия
Мнемоник | Тестируемое условие | Описание
o | OF = 1 | переполнение
no | OF = 0 | нет переполнения
c b nae | CF = 1 | перенос меньше не больше и не равно
nc ae nb | CF = 0 | нет переноса выше или равно не ниже
e z | ZF = 1 | равно ноль
ne nz | ZF = 0 | не равно не ноль
be na | CF or ZF = 1 | ниже или равно не выше
a nbe | CF or ZF = 0 | выше не ниже и не равно
s | SF = 1 | знаковое
ns | SF = 0 | беззнаковое
p pe | PF = 1 | четное
np po | PF = 0 | нечетное
l nge | SF xor OF = 1 | меньше не больше и не равно
ge nl | SF xor OF = 0 | больше или равно не меньше
le ng | (SF xor OF) or ZF = 1 | меньше или равно не больше
g nle | (SF xor OF) or ZF = 0 | больше не меньше и не равно

loop - это условные переходы, которые используют значение из cx (или ecx ) для определения количества повторений цикла. Все инструкции loop автоматически уменьшают на единицу cx (или ecx) и завершают цикл, когда cx (или ecx) равно нулю. cx или ecx используется в зависимости от от текущей утановки битности кода - 16-ти или 32-битной, но вы можете принудительно использовать cx с помощью мнемоника loopw, или ecx с помощью мнемоника loopd. loope и loopz это синонимы этой инструкции, которые работают так же, как стандартный loop, но еще завершают работу, если установлен ZF. loopew и loopzw заставляют использовать регистр cx , а looped и loopzd заставляют использовать регистр ECX. loopne и loopnz это тоже синонимы той же инструкции, которые работают так же, как стандартный loop, но еще завершают работу, если ZF сброшен на ноль. loopnew и loopnzw заставляют использовать регистр cx , а loopned и loopnzd заставляют использовать регистр ecx . Каждая инструкция loop требует операндом число, определяющее адрес назначения, причем это может быть только близкий переход (в пределах 128 байт назад и 127 байт вперед от адреса инструкции, следующей за loop ).

jcxz переходит к метке, указанной в инструкции, если находит в cx ноль, jecxz делает то же, но проверяет регистр ecx . Правила для операндов такие же, как с инструкцией loop .

int активирует стандартный сервис прерывания, который соответствует числу, указанному как операнд в мнемонике. Это число должно находиться в пределах от 1 до 255. Стандартный сервис прерывания заканчивается мнемоником iret , которая возвращает управление инструкции, следующей за int. int3 кодирует короткое (в один байт) системное прерывание, которое вызывает прерывание 3. into вызывает прерывание 4, если установлен флаг OF .

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

bound ax,[bx]    ; тестирует слово на границы
bound eax,[esi]  ; тестирует двойное слово на границы


2.1.7 Инструкции ввода-вывода

in переводит байт, слово или двойное слово из порта ввода в al, ax или eax. Порты ввода-вывода могут быть адресованы либо напрямую, непосредственно с помощью байтового значения, либо непрямо через регистр dx. Операндом-адресатом должен быть регистр al, ax или eax. Операндом-источником должно быть число от 0 до 255 либо регистр dx .

in al,20h        ; ввод байта из порта 20
in ax,dx         ; ввод слова из порта, адресованного регистром dx

out переводит байт, слово или двойное слово из порта вывода в al, ax или eax. Программа может может определить номер порта, используя те же методы, что и в инструкции in. Операндом-адресатом должен быть регистр al, ax или eax. Операндом-источником должно быть число от 0 до 255 либо регистр dx .

out 20h,ax      ; вывод байта в порт 20
out dx,al       ; вывод слова в порт, адресованный регистром dx


2.1.8 Строковые операции

Строковые операции работают с одним элементом строки. Этим элементом может быть байт, слово или двойное слово. Строковые элементы адресуются регистрами si и di (или esi и edi). После каждой сроковой операции si и/или di (или esi и/или edi) автоматически обновляются до указателя на следующий элемент строки. Если DF (флаг направления) равен нулю, регистры индекса увеличиваются, если DF равен единице, они уменьшаются. Число, на которое они увеличиваются или уменьшаются равно 1, 2 или 4 в зависимости от размера элемента строки. Каждая инструкция строковой операции имеет короткую форму без операндов, использующую si и/или di если тип кода 16-битный, и esi и/или edi если тип кода 32-битный. si и esi по умолчанию адрес данных в сегменте, адресованном регистром ds, di и edi всегда адресует данные в сегменте, выбранном в es. Короткая форма образуется добавлением к мнемонику строковой операции буквы, определяющей размер элемента строки, для байта это b, для слова это w, для двойного слова это d. Полная форма инструкции требует операнды, указывающие размер оператора, и адресы памяти, которыми могут быть si или esi с любым сегментным префиксом, или di или edi всегда с сегментным префиксом es .

movs переводит строковый элемент, на который указывает si (или esi) в место, на которое указывает di (или edi). Размер операнда может быть байтом, словом или двойным словом. Операндом-адресатом должна быть память, адресованная di или edi, операндом-источником должна быть память, адресованная es или esi с любым сегментным префиксом.

movs byte [di],[si]        ; переводит байт
movs word [es:di],[ss:si]  ; переводит слово
movsd                      ; переводит двойное слово

cmps вычитает строковый элемент-адресат из строкового элемента-источника и обновляет флаги AF, SF, PF, CF и OF, но не изменяет никакой из сравниваемых элементов. Если строковые элементы эквивалентны, устанавливается ZF, иначе он очищается. Первым операндом этой инструкции должен быть строковый элемент, адресованный si или esi с любым сегментным префиксом, вторым операндом должен быть строковый элемент, адресованный di или edi .

cmpsb                      ; сравнение байтов
cmps word [ds:si],[es:di]  ; сравнение слов
cmps dword [fs:esi],[edi]  ; сравнение двойных слов

scas вычитает строковый элемент-адресат из al, ax или eax (в зависимости от размера этого элемента) и обновляет флаги AF, SF, ZF, PF, CF и OF. Если значения эквивалентны, устанавливается ZF, иначе он очищается. Операндом должен быть строковый элемент, адресованный di или edi .

scas byte [es:di]          ; сканирует байт
scasw                      ; сканирует слово
scas dword [es:edi]        ; сканирует двойное слово

stos помещает значение al, ax или eax в строковый элемент-адресат. Правила для операндов такие же, как с инструкцией scas .

lods строковый элемент в al, ax или eax. Операндом должен быть строковый элемент, адресованный es или esi с любым префиксом сегмента.

lods byte [ds:si]          ; загружает байт
lods word [cs:si]          ; загружает слово
lodsd                      ; загружает двойное слово

ins переводит байт, слово или двойное слово порта ввода, адресованного регистром dx в строковый элемент-приемник. Операндом-адресатом должна быть память, адресованная di или edi , операндом-источником должен быть регистр DX.

insb                       ; ввод байта
ins word [es:di],dx        ; ввод слова
ins dword [edi],dx         ; ввод двойного слова

outs переводит строковый элемент-источник в порт вывода, адресованный регистром dx. Операндом-адресатом должн быть регистр dx, а операндом-источником должна быть память, адресованная si или esi с любым префиксом сегмента.

outs dx,byte [si]          ; вывод байта
outsw                      ; вывод слова
outs dx,dword [gs:esi]     ; вывод двойного слова

Префиксы повторения rep, repe/repz и repne/repnz определяют повторяющуюся строковую операцию. Если инструкция строковой операции имеет префикс повторения, операция выполняется повторно, каждый раз используя другой элемент строки. Повторение прекратится, когда будет выполнено одно из условий, указанных префиксом. Все три префикса автоматически уменьшают регистр cx или ecx (в зависимости от того, какую адресацию использует инструкция строковой операции, 16-битную или 32-битную) после каждой операции и повторяют ассоциированную операцию, пока cx или ecx не станет равным нулю. repe/repz и repne/repnz используются только с инструкциями scas и cmps (описанными выше). Когда используются эти префиксы, повторение следующей инструкции зависит также от флага нуля (ZF), repe и repz прекращают выполнение, если ZF равен нулю, repne и repnz прекращают выполнение, если ZF равен единице.

rep  movsd       ; переводит несколько двойных слов
repe cmpsb       ; сравнивает байты, пока эквивалентны


2.1.9 Инструкции управления флагами

Инструкции управления флагами обеспечивают метод прямого изменения состояния битов во флаговом регистре. Все инструкции, описанные в этом разделе, не имеют операндов.

stc устанавливает CF (флаг переноса) в 1, clc обнуляет CF, cmc изменяет CF на его дополнение.

std устанавливает DF (флаг направления) в 1, cld обнуляет DF.

sti устанавливает IF (флаг разрешения прерываний) в 1 и таким образом разрешает прерывания, cli обнуляет IF и таким образом запрещает прерывания.

lahf копирует SF, ZF, AF, PF и CF в биты 7, 6, 4, 2 и 0 регистра ah. Содержание остальных битов неопределено. Флаги остаются неизменными.

sahf переводит биты 7, 6, 4, 2 и 0 из регистра ah в SF, ZF, AF, PF и CF.

pushf уменьшает esp на два или на четыре и сохраняет нижнее слово или двойного слово флагового регистра в вершине стека. Размер сохраненной информации зависит от текущей настройки кода. Вариант pushfw сохранят слово, независимо от настройки кода, pushfd также независимо от настройки кода сохраняет двойное слово.

popf переводит определенные биты из слова или двойного слова в вершине стека и увеличивает esp на два или на четывре, в зависимости от текущей настройки кода. Вариант popfw сохранят слово, независимо от настройки кода, popfd также независимо от настройки кода сохраняет двойное слово.


2.1.10 Условные операции

Инструкции, образванные с помощью добавления условного мнемоника (смотрите таблицу 2.1) к мнемонику set присваивают байту единицу, если условие истинно, и ноль, если если условие не выполняется. Операндом должен быть 8-битный регистр общего назначения либо байт в памяти.

setne al          ; единицу в al, если флаг нуля пустой
seto byte [bx]    ; единицу в байт, если есть переполнение

salc присваивает всем битам регистра al единицу, если стоит флаг переноса, и нули в другом случае. У этой инструкции нет аргументов.

Инструкции, образованные добавлением условного мнемоника к cmov переводят слово или двойное слово из регистра общего назначения или памяти в регистр общего назначения только если условие верно. Операндом-адресатом должен быть регистр общего назначения, операндом-источником - регистр общего назначения либо память.

cmove ax,bx         ; переводит, если установлен флаг нуля
cmovnc eax,[ebx]    ; переводит, если очищен флаг переноса

cmpxchg сравнивает значение в регистре al, ax или eax с операндом-адресатом. Если значения равны, операнд-источник загружается в операнд-адресат, иначе операнд-адресат загружается в регистр al, ax или eax . Операндом-адресатом должен быть регистр общего назначения или память, операндом-источником - регистр общего назначения.

cmpxchg dl,bl       ; сравнивает и меняет с регистром
cmpxchg [bx],dx     ; сравнивает и меняет с памятью

cmpxchg8b сравнивает с операндом 64-битное значение в регистрах edx и eax. Если значения равны, то 64-битное значение в регистрах ecx и ebx сохраняется в операнде, иначе значение из операнда загружается в регистры edx и eax . Операндом должно быть четверное слово в памяти.

cmpxchg8b [bx]     ; сравнивает и меняет 8 битов


2.1.11 Разные инструкции

nop занимает один байт, но ничего не значит, кроме как указатель инструкции. У неё нет операндов и она ничего не совершает.

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

xlat заменяет байт в регистре al байтом, индексированным его значением в таблице перевода, адресованной bx или ebx. Операндом должен быть байт памяти, адресованный регистром bx или ebx с любым сегментным префиксом. Эта инструкция также имеет короткую форму xlatb, которая не использует операнды и использует адрес из bx или ebx (в зависимости от настройки кода) в сегменте, адресованном ds .

lds переводит переменный указатель из операнда-источника в ds и регистр-адресат. Операндом-источником должна быть память, а операндом-адресатом - регистр общего назначения. Регистр ds получает селектор сегмента указателя, а регистр-адресат получает его смещение. les, lfs, lgs и lss работают точно так же, как lds, только вместо регистра ds используются соответственно es, fs, gs или ss .

lds bx,[si]      ; загружает указатель в ds:bx

lea переводит смещение операнда-источника (вместо его значения) в операнд-адресат. Операндом-источником должна быть память, а операндом-адресатом должен быть регистр общего назначения.

lea dx,[bx+si+1]    ; загружает исполнительный адрес в dx

cpuid возвращает идентификацию процессора и информацию о его свойствах в регистры eax, ebx, ecx и edx. Выдаваемая информация выбирается вводом нужного значения в регистр eax перед тем, как выполнить инструкцию. У этой инструкции нет операндов.

pause задерживает выполнение следующей инструкции, реализуя определенное количество времени. Эта инструкция может быть использована, чтобы улучшить выполнение циклов ожидания. У инструкции нет операндов.

enter создает стековый фрейм, который может быть использован для реализации свода правил блочных языков высокого уровня. Инструкция leave в конце процедуры дополняет enter в начале процедуры, чтобы упростить управление стеком и контролировать доступ к переменным для вложенных процедур. Инструкция enter имеет два параметра. Первый параметр определяет количество байт динамической памяти, которое должно быть отведено для введенной подпрограммы. Второй параметр соответствует лексическому уровню вложенности подпрограммы, может находится в области от 0 до 31. Указанный лексический уровень устанавливает, сколько наборов указателей стековых фреймов CPU копирует в новый стековый фрейм из предыдущего фрейма. Этот список указателей стековых фреймов иногда называется дисплеем. Первое слово (или двойное слово, если код 32-битный) дисплея - это указатель на последний стековый фрейм. Этот указатель делает возможным для инструкции leave совершить в обратном порядке действия предыдущей инструкции enter, эффективно сбрасывая последний стековый фрейм. После того, как enter создает новый дисплей для процедуры, инструкция выделяет для неё место в динамической памяти, уменьшая esp на количество байтов, определенных в первом параметре. Чтобы процедура могла адресовать свой дисплей, enter передает указатель bp (или ebp) в начало нового стекового фрейма. Если лексический уровень равен нулю, enter сохраняет bp (или ebp), копирует sp в bp (или esp в ebp) и далее вычитает первый операнд из sp (или esp ). Для уровней вложенности больших нуля процессор сохраняет дополнительные указатели фреймов в стек перед подгонкой указателя стека.

enter 2048,0     ; ввод и выделение 2048 байтов в стеке


2.1.12 Системные инструкции

lmsw загружает операнд в слово машинного статуса (биты от 0 до 15 регистра cr0), тогда как smsw сохраняет слово машинного статуса в операнд-адресат. Операндом может быть 16-битный или 32-битный регистр общего назначения или слово в памяти.

lmsw ax          ; загружает машинный статус из регистра
smsw [bx]        ; сохраняет машинный статус в память

lgdt и lidt загружают значения из операнда в регистр таблицы глобальных дескрипторов или в регистр таблицы дескрипторов прерываний соответственно. sgdt и sidt сохраняют содержимое регистра таблицы глобальных дескрипторов или регситра таблицы дескрипторов прерываний в операнд-адресат. Операндом должны быть 6 байтов в памяти.

lgdt [ebx]       ; загружает таблицу глобальных дескрипторов

lldt загружает операнд в поле селектора сегмента регистра таблицы локальных дескрипторов, а sldt сохраняет селектор сегмента из регистра таблицы локальных дескрипторов в операнд. ltr загружает операнд в поле селектора сегмента регистра задачи, а str сохраняет селектор сегмента из регистра задачи в операнд. Правила для операндов такие же, как в инструкциях lmsw и smsw .

lar загружает права доступа из сегментного дескриптора, указанного селектором в операнде-источнике, в операнд-адресат и ставит флаг ZF . Операндом-адресатом может быть 16-битный или 32-битный регистр общего назначения. Операндом-источником должен быть 16-битный регистр общего назначения или память.

lar ax,[bx] ; загружает права доступа в слово
lar eax,dx ; загружает права доступа в двойное слово

lsl загружает сегментный предел из сегментного дескриптора, указанного селектором в операнде-источнике, в операнд-адресат и ставит флаг ZF. Правила для операндов такие же, как в инструкции lsl .

verr и verw проверяют, поддаётся ли чтению или записи на данном уровне привилегий сегмент кода или данных, заданный в операнде. Операндом должно быть слово, это может быть регистр общего назначения или память. Если сегмент доступен и читаем (для verr) или изменяем, устанавливается флаг ZF, иначе он очищается. Правила для операндов такие же, как в инструкции lldt .

arpl сравнивает поля RPL (уровень привилегий запрашивающего) двух селекторов сегментов. Первый операнд содержит один селектор сегмента, второй содержит другой. Если поле RPL операнда-адресата меньше, чем поле RPL операнда-источника, то устанавливается флаг ZF, и поле RPL операнда-адресата увеличивается до соответствия операнду-источнику. Иначе флаг ZF очищается и в операнде никаких изменений не производится. Операндом-адресатом должен быть регистр общего назначения или память длиной в слово, операндом-источником должен быть регистр общего назначения тоже длиной в слово.

arpl bx,ax       ; подгоняет RPL селектора в регистре
arpl [bx],ax     ; подгоняет RPL селектора в памяти

clts очищает флаг TS (переключение задач) в регистре cr0 . У этой инструкции нет операндов.

Префикс lock заставляет процессор объявить сигнал "bus-lock" (или LOCK) во время выполнения сопутствующей инструкции. В многопроцессорной среде сигнал "bus-lock" гарантирует, что пока он объявлен, процессор эксклюзивно использует любую общую память. Префикс lock может быть присоединен только к следующим инструкциям и причем только к тем их формам, в которых операндом-адресатом является память: add, adc, and, btc, btr, bts, cmpxchg, cmpxchg8b, dec, inc, neg, not, or, sbb , sub, xor, xadd и xchg. Если этот префикс используется с одной из этих инструкций, но операндом-источником является память, может быть сгенерирован не определенный ошибочный опкод. Он может быть сгенерирован также, если префикс lock используется с инструкцией, не перечисленной выше. Инструкция xchg всегда объявляет сигнал "bus-lock", независимо от отсутствия или присутствия префикса lock .

hlt прекращает выполнение инструкции и переводит прощессор в состояние остановки. Запущенное прерывание, отладочное исключение, BINIT, INIT или RESET сигналы продолжат выполнение. У этой инструкции нет операндов.

invlpg делает недействительными (сбрасывает) любые записи трансляционного буфера (TLB), указанные исходным операндом. Исходный операнд адрес памяти. Процессор определяет страницу, которая содержит этот адрес, и сбрасывает все записи TLB для этой страницы.

rdmsr загружает содержимое 64-битного MSR (модельно-специфический регистр) по адресу, определенному в ecx, в edx и eax. wrmsr загружает содержимое регистров edx и eax в 64-битный MSR по адресу, определенному в ecx. rdtsc загружает текущее значение счетчика времени процессора из 64-битного MSR в регистры edx и eax. Процессор увеличивает значение счетчика времени MSR каждый цикл тактового генератора и сбрасывается на 0, когда процессор перезагружается. rdpmc загружает содержимое 40-битного счетчика событий производительности, заданного в ecx, в edx и eax . Эти инструкции не имеют операндов.

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

rsm возвращает программное управление из из системного режима управления программе, которая была прервана, когда процессор получил прерывание SMM. Эта инструкция не имеет операндов.

sysenter выполняет быстрый вызов системный вызов процедуры уровня 0, sysexit выполняет быстрый возврат к коду пользователя уровня 3. Адреса, использованные этими инструкциями, сохраняются в MSR-ах. Эти инструкции не имеют операндов


2.1.13 Инструкции FPU

Инструкции FPU (модуль операций с плавающей точкой) оперируют со значениями с плавающей точкой в трех форматах: одинарная точность (32-битная), двойная точность (64-битная) и расширенная точность (80-битная). Регистры FPU формируют стек и каждый из них вмещает значение с плавающей точкой расширенной точности. Если некоторые значения задвигаются в стек или вытаскиваются из вершины, регистры FPU сдвигаются, таким образом st0 - это всегда значение в вершине стека FPU, st1 - это первое значение ниже вершины и т. д.. Название st0 имеет также синоним st .

fld задвигает значение с плавающей точкой стек регистров FPU. Операндом может быть 32-битное, 64-битное или 80-битное расположение в памяти или регистр FPU, его значение загружается в вершину стека регистров FPU (регистр st0 ) и автоматически конвертируется в формат расширенной точности.

fld dword [bx]   ; загружает значение одинарной точности из памяти
fld st2          ; загружает значение st2 в вершину стека

fld1, fldz, fldl2t, fldl2e, fldpi , fldlg2 и fldln2 загружают часто ипользуемые константы в стек регистров FPU. Эти константы +1.0, +0.0, lb 10, lb e, pi, lg 2 и ln 2 соответственно. Эти инструкции не имеют операндов.

fild конвертирует знаковый целочисленный операнд-источник в расширенный формат с плавающей точкой и задвигает результат в стэк регистров FPU. Операндом-источником может быть 16-битное, 32-битное или 64-битное расположение в памяти.

fild qword [bx]  ; загружает 64-битное целое число из памяти

fst копирует значение из регистра st0 в операнд-адресат, которым может быть 32-битное или 64-битное расположение в памяти или другой регистр FPU. fstp совершает ту же операцию, но далее выдвигает стек регистров, освобождая st0. fstp поддерживает те же операнды, что и fst и ещё может сохранять 80-битное значение в память.

fst st3          ; копирует значение ST0 в регистр ST3
fstp tword [bx]  ; сохраняет значение в память и выдвигает стэк

fist конвертирует значение из st0 в знаковое целое число и сохраняет результат в операнд-адресат. Операндом может быть 32-битное или 64-битное расположение в памяти. fistp совершает ту же операцию, но далее выдвигает стек регистров. Инструкция поддерживает те же операнды, что и fist и ещё может сохранять 64-битное целочисленное значение в память, таким образом, у неё правила для операндов такие же, как с инструкцией fild .

fbld конвертирует сжатое целое число BCD в в расширенный формат с плавающей точкой и задвигает это значение в стек FPU. fbstp конвертирует значение из st0 в 18-знаковое сжатое число BCD, сохраняет результат в операнд-адресат и выдвигает стэк регистров. Операндом должно быть 80-битное расположение в памяти.

fadd складывает операнд-источник и операнд-адресат и сохраняет сумму в адресате. Операндом-адресатом всегда должен быть регистр FPU, если источник - это расположение в памяти, то адресат это регистр st0 и нужно указать только источник. Если обоими операндами являются регистры FPU, то одним из них должен быть st0 . Операндом в памяти может быть 32-битное или 64-битное значение.

fadd qword [bx]  ; прибавляет значение двойной точности к st0
fadd st2,st0     ; прибавляет st0 к ST2

faddp складывает операнд-источник и операнд-адресат, сохраняет сумму в адресате и далее выдвигает стэк регистров. Операндом-адресатом должен быть регистр FPU, а операндом-источником - st0 . Если операнды не указаны, то в качестве операнда-адресата используется st1 .

faddp            ; прибавляет st0 к st1 и выдвигает стэк
faddp st2,st0    ; прибавляет st0 к st2 и выдвигает стэк

fiadd конвертирует целочисленный операнд-источник в расширенный формат с плавающей точкой и прибавляет его операндe-адресату. Операндом должно быть 32-битное или 64-битное расположение в памяти.

fiadd word [bx]  ; прибавляет целочисленное слово к st0

fsub, fsubr, fmul, fdiv и fdivr похожи на fadd, имеют такие же правила для операндов и различаются только в совершаемых вычислениях. fsub вычитает операнд-источник из операнда-адресата, fsubr вычитает операнд-адресат из операнда-источника, fmul перемножает источник и адресат, fdiv делит операнд-адресат на операнд-источник, fdivr делит операнд-источник на операнд-адресат. fsubp, fsubrp, fmulp, fdivrp и fdivrp совершают те же операции и выдвигают стек регистров, правила для операнда такие же, как с инструкцией faddp . fisub, fisubr, fimul, fidivr и fidivr совершают те же операции после конвертации целочисленного операнда-источника в формат с плавающей точкой, они имеют такие же правила для операндов, как и инструкция fiadd .

fsqrt вычисляет квадратный корень из значения в регистре st0 , fsin вычисляет синус этого значения, fcos вычисляет его косинус, fchs дополняет его знаковый бит, fabs очищает знак, чтобы создать абсолютное значение, frndint округляет до ближайшего целого значения, зависящего от текущего режима округления. f2xm1 вычисляет экспоненциальное значение 2 в степени st0 и вычитает из результата 1.0 (2^x-1), значение в st0 должно лежать в пределах от -1.0 до +1.0. Все вышеперецисленные инструкции сохраняют значение в st0 и не имеют операндов.

fsincos вычисляет синус и косинус значения в st0, сохраняет синус в st0 и задвигает косинус в вершину стека регистров FPU. fptan вычисляет тангенс значения в st0, сохраняет результат в st0 и задвигает 1.0 в вершину стека регистров FPU. fpatan вычисляет арктангенс значения в st1, деленного на значение в st0, сохраняет результат в st1 и выдвигает стек регистров FPU. fyl2x вычисляет двоичный логарифм st0, умножает его на st1, сохраняет результат в st1 и выдвигает стек регистров FPU; fyl2xp1 совершает ту же операцию, перед вычислением логарифма помешает в st0 значение 1.0. fprem вычисляет остаток, зависящий от деления значения из st0 на значение из st1, и сохраняет результат в st0. fprem1 совершает ту же операцию, что и fprem, но вычисляет остаток способом, указанном в стандарте IEEE 754. fscale оставляет целую часть значения в st1 и увеличивает экспоненту st0 на полученное число. fxtract разделяет значение в st0 на экспоненту и мантиссу, сохраняет экспоненту в st0 и задвигает мантиссу в стек регистров. fnop не делает ничего. Эти инструкции не имеют операндов.

fxch меняет местами содержимое регистра st0 и другого регистра FPU. Операндом должен служить регистр FPU, а если он не указан, меняются местами регистры st0 и st1 .

fcom и fcomp сравнивают содержимое st0 и операнда-источника и в зависимости от результатов ставят флаги статуса FPU. fcomp дополнительно после сравнения выдвигает стек регистров. Операндом может служить значение одинарной или двойной точности в памяти или регистр FPU. Если операнд не определен, в этой роли используется st1 .

fcom             ; сравнивает st0 с st1
fcomp st2        ; сравнивает st0 с st2 и выдвигает стек

fcompp сравнивает содержимое st0 и st1 , устанавливает флаги вслове статуса FPU и дважды выдвигает стек регистров. У этой инструкции нет операндов.

fucom, fucomp и fucompp совершают неупорядоченное сравнение двух регистров FPU. Правила для операндов такие же, как с инструкциями fcom, fcomp и fcompp , но операндом-источником должен быть регистр FPU.

ficom и ficomp сравнивают значение в st0 с целочисленным операндом-источником и устанавливают флаги в слове статуса FPU в зависимости от результатов. ficomp дополнительно после сравнения выдвигает стек регистров. Перед операцией сравнения целочисленный операнд-источник конвертируется в расширенный формат с плавающей точкой. Операндом должно служить 16-битное или 32-битное расположение в памяти.

ficom word [bx]  ; сравнивает st0 с 16-битным целым числом

fcomi, fcomip, fucomi, fucomip сравнивают st0 с другим регистром FPU и ставят, в зависимости от результатов, флаги ZF, PF и CF. fcomip и fucomip ещё выдвигают стек регистров после завершения сравнения. Инструкции, образованные добавлением условного мнемоника FPU (смотрите таблицу 2.2) к мнемонику fcmov, переводят указанный регистр FPU в регистр st0, если данное условие выполняется. Эти инструкции поддерживают два разных синтаксиса, первый с одним операндом, определяющим регистр FPU, второй с двумя операндами, где операнд-адресат - регистр st0 , а операнд-источник, идущий вторым, - нужный регистр FPU.

fcomi st2        ; сравнивает st0 с st2 и устанавливает флаги
fcmovb st0,st2   ; переводит st2 в st0 если меньше

Таблица 2.2 Условия FPU
Мнемоник | Тестируемое условие | Описание
b | CF = 1 | меньше
e | ZF = 1 | равно
be | CF or ZF = 1 | меньше или равно
u | PF = 1 | неномализованное
nb | CF = 0 | не меньше
ne | ZF = 0 | не равно
nbe | CF and ZF = 0 | не меньше и не равно
nu | PF = 0 | нормализованное

ftst сравнивает значение в st0 с 0.0 и в зависимости от результатов устанавливает флаги в слове статуса FPU. fxam проверяет содержимое регистра st0 и устанавливает флаги в слове статуса FPU, показывая класс значения в регистре. Эти инструкции не имеют операндов.

fstsw и fnstsw сохраняют текущее значение слова статуса FPU в указанном месте. Операндом-адресатом может быть либо 16 бит в памяти, либо регистр ax. fstsw перед сохранением слова проверяет на подвешенные немаскируемые численные исключения, fnstsw этого не делает.

fstcw и fnstcw сохраняют текущее значение управляющего слова FPU в указанном месте в памяти. fstcw перед сохранением слова проверяет на подвешенные немаскируемые численные исключения, fnstcw этого не делает. fldcw загружает операнд в управляющее слово FPU. Операндом должно быть 16-битное расположение в памяти.

fstenv и fnstenv сохраняют текущий контекст FPU в расположении в памяти, указанном в операнде-адресате, и далее маскируют все исключения операций с плавающей точкой. fstenv перед совершением операции проверяет на подвешенные немаскируемые численные исключения, fnstenv этого не делает. fldenv загружает полный контекст FPU из памяти в FPU. fsave и fnsave сохраняют текущий статус FPU (контекст и регистры стека) в указанном месте в памяти и затем ре-инициализируют FPU. fsave перед совершением операции проверяет на подвешенные немаскируемые численные исключения, fnsave этого не делает. frstor загружает статус FPU из указанного места в памяти. Все эти инструкции в качестве операнда требуют роасположение в памяти.

finit и fninit устанавливают контекст FPU в его значение по умолчанию. finit перед совершением операции проверяет на подвешенные немаскируемые численные исключения, fninit этого не делает. fclex and fnclex очищают флаги исключений FPU в слове статуса FPU. fclex перед совершением операции проверяет на подвешенные немаскируемые численные исключения, fnclex этого не делает. wait и fwait - это синонимы одной и той же инструкции, которая указывает процессор проверить наличие подвешенных немаскируемых численных исключений и разобраться с ними до продолжения работы. Эти инструкции не имеют операндов.

ffree помечает тэг, асооциированный с указанным регистром FPU, как пустой. Операндом должен служить регистр FPU.

fincstp и fdecstp вращают стек FPU на единицу, прибавляя или отнимая единицу от поля TOP слова статуса FPU. У этих инструкций нет операндов.


2.1.14 Инструкции MMX

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

movq копирует четверное слово из операнда-источника в операнд-адресат. По крайней мере одним из операндов должен являться регистр MMX, вторым может быть либо регистр MMX, либо 64-битное расположение в памяти.

movq mm0,mm1     ; копирует четверное слово из регистра в регистр
movq mm2,[ebx]   ; копирует четверное слово из памяти в регистр

movd копирует двойное слово из операнда-источника в операнд-адресат. Одним из операндов должен быть регистр MMX, вторым может быть регистр общего назначения либо 32-битное расположение в памяти. Используется только нижнее двойное слово регистра MMX.

Все основные операции MMX имеют два операнда, где операндом-адресатом должен быть регистр MMX, а операндом-источником может быть либо регистр MMX, либо 64-битное расположение в памяти. Операция совершается на соответствующих элементах данных источника и адресата и сохраняется элементах данных адресата. paddb, paddw и paddd совершают сложение сжатых байтов, сжатых слов и сжатых двойных слов. psubb, psubw и psubd совершают вычитание соответсвующих типов. paddsb, paddsw, psubsb и psubsw совершают сложение или вычитание сжатых байтов или сжатых слов со знаковым насыщением. paddusb, paddusw, psubusb, psubusw - это аналоги, но без знакового насыщения. pmulhw и pmullw совершают знаковое умножение сжатых слов и сохраняют верхние или нижние слова результатов в операнде-адресате. pmaddwd совершает умножение сжатых слов и складывает четыре промежуточных продукта в виде двойных слов в парах, чтобы получить результат в виде сжатых двойных слов. pand, por и pxor совершают логические операции над четверными словами, pandn также производит логическое отрицание перед операцией and. pcmpeqb, pcmpeqw и pcmpeqd сравнивают на эквивалентность сжатые байты, сжатые слова или сжатые двойные слова. Если пара элементов данных эквивалентна, то соответствующий элемент данных операнда-адресата покрывается единичными битам, иначе нулевыми. pcmpgtb, pcmpgtw и pcmpgtd совершают похожую операцию, но они проверяют, больше ли элементы данных в операнде-адресате, чем соответствующие элементы данных в операнде-источнике. packsswb конвертирует сжатые знаковые слова в сжатые знаковые байты, packssdw конвертирует сжатые знаковые двойные слова в сжатые знаковые слова, используя насыщение, чтобы удовлетворить условиям переполнения. packuswb конвертирует сжатые знаковые слова в сжатые беззнаковые байты. Сконвертированные элементы данных из операнда-источника сохраняются в нижней части операнда-адресата, тогда как сконвертированные элементы данных операнда-адресата сохраняются в его верхней части. punpckhbw, punpckhwd и punpckhdq чередуют элементы данных из верхних частей источника и адресата и сохраняют результат в операнд-адресат. punpcklbw, punpcklwd и punpckldq совершают те же операции, но с нижними частями операндов.

paddsb mm0,[esi] ; складывает сжатые байты со знаковым насыщением
pcmpeqw mm3,mm7  ; проверяет сжатые слова на эквивалентность

psllw, pslld и psllq совершают логический сдвиг влево сжатых слов, сжатых двойных слов или одиночных четверных слов в операнде-адресате, на число битов, указанное в операнде-источнике. psrlw, psrld и psrlq совершают логический сдвиг вправо сжатых слов, сжатых двойных слов или одиночных четверных слов. psraw и psrad совершают арифметический сдвиг сжатых слов или двойных слов. Операндом-адресатом должен быть регистр MMX, а операндом-источником может быть регистр MMX, 64-битное расположение в памяти или 8-битное непосредственное значение.

psllw mm2,mm4    ; сдвигает слова влево логически
psrad mm4,[ebx]  ; сдвигает двойные слова вправо арифметически

emms делает регистры FPU используемыми для инструкций FPU. Эта инструкция должна быть применена перед использованием инструкций FPU, если в ход пускались инструкции MMX.


2.1.15 Инструкции SSE

Расширение SSE добавляет больше инструкций MMX, а также представляет операции со сжатыми значениями одинарной точности с плавающей точкой. 128-битный сжатый формат одинарной точности содержит четыре значения одинарной точности с плавающей точкой. 128-битные регистры SSE созданы для поддержки операций этого типа данных.

movaps и movups переводят операнд размером в двойное четверное слово, содержащий значения одинарной точности из операнда-источника в операнд-адресат. По крайней мере одним из операндов должен быть регистр SSE, вторым может быть либо тоже регистр SSE, либо 128-битное расположение в памяти. Операнды в памяти для movaps должны быть выровнены по 16-битной меже, для movups этого не требуется.

movups xmm0,[ebx]  ; переводит не выровненное двойное четверное слово

movlps переводит два сжатых значения одинарной точности из нижнего четверного слова регистра-источника в верхнее четверное слово регистра-адресата. movhlps переводит два сжатых значения одинарной точности из верхнего четверного слова регистра-источника в нижнее четверное слово регистра-адресата. Обоими операндами должны быть регистры SSE.

movmskps переводит знаковые биты всех значений одинарной точности в регистре SSE в нижние четыре бита регистра общего назначения. Операндом-источником должен быть регистр SSE, операндом-адресатом должен быть регистр общего назначения.

movss переводит значение одинарной точности между источником и адресатом (переводится только нижнее двойное слово). По крайней мере одним из операндов должен быть регистр SSE, вторым может быть либо тоже регистр SSE, либо 32-битное располжение в памяти.

movss [edi],xmm3   ; переводит нижнее двойное слово из xmm3 в память

Каждая арифметическая операция SSE имеет два варианта. Если мнемоник заканчивается на ps, операндом-источником может быть 128-битное расположение в памяти или регистр SSE, операндом-адресатом должен быть регистр SSE, и операция производится над четырьмя сжатыми значениями одинарной точности, для каждой пары соответствующих элементов данных отдельно, и результат сохраняется в регистре-адресате. Если мнемоник заканчивается на ss, то операндом-источником может быть 32-битное расположение в памяти или регистр SSE, операндом-адресатом должен быть регистр SSE, и операция производится над одним значением одинарной точности, используются только нижние двойные слова регистров SSE, и результат сохраняется в нижнем двойном слове регистра-адресата. addps и addss складывают значения, subps и subss вычитают источник из адресата, mulps и mulss перемножают значения, divps и divss делят адресат на источник, rcpps и rcpss вычисляют аппроксимированную обратную величину источника, sqrtps и sqrtss вычисляют квадратный корень источника, rsqrtps и rsqrtss вычисляют аппроксимированную обратную величину квадратного корня источника, maxps и maxss сравнивают источник и адресат и возвращают большее значение, minps и minss сравнивают источник и адресат и возвращают меньшее значение.

mulss xmm0,[ebx]   ; перемножает значения одинарной точности
addps xmm3,xmm7    ; складывает сжатые значения одинарной точности

andps, andnps, orps и xorps производят логические операции над сжатыми значениями одинарной точности. Операндом-источником может быть 128-битное расположение в памяти или регистр SSE, операндом-адресатом должен быть регистр SSE.

cmpps сравнивает сжатые значения одинарной точности и возвращают маскируемый результат в операнд-адресат, которым должен быть регистр SSE. Операндом-источником может быть либо регистр SSE, либо 128-битное расположение в памяти, третьим операндом должно быть непосредственное значение, выбирающее код одного из восьми условий сравнения (таблица 2.3). cmpss совершает ту же над значениями одинарной точности, изменяется только нижнее двойное слово регистра-адресата, таким образом операндом-источником должен быть либо регистр SSE, либо 32-битное расположение в памяти. Эти две инструкции имеют также варианты с двумя операндами и условие, закодированным в мнемоник. Их мнемоники образуются путем добавления мнемоников из Таблицы 2.3 к cmp и после добавления к ним в конце ps илои ss .

cmpps xmm2,xmm4,0  ; сравнивает сжатые значения одинарной точности
cmpltss xmm0,[ebx] ; сравнивает значения одинарной точности

Таблица 2.3 Условия SSE
Код | Мнемоник | Описание
0 | eq | равно
1 | lt | меньше
2 | le | меньше или равно
3 | unord | ненормализованное
4 | neq | не равно
5 | nlt | не меньше
6 | nle | не меньше и не равно
7 | ord | нормализованное

comiss и ucomiss сравнивают значения одинарной точности и ставят в зависимости от результата флаги ZF, PF и CF . Операндом-адресатом должен быть регистр SSE, операндом-источником может быть 32-битное расположение в памяти или регистр SSE.

shufps переводит некоторые два из четырех значений одинарной точности из операнда-адресата в нижнее четверное слово операнда-адресата и некоторые два из четырех значений одинарной точности из операнда-источника в верхнее четверное слово операнда-адресата. Операндом-адресатом должен быть регистр SSE, операндом-источником может быть 128-битное расположение в памяти или регистр SSE, а третьим операндом должно быть 8-битное непосредственное значение, какие конкретно значения будут задействованы. Биты 0 и 1 указывают значение из адресата, которое должно быть в нижнем двойном слове результата, биты 2 и 3 указывают значение из адресата, которое должно быть во втором двойном слове результата, биты 4 и 5 указывают значение из источника, которое должно быть в третьем двойном слове результата, биты 6 и 7 указывают значение из источника, которое должно быть в нижнем верхнем слове результата.

shufps xmm0,xmm0,10010011b ; перемешивает двойные слова

unpckhps совершает перемежающуюся распаковку значений из верхних частей источника и адресата и сохраняет результат в адресат, которым должен быть регистр SSE. Операндом-источником может быть 128-битное расположение в памяти или регистр SSE. unpcklps совершает перемежающуюся распаковку значений из нижних частей источника и адресата и сохраняет результат в адресат, правила для операндов такие же.

cvtpi2ps конвертирует два сжатых целых числа размером в двойное слово в два сжатых значения с плавающей точкой одинарной точности и сохраняет результат в нижнем четверном слове адресата, которым должен быть регистр SSE. Операндом-источником может быть 64-битное расположение в памяти или регистр MMX.

cvtpi2ps xmm0,mm0  ; конвертирует целые числа в значения одинарной точности

cvtsi2ss конвертирует целое число размером в двойное слово в сжатое значение с плавающей точкой одинарной точности и сохраняет результат в нижнем двойном слове адресата, которым должен быть регистр SSE. Операндом-источником может быть 32-битное расположение в памяти или 32-битный регистр общего назначения.

cvtsi2ss xmm0,eax  ; конвертирует целое число в значение одинарной точности

cvtps2pi конвертирует два сжатых значения с плавающей точкой одинарной точности в два сжатых целых числа размером в двойное слово и сохраняет результат в адресате, которым должен быть регистр MMX. Операндом-источником может быть 64-битное расположение в памяти либо регистр SSE, в котором будет использовано только нижнее четверное слово. cvttps2pi совершает похожую операцию, но для округления здесь используется отбрасывание дробной части, правила для операндов у этой инструкции такие же.

cvtps2pi mm0,xmm0  ; конвертирует значения одинарной точности в целые числа

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

cvtss2si eax,xmm0  ; конвертирует значение одинарной точности в целое число

pextrw копирует слово, указанное третьим операндом, из источника в адресат. Операндом-источником должен быть регистр MMX, операндом-адресатом должен быть 32-битный регистр общего назначения (но используется только нижнее его слово), третьим операндом должно быть 8-битное непосредственное значение.

pextrw eax,mm0,1   ; извлекает слово в eax

pinsrw вставляет слово из источника в место в адресате, указанное третьим операндом, которым должно быть 8-битное непосредственное значение. Операндом-адресатом должен быть регистр MMX, операндом-источником должен быть 32-битный регистр общего назначения (но используется только нижнее его слово).

pinsrw mm1,ebx,2   ; вставляет слово из ebx

pavgb and pavgw вычисляют среднее сжатых байтов или слов. pmaxub возвращает максимум сжатых беззнаковых байтов, pminub возвращает минимум сжатых беззнаковых байтов, pmaxsw возвращает максимум сжатых знаковых слов, pminsw возвращает минимум сжатых знаковых слов. pmulhuw совершает беззнаковое умножение сжатых слов и сохраняет верхние слова результатов в операнд-адресат. psadbw вычисляет абсолютные разности сжатых беззнаковых байтов, суммирует эти разности и сохраняет результат в нижнее слово операнда-адресата. Все эти инструкции следуют тем же правилам для операндов, что и основные операции MMX, описанные в предыдущем параграфе.

pmovmskb создает маску из знаковых битов всех байтов в источнике и сохраняет результат в нижнем байте адресата. Операндом-источником должен быть регистр MMX, операндом-адресатом должен быть 32-битный регистр общего назначения.

pshufw копирует слова, указанные третьим операндом, из источника в адресат. Операндом-адресатом долже быть регистр MMХ, операндом-источником может быть 64-битное расположение в памяти или регистр MMX, третьим операндом должно быть 8-битное непосредственное значение, выбирающее, какие значения будут помещены в адресат, таким же образом как третий операнд в инструкции shufps .

movntq переводит четверное слово из операнда-источника в память, используя "не-временное малое количество" (non-temporal hint), чтобы минимизировать загрязнение кэша. Операндом-источником должен быть регистр MMX, операндом-адресатом должно быть 64-битное расположение в памяти. movntps сохраняет сжатые значения одинарной точности из регистра SSE в память, используя "не-временное малое количество". Операндом-источником должен быть регистр SSE, операндом-адресатом должно быть 128-битное расположение в памяти. maskmovq сохраняет выбранные байты из первого операнда в 64-битное расположение в памяти, используя "не-временное малое количество". Обоими операндами должны служить регистры MMX, второй операнд указывает, какие байты из первого операнда должны быть записаны в память. Расположение в памяти указывается регистром di (или edi) в сегменте, определенном в ds .

prefetcht0, prefetcht1, prefetcht2 and prefetchnta помещает строку данных из памяти, которая содержит байт, указанный в операнде, в определенное место в иерархии кеша. Операндом должно быть 8-битное расположение в памяти.

sfence переводит в последовательный режим все предыдущие команды, совершающие запись в память. У этой инструкции нет операндов.

ldmxcsr загружает 32-битный операнд в памяти в регистр MXCSR. stmxsr сохраняет содержимое MXCSR в 32-битный операнд в памяти.

fxsave сохраняет текущий статус FPU, регистр MXCSR и все регистры FPU и SSE в 512-байтное расположение в памяти, указанное в операнде-адресате. fxstor перезагружает данные, ранее сохраненные инструкцией fxsave из 512-байтного расположения в памяти. Операнд для обеих этих инструкций должен быть выровнен по 16 байтам, нужно объявить операнд неопределенного размера.


2.1.16 SSE2 instructions

The SSE2 extension introduces the operations on packed double precision floating point values, extends the syntax of MMX instructions, and adds also some new instructions.

movapd and movupd transfer a double quad word operand containing packed double precision values from source operand to destination operand. These nstructions are analogous to movaps and movups and have the same rules for operands.

movlpd moves double precision value between the memory and the low quad word of SSE register. movhpd moved double precision value between the memory and the high quad word of SSE register. These instructions are analogous to movlps and movhps and have the same rules for operands.

movmskpd transfers the most significant bit of each of the two double precision values in the SSE register into low two bits of a general register. This instruction is analogous to movmskps and has the same rules for operands.

movsd transfers a double precision value between source and destination operand (only the low quad word is trasferred). At least one of the operands have to be a SSE register, the second one can be also a SSE register or 64-bit memory location.

Arithmetic operations on double precision values are: addpd, addsd , subpd, subsd, mulpd, mulsd, divpd, divsd, sqrtpd, sqrtsd, maxpd, maxsd, minpd, minsd , and they are analoguous to arithmetic operations on single precision values described in previous section. When the mnemonic ends with pd instead of ps , the operation is performed on packed two double precision values, but rules for operands are the same. When the mnemonic ends with sd instead of ss , the source operand can be a 64-bit memory location or a SSE register, the destination operand must be a SSE register and the operation is performed on double precision values, only low quad words of SSE registers are used in this case.

andpd, andnpd, orpd and xorpd perform the logical operations on packed double precision values. They are analoguous to SSE logical operations on single prevision values and have the same rules for operands.

cmppd compares packed double precision values and returns and returns a mask result into the destination operand. This instruction is analoguous to cmpps and has the same rules for operands. cmpsd performs the same operation on double precision values, only low quad word of destination register is affected, in this case source operand can be a 64-bit memory or SSE register. Variant with only two operands are obtained by attaching the condition mnemonic from table 2.3 to the cmp mnemonic and then attaching the pd or sd at the end.

comisd and ucomisd compare the double precision values and set the ZF, PF and CF flags to show the result. The destination operand must be a SSE register, the source operand can be a 128-bit memory location or SSE register.

shufpd moves any of the two double precision values from the destination operand into the low quad word of the destination operand, and any of the two values from the source operand into the high quad word of the destination operand. This instruction is analoguous to shufps and has the same rules for operand. Bit 0 of the third operand selects the value to be moved from the destination operand, bit 1 selects the value to be moved from the source operand, the rest of bits are reserved and must be zeroed.

unpckhpd performs an unpack of the high quad words from the source and destination operands, unpcklpd performs an unpack of the low quad words from the source and destination operands. They are analoguous to unpckhps and unpcklps , and have the same rules for operands.

cvtps2pd converts the packed two single precision floating point values to two packed double precision floating point values, the destination operand must be a SSE register, the source operand can be a 64-bit memory location or SSE register. cvtpd2ps converts the packed two double precision floating point values to packed two single precision floating point values, the destination operand must be a SSE register, the source operand can be a 128-bit memory location or SSE register. cvtss2sd converts the single precision floating point value to double precision floating point value, the destination operand must be a SSE register, the source operand can be a 32-bit memory location or SSE register. cvtsd2ss converts the double precision floating point value to single precision floating point value, the destination operand must be a SSE register, the source operand can be 64-bit memory location or SSE register.

cvtpi2pd converts packed two double word integers into the the packed double precision floating point values, the destination operand must be a SSE register, the source operand can be a 64-bit memory location or MMX register. cvtsi2sd converts a double word integer into a double precision floating point value, the destination operand must be a SSE register, the source operand can be a 32-bit memory location or 32-bit general register. cvtpd2pi converts packed double precision floating point values into packed two double word integers, the destination operand should be a MMX register, the source operand can be a 128-bit memory location or SSE register. cvttpd2pi performs the similar operation, except that truncation is used to round a source values to integers, rules for operands are the same. cvtsd2si converts a double precision floating point value into a double word integer, the destination operand should be a 32-bit general register, the source operand can be a 64-bit memory location or SSE register. cvttsd2si performs the similar operation, except that truncation is used to round a source value to integer, rules for operands are the same.

cvtps2dq and cvttps2dq convert packed single precision floating point values to packed four double word integers, storing them in the destination operand. cvtpd2dq and cvttpd2dq convert packed double precision floating point values to packed two double word integers, storing the result in the low quad word of the destination operand. cvtdq2ps converts packed four double word integers to packed single precision floating point values. For all these instructions destination operand must be a SSE register, the source operand can be a 128-bit memory location or SSE register. cvtdq2pd converts packed two double word integers from the source operand to packed double precision floating point values, the source can be a 64-bit memory location or SSE register, destination has to be SSE register.

movdqa and movdqu transfer a double quad word operand containing packed integers from source operand to destination operand. At least one of the operands have to be a SSE register, the second one can be also a SSE register or 128-bit memory location. Memory operands for movdqa instruction must be aligned on boundary of 16 bytes, operands for movdqu instruction don't have to be aligned.

movq2dq moves the contents of the MMX source register to the low quad word of destination SSE register. movdq2q moves the low quad word from the source SSE register to the destination MMX register.

movq2dq xmm0,mm1   ; move from MMX register to SSE register
movdq2q mm0,xmm1   ; move from SSE register to MMX register

All MMX instructions operating on the 64-bit packed integers (those with mnemonics starting with p ) are extended to operate on 128-bit packed integers located in SSE registers. Additional syntax for these instructions needs an SSE register where MMX register was needed, and the 128-bit memory location or SSE register where 64-bit memory location or MMX register were needed. The exception is pshufw instruction, which doesn't allow extended syntax, but has two new variants: pshufhw and pshuflw , which allow only the extended syntax, and perform the same operation as pshufw on the high or low quad words of operands respectively. Also the new instruction pshufd is introduced, which performs the same operation as pshufw , but on the double words instead of words, it allows only the extended syntax.

psubb xmm0,[esi]   ; subtract 16 packed bytes
pextrw eax,xmm0,7  ; extract highest word into eax

paddq performs the addition of packed quad words, psubq performs the subtraction of packed quad words, pmuludq performs an unsigned multiplication of low double words from each corresponding quad words and returns the results in packed quad words. These instructions follow the same rules for operands as the general MMX operations described in 2.1.14.

pslldq and psrldq perform logical shift left or right of the double quad word in the destination operand by the amount of bytes specified in the source operand. The destination operand should be a SSE register, source operand should be an 8-bit immediate value.

punpckhqdq interleaves the high quad word of the source operand and the high quad word of the destination operand and writes them to the destination SSE register. punpcklqdq interleaves the low quad word of the source operand and the low quad word of the destination operand and writes them to the destination SSE register. The source operand can be a 128-bit memory location or SSE register.

movntdq stores packed integer data from the SSE register to memory using non-temporal hint. The source operand should be a SSE register, the destination operand should be a 128-bit memory location. movntpd stores packed double precision values from the SSE register to memory using a non-temporal hint. Rules for operand are the same. movnti stores integer from a general register to memory using a non-temporal hint. The source operand should be a 32-bit general register, the destination operand should be a 32-bit memory location. maskmovdqu stores selected bytes from the first operand into a 128-bit memory location using a non-temporal hint. Both operands should be a SSE registers, the second operand selects wich bytes from the source operand are written to memory. The memory location is pointed by DI (or EDI) register in the segment selected by DS and does not need to be aligned.

clflush writes and invalidates the cache line associated with the address of byte specified with the operand, which should be a 8-bit memory location.

lfence performs a serializing operation on all instruction loading from memory that were issued prior to it. mfence performs a serializing operation on all instruction accesing memory that were issued prior to it, and so it combines the functions of sfence (described in previous section) and lfence instructions. These instructions have no operands.


2.1.17 SSE3 instructions

Prescott technology introduced some new instructions to improve the performance of SSE and SSE2 - this extension is called SSE3.

fisttp behaves like the fistp instruction and accepts the same operands, the only difference is that it always used truncation, irrespective of the rounding mode.

movshdup loads into destination operand the 128-bit value obtained from the source value of the same size by filling the each quad word with the two duplicates of the value in its high double word. movsldup performs the same action, except it duplicates the values of low double words. The destination operand should be SSE register, the source operand can be SSE register or 128-bit memory location.

movddup loads the 64-bit source value and duplicates it into high and low quad word of the destination operand. The destination operand should be SSE register, the source operand can be SSE register or 64-bit memory location.

lddqu is functionally equivalent to movdqu with memory as source operand, but it may improve performance when the source operand crosses a cacheline boundary. The destination operand has to be SSE register, the source operand must be 128-bit memory location.

addsubps performs single precision addition of second and fourth pairs and single precision subtracion of the first and third pairs of floating point values in the operands. addsubpd performs double precision addition of the second pair and double precision subtraction of the first pair of floating point values in the operand. haddps performs the addition of two single precision values within the each quad word of source and destination operands, and stores the results of such horizontal addition of values from destination operand into low quad word of destination operand, and the results from the source operand into high quad word of destination operand. haddpd performs the addition of two double precision values within each operand, and stores the result from destination operand into low quad word of destination operand, and the result from source operand into high quad word of destination operand. All these instructions need the destination operand to be SSE register, source operand can be SSE register or 128-bit memory location.

monitor sets up an address range for monitoring of write-back stores. It need its three operands to be EAX, ECX and EDX register in that order. mwait waits for a write-back store to the address range set up by the monitor instruction. It uses two operands with additional parameters, first being the EAX and second the ECX register.

The functionality of SSE3 is further extended by the set of Supplemental SSE3 instructions (SSSE3). They generally follow the same rules for operands as all the MMX operations extended by SSE.

phaddw and phaddd perform the horizontal additional of the pairs of adjacent values from both the source and destination operand, and stores the sums into the destination (sums from the source operand go into lower part of destination register). They operate on 16-bit or 32-bit chunks, respectively. phaddsw performs the same operation on signed 16-bit packed values, but the result of each addition is saturated. phsubw and phsubd analogously perform the horizontal subtraction of 16-bit or 32-bit packed value, and phsubsw performs the horizontal subtraction of signed 16-bit packed values with saturation.

pabsb, pabsw and pabsd calculate the absolute value of each signed packed signed value in source operand and stores them into the destination register. They operator on 8-bit, 16-bit and 32-bit elements respectively.

pmaddubsw multiplies signed 8-bit values from the source operand with the corresponding unsigned 8-bit values from the destination operand to produce intermediate 16-bit values, and every adjacent pair of those intermediate values is then added horizontally and those 16-bit sums are stored into the destination operand.

pmulhrsw multiplies corresponding 16-bit integers from the source and destination operand to produce intermediate 32-bit values, and the 16 bits next to the highest bit of each of those values are then rounded and packed into the destination operand.

pshufb shuffles the bytes in the destination operand according to the mask provided by source operand - each of the bytes in source operand is an index of the target position for the corresponding byte in the destination.

psignb, psignw and psignd perform the operation on 8-bit, 16-bit or 32-bit integers in destination operand, depending on the signs of the values in the source. If the value in source is negative, the corresponding value in the destination register is negated, if the value in source is positive, no operation is performed on the corresponding value is performed, and if the value in source is zero, the value in destination is zeroed, too.

palignr appends the source operand to the destination operand to form the intermediate value of twice the size, and then extracts into the destination register the 64 or 128 bits that are right-aligned to the byte offset specified by the third operand, which should be an 8-bit immediate value. This is the only SSSE3 instruction that takes three arguments.


2.1.18 AMD 3DNow! instructions

The 3DNow! extension adds a new MMX instructions to those described in 2.1.14, and introduces operation on the 64-bit packed floating point values, each consisting of two single precision floating point values.

These instructions follow the same rules as the general MMX operations, the destination operand should be a MMX register, the source operand can be a MMX register or 64-bit memory location. pavgusb computes the rounded averages of packed unsigned bytes. pmulhrw performs a signed multiplication of the packed words, round the high word of each double word results and stores them in the destination operand. pi2fd converts packed double word integers into packed floating point values. pf2id converts packed floating point values into packed double word integers using truncation. pi2fw converts packed word integers into packed floating point values, only low words of each double word in source operand are used. pf2iw converts packed floating point values to packed word integers, results are extended to double words using the sign extension. pfadd adds packed floating point values. pfsub and pfsubr subtracts packed floating point values, the first one subtracts source values from destination values, the second one subtracts destination values from the source values. pfmul multiplies packed floating point values. pfacc adds the low and high floating point values of the destination operand, storing the result in the low double word of destination, and adds the low and high floating point values of the source operand, storing the result in the high double word of destination. pfnacc subtracts the high floating point value of the destination operand from the low, storing the result in the low double word of destination, and subtracts the high floating point value of the source operand from the low, storing the result in the high double word of destination. pfpnacc subtracts the high floating point value of the destination operand from the low, storing the result in the low double word of destination, and adds the low and high floating point values of the source operand, storing the result in the high double word of destination. pfmax and pfmin compute the maximum and minimum of floating point values. pswapd reverses the high and low double word of the source operand. pfrcp returns an estimates of the reciprocals of floating point values from the source operand, pfrsqrt returns an estimates of the reciprocal square roots of floating point values from the source operand, pfrcpit1 performs the first step in the Newton-Raphson iteration to refine the reciprocal approximation produced by pfrcp instruction, pfrsqit1 performs the first step in the Newton-Raphson iteration to refine the reciprocal square root approximation produced by pfrsqrt instruction, pfrcpit2 performs the second final step in the Newton-Raphson iteration to refine the reciprocal approximation or the reciprocal square root approximation. pfcmpeq, pfcmpge and pfcmpgt compare the packed floating point values and sets all bits or zeroes all bits of the correspoding data element in the destination operand according to the result of comparison, first checks whether values are equal, second checks whether destination value is greater or equal to source value, third checks whether destination value is greater than source value.

prefetch and prefetchw load the line of data from memory that contains byte specified with the operand into the data cache, prefetchw instruction should be used when the data in the cache line is expected to be modified, otherwise the prefetch instruction should be used. The operand should be an 8-bit memory location.

femms performs a fast clear of MMX state. This instruction has no operands.


2.1.19 Инструкции длинного x86-64 режима

Архитектуры AMD64 и EM64T (мы будем использовать общее имя x86-64 для них обоих) расширяют набор команд x86 для 64-битной обработки. Хотя в устаревших режимах и режимах совместимости используется один и тот же набор регистров и инструкций, новый длинный режим расширяет операции x86 до 64 бит и вводит несколько новых регистров. Вы можете включить генерацию кода для этого режима с помощью директивы use64 .

Каждый из регистров общего назначения расширяется до 64 битов, и добавляются восемь совершенно новых регистров общего назначения, а также восемь новых регистров SSE. См. Таблицу 2.4 для сводки новых регистров (только те, которые не были перечислены в таблице 1.2). Регистры общего назначения меньших размеров являются младшими частями больших. Вы по-прежнему можете обращаться к регистрам ah, bh, ch и dh в длинном режиме, но вы не можете использовать их в одной и той же инструкции с любым из новых регистров.

Таблица 2.4 Новые регистры в длинном режиме
Type | Bits
General 8
16
32
64 		                 spl bpl sil dil r8b  r9b  r10b  r11b  r12b  r13b  r14b  r15b
                                 r8w  r9w  r10w  r11w  r12w  r13w  r14w  r15w
                                 r8d  r9d  r10d  r11d  r12d  r13d  r14d  r15d
 rax rcx rdx rbx rsp rbp rsi rdi r8   r9   r10   r11   r12   r13   r14   r15
SSE | 128 | xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
AVX | 256 | ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15

В общем, любая инструкция из архитектуры x86, которая допускает 16-битные или 32-битные размеры операндов, в длинном режиме также допускает 64-битные операнды. 64-битные регистры должны использоваться для адресации в длинном режиме, 32-битная адресация также разрешена, но невозможно использовать адреса на основе 16-битных регистров. Ниже приведены примеры новых операций, возможных в длинном режиме на примере инструкции mov :

mov rax,r8   ; передача общего 64-битного регистра
mov al,[rbx] ; передача памяти, адресованная 64-битным регистром

В длинном режиме также используются адреса, основанные на указателе команд, вы можете указать его вручную с помощью специального символа регистра rip , и такая адресация также автоматически генерируется плоским ассемблером, поскольку в длинном режиме 64-битная абсолютная адресация отсутствует. Вы все равно можете заставить ассемблер использовать 32-битную абсолютную адресацию, поместив переопределение размера dword для адреса в квадратных скобках. Существует также одно исключение, при котором возможна 64-битная абсолютная адресация, это инструкция mov, где один из операндов является регистром-аккумулятором, а второй - операндом в памяти. Чтобы заставить ассемблер использовать 64-битную абсолютную адресацию, используйте оператор размера qword для адреса в квадратных скобках. Когда к адресу не применяется оператор размера, ассемблер автоматически генерирует оптимальную форму.

mov [qword 0],rax  ; абсолютная 64-битная адресация
mov [dword 0],r15d ; абсолютная 32-битная адресация
mov [0],rsi        ; автоматическая RIP-относительная адресация
mov [rip+3],sil    ; ручная RIP-относительная адресация

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

Если какая-либо операция выполнена с помощью 32-битных общих регистров в длинном режиме, верхние 32 бита 64-битных регистров, содержащих их, заполняются нолями. Это непохоже на операции над 16-битными или 8-битными частями тех регистров, которые сохраняют верхние биты.

Доступны три новые инструкции по преобразованию типов. Знак cdqe расширяет двойное слово в eax до четырех слов и сохраняет результат в регистре rax. cqo расширяет знак четвертного слова в rax до двойного четвертного слова и сохраняет дополнительные биты в регистре rdx. Эти инструкции не имеют операндов. movsxd расширяет знак исходного операнда размером в двойное слово, являющийся либо 32-битным регистром, либо памятью, до 64-битного операнд-адресата, который должен быть регистром. Для преобразования с нулевым расширением аналогичной инструкции не требуется, поскольку она выполняется автоматически при любых операциях с 32-битными регистрами, как отмечено в предыдущем абзаце. А инструкции movzx и movsx , соответствующие общему правилу, могут использоваться с 64-битным операндом-адресатом, что позволяет расширять значения байтов или слов в четвертные слова.

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

В стековых операциях, таких как push и pop в длинном режиме, по умолчанию используются 64-битные операнды, и с ними невозможно использовать 32-битные операнды. pusha и popa запрещены в длинном режиме.

Непрямые ближние переходы и вызовы в длинном режиме по умолчанию используют 64-битные операнды, и с ними невозможно использовать 32-битные операнды. С другой стороны, косвенные переходы и вызовы разрешают любые операнды, которые были разрешены архитектурой x86, а также разрешен 80-битный операнд памяти (хотя, похоже, только EM64T реализует такой вариант), причем первые восемь байтов определяют смещение и два последних байта, указывают на селектор. Прямые дальние прыжки и вызовы не допускаются в длинном режиме.

Инструкции ввода-вывода in, out, ins и outs - это исключительные инструкции, которые не расширяются для приема операндов в четверное слово в длинном режиме. Но все остальные строковые операции сохраняются, и для строковых операций с 64-битными строковыми элементами введены новые короткие формы movsq, cmpsq, scasq, lodsq и stosq. Регистры rsi и rdi используются по умолчанию для обращения к строковым элементам.

Инструкции lfs, lgs и lss расширены, чтобы использовать 80-битный операнд-источник памяти с 64-битным регистром назначения (хотя только EM64T, кажется, реализует такой вариант). lds и les запрещены в длинном режиме.

Системные инструкции, такие как lgdt , для которых требуется 48-битный операнд памяти, в длинном режиме требуют 80-битный операнд памяти..

cmpxchg16b является 64-битным эквивалентом инструкции cmpxchg8b , он использует операнд памяти с двойным четвертным словом и 64-битные регистры для выполнения аналогичной операции.

fxsave64 и fxrstor64 это новые варианты fxsave и fxrstor , доступные только в длинном режиме, которые используют другой формат области хранения, чтобы хранить некоторые указатели в полном 64-битном формате.

swapgs - это новая инструкция, которая меняет содержимое регистра gs и регистра, зависящего от модели KernelGSbase (адрес MSR 0C0000102h).

syscall и sysret - это пара новых инструкций, которые обеспечивают функциональность, аналогичную sysenter и sysexit в длинном режиме, где последняя пара запрещена. Мнемоники sysexitq и sysretq предоставляют 64-битные версии команд sysexit и sysret .

Мнемоники rdmsrq и wrmsrq являются 64-битными вариантами команд rdmsr и wrmsr .


2.1.20 SSE4 instructions

There are actually three different sets of instructions under the name SSE4. Intel designed two of them, SSE4.1 and SSE4.2, with latter extending the former into the full Intel's SSE4 set. On the other hand, the implementation by AMD includes only a few instructions from this set, but also contains some additional instructions, that are called the SSE4a set.

The SSE4.1 instructions mostly follow the same rules for operands, as the basic SSE operations, so they require destination operand to be SSE register and source operand to be 128-bit memory location or SSE register, and some operations require a third operand, the 8-bit immediate value.

pmulld performs a signed multiplication of the packed double words and stores the low double words of the results in the destination operand. pmuldq performs a two signed multiplications of the corresponding double words in the lower quad words of operands, and stores the results as packed quad words into the destination register. pminsb and pmaxsb return the minimum or maximum values of packed signed bytes, pminuw and pmaxuw return the minimum and maximum values of packed unsigned words, pminud, pmaxud, pminsd and pmaxsd return minimum or maximum values of packed unsigned or signed words. These instructions complement the instructions computing packed minimum or maximum introduced by SSE.

ptest sets the ZF flag to one when the result of bitwise AND of the both operands is zero, and zeroes the ZF otherwise. It also sets CF flag to one, when the result of bitwise AND of the destination operand with the bitwise NOT of the source operand is zero, and zeroes the CF otherwise. pcmpeqq compares packed quad words for equality, and fills the corresponding elements of destination operand with either ones or zeros, depending on the result of comparison.

packusdw converts packed signed double words from both the source and destination operand into the unsigned words using saturation, and stores the eight resulting word values into the destination register.

phminposuw finds the minimum unsigned word value in source operand and places it into the lowest word of destination operand, setting the remaining upper bits of destination to zero.

roundps, roundss, roundpd and roundsd perform the rounding of packed or individual floating point value of single or double precision, using the rounding mode specified by the third operand.

roundsd xmm0,xmm1,0011b ; round toward zero

dpps calculates dot product of packed single precision floating point values, that is it multiplies the corresponding pairs of values from source and destination operand and then sums the products up. The high four bits of the 8-bit immediate third operand control which products are calculated and taken to the sum, and the low four bits control, into which elements of destination the resulting dot product is copied (the other elements are filled with zero). dppd calculates dot product of packed double precision floating point values. The bits 4 and 5 of third operand control, which products are calculated and added, and bits 0 and 1 of this value control, which elements in destination register should get filled with the result. mpsadbw calculates multiple sums of absolute differences of unsigned bytes. The third operand controls, with value in bits 0-1, which of the four-byte blocks in source operand is taken to calculate the absolute differencies, and with value in bit 2, at which of the two first four-byte block in destination operand start calculating multiple sums. The sum is calculated from four absolute differencies between the corresponding unsigned bytes in the source and destination block, and each next sum is calculated in the same way, but taking the four bytes from destination at the position one byte after the position of previous block. The four bytes from the source stay the same each time. This way eight sums of absolute differencies are calculated and stored as packed word values into the destination operand. The instructions described in this paragraph follow the same rules for operands, as roundps instruction.

blendps, blendvps, blendpd and blendvpd conditionally copy the values from source operand into the destination operand, depending on the bits of the mask provided by third operand. If a mask bit is set, the corresponding element of source is copied into the same place in destination, otherwise this position is destination is left unchanged. The rules for the first two operands are the same, as for general SSE instructions. blendps and blendpd need third operand to be 8-bit immediate, and they operate on single or double precision values, respectively. blendvps and blendvpd require third operand to be the XMM0 register.

blendvps xmm3,xmm7,xmm0 ; blend according to mask

pblendw conditionally copies word elements from the source operand into the destination, depending on the bits of mask provided by third operand, which needs to be 8-bit immediate value. pblendvb conditionally copies byte elements from the source operands into destination, depending on mask defined by the third operand, which has to be XMM0 register. These instructions follow the same rules for operands as blendps and blendvps instructions, respectively.

insertps inserts a single precision floating point value taken from the position in source operand specified by bits 6-7 of third operand into location in destination register selected by bits 4-5 of third operand. Additionally, the low four bits of third operand control, which elements in destination register will be set to zero. The first two operands follow the same rules as for the general SSE operation, the third operand should be 8-bit immediate.

extractps extracts a single precision floating point value taken from the location in source operand specified by low two bits of third operand, and stores it into the destination operand. The destination can be a 32-bit memory value or general purpose register, the source operand must be SSE register, and the third operand should be 8-bit immediate value.

extractps edx,xmm3,3 ; extract the highest value

pinsrb, pinsrd and pinsrq copy a byte, double word or quad word from the source operand into the location of destination operand determined by the third operand. The destination operand has to be SSE register, the source operand can be a memory location of appropriate size, or the 32-bit general purpose register (but 64-bit general purpose register for pinsrq, which is only available in long mode), and the third operand has to be 8-bit immediate value. These instructions complement the pinsrw instruction operating on SSE register destination, which was introduced by SSE2.

pinsrd xmm4,eax,1 ; insert double word into second position

pextrb, pextrw, pextrd and pextrq copy a byte, word, double word or quad word from the location in source operand specified by third operand, into the destination. The source operand should be SSE register, the third operand should be 8-bit immediate, and the destination operand can be memory location of appropriate size, or the 32-bit general purpose register (but 64-bit general purpose register for pextrq, which is only available in long mode). The pextrw instruction with SSE register as source was already introduced by SSE2, but SSE4 extends it to allow memory operand as destination.

pextrw [ebx],xmm3,7 ; extract highest word into memory

pmovsxbw and pmovzxbw perform sign extension or zero extension of eight byte values from the source operand into packed word values in destination operand, which has to be SSE register. The source can be 64-bit memory or SSE register - when it is register, only its low portion is used. pmovsxbd and pmovzxbd perform sign extension or zero extension of the four byte values from the source operand into packed double word values in destination operand, the source can be 32-bit memory or SSE register. pmovsxbq and pmovzxbq perform sign extension or zero extension of the two byte values from the source operand into packed quad word values in destination operand, the source can be 16-bit memory or SSE register. pmovsxwd and pmovzxwd perform sign extension or zero extension of the four word values from the source operand into packed double words in destination operand, the source can be 64-bit memory or SSE register. pmovsxwq and pmovzxwq perform sign extension or zero extension of the two word values from the source operand into packed quad words in destination operand, the source can be 32-bit memory or SSE register. pmovsxdq and pmovzxdq perform sign extension or zero extension of the two double word values from the source operand into packed quad words in destination operand, the source can be 64-bit memory or SSE register.

pmovzxbq xmm0,word [si]  ; zero-extend bytes to quad words
pmovsxwq xmm0,xmm1       ; sign-extend words to quad words

movntdqa loads double quad word from the source operand to the destination using a non-temporal hint. The destination operand should be SSE register, and the source operand should be 128-bit memory location.

The SSE4.2, described below, adds not only some new operations on SSE registers, but also introduces some completely new instructions operating on general purpose registers only.

pcmpistri compares two zero-ended (implicit length) strings provided in its source and destination operand and generates an index stored to ECX; pcmpistrm performs the same comparison and generates a mask stored to XMM0. pcmpestri compares two strings of explicit lengths, with length provided in EAX for the destination operand and in EDX for the source operand, and generates an index stored to ECX; pcmpestrm performs the same comparision and generates a mask stored to XMM0. The source and destination operand follow the same rules as for general SSE instructions, the third operand should be 8-bit immediate value determining the details of performed operation - refer to Intel documentation for information on those details.

pcmpgtq compares packed quad words, and fills the corresponding elements of destination operand with either ones or zeros, depending on whether the value in destination is greater than the one in source, or not. This instruction follows the same rules for operands as pcmpeqq .

crc32 accumulates a CRC32 value for the source operand starting with initial value provided by destination operand, and stores the result in destination. Unless in long mode, the destination operand should be a 32-bit general purpose register, and the source operand can be a byte, word, or double word register or memory location. In long mode the destination operand can also be a 64-bit general purpose register, and the source operand in such case can be a byte or quad word register or memory location.

crc32 eax,dl          ; accumulate CRC32 on byte value
crc32 eax,word [ebx]  ; accumulate CRC32 on word value
crc32 rax,qword [rbx] ; accumulate CRC32 on quad word value

popcnt calculates the number of bits set in the source operand, which can be 16-bit, 32-bit, or 64-bit general purpose register or memory location, and stores this count in the destination operand, which has to be register of the same size as source operand. The 64-bit variant is available only in long mode.

popcnt ecx,eax        ; count bits set to 1

The SSE4a extension, which also includes the popcnt instruction introduced by SSE4.2, at the same time adds the lzcnt instruction, which follows the same syntax, and calculates the count of leading zero bits in source operand (if the source operand is all zero bits, the total number of bits in source operand is stored in destination).

extrq extract the sequence of bits from the low quad word of SSE register provided as first operand and stores them at the low end of this register, filling the remaining bits in the low quad word with zeros. The position of bit string and its length can either be provided with two 8-bit immediate values as second and third operand, or by SSE register as second operand (and there is no third operand in such case), which should contain position value in bits 8-13 and length of bit string in bits 0-5.

extrq xmm0,8,7        ; extract 8 bits from position 7
extrq xmm0,xmm5       ; extract bits defined by register

insertq writes the sequence of bits from the low quad word of the source operand into specified position in low quad word of the destination operand, leaving the other bits in low quad word of destination intact. The position where bits should be written and the length of bit string can either be provided with two 8-bit immediate values as third and fourth operand, or by the bit fields in source operand (and there are only two operands in such case), which should contain position value in bits 72-77 and length of bit string in bits 64-69.

insertq xmm1,xmm0,4,2 ; insert 4 bits at position 2
insertq xmm1,xmm0     ; insert bits defined by register

movntss and movntsd store single or double precision floating point value from the source SSE register into 32-bit or 64-bit destination memory location respectively, using non-temporal hint.


2.1.21 AVX instructions

The Advanced Vector Extensions introduce instructions that are new variants of SSE instructions, with new scheme of encoding that allows extended syntax having a destination operand separate from all the source operands. It also introduces 256-bit AVX registers, which extend up the old 128-bit SSE registers. Any AVX instruction that puts some result into SSE register, puts zero bits into high portion of the AVX register containing it.

The AVX version of SSE instruction has the mnemonic obtained by prepending SSE instruction name with v . For any SSE arithmetic instruction which had a destination operand also being used as one of the source values, the AVX variant has a new syntax with three operands - the destination and two sources. The destination and first source can be SSE registers, and second source can be SSE register or memory. If the operation is performed on single pair of values, the remaining bits of first source SSE register are copied into the the destination register.

vsubss xmm0,xmm2,xmm3         ; subtract two 32-bit floats
vmulsd xmm0,xmm7,qword [esi]  ; multiply two 64-bit floats

In case of packed operations, each instruction can also operate on the 256-bit data size when the AVX registers are specified instead of SSE registers, and the size of memory operand is also doubled then.

vaddps ymm1,ymm5,yword [esi]  ; eight sums of 32-bit float pairs

The instructions that operate on packed integer types (in particular the ones that earlier had been promoted from MMX to SSE) also acquired the new syntax with three operands, however they are only allowed to operate on 128-bit packed types and thus cannot use the whole AVX registers.

vpavgw xmm3,xmm0,xmm2         ; average of 16-bit integers
vpslld xmm1,xmm0,1            ; shift double words left

If the SSE version of instruction had a syntax with three operands, the third one being an immediate value, the AVX version of such instruction takes four operands, with immediate remaining the last one.

vshufpd ymm0,ymm1,ymm2,10010011b ; shuffle 64-bit floats
vpalignr xmm0,xmm4,xmm2,3        ; extract byte aligned value

The promotion to new syntax according to the rules described above has been applied to all the instructions from SSE extensions up to SSE4, with the exceptions described below.

vdppd instruction has syntax extended to four operans, but it does not have a 256-bit version.

The are a few instructions, namely vsqrtpd, vsqrtps, vrcpps and vrsqrtps , which can operate on 256-bit data size, but retained the syntax with only two operands, because they use data from only one source:

vsqrtpd ymm1,ymm0         ; put square roots into other register

In a similar way vroundpd and vroundps retained the syntax with three operands, the last one being immediate value.

vroundps ymm0,ymm1,0011b  ; round toward zero

Also some of the operations on packed integers kept their two-operand or three-operand syntax while being promoted to AVX version. In such case these instructions follow exactly the same rules for operands as their SSE counterparts (since operations on packed integers do not have 256-bit variants in AVX extension). These include vpcmpestri, vpcmpestrm, vpcmpistri, vpcmpistrm, vphminposuw, vpshufd, vpshufhw, vpshuflw. And there are more instructions that in AVX versions keep exactly the same syntax for operands as the one from SSE, without any additional options: vcomiss, vcomisd, vcvtss2si, vcvtsd2si, vcvttss2si, vcvttsd2si, vextractps, vpextrb, vpextrw, vpextrd, vpextrq, vmovd, vmovq, vmovntdqa, vmaskmovdqu, vpmovmskb, vpmovsxbw, vpmovsxbd, vpmovsxbq, vpmovsxwd, vpmovsxwq, vpmovsxdq, vpmovzxbw, vpmovzxbd, vpmovzxbq, vpmovzxwd, vpmovzxwq and vpmovzxdq .

The move and conversion instructions have mostly been promoted to allow 256-bit size operands in addition to the 128-bit variant with syntax identical to that from SSE version of the same instruction. Each of the vcvtdq2ps, vcvtps2dq and vcvttps2dq, vmovaps, vmovapd, vmovups, vmovupd, vmovdqa, vmovdqu, vlddqu, vmovntps, vmovntpd, vmovntdq, vmovsldup, vmovshdup, vmovmskps and vmovmskpd inherits the 128-bit syntax from SSE without any changes, and also allows a new form with 256-bit operands in place of 128-bit ones.

vmovups [edi],ymm6        ; store unaligned 256-bit data

vmovddup has the identical 128-bit syntax as its SSE version, and it also has a 256-bit version, which stores the duplicates of the lowest quad word from the source operand in the lower half of destination operand, and in the upper half of destination the duplicates of the low quad word from the upper half of source. Both source and destination operands need then to be 256-bit values.

vmovlhps and vmovhlps have only 128-bit versions, and each takes three operands, which all must be SSE registers. vmovlhps copies two single precision values from the low quad word of second source register to the high quad word of destination register, and copies the low quad word of first source register into the low quad word of destination register. vmovhlps copies two single precision values from the high quad word of second source register to the low quad word of destination register, and copies the high quad word of first source register into the high quad word of destination register.

vmovlps, vmovhps, vmovlpd and vmovhpd have only 128-bit versions and their syntax varies depending on whether memory operand is a destination or source. When memory is destination, the syntax is identical to the one of equivalent SSE instruction, and when memory is source, the instruction requires three operands, first two being SSE registers and the third one 64-bit memory. The value put into destination is then the value copied from first source with either low or high quad word replaced with value from second source (the memory operand).

vmovhps [esi],xmm7       ; store upper half to memory
vmovlps xmm0,xmm7,[ebx]  ; low from memory, rest from register

vmovss and vmovsd have syntax identical to their SSE equivalents as long as one of the operands is memory, while the versions that operate purely on registers require three operands (each being SSE register). The value stored in destination is then the value copied from first source with lowest data element replaced with the lowest value from second source.

vmovss xmm3,[edi]        ; low from memory, rest zeroed
vmovss xmm0,xmm1,xmm2    ; one value from xmm2, three from xmm1

vcvtss2sd, vcvtsd2ss, vcvtsi2ss and vcvtsi2d use the three-operand syntax, where destination and first source are always SSE registers, and the second source follows the same rules and the source in syntax of equivalent SSE instruction. The value stored in destination is then the value copied from first source with lowest data element replaced with the result of conversion.

vcvtsi2sd xmm4,xmm4,ecx  ; 32-bit integer to 64-bit float
vcvtsi2ss xmm0,xmm0,rax  ; 64-bit integer to 32-bit float

vcvtdq2pd and vcvtps2pd allow the same syntax as their SSE equivalents, plus the new variants with AVX register as destination and SSE register or 128-bit memory as source. Analogously vcvtpd2dq, vcvttpd2dq and vcvtpd2ps , in addition to variant with syntax identical to SSE version, allow a variant with SSE register as destination and AVX register or 256-bit memory as source.

vinsertps, vpinsrb , vpinsrw, vpinsrd, vpinsrq and vpblendw use a syntax with four operands, where destination and first source have to be SSE registers, and the third and fourth operand follow the same rules as second and third operand in the syntax of equivalent SSE instruction. Value stored in destination is the the value copied from first source with some data elements replaced with values extracted from the second source, analogously to the operation of corresponding SSE instruction.

vpinsrd xmm0,xmm0,eax,3  ; insert double word

vblendvps, vblendvpd and vpblendvb use a new syntax with four register operands: destination, two sources and a mask, where second source can also be a memory operand. vblendvps and vblendvpd have 256-bit variant, where operands are AVX registers or 256-bit memory, as well as 128-bit variant, which has operands being SSE registers or 128-bit memory. vpblendvb has only a 128-bit variant. Value stored in destination is the value copied from the first source with some data elements replaced, according to mask, by values from the second source.

vblendvps ymm3,ymm1,ymm2,ymm7  ; blend according to mask

vptest allows the same syntax as its SSE version and also has a 256-bit version, with both operands doubled in size. There are also two new instructions, vtestps and vtestpd , which perform analogous tests, but only of the sign bits of corresponding single precision or double precision values, and set the ZF and CF accordingly. They follow the same syntax rules as "vptest".

vptest ymm0,yword [ebx]  ; test 256-bit values
vtestpd xmm0,xmm1        ; test sign bits of 64-bit floats

vbroadcastss, vbroadcastsd and vbroadcastf128 are new instructions, which broadcast the data element defined by source operand into all elements of corresponing size in the destination register. vbroadcastss needs source to be 32-bit memory and destination to be either SSE or AVX register. vbroadcastsd requires 64-bit memory as source, and AVX register as destination. vbroadcastf128 requires 128-bit memory as source, and AVX register as destination.

vbroadcastss ymm0,dword [eax]  ; get eight copies of value

vinsertf128 is the new instruction, which takes four operands. The destination and first source have to be AVX registers, second source can be SSE register or 128-bit memory location, and fourth operand should be an immediate value. It stores in destination the value obtained by taking contents of first source and replacing one of its 128-bit units with value of the second source. The lowest bit of fourth operand specifies at which position that replacement is done (either 0 or 1).

vextractf128 is the new instruction with three operands. The destination needs to be SSE register or 128-bit memory location, the source must be AVX register, and the third operand should be an immediate value. It extracts into destination one of the 128-bit units from source. The lowest bit of third operand specifies, which unit is extracted.

vmaskmovps and vmaskmovpd are the new instructions with three operands that selectively store in destination the elements from second source depending on the sign bits of corresponding elements from first source. These instructions can operate on either 128-bit data (SSE registers) or 256-bit data (AVX registers). Either destination or second source has to be a memory location of appropriate size, the two other operands should be registers.

vmaskmovps [edi],xmm0,xmm5  ; conditionally store
vmaskmovpd ymm5,ymm0,[esi]  ; conditionally load

vpermilpd and vpermilps are the new instructions with three operands that permute the values from first source according to the control fields from second source and put the result into destination operand. It allows to use either three SSE registers or three AVX registers as its operands, the second source can be a memory of size equal to the registers used. In alternative form the second source can be immediate value and then the first source can be a memory location of the size equal to destination register.

vperm2f128 is the new instruction with four operands, which selects 128-bit blocks of floating point data from first and second source according to the bit fields from fourth operand, and stores them in destination. Destination and first source need to be AVX registers, second source can be AVX register or 256-bit memory area, and fourth operand should be an immediate value.

vperm2f128 ymm0,ymm6,ymm7,12h  ; permute 128-bit blocks

vzeroall instruction sets all the AVX registers to zero. vzeroupper sets the upper 128-bit portions of all AVX registers to zero, leaving the SSE registers intact. These new instructions take no operands.

vldmxcsr and vstmxcsr are the AVX versions of ldmxcsr and stmxcsr instructions. The rules for their operands remain unchanged.


2.1.22 AVX2 instructions

The AVX2 extension allows all the AVX instructions operating on packed integers to use 256-bit data types, and introduces some new instructions as well.

The AVX instructions that operate on packed integers and had only a 128-bit variants, have been supplemented with 256-bit variants, and thus their syntax rules became analogous to AVX instructions operating on packed floating point types.

vpsubb ymm0,ymm0,[esi]   ; subtract 32 packed bytes
vpavgw ymm3,ymm0,ymm2    ; average of 16-bit integers

However there are some instructions that have not been equipped with the 256-bit variants. vpcmpestri, vpcmpestrm, vpcmpistri, vpcmpistrm, vpextrb, vpextrw, vpextrd, vpextrq, vpinsrb, vpinsrw, vpinsrd, vpinsrq and vphminposuw are not affected by AVX2 and allow only the 128-bit operands.

The packed shift instructions, which allowed the third operand specifying amount to be SSE register or 128-bit memory location, use the same rules for the third operand in their 256-bit variant.

vpsllw ymm2,ymm2,xmm4        ; shift words left
vpsrad ymm0,ymm3,xword [ebx] ; shift double words right

There are also new packed shift instructions with standard three-operand AVX syntax, which shift each element from first source by the amount specified in corresponding element of second source, and store the results in destination. vpsllvd shifts 32-bit elements left, vpsllvq shifts 64-bit elements left, vpsrlvd shifts 32-bit elements right logically, vpsrlvq shifts 64-bit elements right logically and vpsravd shifts 32-bit elements right arithmetically.

The sign-extend and zero-extend instructions, which in AVX versions allowed source operand to be SSE register or a memory of specific size, in the new 256-bit variant need memory of that size doubled or SSE register as source and AVX register as destination.

vpmovzxbq ymm0,dword [esi]   ; bytes to quad words

Also vmovntdqa has been upgraded with 256-bit variant, so it allows to transfer 256-bit value from memory to AVX register, it needs memory address to be aligned to 32 bytes.

vpmaskmovd and vpmaskmovq are the new instructions with syntax identical to vmaskmovps or vmaskmovpd , and they performs analogous operation on packed 32-bit or 64-bit values.

vinserti128, vextracti128, vbroadcasti128 and vperm2i128 are the new instructions with syntax identical to vinsertf128, vextractf128, vbroadcastf128 and vperm2f128 respectively, and they perform analogous operations on 128-bit blocks of integer data.

vbroadcastss and vbroadcastsd instructions have been extended to allow SSE register as a source operand (which in AVX could only be a memory).

vpbroadcastb, vpbroadcastw, vpbroadcastd and vpbroadcastq are the new instructions which broadcast the byte, word, double word or quad word from the source operand into all elements of corresponing size in the destination register. The destination operand can be either SSE or AVX register, and the source operand can be SSE register or memory of size equal to the size of data element.

vpbroadcastb ymm0,byte [ebx]  ; get 32 identical bytes

vpermd and vpermps are new three-operand instructions, which use each 32-bit element from first source as an index of element in second source which is copied into destination at position corresponding to element containing index. The destination and first source have to be AVX registers, and the second source can be AVX register or 256-bit memory.

vpermq and vpermpd are new three-operand instructions, which use 2-bit indexes from the immediate value specified as third operand to determine which element from source store at given position in destination. The destination has to be AVX register, source can be AVX register or 256-bit memory, and the third operand must be 8-bit immediate value.

The family of new instructions performing gather operation have special syntax, as in their memory operand they use addressing mode that is unique to them. The base of address can be a 32-bit or 64-bit general purpose register (the latter only in long mode), and the index (possibly multiplied by scale value, as in standard addressing) is specified by SSE or AVX register. It is possible to use only index without base and any numerical displacement can be added to the address. Each of those instructions takes three operands. First operand is the destination register, second operand is memory addressed with a vector index, and third operand is register containing a mask. The most significant bit of each element of mask determines whether a value will be loaded from memory into corresponding element in destination. The address of each element to load is determined by using the corresponding element from index register in memory operand to calculate final address with given base and displacement. When the index register contains less elements than the destination and mask registers, the higher elements of destination are zeroed. After the value is successfuly loaded, the corresponding element in mask register is set to zero. The destination, index and mask should all be distinct registers, it is not allowed to use the same register in two different roles.

vgatherdps loads single precision floating point values addressed by 32-bit indexes. The destination, index and mask should all be registers of the same type, either SSE or AVX. The data addressed by memory operand is 32-bit in size.

vgatherdps xmm0,[eax+xmm1],xmm3    ; gather four floats
vgatherdps ymm0,[ebx+ymm7*4],ymm3  ; gather eight floats

vgatherqps loads single precision floating point values addressed by 64-bit indexes. The destination and mask should always be SSE registers, while index register can be either SSE or AVX register. The data addressed by memory operand is 32-bit in size.

vgatherqps xmm0,[xmm2],xmm3        ; gather two floats
vgatherqps xmm0,[ymm2+64],xmm3     ; gather four floats

vgatherdpd loads double precision floating point values addressed by 32-bit indexes. The index register should always be SSE register, the destination and mask should be two registers of the same type, either SSE or AVX. The data addressed by memory operand is 64-bit in size.

vgatherdpd xmm0,[ebp+xmm1],xmm3    ; gather two doubles
vgatherdpd ymm0,[xmm3*8],ymm5      ; gather four doubles

vgatherqpd loads double precision floating point values addressed by 64-bit indexes. The destination, index and mask should all be registers of the same type, either SSE or AVX. The data addressed by memory operand is 64-bit in size.

vpgatherdd and vpgatherqd load 32-bit values addressed by either 32-bit or 64-bit indexes. They follow the same rules as vgatherdps and vgatherqps respectively.

vpgatherdq and vpgatherqq load 64-bit values addressed by either 32-bit or 64-bit indexes. They follow the same rules as vgatherdpd and vgatherqpd respectively.


2.1.23 Auxiliary sets of computational instructions

There is a number of additional instruction set extensions related to AVX. They introduce new vector instructions (and sometimes also their SSE equivalents that use classic instruction encoding), and even some new instructions operating on general registers that use the AVX-like encoding allowing the extended syntax with separate destination and source operands. The CPU support for each of these instructions sets needs to be determined separately.

The AES extension provides a specialized set of instructions for the purpose of cryptographic computations defined by Advanced Encryption Standard. Each of these instructions has two versions: the AVX one and the one with SSE-like syntax that uses classic encoding. Refer to the Intel manuals for the details of operation of these instructions.

aesenc and aesenclast perform a single round of AES encryption on data from first source with a round key from second source, and store result in destination. The destination and first source are SSE registers, and the second source can be SSE register or 128-bit memory. The AVX versions of these instructions, vaesenc and vaesenclast , use the syntax with three operands, while the SSE-like version has only two operands, with first operand being both the destination and first source.

aesdec and aesdeclast perform a single round of AES decryption on data from first source with a round key from second source. The syntax rules for them and their AVX versions are the same as for "aesenc".

aesimc performs the InvMixColumns transformation of source operand and store the result in destination. Both aesimc and vaesimc use only two operands, destination being SSE register, and source being SSE register or 128-bit memory location.

aeskeygenassist is a helper instruction for generating the round key. It needs three operands: destination being SSE register, source being SSE register or 128-bit memory, and third operand being 8-bit immediate value. The AVX version of this instruction uses the same syntax.

The CLMUL extension introduces just one instruction, pclmulqdq, and its AVX version as well. This instruction performs a carryless multiplication of two 64-bit values selected from first and second source according to the bit fields in immediate value. The destination and first source are SSE registers, second source is SSE register or 128-bit memory, and immediate value is provided as last operand. vpclmulqdq takes four operands, while pclmulqdq takes only three operands, with the first one serving both the role of destination and first source.

The FMA (Fused Multiply-Add) extension introduces additional AVX instructions which perform multiplication and summation as single operation. Each one takes three operands, first one serving both the role of destination and first source, and the following ones being the second and third source. The mnemonic of FMA instruction is obtained by appending to vf prefix: first either m or nm to select whether result of multiplication should be taken as-is or negated, then either add or sub to select whether third value will be added to the product or subtracted from the product, then either 132, 213 or 231 to select which source operands are multiplied and which one is added or subtracted, and finally the type of data on which the instruction operates, either ps, pd, ss or sd . As it was with SSE instructions promoted to AVX, instructions operating on packed floating point values allow 128-bit or 256-bit syntax, in former all the operands are SSE registers, but the third one can also be a 128-bit memory, in latter the operands are AVX registers and the third one can also be a 256-bit memory. Instructions that compute just one floating point result need operands to be SSE registers, and the third operand can also be a memory, either 32-bit for single precision or 64-bit for double precision.

vfmsub231ps ymm1,ymm2,ymm3     ; multiply and subtract
vfnmadd132sd xmm0,xmm5,[ebx]   ; multiply, negate and add

In addition to the instructions created by the rule described above, there are families of instructions with mnemonics starting with either vfmaddsub or vfmsubadd, followed by either 132, 213 or 231 and then either ps or pd (the operation must always be on packed values in this case). They add to the result of multiplication or subtract from it depending on the position of value in packed data - instructions from the vfmaddsub group add when the position is odd and subtract when the position is even, instructions from the vfmsubadd group add when the position is even and subtstract when the position is odd. The rules for operands are the same as for other FMA instructions.

The FMA4 instructions are similar to FMA, but use syntax with four operands and thus allow destination to be different than all the sources. Their mnemonics are identical to FMA instructions with the 132, 213 or 231 cut out, as having separate destination operand makes such selection of operands superfluous. The multiplication is always performed on values from the first and second source, and then the value from third source is added or subtracted. Either second or third source can be a memory operand, and the rules for the sizes of operands are the same as for FMA instructions.

vfmaddpd ymm0,ymm1,[esi],ymm2  ; multiply and add
vfmsubss xmm0,xmm1,xmm2,[ebx]  ; multiply and subtract

The F16C extension consists of two instructions, vcvtps2ph and vcvtph2ps, which convert floating point values between single precision and half precision (the 16-bit floating point format). vcvtps2ph takes three operands: destination, source, and rounding controls. The third operand is always an immediate, the source is either SSE or AVX register containing single precision values, and the destination is SSE register or memory, the size of memory is 64 bits when the source is SSE register and 128 bits when the source is AVX register. vcvtph2ps takes two operands, the destination that can be SSE or AVX register, and the source that is SSE register or memory with size of the half of destination operand's size.

The AMD XOP extension introduces a number of new vector instructions with encoding and syntax analogous to AVX instructions. vfrczps, vfrczss, vfrczpd and vfrczsd extract fractional portions of single or double precision values, they all take two operands. The packed operations allow either SSE or AVX register as destination, for the other two it has to be SSE register. Source can be register of the same type as destination, or memory of appropriate size (256-bit for destination being AVX register, 128-bit for packed operation with destination being SSE register, 64-bit for operation on a solitary double precision value and 32-bit for operation on a solitary single precision value).

vfrczps ymm0,[esi]           ; load fractional parts

vpcmov copies bits from either first or second source into destination depending on the values of corresponding bits in the fourth operand (the selector). If the bit in selector is set, the corresponding bit from first source is copied into the same position in destination, otherwise the bit from second source is copied. Either second source or selector can be memory location, 128-bit or 256-bit depending on whether SSE registers or AVX registers are specified as the other operands.

vpcmov xmm0,xmm1,xmm2,[ebx]  ; selector in memory
vpcmov ymm0,ymm5,[esi],ymm2  ; source in memory

The family of packed comparison instructions take four operands, the destination and first source being SSE register, second source being SSE register or 128-bit memory and the fourth operand being immediate value defining the type of comparison. The mnemonic or instruction is created by appending to vpcom prefix either b or ub to compare signed or unsigned bytes, w or uw to compare signed or unsigned words, d or ud to compare signed or unsigned double words, q or uq to compare signed or unsigned quad words. The respective values from the first and second source are compared and the corresponding data element in destination is set to either all ones or all zeros depending on the result of comparison. The fourth operand has to specify one of the eight comparison types (table 2.5). All these instructions have also variants with only three operands and the type of comparison encoded within the instruction name by inserting the comparison mnemonic after vpcom .

vpcomb   xmm0,xmm1,xmm2,4    ; test for equal bytes
vpcomgew xmm0,xmm1,[ebx]     ; compare signed words

Table 2.5 XOP comparisons
Code | Mnemonic | Description
0 | lt | less than
1 | le | less than or equal
2 | gt | greater than
3 | ge | greater than or equal
4 | eq | equal
5 | neq | not equal
6 | false | false
7 | true | true

vpermil2ps and vpermil2pd set the elements in destination register to zero or to a value selected from first or second source depending on the corresponding bit fields from the fourth operand (the selector) and the immediate value provided in fifth operand. Refer to the AMD manuals for the detailed explanation of the operation performed by these instructions. Each of the first four operands can be a register, and either second source or selector can be memory location, 128-bit or 256-bit depending on whether SSE registers or AVX registers are used for the other operands.

vpermil2ps ymm0,ymm3,ymm7,ymm2,0  ; permute from two sources

vphaddbw adds pairs of adjacent signed bytes to form 16-bit values and stores them at the same positions in destination. vphaddubw does the same but treats the bytes as unsigned. vphaddbd and vphaddubd sum all bytes (either signed or unsigned) in each four-byte block to 32-bit results, vphaddbq and vphaddubq sum all bytes in each eight-byte block to 64-bit results, vphaddwd and vphadduwd add pairs of words to 32-bit results, vphaddwq and vphadduwq sum all words in each four-word block to 64-bit results, vphadddq and vphaddudq add pairs of double words to 64-bit results. vphsubbw subtracts in each two-byte block the byte at higher position from the one at lower position, and stores the result as a signed 16-bit value at the corresponding position in destination, vphsubwd subtracts in each two-word block the word at higher position from the one at lower position and makes signed 32-bit results, vphsubdq subtract in each block of two double word the one at higher position from the one at lower position and makes signed 64-bit results. Each of these instructions takes two operands, the destination being SSE register, and the source being SSE register or 128-bit memory.

vphadduwq xmm0,xmm1          ; sum quadruplets of words

vpmacsww and vpmacssww multiply the corresponding signed 16-bit values from the first and second source and then add the products to the parallel values from the third source, then vpmacsww takes the lowest 16 bits of the result and vpmacssww saturates the result down to 16-bit value, and they store the final 16-bit results in the destination. vpmacsdd and vpmacssdd perform the analogous operation on 32-bit values. vpmacswd and vpmacsswd do the same calculation only on the low 16-bit values from each 32-bit block and form the 32-bit results. vpmacsdql and vpmacssdql perform such operation on the low 32-bit values from each 64-bit block and form the 64-bit results, while vpmacsdqh and vpmacssdqh do the same on the high 32-bit values from each 64-bit block, also forming the 64-bit results. vpmadcswd and vpmadcsswd multiply the corresponding signed 16-bit value from the first and second source, then sum all the four products and add this sum to each 16-bit element from third source, storing the truncated or saturated result in destination. All these instructions take four operands, the second source can be 128-bit memory or SSE register, all the other operands have to be SSE registers.

vpmacsdd xmm6,xmm1,[ebx],xmm6  ; accumulate product

vpperm selects bytes from first and second source, optionally applies a separate transformation to each of them, and stores them in the destination. The bit fields in fourth operand (the selector) specify for each position in destination what byte from which source is taken and what operation is applied to it before it is stored there. Refer to the AMD manuals for the detailed information about these bit fields. This instruction takes four operands, either second source or selector can be a 128-bit memory (or they can be SSE registers both), all the other operands have to be SSE registers.

vpshlb, vpshlw, vpshld and vpshlq shift logically bytes, words, double words or quad words respectively. The amount of bits to shift by is specified for each element separately by the signed byte placed at the corresponding position in the third operand. The source containing elements to shift is provided as second operand. Either second or third operand can be 128-bit memory (or they can be SSE registers both) and the other operands have to be SSE registers.

vpshld xmm3,xmm1,[ebx]       ; shift bytes from xmm1

vpshab, vpshaw, vpshad and vpshaq arithmetically shift bytes, words, double words or quad words. These instructions follow the same rules as the logical shifts described above. vprotb, vprotw, vprotd and vprotq rotate bytes, word, double words or quad words. They follow the same rules as shifts, but additionally allow third operand to be immediate value, in which case the same amount of rotation is specified for all the elements in source.

vprotb xmm0,[esi],3          ; rotate bytes to the left

The MOVBE extension introduces just one new instruction, movbe , which swaps bytes in value from source before storing it in destination, so can be used to load and store big endian values. It takes two operands, either the destination or source should be a 16-bit, 32-bit or 64-bit memory (the last one being only allowed in long mode), and the other operand should be a general register of the same size.

The BMI extension, consisting of two subsets - BMI1 and BMI2, introduces new instructions operating on general registers, which use the same encoding as AVX instructions and so allow the extended syntax. All these instructions use 32-bit operands, and in long mode they also allow the forms with 64-bit operands.

andn calculates the bitwise AND of second source with the inverted bits of first source and stores the result in destination. The destination and the first source have to be general registers, the second source can be general register or memory.

andn edx,eax,[ebx]   ; bit-multiply inverted eax with memory

bextr extracts from the first source the sequence of bits using an index and length specified by bit fields in the second source operand and stores it into destination. The lowest 8 bits of second source specify the position of bit sequence to extract and the next 8 bits of second source specify the length of sequence. The first source can be a general register or memory, the other two operands have to be general registers.

bextr eax,[esi],ecx  ; extract bit field from memory

blsi extracts the lowest set bit from the source, setting all the other bits in destination to zero. The destination must be a general register, the source can be general register or memory.

blsi rax,r11         ; isolate the lowest set bit

blsmsk sets all the bits in the destination up to the lowest set bit in the source, including this bit. blsr copies all the bits from the source to destination except for the lowest set bit, which is replaced by zero. These instructions follow the same rules for operands as blsi .

tzcnt counts the number of trailing zero bits, that is the zero bits up to the lowest set bit of source value. This instruction is analogous to lzcnt and follows the same rules for operands, so it also has a 16-bit version, unlike the other BMI instructions.

bzhi is BMI2 instruction, which copies the bits from first source to destination, zeroing all the bits up from the position specified by second source. It follows the same rules for operands as bextr .

pext uses a mask in second source operand to select bits from first operands and puts the selected bits as a continuous sequence into destination. pdep performs the reverse operation - it takes sequence of bits from the first source and puts them consecutively at the positions where the bits in second source are set, setting all the other bits in destination to zero. These BMI2 instructions follow the same rules for operands as andn .

mulx is a BMI2 instruction which performs an unsigned multiplication of value from EDX or RDX register (depending on the size of specified operands) by the value from third operand, and stores the low half of result in the second operand, and the high half of result in the first operand, and it does it without affecting the flags. The third operand can be general register or memory, and both the destination operands have to be general registers.

mulx edx,eax,ecx     ; multiply edx by ecx into edx:eax

shlx, shrx and sarx are BMI2 instructions, which perform logical or arithmetical shifts of value from first source by the amount specified by second source, and store the result in destination without affecting the flags. The have the same rules for operands as bzhi instruction.

rorx is a BMI2 instruction which rotates right the value from source operand by the constant amount specified in third operand and stores the result in destination without affecting the flags. The destination operand has to be general register, the source operand can be general register or memory, and the third operand has to be an immediate value.

rorx eax,edx,7       ; rotate without affecting flags

The TBM is an extension designed by AMD to supplement the BMI set. The bextr instruction is extended with a new form, in which second source is a 32-bit immediate value. blsic is a new instruction which performs the same operation as blsi, but with the bits of result reversed. It uses the same rules for operands as blsi. blsfill is a new instruction, which takes the value from source, sets all the bits below the lowest set bit and store the result in destination, it also uses the same rules for operands as blsi .

blci, blcic, blcs, blcmsk and blcfill are instructions analogous to blsi, blsic, blsr, blsmsk and blsfill respectively, but they perform the bit-inverted versions of the same operations. They follow the same rules for operands as the instructions they reflect.

tzmsk finds the lowest set bit in value from source operand, sets all bits below it to 1 and all the rest of bits to zero, then writes the result to destination. t1mskc finds the least significant zero bit in the value from source operand, sets the bits below it to zero and all the other bits to 1, and writes the result to destination. These instructions have the same rules for operands as blsi .


2.1.24 AVX-512 instructions

The AVX-512 introduces 512-bit vector registers, which extend the 256-bit registers used by AVX and AVX2. It also extends the set of vector registers from 16 to 32, with the additional registers zmm16 to zmm31, their low 256-bit portions ymm16 to ymm31 and their low 128-bit portions xmm16 to xmm31 . These additional registers can only be accessed in the long mode.

Table 2.6 New registers available in long mode with AVX-512
Size Registers
128-bit   xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23
 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31
256-bit ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23
 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31
512-bit zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23
 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31

In addition to new operand sizes and registers, the AVX-512 introduces a number of supplementary settings that can be included in the operands of AVX instructions.

The destination operand of the most of AVX instructions can be followed by the name of an opmask register enclosed in braces, this modifier specifies a mask that decides which units of data in the destination operand are going to be updated. The k0 register cannot be used as a destination mask. This setting can be further followed by {z} modifier to choose that the data units not selected by mask should be zeroed instead of leaving them unchanged.

vaddpd zmm1{k1},zmm5,zword [rsi]  ; update selected floats
vaddps ymm6{k1}{z},ymm12,ymm24    ; update selected, zero other ones

When an instruction that operates on packed data has a source operand loaded from a memory, the memory location may be just a single unit of data and the source used for the operation is created by broadcasting this value into all the units within the required size. To specify that such broadcasting method is used the memory operand should be followed by one of the {1to2}, {1to4}, {1to8}, {1to16}, {1to32} and {1to64} modifiers, selecting the appropriate multiply of a unit.

vsubps zmm1,zmm2,dword [rsi] {1to16} ; subtract from all floats

When an instruction does not use a memory operand often an additional operand may follow the source operands, containing the rounding mode specifier. When an instruction has variants that operate on different sizes of data, the rounding mode can be specified only when the register operands are 512-bit.

vdivps zmm2,zmm3,zmm5,{ru-sae}    ; round results up

Table 2.7 AVX-512 rounding modes
Operand | Description
{rn-sae} | round to nearest and suppress all exceptions
{rd-sae} | round down and suppress all exceptions
{ru-sae} | round up and suppress all exceptions
{rz-sae} | round toward zero and suppress all exceptions

Some of the instructions do not use a rounding mode but still allow to specify the exception suppression option with {sae} modifier in the additional operand.

vmaxpd zmm0,zmm1,zmm2,{sae}       ; suppress all exceptions

The family of gather instructions in their AVX-512 variants use a new syntax with only two operands. The opmask register takes the role which was played by the third operand in the AVX2 syntax and it is mandatory in this case.

vgatherdps xmm0{k1},[eax+xmm1]    ; gather four floats
vgatherdpd zmm0{k3},[ymm3*8]      ; gather eight doubles

The new family of scatter instructions perform an operation reverse to the one of gather . They also take two operands, the destination is a memory with vector indexing and opmask modifier, and the source is a vector register.

vscatterdps [eax+xmm1]{k1},xmm0    ; scatter four floats
vscatterdpd [ymm3*8]{k3},zmm0      ; scatter eight doubles


2.1.25 Other extensions of instruction set

There is a number of additional instruction set extensions recognized by flat assembler, and the general syntax of the instructions introduced by those extensions is provided here. For a detailed information on the operations performed by them, check out the manuals from Intel (for the VMX, SMX, XSAVE, RDRAND, FSGSBASE, INVPCID, HLE, RTM, and MPX extensions) or AMD (for the SVM extension).

The Virtual-Machine Extensions (VMX) provide a set of instructions for the management of virtual machines. The vmxon instruction, which enters the VMX operation, requires a single 64-bit memory operand, which should be a physical address of memory region, which the logical processor may use to support VMX operation. The vmxoff instruction, which leaves the VMX operation, has no operands. The vmlaunch and vmresume, which launch or resume the virtual machines, and vmcall , which allows guest software to call the VM monitor, use no operands either.

The vmptrld loads the physical address of current Virtual Machine Control Structure (VMCS) from its memory operand, vmptrst stores the pointer to current VMCS into address specified by its memory operand, and vmclear sets the launch state of the VMCS referenced by its memory operand to clear. These three instruction all require single 64-bit memory operand.

The vmread reads from VCMS a field specified by the source operand and stores it into the destination operand. The source operand should be a general purpose register, and the destination operand can be a register of memory. The vmwrite writes into a VMCS field specified by the destination operand the value provided by source operand. The source operand can be a general purpose register or memory, and the destination operand must be a register. The size of operands for those instructions should be 64-bit when in long mode, and 32-bit otherwise.

The invept and invvpid invalidate the translation lookaside buffers (TLBs) and paging-structure caches, either derived from extended page tables (EPT), or based on the virtual processor identifier (VPID). These instructions require two operands, the first one being the general purpose register specifying the type of invalidation, and the second one being a 128-bit memory operand providing the invalidation descriptor. The first operand should be a 64-bit register when in long mode, and 32-bit register otherwise.

The Safer Mode Extensions (SMX) provide the functionalities available throught the getsec instruction. This instruction takes no operands, and the function that is executed is determined by the contents of EAX register upon executing this instruction.

The Secure Virtual Machine (SVM) is a variant of virtual machine extension used by AMD. The skinit instruction securely reinitializes the processor allowing the startup of trusted software, such as the virtual machine monitor (VMM). This instruction takes a single operand, which must be EAX, and provides a physical address of the secure loader block (SLB).

The vmrun instruction is used to start a guest virtual machine, its only operand should be an accumulator register (AX, EAX or RAX, the last one available only in long mode) providing the physical address of the virtual machine control block (VMCB). The vmsave stores a subset of processor state into VMCB specified by its operand, and vmload loads the same subset of processor state from a specified VMCB. The same operand rules as for the vmrun apply to those two instructions.

vmmcall allows the guest software to call the VMM. This instruction takes no operands.

stgi set the global interrupt flag to 1, and clgi zeroes it. These instructions take no operands.

invlpga invalidates the TLB mapping for a virtual page specified by the first operand (which has to be accumulator register) and address space identifier specified by the second operand (which must be ECX register).

The XSAVE set of instructions allows to save and restore processor state components. xsave and xsaveopt store the components of processor state defined by bit mask in EDX and EAX registers into area defined by memory operand. xrstor restores from the area specified by memory operand the components of processor state defined by mask in EDX and EAX. The xsave64, xsaveopt64 and xrstor64 are 64-bit versions of these instructions, allowed only in long mode.

xgetbv read the contents of 64-bit XCR (extended control register) specified in ECX register into EDX and EAX registers. xsetbv writes the contents of EDX and EAX into the 64-bit XCR specified by ECX register. These instructions have no operands.

The RDRAND extension introduces one new instruction, rdrand , which loads the hardware-generated random value into general register. It takes one operand, which can be 16-bit, 32-bit or 64-bit register (with the last one being allowed only in long mode).

The FSGSBASE extension adds long mode instructions that allow to read and write the segment base registers for FS and GS segments. rdfsbase and rdgsbase read the corresponding segment base registers into operand, while wrfsbase and wrgsbase write the value of operand into those register. All these instructions take one operand, which can be 32-bit or 64-bit general register.

The INVPCID extension adds invpcid instruction, which invalidates mapping in the TLBs and paging caches based on the invalidation type specified in first operand and PCID invalidate descriptor specified in second operand. The first operands should be 32-bit general register when not in long mode, or 64-bit general register when in long mode. The second operand should be 128-bit memory location.

The HLE and RTM extensions provide set of instructions for the transactional management. The xacquire and xrelease are new prefixes that can be used with some of the instructions to start or end lock elision on the memory address specified by prefixed instruction. The xbegin instruction starts the transactional execution, its operand is the address a fallback routine that gets executes in case of transaction abort, specified like the operand for near jump instruction. xend marks the end of transcational execution region, it takes no operands. xabort forces the transaction abort, it takes an 8-bit immediate value as its only operand, this value is passed in the highest bits of EAX to the fallback routine. xtest checks whether there is transactional execution in progress, this instruction takes no operands.

The MPX extension adds instructions that operate on new bounds registers and aid in checking the memory references. For some of these instructions flat assemblers allows a special syntax that allows a fine control over their operation, where an address of a memory operand is separated into two parts with a comma. With bndmk instruction the first part of such address specifies the lower bound and the second one the upper bound. The lower bound can be either zero or a register, the upper bound can be any address that uses no more than one register (multiplied by 1, 2, 4, or 8). The addressing registers need to be 64-bit when in long mode, and 32-bit otherwise.

bndmk bnd0,[rbx,100000h] ; lower bound in register, upper directly
bndmk bnd1,[0,rbx]       ; lower bound zero, upper in register

In case of bndldx and bndstx , the first part of memory operand specifies an address used to access a bound table entry, while the second part is either zero or a register that plays a role of an additional operand for such instruction. The address in the first part may use no more than one register and the register cannot be multiplied by a number other than 1.

bndstx [rcx,rsi],bnd3  ; store bnd3 and rsi at rcx in the bound table
bndldx bnd2,[rcx,rsi]  ; load from bound table if entry matches rsi


2.2 Директивы управления

Этот параграф описывает директивы, которые управляют процессом ассемблирования. Эти директивы выполняются во время ассемблирования и могут делать так, чтобы некоторые блоки инструкций ассемблировались по-разному или не ассемблировались вовсе.


2.2.1 Числовые константы

Директива = позволяет определить числовую константу. Ей должно предшествовать имя константы, а после - числовое выражение, представляющее значение. Значением таких констант может быть число или адрес, но - в отличие от меток - числовые константы не могут содержать адреса на основе регистра. Помимо этой разницы, в базовом варианте числовые константы ведут себя очень похоже на метки, и вы можете даже ссылаться на них вперед (получить доступ к их значениям до того, как они действительно будут определены).

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

dd sum
x = 1
x = x+2
sum = x

Здесь x - это переменная времени сборки, и каждый раз, когда к ней обращаются, используется последнее присвоено ей значение. Таким образом, если бы мы попытались получить доступ к х , прежде чем она будет определена в первый раз, например, если мы написали dd х вместо инструкции dd sum , то это приведет к ошибке. И когда она переопределяется директивой х = x + 2 , предыдущее значениех используется для рассчета новой. Таким образом, когда константа sum будет определена, х имеет значение 3, и это значение присваивается sum . Так как sum определена только один раз в исходнике, то это стандартная числовая константа, и на нее может быть ссылки вперед. Таким образом, dd sum собирается как dd 3. Чтобы узнать больше о том как ассемблер может решить эту проблему, см. раздел 2.2.6.

Значению числовой константы может предшествовать оператор размера, который может гарантировать, что значение поместится в диапазон для указанного размера, а также повлиять на выполнение некоторых вычислений внутри числового выражения. Например:

c8 = byte -1
c32 = dword -1

определяет две разные константы, первая помещается в 8 бит, вторая
помещается в 32 бита.

Когда вам нужно определить константу со значением адреса, которое может быть основано на регистре (и поэтому вы не можете использовать числовую константу для этой цели), вы можете использовать расширенный синтаксис директивы label (уже описанной в разделе 1.2.3), например:

label myaddr at ebp+4

который объявляет метку, размещенную по адресу ebp + 4. Однако помните, что метки, в отличие от числовых констант, не могут стать переменными времени ассемблирования.


2.2.2 Условное ассемблирование

С помощью директивы if можно ассемблировать или не ассемблировать блок инструкций в зависимости от выполнения условия. За ней должно следовать логическое выражение, определяющее условие. Инструкции на следующих строках ассемблируются, только если это условие выполняется, иначе они пропускаются. Опциональная директива else if со следущим за ней логическим выражением, определяющим дополнительное условие, начинает следующий блок инструкций, который ассемблируется, если предыдущие условия не выполняются, а данное дополнительное условие выполняется. Опциональная директива else начинает блок инструкций, которые ассемблируются, если не выполняется ни одно из условий. end if заканчивает последний блок инструкций.

Вы должны помнить, что директива if обрабатывается на стадии ассемблирования и поэтому не влияет на директивы препроцессора, такие как определения символьных констант и макроинструкции - когда ассемблер распознает директиву if, весь препроцессинг уже закончен.

Логическое выражение состоит из логических значений и логических операторов. Логические операторы выглядят так: ~ для логического отрицания, & для логического И, | для логического ИЛИ. Отрицание имеет высший приоритет. Логическое значение может быть числовым выражением, оно будет считаться ложным в случае равенства нулю, иначе оно будет истинным. Для создания логического значения можно сравнить два числовых выражения, используя один из следующих операторов: = (равно), < (меньше), > (больше), <= (меньше или равно), >= (больше или равно), <> (не равно).

used со следующим за ним символом имени, это логическое значение, которое проверяет, использовался ли где-нибудь данный символ (он возвращает правильный результат даже если символ используется только после этой проверки). За оператором defined может следовать любое выражение, обычно это только одно символьное имя, этот оператор проверяет, содержит ли данное выражение исключительно символы, определенные в коде, и доступные из текущей позиции.

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

Следующий простой пример использует константу count которая должна быть определена где-то в коде:

if count>0
    mov cx,count
    rep movsb
end if

Эти две инструкции будут ассемблированы только если константа count больше нуля. Следующий пример показывает более комплексную условную структуру:

if count & ~ count mod 4
    mov cx,count/4
    rep movsd
else if count>4
    mov cx,count/4
    rep movsd
    mov cx,count mod 4
    rep movsb
else
    mov cx,count
    rep movsb
end if

Первый блок инструкций ассемблируется, если константа count не равна нулю и кратна четырем, если это условие не выполняется, оценивается второе логическое условие, следующее за else if, и если оно верно, ассемблируется второй блок инструкций, иначе ассемблируется последний блок, который следует за строкой, содержащей только else.

Также есть операторы, которые позволяют сравнивать значения, которые представляют собой последовательности символов. eq проверяет такие значения на тождественность. Оператор in проверяет, принадлежит ли данное значение к списку значений, следующему за оператором. Список должен быть заключен между символами < и >, а его члены должны быть разделены запятыми. Символы считаются одинаковыми, если они имеют одно и то же значение для ассемблера - например, pword и fword для ассемблера одинаковы поэтому не различаются вышеуказанными операторами. Так же 16 eq 10h является истиной, однако 16 eq 10+4 нет.

Оператор eqtype имеют ли сравниваемые значения одинаковую структуру, и принадлежат ли структурные элементы одному типу. Различаемые типы включают в себя числовые выражения, строки, заключенные в кавычки, значения с плавающей точкой, адресные выражения (выражения в квадратных скобках или предваренные оператором ptr), мнемоники инструкций, регистры, операторы размера, операторы перехода и операторы типа кода. И каждый из специальных символов, действующих как разделители, такой как запятая или двоеточие, это отдельный тип сам по себе. Например, два значения, каждое из которых состоит из имени регистра и числового выражения, разделенных запятой, будут распознаны как один тип, независимо от вида регистра и сложности числового выражения; за исключением строк, заключенных в кавычки и значений с плавающей точкой, которые относятся к специальным видом числовых выражений и распознаются как разные типы. Поэтому условие eax,16 eqtype fs,3+7 является истиной, но eax,16 eqtype eax,1.6 - ложь.


2.2.3 Повторение блоков инструкций

times повторяет одну инструкцию указанное количество раз. За ней должно следовать числовое выражение, определяющее количество повторений, и инструкция, которую нужно повторять (опционально для того, чтобы отделить число и инструкцию, можно использовать двоеточие). Специальный символ %, использующийся внутри инструкции, эквивалентен номеру текущего повтора. Например, times 5 db % определит пять байтов со значениями 1, 2, 3, 4, 5. Поддерживается также рекурсивное использование директивы times, например, times 3 times % db % определит шесть байтов со значениями 1, 1, 2, 1, 2, 3.

repeat повторяет целый блок инструкций. За ней должно следовать числовое выражение, определяющее количество повторений. Инструкции для повторения предполагаются на следующих строках, а заканчиваться блок должен директивой end repeat, например:

repeat 8
    mov byte [bx],%
    inc bx
end repeat

Сгенерированный код сохраняет байты со значениями от одного до восьми в памяти, адресованной регистром bx .

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

break позволяет остановить повторение раньше и продолжить ассемблирование с первой строки после end repeat. В сочетании с директивой if она позволяет остановить повторение при выполнении некоторого особого условия, например:

s = x/2
repeat 100
    if x/s = s
        break
    end if
    s = (s+x/s)/2
end repeat

while повторяет блок инструкций, пока выполняется следующее за ней условие, определенное логическим выражением. Блок инструкций для повторения должен заканчиваться директивой end while. Перед каждым повторением логическое выражение вычисляется и если его значение ложь, ассемблирование продолжается, начиная с первой строки после end while. Также в этом случае символ % содержит номер текущего повторения. Директива break может быть использована для остановки этого типа цикла так же , как с директивой repeat. Предыдущий пример может быть переписан с использованием while вместо repeat таким образом:

s = x/2
while x/s <> s
    s = (s+x/s)/2
    if % = 100
        break
    end if
end while

Блоки, определенные с использованием if, repeat и while могут быть вложены в любом порядке, однако и закрыты в обратном. Директива break всегда останавливает обработку блока, который был начат последним либо директивой repeat, либо while.


2.2.4 Адресные пространства

org устанавливает адрес, по которому следующий за ней код должен появиться в памяти. За ней должно следовать числовое выражение, указывающее адрес. Эта директива начинает новое адресное пространство, следующий код сам по себе никуда не двигается, но все метки, определенные в нем и значение символа $ изменяются как если бы он был бы помещен по этому адресу. Тем не менее обязанность поместить во время выполнения код по правильному адресу лежит на программисте.

load позволяет определить константу двоичным значением, загруженным из уже сассемблированного кода. За директивой должно следовать имя константы, затем опционально оператор размера, затем оператор from и числовое выражение, определяющее валидный адрес в текущем адресном пространстве. Оператор размера здесь имеет необычное значение - он определяет, сколько байтов (до 8) должно быть загружено из двоичного значения константы. Если оператор размера не определен, загружается один байт (таким образом значение оказывается в пределах от 0 до 255). Загруженные данные не могут превосходить текущее смещение.

store может модифицировать уже сгенерированный код заменой некоторых ранее сгенерированных байтов значением, задаваемым следующим за инструкцией числовым выражением. Перед этим выражением может идти оператор размера, определяющий, насколько длинное значение оно задает, то есть сколько будет сохранено байт. Если оператор размера не задан, подразумевается длина в один байт. Далее должен следовать оператор at и числовое выражение, указывающее валидный адрес в текущем адресном пространстве кода. По этому адресу будет сохранено задаваемое значение. Это директива для продвинутого применения и её следует использовать осторожно.

Обе директивы load и store в их базовом варианте (определенном выше) ограничены оперированием только в пределах текущего адресного пространства. Символ $$ всегда равен базовому адресу в текущем адресном пространстве, а символ $ - это адрес текущей позиции в нём, то есть эти два значения определяют границы действия директив load и store .

Сочетая директивы load и store можно делать вещи, такие как шифрование некоторого из уже сгенерированного кода. Например, для шифрования всего кода, сгенерированного в текущем адресном пространстве вы можете использовать такой блок директив:

repeat $-$$
    load a byte from $$+%-1
    store byte a xor c at $$+%-1
end repeat

и каждый байт кола будет проксорен со значением, определенным константой c .

virtual определяет виртуальные данные по указанному адресу. Эти данные не будут включены в файл вывода, но но метки, определенные здесь, могут использоваться в других частях кода. За этой директивой может следовать оператор at и числовое выражение, определяющее адрес виртуальных данных, иначе будет использован текущий адрес, что равносильно директиве virtual at $ . Инструкции определяемых данных должны быть расположены на следующих строках и заканчиваться директивой end virtual . Блок виртуальных инструкций сам по себе независимое адресное пространство, и после того, как оно заканчивается, восстанавливается контекст предыдущего адресного пространства.

Директива virtual может быть использована для создания объединения нескольких переменных, например:

GDTR dp ?
virtual at GDTR
    GDT_limit dw ?
    GDT_address dd ?
end virtual

Здесь определяются две части 48-битной переменной по адресу GDTR .

Директива также может быть использована для определения меток некоторых структур, адресованных регистром, например:

virtual at bx
    LDT_limit dw ?
    LDT_address dd ?
end virtual

С таким определением инструкция mov ax,[LDT_limit] будет сассемблирована в mov ax,[bx] .

Также может быть полезно объявление инструкций и значений данных внутри виртуально блока, так как директиву load можно использовать для загрузки в константы значений из виртуально сгенерированного кода. Эта директива должна быть использована после загружаемого кода, но до окончания виртуального блока, так как она может загружать значения только из того же адресного пространства. Например:

virtual at 0
    xor eax,eax
    and edx,eax
    load zeroq dword from 0
end virtual

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

virtual at 0
    file 'a.txt':10h,1
    load char from 0
end virtual

загружает один байт со смещением 10h из файла a.txt в константу char .

Вместо или в дополнение к аргументу at, после virtual также может следовать ключевое слово as и строка, определяющая расширение дополнительного файла, в котором инициализированное содержимое блока будет сохранено в конце удачная сборка.

virtual at 0 as 'asc'
    times 256 db %-1
end virtual

Все директивы section , описанные в 2.4, также начинают новое адресное пространство.

Можно объявить специальный вид метки, который отмечает текущее адресное пространство, добавив :: - двойное двоеточие вместо одного после имени метки. Этот символ затем нельзя использовать в числовых выражениях, единственное место, где разрешено его использовать, - это расширенный синтаксис директив load и store . Можно сделать так, чтобы эти директивы работали в другом адресном пространстве, чем текущее, указав адрес с двумя компонентами: сначала имя специальной метки, которая отмечает адресное пространство, затем символ двоеточия и числовое выражение, определяющее действительный адрес внутри этого адресного пространства. В следующем примере этот расширенный синтаксис используется для загрузки значения из блока после его закрытия:

virtual at 0
    hex_digits::
    db '0123456789ABCDEF'
end virtual
load a byte from hex_digits:10

Таким образом, можно работать со значениями внутри любого блока кода, включая все те, которые определены как virtual . Однако не разрешено указать адресное пространство, которое еще не было объявлено, так же как не разрешено указывать адрес в текущем адресном пространстве, который превышает текущее смещение. Адреса в любом другом адресном пространстве также ограничены границами блока.

Виртуальная директива может иметь предварительно определенную метку адресного пространства в качестве единственного аргумента. Этот вариант позволяет расширить ранее определенный и закрытый блок с дополнительными данными. Любое определение данных в расширяющемся блоке будет иметь такой же эффект, как если бы это определение присутствовало в исходном блоке virtual .

virtual at 0 as 'log'
    Log::
end virtual

virtual Log
    db 'Hello!',13,10
end virtual


2.2.5 Другие директивы

align выравнивает код или данные по указанной границе. За ней должно следовать числовое выражение, определяющее количество байтов, на кратность которому должен быть выровнен текущий адрес. Значение границы должно быть степенью двойки.

Директива align заполняет байты, которые должны быть пропущены, чтобы совершить выравнивание, инструкциями nop, и в это же время маркирует эту область как неинициализированные данные, то есть если её поместить среди других неинициализированных данных, это не займет места в файле вывода, выравнивание байтов происходит таким же образом. Если вам нужно заполнить область выравнивания какими-то другими значениями, вы можете сочетать align и virtual , чтобы получить требуемый размер выравнивания и далее создать выравнивание самостоятельно, например:

virtual
    align 16
    a = $ - $$
end virtual
db a dup 0

Константа a определяется как разница между адресом после выравнивания и адресом блока virtual (смотрите предыдущий параграф), то есть она равна размеру требуемого пространства выравнивания.

display во время ассемблирования показывает сообщение. За ней должны следовать строка в кавычках или значения байтов, разделенные запятыми. Директива может быть использована для показа значений некоторых констант, например:

bits = 16
display 'Current offset is 0x'
repeat bits/4
    d = '0' + $ shr (bits-%*4) and 0Fh
    if d > '9'
        d = d + 'A'-'9'-1
    end if
    display d
end repeat
display 13,10

Этот блок директив рассчитывает четыре цифры 16-битного значения и конвертирует их в знаки для показа. Помните что это не будет работать, если адреса в текущем адресном пространстве перемещаемы (как это может быть с объектным форматом вывода (OBJ) и форматом PE), так как таким образом могут быть использованы только абсолютные значения. Абсолютное значение может быть получено вычислением относительного адреса, например $-$$ или rva $ в случае формата PE.

Директива err немедленно завершает процесс ассемблирования, когда она встречается ассемблеру.

Директива assert проверяет, является ли логическое выражение, которое следует за ним, истинным, и если нет, оно сигнализирует об ошибке.


2.2.6 Множественные проходы

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

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

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

Если в коде встречается директива display , фактически отображаются только сообщения, созданные в последний совершённый проход. В случае, если ассемблер остановился из-за ошибки, эти сообщения могут отражать спрогнозированные значения, которые еще не разрешены правильно.

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

if ~ defined alpha
    alpha:
end if

Если оператор defined выдает значение истина, если выражение, следующее за ним, в этом месте может быть вычислено, что в даном случае означает, что метка alpha где-то определена. Но блок выше определяет эту метку только, если значение, данное оператором defined - ложь, что ведет к противоречию и делает невозможным разрешить такой код. Если, обрабатывая директиву if ассемблер должен прогнозировать, будет ли где-нибудь определена метка alpha (этого делать не приходится только если метка уже определена раньше), то какой бы ни был прогноз, всегда присходит противоположное. Поэтому ассемблирование остановится,если только метка alpha не определена где-то в коде перед вышеукзанным блоком - в этом случае, как уже было отмечено прогнозирование не требуется и блок просто будет пропущен.

Предыдущий пример может быть создан как попытка определить ментку, только если этого все ещё небыло сделано. Эти строки неправильны, поскольку операто defined проверяет определена ли метка где-либо вообще, и это включает определение внутри этого условного блока. Однако есть способ бойти эту проблему:

if ~ defined alpha | defined @f
    alpha:
    @@:
end if

@f всегда является той же меткой, что и ближайший символ @@ в источнике, следующий за ним, поэтому приведенный выше пример будет означать то же самое, если вместо анонимной метки будет использоваться любое уникальное имя. Если метка alpha еще не определена в источнике, единственным возможным решением является, когда этот блок определяется, и на этот раз это не приводит к антиномии, из-за анонимной метки, которая делает этот блок самоустанавливающимся. Чтобы лучше понять это, посмотрите на блок, который не имеет ничего больше, чем эту самоустановку:

if defined @f
    @@:
end if

Этот пример может иметь более одного решения, поскольку оба случая, когда этот блок кода обрабатывается или нет, одинаково верны. Какое из этих двух решений мы получим, зависит от алгоритма на ассемблере, в случае с Fasm - от алгоритма предсказаний. Возвращаясь к предыдущему примеру, когда alpha не определена где-либо еще, условие для блока if не может быть ложным, поэтому у нас остается только одно возможное решение, и мы можем надеяться, что ассемблер придет к нему. С другой стороны, когда alpha определена в каком-то другом месте, у нас снова есть два возможных решения, но одно из них приводит к тому, что alpha определяется дважды, и такая ошибка приводит к тому, что ассемблер немедленно прерывает сборку, поскольку эта ошибка глубоко мешает процессу разрешения кода. Таким образом, мы можем получить такой источник либо правильно разрешенным, либо вызывающим ошибку, и то, что мы получаем, может зависеть от внутреннего выбора, сделанного ассемблером.

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

used оператор, как можно ожидать, будет вести себя подобным образом в аналогичных ситиуациях, однако любые другие виды предсказаний могут не быть настолько простыми, и Вы никогда не должны полагаться на них таким образом.

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

assert директива сигнализирует об ошибках, только если выражение имеет значение false после того, как все символы были решены. Вы можете использовать assert 0 вместо err , если вы не хотите, чтобы сборка была остановлена во время промежуточных проходов.


2.3 Директивы препроцессора

Все директивы препроцессора выполняются перед основным ассемблированием, и таким образом директивы управления на них никак не влияют. В это время также удаляются все комментарии.


2.3.1 Включение файлов-исходников

include включает указанный файл-исходник туда, где эта директива используется. За ней должно следовать в кавычках имя файла, который должен быть включен, например:

include 'macros.inc'

Весь включенный файл обрабатывается препроцессором перед обработкой строк, следущих за содержащей директиву include . Нет предела для количества включаемых файлов, пока они умещаются в память.

Заключенный в кавычки путь может содержать переменные окружения, заключенные в знаки %, они будут заменены на их значения внутри пути. Знаки \ и / трактуются как разделители пути. Файл сначала ищется в каталоге, содержащем файл, который его включил, и когда он там не найден, поиск продолжается в каталогах, указанных в переменной среды INCLUDE (там можно определить несколько путей, разделенных точками с запятой, они будут искаться в том же порядке, что и указано). Если файл не найден ни в одном из этих мест, препроцессор ищет его в каталоге, содержащем основной исходный файл (указанный в командной строке). Эти правила касаются также путей, заданных директивой file .


2.3.2 Символьные константы

Символьные константы отличаются от числовых констант тем, что перед процессом ассемблирования они заменяются на их значения во всех строках кода, следующих за их определением, и их значением может стать все что угодно.

Определение символьных констант состоит из имени константы, за которой следует директива equ . Все, что следует за этой дирекивой, станет значением константы. Если значение символьной константы содержит другие символьные константы, они заменяются на их значения перед присвоением значения новой константе. Например:

d equ dword
NULL equ d 0
d equ edx

После этих трех определений значение NULL будет dword 0, а значение d будет edx. Так, например, push NULL будет сассемблировано как push dword 0, а push d как push edx . А, например, в такой строке:

d equ d,eax

константе d будет присвоено новое значение edx,eax . Таким образом могут определяться растущие списки символов.

restore позволяет присвоить назад предыдущее значение переопределенной константы. За ней должно следовать одно или больше имен символьных констант, разделенных запятыми. Так, restore d после предыдущего переопределения вернет константе значение edx, следующее применение этой директивы вернет ей значение dword, а ещё одно применение восстановит первоначальное значение, как будто такая константа не опредеяась. Если константа с заданным именем не определена, то restore не вызовет ошибку, а будет просто проигнорирована.

Символьные константы могут использоваться для адаптации синтаксиса ассемблера к персональным предпочтениям. Например, следующие определения создают удобные ярлыки для всех операторов размера:

b equ byte
w equ word
d equ dword
p equ pword
f equ fword
q equ qword
t equ tword
x equ dqword

Так как символьная константа может так же иметь пустое значение, она может использоваться для того, чтобы допустить синтаксис со словом offset перед каким-нибудь значением адреса:

offset equ

После такого определения mov ax, offset char будет правильной конструкцией, которая будет копировать смещение переменной char в регистр ax,так как offset заменяется пустым значением, и поэтому игнорируется.

Директива define, за которой следует имя константы, а затем значение, является альтернативным способом определения символьной константы. Единственное различие между define и equ заключается в том, что define присваивает значение как оно есть, оно не заменяет символьные константы их значениями внутри него.

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

Директива fix может использоваться дл адаптирования директив препроцессора, что нельзя сделать директивой equ . Например:

incl fix include

определяет короткое имя для директивы include, тогда как такое же определение директивой equ не даст такого результата, так как стандартные символьные константы заменяются на из значения после поиска строк с директивами препроцессора.


2.3.3 Макроинструкции

macro позволяет вам определить собственный комплекс инструкций, называемых макроинструкциями. Их использование может существенно упростить процесс программирования. В своей простейшей форме директива похожа на описание символьной константы. Например, следующая строка определяет ярлык для инструкции test al,0xFF :

macro tst {test al,0xFF}

После директивы macro должно идти имя макроинструкции и далее её содержание, заключенное между знаками { и }. Вы можете использовать инструкцию tst в любом месте после её определения и она будет ассемблирована как test al,0xFF . Определение символьной константы с таким значением даст похожий результат, различие лишь в том, что имя макроинструкции будет распознаваться только как мнемоник инструкции. Также, макроинструкции заменяются соответствующим кодом даже перед заменой символьных констант на их значения. То есть, если вы вы определите макроинструкцию и символьную константу под одним и тем же именем и используете это имя как мнемоник инструкции, оно будет заменено на содержание макроинструкции, но если вы используете его внутри операндов, имя будет заменено на значение символьной константы.

Определение макроинструкции может сотоять из нескольких строк, потому что знаки { и } не обязательно должны находиться на одной строке директивой macro . Например:

macro stos0
 {
    xor al,al
    stosb
 }

Макроинструкция stos0 будет заменена на эти две инструкции ассемблера, где бы он не использовался.

Как и инструкции, которым требуются несколько операндов, для макроинструкции можно задать требование нескольких аргументов, разделяя их запятыми. Имена этих аргументов должны следовать за именем макроинструкции на строке с директивой macro . В любом месте в макроинструкции, где эти имена появятся, они будут заменены соответствующими значениями, указанными там, где макроинструкция используется. Вот пример макроинструкции, которая делает выравнивание данных для двоичного формата вывода:

macro align value { rb (value-1)-($+value-1) mod value }

Когда инструкция align 4 встречается после этого задания макроинструкции, она заменяется на его содержание, и здесь value станет 4, а результат будет rb (4-1)-($+4-1) mod 4 .

Если в определении макроинструкции встречается её же имя, то используется предыдущее значение этого имени. Таким образом могут быть сделаны полезные переопределения макросинструкций, например:

macro mov op1,op2
 {
  if op1 in <ds,es,fs,gs,ss> & op2 in <cs,ds,es,fs,gs,ss>
    push  op2
    pop   op1
  else
    mov   op1,op2
  end if
 }

Эта макроинструкция расширяет синтаксис инструкции mov, позволяя обоим операндам быть сегментными регистрами. Например, mov ds,es будет ассемблировано как push es и pop ds. Во всех других случаях будет использована стандартная инструкция mov . Синтаксис этого mov может быть расширен далее определением следующей макроинструкции с таким именем, который будет использовать предыдущий:

macro mov op1,op2,op3
 {
  if op3 eq
    mov   op1,op2
  else
    mov   op1,op2
    mov   op2,op3
  end if
 }

Это позволяет инструкции mov иметь три операнда, но она так же все ещё может иметь два операнда, так как если макроинструкции задается меньше аргументов, чем ему требуется, оставшиеся заполняются пустыми значениями. Если заданы три операнда, то макроинструкция превратится в две ранее определенных, то есть mov es,ds,dx будет ассемблировано как push ds, pop es и mov ds,dx .

Поставив * после имени аргумента, можно отметить аргумент как обязательный - препроцессор не позволит ему иметь пустое значение. Например вышеуказанная макроинструкция может быть объявлена как macro mov op1*,op2*,op3 убедитесь, что первые два аргумента всегда должны быть заданы непустыми значениями.

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

Если требуется создать макроинструкцию с аргументом, который содержит запятые, этот аргумент следует заключить между < и >. Если он содержит больше одного знака <, то для окончания его описания должно быть использовано такое же количество > .

Когда за именем последнего аргумента макроинструкции следует символ & , этим аргументом становится все что написано до конца строки, в том числе запятые.

purge позволяет отменить последнее определение указанной макроинструкции. За директивой должно следовать одно или больше имен макроинструкций, разделенных запятыми. Если указанная макроинструкция не определена, это не вызовет ошибку. Например, после расширения синтаксиса mov вышеуказанными макроинструкциями вы можете отключить синтаксис с тремя операндами, используя директиву purge mov. Следующее purge mov отключит синтаксис для сегментных регистров, а дальнейшее применение этой директивы не возымеет эффекта.

Если после директивы macro вы заключаете некоторую группу аргументов в квадратные скобки, это позволит при использовании макроинструкции задать данной группе аргументов больше значений. Любой следующий аргумент данный после последнего аргумента данной группы начнет новую группу и станет её первым членом. Поэтому после закрытия квадратных скобок не должно быть имен аргументов. Содержание макроинструкции будет обрабатываться для каждой такой группы аргументов отдельно. Простейший пример - это заключение одного имени аргумента в квадратные скобки:

macro stoschar [char]
 {
    mov al,char
    stosb
 }

Эта макроинструкция допускает неограниченное число аргументов, и каждый будет обработан этими двумя инструкциями отдельно. Например, stoschar 1,2,3 будет ассемблирован как следующие инструкции:

mov al,1
stosb
mov al,2
stosb
mov al,3
stosb

Существуют некоторые специальные директивы, возможные только внутри определений макроинструкций. Директива local задает локальные имена, которые будут заменены уникальными значениями каждый раз, когда используется макроинструкция. За ней должны следовать имена, разделенные запятыми. Если имя, указанное в качестве параметра для локальной директивы, начинается с точки или двух точек, уникальные метки, генерируемые при каждом вызове макроинструкции, будут иметь те же свойства. Эта директива обычно требуется для внутренних констант или меток макроинструкции. Например:

macro movstr
 {
    local move
  move:
    lodsb
    stosb
    test al,al
    jnz move
 }

Каждый раз, когда используется эта макроинструкция, move заменяется новым уникальным именем. То есть вы не получите ошибку, это обычный случай, когда метка определяется больше, чем один раз.

forward, reverse и common делят макроинструкцию на блоки, каждый из которых обрабатывается после окончания обработки предыдущего. Они различаются в поведении только если макроинструкция поддерживает много групп аргументов. Блок инструкций, следующий за forward будет обрабатываться для каждой группы аргументов от первой до последней, как блок по умолчанию (без этих директив). Блок, идущий за reverse будет обрабатываться для каждой группы аргументов в обратном порядке - от последней до первой. Блок за директивой common обрабатывается лишь один раз, просто для всех групп аргументов. Локальное имя, определенное в одном блоке, доступно во всех следующих блоках при обработке той же группы аргументов. Если оно было определено в блоке common , оно доступно во всех следующих блоках, независимо от обрабатываемой группы.

Вот пример макроинструкции, которая создает таблицу адресов строк и следующих за ними строк.

macro strtbl name,[string]
 {
  common
    label name dword
  forward
    local label
    dd label
  forward
    label db string,0
 }

Первый аргумент, задаваемый этой макроинструкции, станет меткой для таблицы адресов, следующими аргументами должны быть строки. Первый блок обрабатывается однажды и определяет метку, второй блок назначает локальную метку для каждой строки и определяет запись в таблице, содержащий адрес этой строки. Третий блок определяет данные каждой строки с соответствующей меткой.

Первая инструкция, следующая за директивой, начинающей блок в макроинструкции, может идти с ней на той же строке, как на следующем примере:

macro stdcall proc,[arg]
 {
  reverse push arg
  common call proc
 }

Это макрос может применяться для вызова процедур, используя соглашение STDCALL, аргументы сохраняются в стеке в обратном порядке. Например, stdcall foo,1,2,3 будет ассемблировано так:

push 3
push 2
push 1
call foo

Если некоторое имя внутри макроинструкции имеет несколько значений (это либо один из аргументов, заключенных в квадратные скобки, либо локальное имя, определенное в блоке, следующем за директивой forward или reverse) и используется в блоке, следующем за директивой common, оно будет заменено на все значения, разделенные запятыми. Например, следующий макрос передать все дополнительные аргументы ранее определенной макроинструкции stdcall :

macro invoke proc,[arg]
 { common stdcall [proc],arg }

Он может применяться для непрямого вызова (через указатель в памяти) процедуры, используя соглашение STDCALL.

Внутри макроинструкции также может быть использован специальный оператор #. Этот оператор соединяет два имени в одно. Это может быть полезно, так как делается после того, как аргументы и локальные имена заменяются на свои значения. Следующая макроинструкция генерирует условный переход в зависимости от аргумента cond :

macro jif op1,cond,op2,label
 {
    cmp op1,op2
    j#cond label
 }

Например, jif ax,ae,10h,exit будет ассемблировано как инструкции cmp ax,10h и jae exit .

Оператор # может также использоваться для объединения двух строк, заключенных в кавычки. Возможно преобразование имени в строку в кавычках с помощью оператора `, который также может быть использован внутри макроинструкции. Он конвертирует следующее за ним имя в строку, заключенную в скобки, но имейте в виду, что если за ним следует аргумент, который заменяется на значение, содержащее больше, чем один символ, будет преобразован только первый из них, так как оператор ` конвертирует только символ, идущий непосредственно за ним. Здесь пример использования этих двух свойств:

macro label name
 {
    label name
    if ~ used name
      display `name # " is defined but not used.",13,10
    end if
 }

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

Чтобы создать макроинструкцию, ведущую себя по-разному в зависимости от типа аргументов, например если это строки в кавычках, вы можете использовать оператор сравнения eqtype . Вот пример его использования для отделения строки в кавычках от других типов аргументов:

macro message arg
 {
  if arg eqtype ""
    local str
    jmp   @f
    str   db arg,0Dh,0Ah,24h
    @@:
    mov   dx,str
  else
    mov   dx,arg
  end if
    mov   ah,9
    int   21h
 }

Вышеописанный макрос создан для показа сообщений в программах DOS. Если аргумент этого макроса некоторое число, метка или переменная, показывается строка из этого адреса, но если аргумент - это строка в кавычках, то созданный код покажет эту строку, за которой следует возврат каретки и перевод строки.

Также возможно объявить макроинструкцию внутри другой макроинструкции, то есть один макрос может определить другой, но с такими определениями есть проблема, вызванная тем, что знак } не может появляться внутри макроинструкции, он всегда означает конец его определения. Чтобы обойти эту проблему, можно избавиться от мешающих символов. Это делается путем подстановки одного или больше обратных слэшей \ перед любыми другими символами (даже специальными знаками). Препроцессор видит эту последовательность как один символ, но каждый раз, когда он видит такой символ во время обработки макроса, он обрезает обратные слэши с его начала. Например, \{ трактуется как один символ, но во время обработки макроса он станет символом { . Это позволит вам определить одну макроинструкцию внутри другой:

macro ext instr
 {
  macro instr op1,op2,op3
  \{
    if op3 eq
      instr op1,op2
    else
      instr op1,op2
      instr op2,op3
    end if
   \}
 }

ext add
ext sub

Макрос ext определен корректно, но когда он используется, символы \{ и \} становятся { и }. То есть когда обрабатывается ext add, содержание макроса становится действительным определением макроинструкции, и таким образом определяется макрос add. Так же ext sub определяет макрос sub. Использование символа \ не было здесь действительно необходимо, но сделано таким образом для того, чтобы определение было боее ясным.

Если некоторые директивы, специфические для макроинструкций, такие как local или common , требуются в некотором макросе, включенном таким образом, то их можно избежать таким же путем. Исключение символа больше чем одним обратным слэшем так же поддерживается, это позволяет допустить моножественные уровни вложения определений макросов.

Другая техника определения макроинструкций внутри других состоит в использовании директивы fix , которая становится полезной, когда некоторый макрос только начинает определение другого, без его закрытия. Например:

macro tmacro params
 {
  macro params {
 }

MACRO fix tmacro
ENDM fix }

определяет альтернативный синтаксис определения макросов, который выглядит как:

MACRO stoschar char
    mov al,char
    stosb
ENDM

Имейте в виду, что таким образом заданное определение должно быть создано с применением директивы fix, так как перед тем, как процессор ищет знак } во время определения макроса, обрабатываются только символьные константы высшего приоритета! Может возникнуть проблема, если требуется выполнить некоторые дополнительные задания в конце такого определения, но есть еще одно свойство, которое в таких случаях поможет вам. А именно возможно поместить любую директиву, инструкцию или макроинструкцию сразу после символа } , который заканчивает макроинструкцию и она будет обработана так же, как если бы была на следующей строке.

Директива postpone может использоваться для определения специального типа макроинструкции, которая не имеет имени или аргументов и будет автоматически вызываться, когда препроцессор достигает конца источника:

postpone
 {
  code_size = $
 }

Это очень упрощенный вид макроинструкции, и он просто делегирует блок инструкций, которые нужно поставить в конце.


2.3.4 Структуры

struc - это специальный вариант директивы macro, который используется для определения структур данных. Макроинструкции, определенные директивой struc, когда используются, должны предваряться меткой (как директивы определения данных). Эта метка будет также присоединена к началу каждого имени, начинающегося с точки, в содержании макроинструкции. Макроинструкция, определенная с использованием директивы struc, может иметь такое же имя, как макросы, определенные с использованием директивы macro. Структурная макроинструкция не будет мешать обычному макросу, выполняющемуся без метки перед ним и наоборот. Все правила и свойства, касающиеся стандартных макросов, применимы к структурным макроинструкциям.

Вот пример структуры:

struc point x,y
 {
    .x dw x
    .y dw y
 }

Например my point 7,11 определит структуру, помеченную my, содержащую две переменные: my.x со значением 7 и my.y со значением 11.

Если где-то в определении структуры находится имя, состоящие из одной лишь точки, оно заменяется на имя метки для данного примера структуры и эта метка таким образом не будет определена автоматически, позволяя полностью задать определение. Следующий пример использует это свойство, чтобы расширить определение директивы db с возможностью вычисления размера определяемых данных:

struc db [data]
 {
   common
    . db data
    .size = $ - .
 }

Таким образом строка msg db 'Hello!',13,10 определит так же константу msg.size, арвную размеру определяемых данных в байтах.

Определение структур данных, адресованных регистрами или абсолютными значениями может быть сделано структурными макроинструкциями с использованием директивы virtual (смотрите 2.2.4).

restruc удаляет последнее определение структуры, так же как purge делает с макросами и restore с символьными константами. Директива имеет тот же синтаксис - за ней должно следовать одно или несколько имен структурных макросов, разделенных запятыми.


2.3.5 Повторение макроинструкций

Директива rept - это специальный вид макроинструкций, который делает заданное число дубликатов блока, заключенного в фигурные скобки. Простой синтаксис - число, следующее за rept, и блок кода, заключенный между знаками { и } . Простейший пример:

rept 5 { in al,dx }

создает пять дубликатов строки in al,dx . Блок инструкций определяется таким же образом, как для стандартных макросов, и допускаются все специальные операторы и директивы, которые могут использоваться только внутри макроинструкций. Если заданное число равно нулю, блок просто пропускается, как если бы вы определили макрос, но не использовали его. За количеством повторений может следовать имя символа-счетчика, который символьно будет заменяться на номер текущего повторения. Таким образом:

rept 3 counter
 {
    byte#counter db counter
 }

Сгенерирует строки:

byte1 db 1
byte2 db 2
byte3 db 3

Механизм повторения, применяемый к блокам rept такой же, как тот, что используется для обработки множественных групп аргументов макросов, то есть директивы, такие как forward, common и reverse могут использоваться их обычном значении.

Таким образом такой макрос:

rept 7 num { reverse display `num }

покажет символы от 7 до 1 как текст. Директива local работает так же, как внутри макросов с несколькими группами аргументов, то есть:

rept 21
 {
   local label
   label: loop label
 }

сгенерирует уникальную метку для каждого дубликата.

Символ-счетчик обычно начинает с 1, но вы можете объявить другое базовое значение, предваренное запятой, сразу же после имени счетчика. Например:

rept 8 n:0 { pxor xmm#n,xmm#n }

сгенерирует код, очищающий содержимое регистров SSE. Вы можете определить несколько счетчиков, разделенных запятыми, и каждый может иметь свою базу.

Количество повторений и базовые значения для счетчиков могут быть указаны с помощью числовых выражений с правилами операторов такими, как в самом ассемблер. Однако каждое значение, используемое в таком выражении, должно быть либо непосредственным значением, либо символической константой, значение которой также является выражением, которое может быть вычислено препроцессором (в этом случае сначала вычисляется значение выражения, связанного с символической константой, а затем подставляется во внешнее выражение вместо этой константы). Если вам нужны повторения, основанные на значениях, которые могут быть вычислены только во время сборки, используйте одну из директив повторения кода, которые обрабатываются ассемблером, см. Раздел 2.2.3.

irp итерирует один аргумент через данный список параметров. Синтаксис такой: за irp следует имя аргумента, далее запятая и далее список параметров. Параметры определяются таким же образом, как в вызове стандартного макроса, то есть они должны разделяться запятыми и каждый может быть заключен между знаками < и >. Так же за именем аргумента может следовать * для обозначения того, что он не может иметь пустое значение. Такой блок:

irp value, 2,3,5
 { db value }

сгенерирует строки:

db 2
db 3
db 5

irps итерирует через данный список символов, за директивой должно следовать имя аргумента, далее запятая и далее последовательность любых символов. Каждый символ в последовательности, независимо от того, символы ли это имен, знаки символов или строки в кавычках, становится значением аргумента на одну итерацию. Если за запятой никаких символов не следует, то итераций не производится вообще. Этот пример:

irps reg, al bx ecx
 { xor reg,reg }

сгенерирует строки:

xor al,al
xor bx,bx
xor ecx,ecx

Директива irpv выполняет итерацию по всем значениям, присвоенным данной символьной переменной. За ним следует имя аргумента и имя символьной переменной, разделенные запятой. Когда символьная переменная обрабатывается директивой restore для удаления ее последнего значения, это значение удаляется из списка значений, к которым обращается irpv. Но любые изменения, внесенные в этот список во время итераций, выполняемых irpv (путем определения нового значения для символьной переменной или уничтожения значения с помощью директивы restore), не влияют на операцию, выполняемую этой директивой - список, который повторяется, отражает состояние символьной переменной в то время, когда была обнаружена директива irpv. Например, этот фрагмент восстанавливает символьную переменную с именем d в исходное состояние до присвоения ей каких-либо значений:

irpv value, d
 { restore d }

Он просто генерирует столько копий директивы restore , сколько значений нужно удалить.

Блоки, определенные директивами irp, irps и irpv , также обрабатываются так же, как и любые макроструктуры, поэтому операторы и директивы, специфичные для макросов, также могут свободно использоваться.


2.3.6 Условный препроцессинг

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

Есть несколько правил для построения выражения для сравнения, первое - это любые символьные знаки и строки в кавычках должны соответствовать абсолютно точно. В этом примере:

match +,+ { include 'first.inc' }
match +,- { include 'second.inc' }

Первый файл будет включен, так как + после запятой соответствует + в образце, а второй файл не будет включен, так как совпадения нет.

Чтобы соответствовать любому другому символу буквально, он должен предваряться знаком = в образце. Также чтобы привести в соответствие сам знак =, или запятую должны использоваться конструкции == и =, . Например, образец =a== будет соответствовать последовательности a= .

Если в образце стоит некоторый символ имени, он соответствует любой последовательности, содержащей по крайней мере один символ и его имя заменяется на поставленную в соответствие последовательность везде в следующем блоке, аналогично параметрам в макроинструкции. Например:

match a-b, 0-7
 { dw a,b-a }

сгенерирует инструкцию dw 0, 7-0 . Каждое имя всегда ставится в соответствие как можно меньшему количеству символов, оставляя оставшиеся, то есть:

match a b, 1+2+3 { db a }

имя a будет соответствовать символу 1, оставляя последоватеьность +2+3 в соответствие с b . Но, таким образом:

match a b, 1 { db a }

для b ничего не остается, и блок вообще не будет обработан.

Блок кода, определенный директивой match обрабатывается так же, как любая макроинструкция, поэтому здесь могут использоваться любые операторы, специфичные для макроинструкций.

Что делает директиву match очень полезной, так это тот факт, что она заменяет символьные константы на их значения в поставленной в соответствие последовательности символов (то есть везде после запятой до начала блока кода) перед началом сопоставления. Благодаря этому директива может использоваться, например, для обработки некоторого блока кода в зависимости от выполнения условия, что данная символьная константа имеет нужное значение, например:

match =TRUE, DEBUG { include 'debug.inc' }

здесь файл будет включен, только если символьная константа DEBUG определена со значением TRUE .


2.3.7 Порядок обработки

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

V fix {
  macro empty
   V
V fix }
   V

делает допустимое определение пустого макроса. Можно сказать, что директива fix и приоритетные константы обрабатываются на отдельной стадии, и весь остальной препроцессинг делается на результирующем коде.

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

Продемонстрируем это на примере. Пусть foo - это макрос, а bar - это структура. Эти строки:

foo equ
foo bar

обе будут интерпретированы как вызовы макроса foo , так как значение первого символа берет верх над значением второго.

Макроинструкции генерируют новые строки от их блоков определения, заменяя параметры на их значения и далее обрабатывая операторы # и ` . Оператор конверсии имеет высший приоритет, чем оператор сцепления. После завершения этого, заново сгенерированная строка проходит через стандартный препроцессинг, как описано выше.

Хотя обычно символьные константы заменяются исключительно в строках, нет ни директив препроцессора, ни макроинструкций, встречается несколько особых ситуаций, где замены проводятся в частях строк, содержащих директивы. Первая - это определение символьной константы, где замены производятся везде после слова equ и результирующее значение присваивается новой константе (смотрите 2.3.2). Вторая такая ситуация - это директива match , где замены производятся в символах, следующих за запятой перед сопоставлением их с образцом. Эти свойства могут использоваться, например, для сохранения списков, как, например, эта совокупность определений:

list equ

macro append item
 {
   match any, list { list equ list,item }
   match , list { list equ item }
 }

Здесь константа list инициализируется с пустым значением, и макрос append может использоваться для добавления новых пунктов к списку, разделяя их запятыми. Первое сопоставление в этом макросе происходит, только если значение списка непустое (смотрите 2.3.6), таким образом новое его значение - это предыдущее с запятой и новым пунктом, добавленным в конец. Второе сопоставление происходит, только если список все еще пуст, и таким образом список определяется как содержащий только лишь новый пункт. Так, начиная с пустого списка, append 1 определит list equ 1, а append 2, следующий за ним, определит list equ 1,2. Может потребоваься использовать этот список как параметры к некоторому макросу. Но это нельзя сделать прямо - если foo это макрос, то в строке foo list символ list просто прошел бы как параметр к макросу, поскольку символьные константы на этой стадии ещё не развернуты. Для этой цели снова оказывается удобна директива match :

match params, list { foo params }

Значение list, если оно не пустое, соответствует ключевому слову params, которое далее во время генерации строк, заключенных в фигурные скобки, заменяется на соответственное значение. Так, если list имеет значение 1,2 , строка, указанная выше, сгенерирует строку, содержащую foo 1,2 , которая далее пройдет стандартный препроцессинг.

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

Это позволяет выполнять некоторые вычисления во время предварительной обработки, если все используемые значения являются числами, известными на этапе препроцессинга. Одно повторение с rept может использоваться только для вычисления некоторого значения, как в этом примере:

define a b+4
define b 3
rept 1 result:a*b+2 { define c result }

Для вычисления базового значения счетчика result препроцессор заменяет b его значением и рекурсивно вычисляет значение a, получая в результате 7, затем вычисляет основное выражение с результатом 23. Затем c определяется с первым значением счетчика (поскольку блок обрабатывается только один раз), который является результатом вычисления, поэтому значение c является простым символом "23". Обратите внимание, что если b позже переопределяется с некоторым другим числовым значением, и далее вычисляется выражение, содержащее a, значение a будет отражать новое значение b , потому что символьная константа содержит только текст выражения.

Есть ещё один особый случай - когда препроцессор собирается проверить второй символ и натыкается на двоеточие (что далее интерпретируется ассемблером как определение метки), он останавливается в этом месте и заканчивает препроцессинг первого символа (то есть если это символьная константа, она развертывается) и если это все еще выглядит меткой, совершается стандартный препроцессинг, начиная с места после метки. Это позволяет поместить директивы препроцессора и макроинструкции после меток, аналогично инструкциям и директивам, обрабатываемым ассемблером, например:

start: include 'start.inc'

Однако если метка во время препроцессинга разрушается (например, если у символьной константы пустое значение), происходит только замена символьных констант до конца строки.

Следует помнить, что работы, выполняемые препроцессором, являются предварительными операциями над символами текста, которые выполняются за один простой проход перед основным процессом сборки. Текст, который является результатом предварительной обработки, передается ассемблеру, и затем он выполняет несколько проходов по нему. Таким образом, управляющие директивы, которые распознаются и обрабатываются только ассемблером, поскольку они зависят от числовых значений, которые могут даже варьироваться между проходами, препроцессором никоим образом не распознаются и не влияют на предварительную обработку. Рассмотрим этот пример источника:

if 0
    a = 1
    b equ 2
end if
dd b

Во время обработки, единственной директивой, которая распознается препроцессором, является equ, которая определяет символическую константу b, поэтому позже в источнике символ b заменяется значением 2. За исключением этой замены, остальные строки передаются ассемблеру без изменений. Таким образом, после предварительной обработки вышеуказанный источник становится:

if 0
    a = 1
end if
dd 2

Теперь, когда ассемблер обрабатывает его, условие для if - ложно, и константа a не определяется. Однако символьная константа b обрабатывалась нормально, хотя ее определение было поставлено рядом с определением a . Поэтому из-за возможной путаницы вы должны быть очень осторожны каждый раз при смешивании функций препроцессора и ассемблера - в таких случаях важно понимать, каким будет источник после препроцессора, и, таким образом, что увидит ассемблер и сделает свои многократные проходы дальше.


2.4 Директивы форматирования

Эти директивы на самом деле также являются своего рода директивами управления с целью управления форматом сгенерированного кода.

format со следующим за ним идентификатором формата позволяет выбрать формат вывода. Эта директива должна стоять в начале кода. Формат вывода по умолчанию - это простой двоичный файл, он может быть также выбран директивой format binary. За этой директивой может следовать ключевое слово as и строка в кавычках, указывающая расширение файла по умолчанию для выходного файла. Если имя выходного файла не было указано в командной строке, ассемблер будет использовать это расширение при создании выходного файла.

use16 и use32 указывают ассемблеру генерировать 16-битный или 32-битный код, пренебрегая настройкой по умолчанию для выбранного формата вывода. use64 включает генерирование кода для длинного режима процессоров x86.

Ниже описаны разные форматы вывода со специфичными для них директивами.


2.4.1 MZ

Чтобы выбрать формат вывода MZ, используйте директиву format MZ. По умолчанию код для этого формата 16-битный.

segment определяет новый сегмент, за ним должна следовать метка, чьим значением будет номер определяемого сегмента. Опционально за этой директивой может следовать use16 или use32 , чтобы указать битность кода в сегменте. Начало сегмента выровнено по параграфу (16 байт). Все метки, определенные далее, будут иметь значения относительно начала этого сегмента.

entry устанавливает точку входа для формата MZ , за ней должен следовать дальний адрес (имя сегмента, двоеточие и смещение в сегменте) желаемой точки входа.

stack устанавливает стек для MZ . За директивой может следовать числовое выражение, указывающее размер стека для автоматического создания, либо дальний адрес начального стекового фрейма, если вы хотите установить стек вручную. Если стек не определен, он будет создан с размером по умолчанию в 4096 байт.

heap со следующим за ней значением определяет максимальный размер дополнительного места в параграфах (это место в добавление к стеку и для неопределенных данных). Используйте heap 0 , Чтобы всегда отводить столько памяти, сколько программе действительно нужно. Размер кучи по умолчанию - 65535.


2.4.2 PE

Чтобы выбрать формат вывода Portable Executable (PE), используйте директиву format PE, за ней могут следовать дополнительные настройки формата: сначала настройка целевой подсистемы, которая может быть console или GUI для приложений Windows, native для драйверов Windows , EFI, EFIboot или EFIruntime для UEFI, за ним может следовать минимальная версия системы, на которую ориентирован исполняемый файл (указанный в форме значения с плавающей запятой). Необязательные ключевые слова DLL и WDM помечают выходной файл как динамическую библиотеку и драйвер WDM соответственно, ключевое слово large помечает исполняемый файл как способный обрабатывать адреса размером более 2 ГБ, а ключевое слово NX сигнализирует, что исполняемый файл соответствует ограничению неисполнения кода, находящегося в неисполняемых разделах.

После этих настроек могут следовать оператор at и числовое выражение, указывающее базу образа PE, а затем, необязательно, оператор on , за которым следует строка в кавычках, содержащая имя файла, выбирающей произвольную заглушку MZ для PE программы (когда указанный файл не является MZ исполняемым файлом, он обрабатывается как простой двоичный исполняемый файл и преобразуется в формат MZ). Код по умолчанию для этого формата 32-битный. Пример объявления формата PE со всеми свойствами:

format PE GUI 4.0 DLL at 7000000h on 'stub.exe'

Чтобы создать PE-файл для архитектуры x86-64, используйте ключевое слово PE64 вместо PE в объявлении формата (format PE64 ), в этом случае код длинного режима генерируется по умолчанию.

section определяет новую секцию, за ней должна следовать строка в кавычках, определяющая имя секции, и далее могут следовать один или больше флагов секций. Возможные флаги такие: code, data, readable, writeable, executable, shareable, discardable, notpageable . Начало секции выравнивается по странице (4096 байт). Пример объявления сиекции PE:

section '.text' code readable executable

Вместе с флагами также может быть определен один из специальных идентификаторов данных PE, отмечающий всю секцию как специальные данные, возможные идентификаторы: export, import, resource и fixups. Если секция помечена для содержания настроек адресов, они генерируются автоматически, и никаких данных определять больше не требуется. Также данные ресурсов могут быть сгенерированы автоматически из файлов ресурсов, этого можно добиться, написав после идентификатора resourse оператор from и имя файла в кавычках. Ниже вы можете увидеть примеры секций, содержащих некоторые специальные данные:

section '.reloc' data readable discardable fixups
section '.rsrc' data readable resource from 'my.res'

entry создает точку входа для PE, далее должно следовать значение точки входа.

stack устанавливает размер стека для PE, далее должно следовать значение зарезервированного размера стека, опционально может следовать отделенное запятой значение начала стека. Если стек не определен, ему присваивается размер по умолчанию, равный 4096 байт.

heap выбирает размер дополнительного места для PE, далее должно следовать значение для зарезервированного для него места, опционально ещё может быть значение его начала, отделенное запятой. Если дополнительное место не определено, оно ставится по умолчанию равным 65536 байт, если не указано его начало, то оно устанавливается равным 0.

data начинает определение специальных данных PE, за директивой должен следоваь один из идентификаторов данных (export, import, resource или fixups) или номер записи данных в заголовке PE. Данные должны быть определены на следующих строках и заканчиваться директивой end data. Если выбрано определение настроек адресов, они генерируются автоматически, и никаких данных определять больше не требуется. То же самое относится к ресурсам, если за идентификатором resourse следует оператор from и имя файла в кавычках - в этом случае данные берутся из этого файла ресурсов.

Оператор rva может использоваться внутри числовых выражений для получения RVA элемента, к которому относится значение, к которому он применяется, то есть смещение относительно основы изображения PE.


2.4.3 COFF

Чтобы выбрать Common Object File Format (COFF ), используйте директиву format COFF или format MS COFF, если вы хотите создать классический (DJGPP) или вариант COFF файла от Microsoft. По умолчанию код для этого формата 32-битный. Чтобы создать микрософтовский формат COFF для архитектуры x86-64, используйте установку format MS64 COFF , в этом случае автоматически будет генерироваться код длинного режима.

section определяет новую секцию, за директивой должна следовать строка в кавычках, определяющая имя новой секции, и ещё может следовать один или более флагов секций. Возможные флаги такие: code и data для обоих вариантов COFF, readable, writeable, executable, shareable, discardable, notpageable, linkremove и linkinfo только для варианта Microsoft COFF.

По умолчанию секция выровнена по двойному слову (четыре байта), в случае варианта Microsoft COFF можно указать другое выравнивание с помощью оператора align и следующим за ним значением выравнивания (любая степень двойки от двух до 8192) среди фагов секций.

extrn определяет внешний символ, за ним должно следовать имя символа и опционально оператор размера, указывающий размер данных, помеченных этим символом. Имя символа также может предваряться строкой в кавычках, содержащей имя внешнего символа и оператор as . Пара примеров объявления внешних символов:

extrn exit
extrn "__imp__MessageBoxA@16" as MessageBox:dword

public объявляет существующий символ как общедоступный, за ним должно следовать имя символа, и далее опционально оператор as и строка в кавычках, содержащая имя, под которым символ будет действителен как общедоступный. Пара примеров объявления общедоступных символов:

public main
public start as '_start'

Кроме того, в формате COFF можно указать экспортируемый символ как статический, это делается путем добавления к имени символа ключевого слова static .

При использовании формата Microsoft COFF оператор rva может быть использован внутри числовых выражений для получения RVA элемента, к которому относится значение, к которому он применяется.


2.4.4 ELF

Чтобы выбрать формат вывода ELF, используйте директиву format ELF . По умолчанию код для этого формата 32-битный. Чтобы создать формат ELF для архитектуры x86-64, используйте установку format ELF64 , в этом случае автоматически будет генерироваться код длинного режима.

section определяет новую секцию, за директивой должна следовать строка в кавычках,
определяющая имя новой секции, и ещё может следовать один или оба флага executable и writeable, опционально также может идти оператор align со следующим за ним числом, определяющим выравнивание секции (это должна быть степень двойки), если выравнивание не указано, используется значение по умолчанию, которое равно 4 или 8, в зависимости от варианта выбранного формата.

extrn и public имеют те же значения и синтаксис у них, как в случае формата COFF (описанного в предыдущем параграфе).

Оператор rva также может использоваться в случае этого формата (однако, если целевая архитектура не x86-64), он преобразует адрес в смещение относительно таблицы GOT, поэтому он может быть полезен для создания независимого от позиции кода. Также есть специальный оператор plt , который позволяет вызывать внешние функции через таблицу связей процедур. Вы даже можете создать псевдоним для внешней функции, который всегда будет вызывать ее через PLT с помощью следующего кода:

extrn 'printf' as _printf
printf = PLT _printf

Чтобы создать исполняемый файл, следуйте директиве выбора формата с ключевым словом executable или dynamic и, возможно, номером, указывающим номер целевой операционной системы (например, значение 3 будет отмечать исполняемый файл для системы Linux). При выбранном формате разрешается использовать директиву entry, за которой следует значение для установки точки входа программы. С другой стороны, это делает директивы extrn и public недоступными, и вместо section должна использоваться директива segment, за которой следует один или несколько флагов разрешений сегмента и, возможно, маркер специального исполняемого сегмента ELF, который может быть interpreter, dynamic, note, gnuehframe, gnustack или gnurelro. Доступные флаги разрешений: readable, writeable и executable . Начало не специального сегмента выравнивается по странице (4096 байт).


Глава 3 Программирование под Windows

Содержание
Глава 3 Программирование под Windows

В версии Windows пакет Fasm поставляется вместе со стандартными подключаемыми файлами, предназначенные для помощи в написании программ для Windows.

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

Вы можете выбрать один из шести заголовков для 32-битной Windows, имена которых начинаются с win32, за которыми следует буква a для использования кодировки ASCII или буква w для кодировки WideChar. win32a.inc и win32w.inc являются основными заголовками, win32ax.inc и win32wx.inc являются расширенными заголовками, они предоставляют более продвинутые макроинструкции, эти расширения будут обсуждаться отдельно. Наконец, win32axp.inc и win32wxp.inc - это одни и те же расширенные заголовки с включенной функцией проверки количества параметров в вызовах процедур.

Для 64-битной Windows существует шесть аналогичных пакетов, имена которых начинаются с win64. Они предоставляют в целом те же функциональные возможности, что и для 32-битных Windows, с некоторыми отличиями, объясненными позже.

Вы можете включить заголовки любым удобным для вас способом, указав полный путь или используя пользовательскую переменную среды, но самый простой способ - определить переменную среды INCLUDE , правильно указывающую на каталог, содержащий заголовки, а затем включить их так:

include 'win32a.inc'

Важно отметить, что все макроинструкции, в отличие от внутренних директив плоского ассемблера, чувствительны к регистру, и для большинства из них используется нижний регистр. Если вы предпочитаете использовать другим образом, чем по умолчанию, вам следует выполнить соответствующие настройки с помощью директивы fix.


3.1 Основные заголовки

Основные заголовки win32a.inc, win32w.inc, win64a.inc и win64w.inc включают в себя объявления констант и структур Windows и предоставляют стандартный набор макроинструкций.


3.1.1 Структуры

Все заголовки включают макроинструкцию struct, которая позволяет определять структуры способом, более похожим на другие ассемблеры, чем директива struc. Определение структуры должно начинаться с макроинструкции struct, за которой следует имя, и заканчивается макроинструкцией ends. В строках между ними разрешены только директивы определения данных, причем метки являются чистыми именами для полей структуры:

struct POINT
  x dd ?
  y dd ?
ends

При таком определении строка:

point1 POINT

объявляет структуру point1 с полями point1.x и point1.y, давая им значения по умолчанию - те же, что и в определении структуры (в этом случае значения по умолчанию являются неинициализированными значениями). Но объявление структуры также принимает параметры в том же количестве, что и количество полей в структуре, и эти параметры, если они указаны, переопределяют для полей значения по умолчанию. Например:

point2 POINT 10,20

инициализирует поле point2.x со значением 10, а point2.y со значением 20.

Макрос struct не только позволяет объявлять структуры данного типа, но также определяет метки для смещений полей внутри структуры и константы для размеров каждого поля и всей структуры. Например, приведенное выше определение структуры POINT определяет метки POINT.x и POINT.y как смещения полей внутри структуры, а sizeof.POINT.x, sizeof.POINT.y и sizeof.POINT как размеры соответствующих полей и всей структуры. Метки смещения могут использоваться для доступа к структурам, адресованным косвенно, например:

mov eax,[ebx+POINT.x]

когда регистр ebx содержит указатель на структуру POINT. Обратите внимание, что проверка размера поля будет выполняться и при таком доступе.

Сами структуры также разрешены внутри определений структуры, поэтому структуры могут иметь другие структуры в виде полей:

struct LINE
  start POINT
  end   POINT
ends

Если значения по умолчанию для полей подструктуры не указаны, как в этом примере, применяются значения по умолчанию из определения типа подструктуры.

Поскольку значение для каждого поля является отдельным параметром в объявлении структуры, для инициализации подструктур с пользовательскими значениями параметры для каждой подструктуры должны быть сгруппированы в один параметр для структуры:

line1 LINE <0,0>,<100,100>

Приведенное выше объявление инициализирует каждое из полей line1.start.x и line1.start.y 0 (нулем), а каждое из line1.end.x и line1.end.y - 100.

Когда размер данных, определяемый некоторым значением, передаваемым в структуру объявления, меньше размера соответствующего поля, он дополняется до этого размера неопределенными байтами (и когда он больше, возникает ошибка). Например:

struct FOO
  data db 256 dup (?)
ends

some FOO <"ABC",0>

заполняет первые четыре байта some.data определенными значениями и резервирует остальные.

Внутри структур также могут быть определены объединения и безымянные подструктуры. Определение объединения должно начинаться с union и заканчиваться ends, как в этом примере:

struct BAR
  field_1 dd ?
  union
    field_2  dd ?
    field_2b db ?
  ends
ends

Каждое из полей, определенных внутри объединения, имеет одинаковое смещение и разделяет одну и ту же память. Только первое поле объединения инициализируется с данным значением, значения для остальных полей игнорируются (однако, если одному из других полей требуется больше памяти, чем первому, объединение дополняется до требуемого размера с неопределенными байтами). Целое объединение инициализируется одним параметром, указанным в объявлении структуры, и этот параметр присваивает значение первому полю объединения.

Безымянная подструктура определяется аналогично объединению, только начинается со строки struct вместо union, например:

struct WBB
  word dw ?
  struct
    byte1 db ?
    byte2 db ?
  ends
ends

Такая подструктура принимает только один параметр в объявлении всей структуры, чтобы определить ее значения, и этот параметр сам может быть группой параметров, определяющих каждое поле подструктуры. Таким образом, вышеупомянутый тип структуры может быть объявлен как:

my WBB 1,<2,3>

Доступ к полям внутри объединений и безымянных подструктур осуществляется так же, как если бы они были непосредственно полями родительской структуры. Например, с объявлением выше my.byte1 и my.byte2 являются правильными метками для полей подструктуры.

Подструктуры и союзы могут быть вложены без ограничений по глубине вложенности:

struct LINE
  union
    start POINT
    struct
      x1  dd ?
      y1  dd ?
    ends
  ends
  union
    end   POINT
    struct
      x2  dd ?
      y2  dd ?
    ends
  ends
ends

Определение структуры может также основываться на некоторых из уже определенных типов структуры, и оно наследует все поля из этой структуры, например:

struct CPOINT POINT
  color dd ?
ends

определяет ту же структуру, что и:

struct CPOINT
  x     dd ?
  y     dd ?
  color dd ?
ends

Все заголовки определяют тип данных CHAR , который можно использовать для определения символьных строк в структурах данных.


3.1.2 Импорт

Макроинструкции импорта помогают построить данные импорта для PE-файла (обычно помещаются в отдельный раздел). Для этого есть две макроинструкции. Первый называется library и должен быть помещен непосредственно в начало данных импорта, и он определяет, из каких библиотек будут импортированы функции. За ним должно следовать любое количество пар параметров, каждая пара является меткой для таблицы импорта из данной библиотеки и строка в кавычках, определяющая имя библиотеки. Например:

library kernel32,'KERNEL32.DLL',\
        user32,'USER32.DLL'

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

import kernel32,\
       ExitProcess,'ExitProcess'

import user32,\
       MessageBeep,'MessageBeep',\
       MessageBox,'MessageBoxA'

Метки, определенные первыми параметрами в каждой паре, передаваемой в макрос import, обращаются к указателям двойных слов, которые после загрузки PE заполняются адресами экспортируемых процедур.

Вместо строки в кавычках для имени импортируемой процедуры может быть дан номер для определения импорта по порядковому номеру, например:

import custom,\
       ByName,'FunctionName',\
       ByOrdinal,17

Макросы импорта оптимизируют данные импорта, поэтому в таблицы импорта помещаются только те импорты для функций, которые используются где-то в программе, и если какая-то таблица импорта будет пустой при этом, на всю библиотеку вообще не будет ссылаться. По этой причине удобно иметь полную таблицу импорта для каждой библиотеки - пакет содержит такие таблицы для некоторых стандартных библиотек, они хранятся в подкаталогах APIA и APIW и импортируют варианты API-функций ASCII и WideChar. Каждый файл содержит одну таблицу импорта с меткой в нижнем регистре, совпадающей с именем файла. Таким образом, полные таблицы для импорта из библиотек KERNEL32.DLL и USER32.DLL могут быть определены таким образом (при условии, что ваша переменная среды INCLUDE указывает на каталог, содержащий подключаемые файлы):

library kernel32,'KERNEL32.DLL',\
        user32,'USER32.DLL'

include 'apia\kernel32.inc'
include 'apiw\user32.inc'


3.1.3 Процедуры (32-bit)

В заголовках для 32-битной Windows предоставлены четыре макроинструкции для вызова процедур с параметрами, передаваемыми через стек. stdcall напрямую вызывает процедуру, указанную в первом аргументе, используя соглашение о вызовах STDCALL. Остальные аргументы, передаваемые макросу, определяют параметры процедуры и сохраняются в стеке в обратном порядке. Макрос invoke делает то же самое, однако вызывает процедуру косвенно через указатель, помеченный первым аргументом. Таким образом, invoke можно использовать для вызова процедур через указатели, определенные в таблицах импорта. Эта строка:

invoke MessageBox,0,szText,szCaption,MB_OK

эквивалентно:

stdcall [MessageBox],0,szText,szCaption,MB_OK

и они оба генерируют этот код:

push MB_OK
push szCaption
push szText
push 0
call [MessageBox]

ccall и cinvoke аналогичны stdcall и invoke, но их следует использовать для вызова процедур, использующих C соглашение о вызовах, в котором кадр стека должен быть восстановлен вызывающей стороной.

Чтобы определить процедуру, которая использует стек для параметров и локальных переменных, вы должны использовать макроинструкцию proc. В простейшем виде за ним должно следовать имя процедуры, а затем имена всех параметров, которые она принимает, например:

proc WindowProc,hwnd,wmsg,wparam,lparam

Запятая между именем процедуры и первым параметром является необязательной. Инструкции процедуры должны следовать в следующих строках, заканчивающихся макроинструкцией endp. Фрейм стека устанавливается автоматически при входе в процедуру, регистр ebp используется в качестве базы для доступа к параметрам, поэтому вам следует избегать использования этого регистра для других целей. Имена, указанные для параметров, используются для определения меток на основе ebp, которые можно использовать для доступа к параметрам в качестве обычных переменных. Например, инструкция mov eax, [hwnd] внутри процедуры, описанной в примере выше, эквивалентна mov eax, [ebp + 8]. Область действия этих меток ограничена процедурой, поэтому вы можете использовать те же имена для других целей, вне данной процедуры.

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

proc WindowProc,hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD

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

За именем процедуры может также следовать ключевое слово stdcall или c, чтобы определить соглашение о вызовах, которое оно использует. Когда такой тип не указан, используется значение по умолчанию, что эквивалентно STDCALL. Затем может следовать ключевое слово uses, а после него список регистров (разделенных только пробелами), которые будут автоматически сохраняться при входе в процедуру и восстанавливаться при выходе. В этом случае запятая после списка регистров и перед первым параметром обязательна. Таким образом, полнофункциональный оператор процедуры может выглядеть так:

proc WindowProc stdcall uses ebx esi edi,\
     hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD

Чтобы объявить локальную переменную, вы можете использовать макроинструкцию local, за которой следует одно или несколько объявлений, разделенных запятыми, каждое из которых состоит из имени переменной, за которой следует двоеточие, и типа переменной - один из стандартных типов (должен быть в верхнем регистре) ) или название структуры данных. Например:

local hDC:DWORD,rc:RECT

Чтобы объявить локальный массив, вы можете поставить после имени переменной его размер, заключенному в квадратные скобки, например:

local str[256]:BYTE

Другой способ определить локальные переменные - объявить их внутри блока, начинающегося с макроинструкции locals и заканчивающегося endl, в этом случае они могут быть определены как обычные данные. Это объявление является эквивалентом предыдущего примера:

locals
  hDC dd ?
  rc RECT
endl

Локальные переменные могут быть объявлены где угодно внутри процедуры, с единственным ограничением, что они должны быть объявлены перед использованием. Область применения меток для переменных, определенных как локальные, ограничена внутри процедуры, вы можете использовать те же имена для других целей вне процедуры. Если вы присваиваете некоторые инициализированные значения переменным, объявленным как локальные, макроинструкция генерирует инструкции, которые будут инициализировать эти переменные с заданными значениями, и помещает эти инструкции в ту же позицию в процедуре, где размещается объявление.

ret помещенный где-нибудь в процедуре, генерирует заключительный код, необходимый, чтобы правильно выйти из процедуры, восстанавливая стековый фрейм и регистры, используемые процедурой. Если ВЫ должны произвести инструкцию возврата из подпрограммы, используйте мнемонику retn или ret с числовым операндом, что также заставляет это интерпретироваться как единственная инструкция.

Подводем итог, полное определение процедуры может выглядеть так:

proc WindowProc uses ebx esi edi,hwnd,wmsg,wparam,lparam
  local hDC:DWORD,rc:RECT
  ; здесь ваш код
  ret
endp


3.1.4 Процедуры (64-bit)

В 64-битной Windows существует только одно соглашение о вызовах, и, таким образом, предоставляются только две макроинструкции для вызова процедур. fastcall напрямую вызывает процедуру, указанную в первом аргументе, используя стандартное соглашение 64-битной системы Windows. Макрос invoke делает то же самое, но косвенно, через указатель, помеченный первым аргументом. Параметры предоставляются следующими аргументами, и они могут иметь любой размер до 64 бит. Макроинструкции используют регистр rax в качестве временного хранилища, когда какое-либо значение параметра не может быть скопировано непосредственно в стек с помощью инструкции mov. Если параметру предшествует слово addr, он обрабатывается как адрес и рассчитывается с помощью команды lea - поэтому, если адрес абсолютный, он будет рассчитан как rip-relative, таким образом предотвращая производить перемещение в случае файла с fixups.

Поскольку в 64-битной Windows параметры плавающей точки передаются по-разному, они должны быть отмечены предшествующим каждому из них словом float. Они могут быть размером либо в двойное слово, либо в четверное слово. Вот пример вызова некоторых процедур OpenGL с параметрами двойной и одинарной точности:

invoke glVertex3d,float 0.6,float -0.6,float 0.0
invoke glVertex2f,float dword 0.1,float dword 0.2

Пространство стека для параметров выделяется перед каждым вызовом и освобождается сразу после него. Однако возможно выделить это пространство только один раз для всех вызовов внутри некоторого данного блока кода, для этого предусмотрены макросы frame и endf. Они должны использоваться для выделения блока, внутри которого регистр rsp не изменяется между вызовами процедуры, и они препятствуют тому, чтобы каждый вызов выделял пространство стека для параметров, поскольку это резервируется только один раз макросом frame и затем освобождается в конце макросом endf .

frame ; выделить место в стеке только один раз
  invoke TranslateMessage,msg
  invoke DispatchMessage,msg
endf

Макрос proc для 64-битной Windows имеет тот же синтаксис и функции, что и 32-битный (хотя параметры для соглашения вызова stdcall и c в этом случае бесполезны). Однако следует отметить, что в соглашении о вызовах, используемом в 64-битной Windows, первые четыре параметра передаются в регистрах (rcx, rdx, r8 и r9), и, следовательно, несмотря на то, что в стеке зарезервировано для них место и оно помечено именем, указанным в определении процедуры, эти четыре параметра изначально не будут находиться там. Они должны быть доступны путем непосредственного чтения регистров. Но если эти регистры необходимо использовать для каких-то других целей, рекомендуется сохранить значение такого параметра в зарезервированной для него ячейке памяти. Начало такой процедуры может выглядеть так:

proc WindowProc hwnd,wmsg,wparam,lparam
  mov [hwnd],rcx
  mov [wmsg],edx
  mov [wparam],r8
  mov [lparam],r9
  ; теперь регистры можно использовать для других целей
  ; и параметры могут быть доступны позже


3.1.5 Настройки процедур

При использовании макроинструкции proc можно создать собственный код для каркаса процедур. Существуют три символические переменные: prologue@proc, epilogue@proc и close@proc, которые определяют имена макроинструкций, которые proc вызывает при входе в процедуру, возвращении из процедуры (создается с помощью макроса ret) и в конце процедуры (выполняется с макросом endp). Эти переменные могут быть переопределены так, чтобы указывать на некоторые другие макроинструкции, так что весь код, сгенерированный с помощью макроса proc, может быть настроен.

Каждая из этих трех макроинструкций принимает пять параметров. Первый предоставляет метку точки входа в процедуру, которая также является названием процедуры. Второй - это битовое поле, содержащее некоторые флаги, в частности, бит 4 устанавливается, когда вызывающий должен восстановить стек, и очищается в противном случае. Третий - это значение, которое определяет количество байтов, которые параметры процедуры принимают в стеке. Четвертый - это значение, которое указывает количество байтов, которые должны быть зарезервированы для локальных переменных. Наконец, пятый и последний параметр - это список разделенных запятыми регистров, процедура которых объявлена для использования и поэтому должна быть сохранена прологом и восстановлена эпилогом.

Макрос пролога помимо генерации кода, который устанавливает фрейм стека и указатель на локальные переменные, должен определять две символические переменные, parmbase@proc и localbase@proc. Первый должен предоставлять базовый адрес для того, где находятся параметры, а второй должен предоставлять адрес для того, где находятся локальные переменные - обычно относительно регистра ebp/rbp, но можно использовать другие базы, если можно гарантировать, что эти указатели будут действительны в любой точке процедуры, к которой обращаются параметры или локальные переменные. Это также зависит от макроса пролога, чтобы сделать любые выравнивания, необходимые для реализации правильной процедуры; размер локальных переменных, предоставляемых в качестве четвертого параметра, может быть вообще не выровнен.

Поведение proc по умолчанию определяется макросами prologuedef и epiloguedef (в случае по умолчанию закрытие макроса не требуется, поэтому close@proc имеет пустое значение). Если необходимо вернуться к настройкам по умолчанию после того, как были использованы некоторые настройки, это следует сделать с помощью следующих трех строк:

prologue@proc equ prologuedef
epilogue@proc equ epiloguedef
close@proc equ

В качестве примера модифицированного пролога ниже приведена макроинструкция, реализующая пролог с проверкой стека для 32-битной Windows. Такой метод выделения должен использоваться каждый раз, когда область локальных переменных может превышать 4096 байт.

macro sp_prologue procname,flag,parmbytes,localbytes,reglist
{ local loc
  loc = (localbytes+3) and (not 3)
  parmbase@proc equ ebp+8
  localbase@proc equ ebp-loc
  if parmbytes | localbytes
   push ebp
   mov ebp,esp
   if localbytes
    repeat localbytes shr 12
     mov byte [esp-%*4096],0
    end repeat
    sub esp,loc
   end if
  end if
  irps reg, reglist \{ push reg \} }

prologue@proc equ sp_prologue

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

64-битные заголовки предоставляют дополнительный набор макросов пролога / эпилога, которые позволяют определить процедуру, которая использует rsp для доступа к параметрам и локальным переменным (поэтому регистр rbp может свободно использоваться для любой другой процедуры), а также выделяет общее пространство для всех вызовов процедур, выполняемых внутри, так что вызываемые fastcall или invoke макросы не должны сами выделять пространство стека. Этот эффект аналогичен эффекту, полученному путем помещения кода внутри процедуры в блок frame, но в этом случае выделение стекового пространства для вызовов процедур объединяется с выделением пространства для локальных переменных. Код внутри такой процедуры не должен каким-либо образом изменять регистр rsp. Чтобы переключиться на это поведение 64-битной процедуры, используйте следующие инструкции:

prologue@proc equ static_rsp_prologue
epilogue@proc equ static_rsp_epilogue
close@proc equ static_rsp_close


3.1.6 Экспорт

Макроинструкция export создает данные экспорта для PE-файла (его следует либо поместить в секцию, помеченную как export, либо в блок экспорта данных. Первым аргументом должна быть строка в кавычках, определяющая имя файла библиотеки, а остальные должны быть любое количество пар аргументов, первое в каждой паре - это имя процедуры, определенной где-то внутри источника, а второе - строка в кавычках, содержащая имя, под которым эта процедура должна быть экспортирована библиотекой.

export 'MYLIB.DLL',\
       MyStart,'Start',\
       MyStop,'Stop

определяет таблицу, экспортирующую две функции, которые определены под именами MyStart и MyStop в исходниках, но будут экспортированы библиотекой под более короткими именами. Макроинструкция позаботится об алфавитной сортировке таблицы, которая требуется для формата PE.


3.1.7 COM

Макрос interface позволяет объявить интерфейс типа COM-объекта, первым параметром является имя интерфейса, а затем должны следовать последовательные имена методов, как в этом примере:

interface ITaskBarList,\
           QueryInterface,\
           AddRef,\
           Release,\
           HrInit,\
           AddTab,\
           DeleteTab,\
           ActivateTab,\
           SetActiveAlt

Макрос comcall может затем использоваться для вызова метода данного объекта. Первый параметр этого макроса должен быть дескриптором объекта, второй - именем COM-интерфейса, реализуемого этим объектом, а затем именем метода и параметрами этого метода. Например:

comcall ebx,ITaskBarList,ActivateTab,[hwnd]

использует содержимое регистра ebx как дескриптор COM-объекта с интерфейсом ITaskBarList и вызывает метод ActivateTab этого объекта с параметром [hwnd].

Вы также можете использовать имя COM-интерфейса так же, как имя структуры данных, чтобы определить переменную, которая будет содержать дескриптор объекта данного типа:

ShellTaskBar ITaskBarList

Приведенная выше строка определяет переменную, в которой может храниться дескриптор COM-объекта. После сохранения дескриптора объекта его методы можно вызывать с помощью cominvk. Этому макросу нужно только имя переменной с назначенным интерфейсом и имя метода в качестве первых двух параметров, а затем параметры для метода. Таким образом, метод объекта ActivateTab, дескриптор которого хранится в переменной ShellTaskBar, как определено выше, можно вызвать следующим образом:

cominvk ShellTaskBar,ActivateTab,[hwnd]

который делает то же, что:

comcall [ShellTaskBar],ITaskBarList,ActivateTab,[hwnd]


3.1.8 Ресурсы

Существует два способа создания ресурсов: один - включить файл внешнего ресурса, созданный другой программой (section '.rsrc' data readable resource from 'my.res' ), а другой - создать раздел ресурсов вручную. Последний метод, хотя и не требует участия какой-либо дополнительной программы, является более трудоемким, но стандартные заголовки предоставляют помощь - набор элементарных макроинструкций, которые служат кирпичиками для составления раздела ресурсов.

Макроинструкция directory должна быть размещена непосредственно в начале данных ресурсов, созданных вручную, и она определяет, какие типы ресурсов она содержит. За ним должны следовать пары значений, первое в каждой паре является идентификатором типа ресурса, а второе - меткой подкаталога ресурсов данного типа. Это может выглядеть так:

directory RT_MENU,menus,\
          RT_ICON,icons,\
          RT_GROUP_ICON,group_icons

Подкаталоги могут быть размещены в любом месте области ресурсов после основного каталога, и они должны быть определены с помощью макроинструкции resource, которая требует, чтобы первый параметр был меткой подкаталога (соответствующего записи в главном каталоге), за которым следуют три параметра - в каждой такой записи первый параметр определяет идентификатор ресурса (это значение свободно выбирается программистом и затем используется для доступа к данному ресурсу из программы), второй указывает язык, а третий - метку ресурса. Стандартные константы должны использоваться для создания языковых идентификаторов. Например, подкаталог меню может быть определен следующим образом:

resource menus,\
         1,LANG_ENGLISH+SUBLANG_DEFAULT,main_menu,\
         2,LANG_ENGLISH+SUBLANG_DEFAULT,other_menu

Если ресурс относится к типу, для которого язык не имеет значения, следует использовать идентификатор языка LANG_NEUTRAL . Для определения ресурсов различных типов существуют специальные макроинструкции, которые следует размещать внутри области ресурсов.

Растровые изображения - это ресурсы с идентификатором типа RT_BITMAP . Чтобы определить ресурс растрового изображения, используйте макроинструкцию bitmap, в которой первый параметр является меткой ресурса (соответствует записи в подкаталоге растровых изображений), а второй - строкой в ??кавычках, содержащей путь к файлу растрового изображения, например:

bitmap program_logo,'logo.bmp'

Есть два типа ресурсов, связанных с иконками. RT_GROUP_ICON - это тип ресурса, который должен быть связан с одним или несколькими ресурсами типа RT_ICON, каждый из которых содержит одно изображение. Это позволяет объявлять изображения разных размеров и глубины цвета под общим идентификатором ресурса. Этот идентификатор, предоставленный ресурсу типа RT_GROUP_ICON, может затем быть передан в функцию LoadIcon, и он выберет изображение подходящих размеров из группы. Чтобы определить иконку, используйте макроинструкцию icon, в которой первым параметром является метка ресурса RT_GROUP_ICON, а затем пары параметров, объявляющих изображения. Первый параметр в каждой паре должен быть меткой ресурса RT_ICON , а второй - строкой в ??кавычках, содержащей путь к файлу иконки. В простейшем варианте, когда группа значков содержит только одно изображение, оно будет выглядеть так:

icon main_icon,icon_data,'main.ico'

где main_icon - это метка для записи в подкаталоге ресурса для типа RT_GROUP_ICON, а icon_data - это метка для записи типа RT_ICON .

Курсоры определяются так же, как иконки, с типами RT_GROUP_CURSOR и RT_CURSOR и макросом cursor, который принимает параметры, аналогичные тем, которые используются макросом icon. Таким образом, определение курсора может выглядеть так:

cursor my_cursor,cursor_data,'my.cur'

Меню имеют тип ресурса RT_MENU и определяются макроинструкцией menu, за которой следуют несколько других, определяющих элементы меню. menu принимает только один параметр - метку ресурса. menuitem определяет элемент в меню, он принимает до пяти параметров, но требуется только два - первый - строка в кавычках, содержащая текст для элемента, а второй - значение идентификатора (значением, которое будет возвращено, когда пользователь выберет данный пункт в меню). menuseparator определяет разделитель в меню и не требует никаких параметров.

Необязательный третий параметр menuitem определяет флаги ресурса меню. Доступно два таких флага: MFR_END - это флаг для последнего элемента в данном меню, а MFR_POPUP отмечает, что данный элемент является подменю, и следующие элементы будут элементами, составляющими это подменю, пока не будет найден элемент с флагом MFR_END. Флаг MFR_END также может быть задан в качестве параметра для menuseparator и является единственным параметром, который может принимать эта макроинструкция. Чтобы определение меню было завершено, каждое подменю должно быть закрыто элементом с флагом MFR_END, и все меню также должно быть закрыто таким образом. Вот пример полного определения меню:

menu main_menu
     menuitem '&File',100,MFR_POPUP
              menuitem '&New',101
              menuseparator
              menuitem 'E&xit',109,MFR_END
     menuitem '&Help',900,MFR_POPUP + MFR_END
              menuitem '&About...',901,MFR_END

Необязательный четвертый параметр menuitem определяет флаги состояния для данного элемента, эти флаги такие же, как и те, которые используются функциями API, такими как MFS_CHECKED или MFS_DISABLED. Точно так же пятый параметр может указывать тип флажка. Например, это определит пункт, отмеченный переключателем:

menuitem 'Selection',102, ,MFS_CHECKED,MFT_RADIOCHECK

Диалоговые окна имеют тип ресурса RT_DIALOG и определяются с помощью макроинструкции dialog, за которой следует любое количество элементов, определенных с помощью dialogitem, заканчивающиеся enddialog.

dialog может принимать до одиннадцати параметров, первые семь обязательны. Первый параметр, как обычно, указывает метку ресурса, второй - строку в кавычках, содержащую заголовок диалогового окна, следующие четыре параметра задают горизонтальные и вертикальные координаты, ширину и высоту диалогового окна соответственно. Седьмой параметр указывает флаги стиля для диалогового окна, необязательный восьмой параметр указывает флаги расширенного стиля. Девятый параметр может указывать меню для окна - это должен быть идентификатор ресурса меню, такой же, как указанный в подкаталоге ресурсов с типом RT_MENU. Наконец, десятый и одиннадцатый параметры могут быть использованы для определения шрифта для диалогового окна - первая из них должна быть строкой в кавычках, содержащей имя шрифта, а второй - числом, определяющим размер шрифта. Если эти необязательные параметры не указаны, используется стандартный MS Sans Serif размером 8.

В этом примере показана макрокоманда диалогового окна со всеми параметрами, кроме меню (в котором оставлено пустое значение), дополнительные параметры находятся во второй строке:

dialog about,'About',50,50,200,100,WS_CAPTION+WS_SYSMENU,\
             WS_EX_TOPMOST, ,'Times New Roman',10

dialogitem имеет восемь обязательных параметров и один необязательный. Первый параметр должен быть строкой в кавычках, содержащей имя класса для элемента. Вторым параметром может быть либо строка в кавычках, содержащая текст для элемента, либо идентификатор ресурса в случае, когда содержимое элемента должно быть определено каким-либо дополнительным ресурсом (например, элементом класса STATIC со стилем SS_BITMAP). Третий параметр - это идентификатор элемента, используемый для идентификации элемента с помощью функций API. Следующие четыре параметра определяют горизонтальные, вертикальные координаты, ширину и высоту элемента соответственно. Восьмой параметр определяет стиль для элемента, а необязательный девятый указывает флаги расширенного стиля. Пример определения элемента диалога:

dialogitem 'BUTTON','OK',IDOK,10,10,45,15,WS_VISIBLE+WS_TABSTOP

И пример статического элемента, содержащего растровое изображение, при условии, что существует растровый ресурс с идентификатором 7:

dialogitem 'STATIC',7,0,10,50,50,20,WS_VISIBLE+SS_BITMAP

Определение ресурса диалога может содержать любое количество элементов или вообще не содержать их, и оно всегда должно заканчиваться макроинструкцией enddialog .

Ресурсы типа RT_ACCELERATOR создаются с помощью макроинструкции accelerator. После того, как первый параметр традиционно является меткой ресурса, далее должны следовать три параметра: флаги акселератора, за которыми следуют код виртуальной клавиши или символ ASCII и значение идентификатора (которое аналогично идентификатору пункта меню). Простое определение ускорителя может выглядеть так:

accelerator main_keys,\
            FVIRTKEY+FNOINVERT,VK_F1,901,\
            FVIRTKEY+FNOINVERT,VK_F10,109

Информация о версии является ресурсом типа RT_VERSION и создается с помощью макроинструкции versioninfo. После метки ресурса второй параметр указывает операционную систему PE-файла (обычно VOS__WINDOWS32), третий параметр - тип файла (наиболее распространенными являются VFT_APP для программы и VFT_DLL для библиотеки), четвертый подтип (обычно VFT2_UNKNOWN), пятый идентификатор языка, шестая кодовая страница и затем строковые параметры в кавычках, представляющие собой пары имени свойства и соответствующего значения. Простейшая информация о версии может быть определена так:

versioninfo vinfo,VOS__WINDOWS32,VFT_APP,VFT2_UNKNOWN,\
            LANG_ENGLISH+SUBLANG_DEFAULT,0,\
            'FileDescription','Description of program',\
            'LegalCopyright','Copyright et cetera',\
            'FileVersion','1.0',\
            'ProductVersion','1.0'

Другие виды ресурсов могут быть определены с помощью макроинструкции resdata, которая принимает только один параметр - метку ресурса и может сопровождаться любыми инструкциями, определяющими данные, и заканчивающимися макроинструкцией endres, например:

resdata manifest
  file 'manifest.xml'
endres


3.1.9 Текстовые кодировки

Макросы ресурсов используют du директиву, чтобы определить любые строки Unicode в ресурсах. Так как эта директива просто расширяет символы, обнуляя 16-разрядные значения, для строк содержащих некоторые символы не ASCII, du возможно, должна быть переопределена. Для части кодировок макросы, переопределяющие du, чтобы генерировать тексты должным образом в Unicode находятся в подкаталоге ENCODING. Например, если исходный текст закодирован с кодовой страницей Windows 1250, такая строка должна быть помещена где-нибудь в начале источника:

include 'encoding\win1250.inc'


3.2 Расширенные заголовки

Файлы win32ax.inc, win32wx.inc, win64ax.inc и win64wx.inc предоставляют все функциональные возможности базовых заголовков и включают в себя еще несколько функций, включающих более сложные макроинструкции. Кроме того, если формат PE не объявлен перед включением расширенных заголовков, заголовки объявляют его автоматически. Файлы win32axp.inc, win32wxp.inc, win64axp.inc и win64wxp.inc являются вариантами расширенных заголовков, которые дополнительно выполняют проверку количества параметров для вызовов процедур.


3.2.1 Параметры процедур

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

invoke MessageBox,HWND_DESKTOP,"Message","Caption",MB_OK

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

Если параметру предшествует слово addr, это означает, что это значение является адресом, и этот адрес должен быть передан процедуре, даже если это нельзя сделать напрямую - как в случае локальных переменных, которые имеют адреса относительно регистра ebp/rbp. В 32-битном случае регистр edx временно используется для вычисления значения адреса и передачи его в процедуру. Например:

invoke RegisterClass,addr wc

в случае, когда wc является локальной переменной с адресом ebp-100h, сгенерирует эту последовательность инструкций:

lea edx,[ebp-100h]
push edx
call [RegisterClass]

Однако когда данный адрес не относится ни к какому регистру, он сохраняется напрямую.

В случае 64-бит префикс addr разрешен, даже когда используются только стандартные заголовки, поскольку он может быть полезен даже в случае обычных адресов, поскольку он обеспечивает вычисление адресов, относящихся к rip.

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

invoke glColor3d,double 1.0,double 0.1,double 0.1

передаст три 64-битных параметра как шесть двойных слов в процедуру. Если параметр, следующий за double , является операндом памяти, у него не должно быть оператора размера, double уже работает как переопределение размера.

Наконец, вызовы процедур могут быть вложенными, то есть вызов одной процедуры может использоваться как параметр другой. В этом случае значение, возвращаемое в eax/rax вложенной процедурой, передается в качестве параметра в процедуру, в которую оно вложено. Пример такого вложения:

invoke MessageBox,<invoke GetTopWindow,[hwnd]>,\
                  "Message","Caption",MB_OK

Нет ограничений по глубине вложенности вызовов процедур.


3.2.2 Структурирование исходников

Расширенные заголовки позволяют использовать некоторые макроинструкции, которые помогают легко структурировать программу. .data и .code - это всего лишь ярлыки для объявлений разделов для данных и для кода. Макроинструкция .end должна быть помещена в конец программы, с одним параметром, определяющим точку входа в программу, и она также автоматически генерирует раздел импорта, используя все стандартные таблицы импорта. В 64-битной Windows .end автоматически выравнивает стек по границе 16 байт.

Макроинструкция .if генерирует фрагмент кода, который проверяет некоторое простое условие во время выполнения и в зависимости от результата продолжает выполнение следующего блока или пропускает его. Блок должен заканчиваться на .endif, но ранее также можно использовать макроинструкцию .elseif один или несколько раз и начинать код, который будет выполняться при каких-то дополнительных условиях, когда предыдущие условия не были выполнены, и .else как последний блок перед .endif , который будет выполнен, когда все условия были ложными.

Условие может быть задано с помощью одного из операторов сравнения: =, < , >, <=, >= и <> - между двумя значениями, первое из которых должно быть либо регистром, либо операндом памяти. Значения сравниваются как беззнаковые, если выражению сравнения не предшествует слово signed . Если вы предоставите только одно значение в качестве условия, оно будет проверено на нулевое значение, и условие будет истинным, только если это не так. Например:

.if eax
  ret
.endif

генерирует инструкции, которые пропускают ret, когда eax равен нулю.

Есть также некоторые специальные символы, которые распознаются как условия: ZERO? - значение true, если установлен флаг ZF, точно так же, как CARRY?, SIGN?, OVERFLOW? и PARITY? соответствуют состоянию флагов CF, SF, OF и PF .

Простые условия, подобные приведенным выше, могут быть составлены в сложные условные выражения с помощью символов &, | операторы для конъюнкции и альтернативы, оператор ~ для отрицания и скобки. Например:

.if eax <= 100 & ( ecx | edx )
  inc ebx
.endif

сгенерирует команды сравнения и перехода, которые приведут к выполнению данного блока только в том случае, если eax ниже или равен 100 и в то же время хотя бы один из регистров ecx и edx не равен нулю.

Макроинструкция .while генерирует инструкции, которые будут повторять выполнение данного блока (завершается макроинструкцией .endw), пока условие true. Условие должно следовать за .while и может быть указано так же, как и для .if .

Пара макроинструкций .repeat и .until определяет блок, который будет повторяться до тех пор, пока не будет выполнено данное условие - на этот раз условие должно следовать за макроинструкцией .until , размещенной в конце блока, например:

.repeat
  add ecx,2
.until ecx > 100


Понимание fasm

Содержание
Понимание fasm

Этот текст представляет собой руководство для продвинутых пользователей, которое суммирует некоторые правила fasm и учит выбирать новые техники, комбинируя его различные возможности. Также мы ставим своей целью объяснить некоторые особенности, которые могут запутать и которые не соответствуют ожиданиям, пока не изучишь точно, как это все работает и взаимодействует на различных уровнях языка, используемого в fasm, который по некоторым аспектам отличается от других ассемблеров.

Это пособие, однако, не может заменить основное руководство по fasm. Мы полагаем, что вы уже получили базовые знания по языку fasm, и теперь можете их углубить.


Ассемблер как компилятор и ассемблер как интерпретатор

Реализации языков программирования могут относиться к одному из двух классов: компиляторы и интерпретаторы. Интерпретатор — принимает программу, написанную на каком-либо языке и исполняет ее. А компилятор просто транслирует программу, написанную на одном языке в программу на другом языке – чаще всего на машинном языке программирования, так, чтобы она могла выполняться процессором.

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

С ассемблером все несколько иначе, поскольку имеется почти точное соответствие между командами языка ассемблера и командами машинного языка, в которые они транслируются. Фактически в большинстве случаев вы знаете точно в какие байты будет оттранслирована конкретная конструкция языка ассемблера. Это делает ассемблер немного похожим на интерпретатор.

Возьмем самую очевидную директиву:

db 90h

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

Также можно не использовать мнемонических инструкций вообще в исходном коде и использовать только директивы DB, чтобы создать, например, какой-нибудь текст. В этом случае результат уже не программа вообще, так как она не содержит каких-либо машинных команд. Это делает ассемблер еще более похожим на интерпретатор.

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

Когда мы пишем некоторую простую последовательность команд на языке ассемблера:

mov     ax,4C00h
int     21h

то обычно не думаем о них, как директивах для интерпретации, которые генерируют команды процессора. Мы в действительности думаем о них, как если бы они и были командами, которые они генерируют, мы думаем о программе, которую мы пишем с помощью языка ассемблера, а не о программе на языке ассемблера. Однако в действительности есть две программы, соединенные в одну, два уровня восприятия одного и того же кода. Это дает ассемблеру новое качество: инструмент, который является одновременно и компилятором, и интерпретатором.


Уровень исполнения и уровень интерпретации

Давайте взглянем на два простых примера на языке ассемблера, в которых регистр EAX складывается с самим собой, повторяя эту операцию, пять раз. Первый пример использует регистр ECX для подсчета повторений:

   mov    ecx,5
square: add    eax,eax
        loop   square

Из этого фрагмента генерируются три машинные команды, и когда процессор выполняет машинный код, полученный из этого фрагмента, пять раз выполняется операция сложения регистра EAX с самим собой. Это делается путем уменьшения содержимого ECX на единицу и перехода снова к команде сложения, если содержимое ECX не равно нулю. Второй пример выглядит проще:

repeat 5
    add    eax,eax
end repeat

На этот раз директива ассемблера используется, чтобы повторить инструкцию пять раз. Но здесь не используется команда перехода. То, что делает ассемблер, встретив данную инструкцию, в действительности тоже самое, как если бы мы написали следующие команды:

add     eax,eax
add     eax,eax
add     eax,eax
add     eax,eax
add     eax,eax

Ассемблер генерирует пять копий одной и той же машинной команды. Если команда LOOP используется чтобы создать цикл времени исполнения – когда процессор исполняет машинный код, директива REPEAT создает цикл времени ассемблирования, она повторяет интерпретируемый блок, который находится между ней и директивой END. В нашем случае в блоке всего одна команда, но как мы сказали ранее, эта команда фактически директива, которая вызывает создание машинной команды. Таким образом, мы получаем пятикратное повторение интерпретируемой директивы ADD, которая каждый раз генерирует код команды сложения EAX с самим собой. Это один из хороших примеров уровня интерпретации языка ассемблера.

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

A=1
repeat 5
    A=A+A
end repeat

Этот фрагмент на диалекте языка ассемблера fasm определяет переменную времени ассемблирования с именем A и затем пять раз складывает ее саму с собой. Все что здесь происходит это интерпретация, здесь нет машинного кода или чего-то другого, влияющего на генерацию вообще. Фрагмент мог бы повлиять на уровень времени исполнения, если величина A в дальнейшем была бы использована в какой-либо машинной команде.


Использование fasm в качестве чистого интерпретатора

Как уже отмечалось на примере с директивой DB, результат работы ассемблера может и не быть программой вообще. В таком случае мы уже не рассматриваем ассемблер в качестве компилятора и ассемблирование становится эквивалентно работе обычного интерпретатора. Скопируйте код ниже в файл interp.asm, чтобы увидеть это на примере:

file 'interp.asm'
repeat $
    load A byte from %-1
    if A>='a' & A<='z'
        A = A-'a'+'A'
    end if
    store byte A at %-1
end repeat

Эта программа написана полностью с помощью интерпретирующего языка fasm, с использованием некоторых его расширенных возможностей. В начале он (fasm) начинает читать все содержимое файла interp.asm (где она сама и содержится) начиная с текущей позиции, которая всегда с момента начала работы ассемблера равна нулю и так повторяет для всех байтов файла. Поскольку $ всегда равна текущей позиции, то в конце загрузки $ становится равна длине файла. Затем берет каждый байт, преобразует его к верхнему регистру, на основе простого алгоритма, записывает обратно в текст программы. Так программа конвертирует все содержимое файла и заносит все в конечном итоге в выходной файл. Таким образом это пример программы преобразования текста, написанной на интерпретирующем языке и, поскольку выходным файлом тоже является текст, не содержащий машинных команды, функция ассемблера как компилятора полностью здесь отсутствует.


Разрешение кода

Последние примеры продемонстрировали функционирование ассемблера в качестве интерпретатора, что полностью объясняет, что он делает. Но после того, как мы признаем (я надеюсь), что ассемблер можно рассматривать в качестве интерпретатора, самое время показать, где эта интерпретация терпит неудачу. Дело в том, что в действительности ассемблер является и компилятором, и интерпретатором одновременно, поэтому оба эти термина в отдельности не объясняют точно, что делает ассемблер fasm.

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

Но ассемблер все равно должен сделать это, и здесь он ведет себя снова как компилятор, а не интерпретатор: имея команды и метки на языке ассемблера, необходимо получить эквивалент команды и адресов на машинном языке. Существует не так много возможных способов достижения этого. В одном из них нужно в начале интерпретировать в начале весь исходный код, оставляя места в созданном машинном коде для величин, которые не известны в тот момент, когда это необходимо. Затем, когда такая обработка закончится, и все значения меток станут известны, все эти пустые места заполняются правильными значениями.

Однако есть и другая проблема, если ассемблер должен сгенерировать возможно более оптимальный код (что касается размера, он также влияет на скорость выполнения). И машинный язык данной процессорной архитектуры, которую поддерживает ассемблер (архитектура x86 – единственная, поддерживаемая fasm) имеет инструкции разной длины в зависимости от диапазона используемых адресов. Например, в архитектуре x86 имеется короткая форма команды перехода на адреса, которые находятся не дальше 128 байтов вперед или назад, и длинная форма, аналогичного перехода, если нужно перейти на более далекие адреса. В таких случаях ассемблер может попытаться сгенерировать короткую форму, когда это возможно, но, если короткая форма используется, когда значение адреса перехода еще не известно, может оказаться, что такой адрес находится слишком далеко, и в данном месте следует использовать длинную команду перехода.

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

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

Весь процесс называется разрешением кода. Этот подход не только позволяет получить оптимальный код из данной последовательности команд и меток, но также позволяет интересным образом комбинировать уровень интерпретации и уровень компиляции. Посмотрите на следующий пример:

alpha:
    repeat gamma-beta
        inc     eax
    end repeat
beta:
    jmp     alpha
gamma:

Ассемблеру необходимо разрешить значения меток beta и gamma, так как они используются до их определения (такую ситуация обычно называют «ссылка вперед»). И эти значения определяют сколько раз команда INC будет повторяться. Уровень интерпретации здесь не очень заметен, так как все что нужно, это разрешение имен и ассемблер должен найти правильное решение самостоятельно. Но может оказаться, что решения вообще нет, например, в том случае, если из gamma мы будем вычитать alpha, а не beta. В этом случае очевидно разность будет каждый раз больше количества повторений, так что исходный код программы содержит противоречие и ассемблер не сможет найти решение.

В предыдущем примере уровень интерпретации как бы не виден на фоне проблемы разрешения. Но в следующем примере мы имеем случай, когда два уровня явно сосуществуют:

mov     eax,gamma
A = 1
repeat 5
    A = A*%
end repeat
label gamma at $+A-7

A – переменная времени ассемблирования и ее конечное значение вычисляется интерпретатором. Но затем эта величина используется, чтобы определить метку gamma, которая ссылается вперед и требует разрешения. Фактически мы могли бы даже использовать знак = для определения величины gamma, так как ассемблер будет рассматривать такое определение (когда определение gamma будет встречаться только один раз в ассемблируемом тексте) как определение глобальной константы, а не как переменной времени ассемблирования, поэтому она может ссылаться вперед. Это ведет нас к еще одному примеру разрешения кода:

dd x,y
x = (y-2)*(y+1)/2-2*y
y = x+1

Здесь знак = используется, чтобы объявить числовые константы, на которые имеются ссылки вперед и ассемблер должен разрешить эти величины. Кроме того, эти величины зависят друг от друга и довольно трудно сразу сказать есть ли решение, где определены все величины. Однако, если мы попробуем ассемблировать этот текст с помощью fasm, то он сможет найти решение за несколько проходов, в результате x=6 и y=7. Это действительно редкий случай, когда алгоритмы, предназначенные для оптимизации машинного кода, могут решить систему подобных уравнений, но это также дает нам чистый пример того, что из себя представляет разрешение кода.

Переместив директиву DD в последнюю строку представленного выше примера, мы получим, что только на y есть ссылка вперед, в то время как x оказывается добавочной переменной, необходимой для расчета независимой величины y. Мы можем даже разбить эти вычисления на несколько отдельных операций над x:

x = y-2
x = x*(y+1)
x = x/2-2*y
y = x+1
dd x,y

чтобы еще раз выделить уровень интерпретации. Но отметим, что мы не можем определить самостоятельную величину y без такой промежуточной переменной, так как fasm не позволяет ссылку вперед на переменную внутри ее собственного определения, так что определения, включающие предыдущие значения этой величины резервируются для переменных времени ассемблирования (как x в данном примере). Разрешение кода становится еще более сложным вопросом, когда мы рассматриваем директиву IF со всеми возможными сложными зависимостями, которые она создает. Вы можете найти некоторые примеры таких задач, в разделе, посвященном многопроходности в руководстве по fasm.

Конечно для данного набора зависимостей может существовать несколько корректных решений. В случае простых зависимостей между формой команды и значениями меток fasm пытается найти решение с возможно более короткими кодами команд, но это не означает, что ассемблер вообще всегда находит решение с самым коротким кодом, в некоторых случаях он не может найти решение, хотя оно и существует. Это потому, что предсказательные алгоритмы, которые он использует, ориентируются на получение качественного машинного кода, а не на решение сложных арифметических проблем, которые можно выразить с помощью языка. Но если fasm закончил свою работу без сообщений об ошибке, вы можете по крайней мере быть уверены, что все зависимости реализованы, а выходной код корректен по отношению к ним.


Препроцессор

Но у fasm есть еще один уровень, который делает всю картину более сложной. Это препроцессор. Основная особенность препроцессора заключается в том, что он обрабатывает весь исходный текст программы до того, как она попадает ассемблеру. То, что он обрабатывает исходный текст позволяет создавать с помощью простых операторов более сложные структуры, состоящие из ассемблерных команд. Имеется набор специальных директив, называемых директивами препроцессора, которые интерпретируются только препроцессором и удаляются из текста до передачи его ассемблеру. Все остальное, что имеется в тексте программы, передается препроцессором ассемблеру для обработки. Например, у нас есть такой текст:

mov ax,bx
include 'nop.inc'
mov cx,dx

и содержимое файла NOP.INC только команда:

nop

Что делает с ним препроцессор? Он интерпретирует вещи, которые он распознает, типа директивы INCLUDE, которая указывает, что следует поместить все содержимое файла NIP.INC в место, где она стоит. Все, что препроцессором не распознается, остается нетронутым. Так что в конечном итоге ассемблеру передается:

mov ax,bx
nop
mov cx,dx

Отметим, что для самого ассемблера не существует такой директивы как INCLUDE. Препроцессор приготовил нужную последовательность инструкций для него. Если, например, есть директива IF перед директивой INCLUDE, а директива END IF внутри включаемого файла, то ни препроцессору, ни ассемблеру не на что жаловаться: для препроцессора их не существует, он просто оставит их ассемблеру, но и ассемблер не увидит каких-либо противоречий, так как после препроцессора останется непрерывная и корректная последовательность ассемблерных директив.

Когда препроцессор вставляет новые строки в текст программы, например, заменяя директиву INCLUDE всеми строками из указанного там файла, он обрабатывает все эти новые строки, прежде, чем идти дальше. Т.е. включаемый файл также может содержать директивы препроцессора, которые распознаются и соответствующим образом обрабатываются. Но и строки, не содержащие препроцессорных директив, также могут измениться при их обработке препроцессором. Это происходит вследствие замещений, которые выполняет препроцессор. Такие замещения выполняются с так называемыми строковыми константами. Определяется строковая константа с помощью директивы EQU так:

A equ +

При таком определении имя A заменяется символом + всюду, где это имя будет обнаружено препроцессором после данного определения. Отметим, что поскольку препроцессор проходит один раз через весь текст программы (т.е. действует как чистый интерпретатор), замещение происходит только после определения A. Например:

mov eax,A
A equ ebx
mov eax,A

после препроцессинга:

mov eax,A
mov eax,ebx

Еще одна важная мысль касательно строковых констант – фактически они не константы, а переменные времени работы препроцессора, аналогичные переменным времени ассемблирования, которые задаются директивой =. Поэтому их можно переопределять и по-видимому, мы должны бы называть их строковыми переменными, хотя в руководстве они и называются константами (по чисто историческим причинам). Аналогично переменным времени ассемблирования мы можем переопределять такие переменные, с использованием их предыдущих значений, получая новые значения. Например:

A equ 2
A equ A+A

Определяет строковую константу со значением 2+2. Это работает потому что осуществляется замена строковых переменных их ранее определенными значениями в строке, содержащей EQU, но только после этого ключевого слова. Таким образом также:

A equ 2
B equ +
A equ A B A

определяет строковую переменную со значением 2+2. С точки зрения препроцессора пробел важен только там, где он используется чтобы отделить имена, которые бы слились в одно имя, если бы их не разделять. Любые другие пробелы игнорируются и удаляются – для препроцессора важна только последовательность атомов (лексем), которые он и учитывает.

Давайте теперь подытожим в чем различие между переменными времени работы препроцессора и переменными времени ассемблирования. Одно различие очевидно. Переменные времени ассемблирования сугубо числовые и всегда равны какому-то числу или значению адреса, тогда как строковые переменные могут иметь любое значение (они могут быть даже пустыми, если после директивы EQU стоят только пробелы и комментарии). Тот факт, что строковые переменные предназначены для текстовой подстановки можно продемонстрировать следующим примером:

nA = 2+2
    mov eax,nA*2
sA equ 2+2
    mov eax,sA*2

Первая строка определяет константу nA, присваивая ей числовое значение 4. В следующей строке эта величина отправляется в регистр EAX, и предварительно умножается на 2. Таким образом инструкция, которая в дальнейшем будет переведена в машинный язык - MOV EAX,8. В третьей строке определяется строковая константа sA, которой присваивается текстовое значение 2+2, и таким образом инструкция в последней строке будет изменена препроцессором на MOV EAX,2+2*2, что будет воспринято ассемблером уже как MOV EAX,6. Этот пример демонстрирует, что мы должны быть аккуратны, имея дело со строковыми переменными и всегда думать, что мы получим в тексте программы, когда вместо переменной будет подставлено текстовое значение, которое ей было присвоено. Еще одна тонкость здесь заключается в том, что директивы EQU и = обрабатываются на различных уровнях. Все замены, которые вызывает EQU делаются до того, как весь текст программы передается ассемблеру. Давайте взглянем на особенности в следующем примере:

A = 0
X equ A = A+< BR > X 4
dd A

После того, как программа будет «пережевана» препроцессором, вот что будет передано «на закуску» ассемблеру:

A = 0
A = A+4
dd A

Таким образом мы окончательно получим 32-битовое данное со значением равным 4. Этот пример показывает, как вы можете получить взаимодействие различных уровней. Однако такая задача требует, чтобы вы точно понимали, что какому уровню принадлежит. Мы еще обсудим далее вопрос смешения различных уровней.


Макроинструкции

Макроинструкции (часто для краткости называемые макросами) еще один инструмент препроцессора. Макрос – это инструкция для препроцессора и когда вы используете макрос с некоторым набором параметров, препроцессор использует эту инструкцию, чтобы создать новые строки программы, подставляя их в место, где макрос вызывается.
Определение макроса рассматривается препроцессором как одна большая директива (которая может охватывать несколько строк), и сама инструкция никак не обрабатывается препроцессором и не передается ассемблеру. Но когда вы вызываете макрос, однако, и препроцессор, используя инструкцию, создает новые строки, он также обрабатывает все эти строки, прежде чем идти дальше, подобно тому как он это делает в директиве INCLUDE. Давайте рассмотрим простой макрос:

macro Def name
{
     name equ 1
}

Так как все, что что стоит между скобками это содержимое макроса, препроцессор не обращает внимание на директиву EQU – все строки здесь это часть инструкции. Все, что препроцессор делает с этим, это отмечает для себя, что инструкция с именем Def есть и идет дальше, не передавая ничего ассемблеру. Но что происходит, когда вы используете макрос? Допустим, мы используем его так:

Def A

Препроцессор заменяет эту строку строками, генерируемыми на основе инструкции для макроса Def, в данном случае это будет только строка:

A equ 1

Эта новая строка интерпретируется обычным образом – препроцессор распознает директиву EQU и применяет ее к константе A.

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

Имеются специальные операторы и директивы, которые можно использовать внутри определения макроса – они управляют тем, как препроцессор выполняет инструкцию генерации новых строк. Так что очевидно, что, когда генерируются новые строки, эти директивы должны быть выполнены до того, как препроцессор закончит обрабатывать эти строки. Например:

macro Inc f
{
     include `f#'.inc'
}
Inc win32a

Имеем таким образом инструкция препроцессору преобразовать первое слово параметра f и затем объединить со строкой '.inc'. Если параметр состоит из одного слова, то результат такой операции будет строка и вызов макроса Inc сгенерирует такой результат:

include 'win32a.inc'

данный результат затем будет обычным образом обработан препроцессором, так что будет загружен файл win32a.inc.

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

macro Defx x
{
     \x db x
}

Здесь x параметр макроса, так что когда препроцессор выполняет инструкцию, то всюду, где встречает этот параметр, подставляет его значение. Однако в этом макросе определяется байт (переменная) с именем x, причем само значение переменной определяется параметром. Конфликт имен можно разрешить просто, дав параметру макроса другое имя, но здесь мы можете увидеть, как можно добиться желаемого результата используя escape-символ - экранирование. Когда генерируется строка препроцессор удаляет первый обратный слеш в начале каждого атома, никак не интерпретируя то что идет далее.

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

macro Parent [name]
{
  common
    macro Child [\name]
    \{
    \common
  forward
      name dd ?
  common
    \forward
      \name dd ?
    \}
}

и посмотрим, что происходит, когда мы вызываем макрос следующим образом:

Parent x,y

Не экранированные директивы макроса и имена параметров, интерпретируются при развертывании родительского макроса, тогда как экранированные имена (лексемы) остаются в определении дочернего макроса. В результате:

macro Child [name]
{
  common
    x dd ?
    y dd ?
  forward
    name dd ?
}

Рекомендуем подробнее рассмотреть данный пример, чтобы точно понять, что здесь происходит.
Ясно почему мы должны различать разные уровни, используя экранирование, как в выше представленном примере. Но иногда задают вопрос, зачем мы должны также экранировать закрывающие фигурные скобки.

Такое экранирование, очевидно, помогает отслеживать (особенно если есть несколько уровней вложения макросов) имена, которые принадлежат конкретным макросам и которые должны иметь столько же обратных слешей в начале, сколько имеют скобки, ограничивающие данный макрос. А в руководстве объясняется, что экранирование закрывающих фигурных скобок необходимо, потому что первая закрывающая скобка, интерпретируется препроцессором как конец макроопределения, так что, если вы не отметите обратным слешем закрывающую скобку дочернего макроса, оно будет принят за конец макроса родительского.

Но почему препроцессор не может сосчитать все скобки, чтобы определить какая из них закрывает какой блок? Ответ скрыт в том, что уже сказано. При разворачивании макроса препроцессор добавляет строку начала другого макроса, но не его конец, как это показано в примере альтернативного определения синтаксиса макроса в руководстве. Во время процесса генерации строк макроса препроцессор генерирует и определение другого вложенного макроса и добавляет строки из определения, пока не встретит закрывающую скобку (отметим, что препроцессор не обрабатывает генерируемые строки вложенного макроса и таким образом единственный способ закрыть такое определение – среагировать на закрывающую фигурную скобку; используя директиву FIX можно сгенерировать закрывающую скобку из другого идентификатора, как показано в руководстве).

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


Непосредственные макроинструкции

Есть ряд директив, которые определяют блок без имени, которые реализуются непосредственно в том месте, где они определены. Это директивы REPT, IRP, IRPS и также MATCH (этой директиве будет посвящен отдельный раздел), имеющие свои блоки, и мы называем их «непосредственными» микроинструкциями, чтобы подчеркнуть, что они реализуются (выполняются) непосредственно в том месте, где определены.

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

Чтобы показать, как непосредственные макроинструкции связаны с обычными макросами рассмотрим четыре эквивалентные конструкции

rept 4 i
{
  ; ...
}

irp i, 1,2,3,4
{
  ; ...
}

irps i, 1 2 3 4
{
  ; ...
}

macro Inst [i]
{
  ; ...
}
Inst 1,2,3,4

Если поместите одну и ту же последовательность директив в каждый из четырех представленных выше блоков, результат будет идентичен. В частности, директивы FORWARD, REVERSE и COMMON дадут один и тот эффект во всех случаях.

Из трех продемонстрированных выше непосредственных макроинструкций, IRP является единственной, где значения параметров в целом передаются также как в именованной макроинструкции – они отделяются друг от друга запятыми и т.о. могут иметь пустое значение (если только с помощью символа * после имени параметра не указано препроцессору, что параметр не может быть пустым). Также вы можете заключить значение параметра в угловые скобки (< и >), если необходимо, чтобы значение параметра содержало запятую.

Директива REPT сама по себе генерирует все возможные значения параметра-счетчика, можно задать базовое значение счетчика, или не использовать счетчик вообще. Однако, счетчик ведет себя также, как параметры макроса, получающие список возможных значений. Что касается подробного объяснения директивы IRPS, то мы в начале рассмотрим несколько деталей по поводу того, как препроцессор воспринимает исходный текст.


Лексемы и генератор строк

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

Первый класс лексем – символы. В руководстве утверждается, что это:

+-/*=<>()[]{}:,|&~#`

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

Любая последовательность символов, не являющихся специальными, например, последовательность букв и цифр, становится именованной лексемой. Такая последовательность может быть разбита на отдельные лексемы при помощи пробелов или других специальных символов. Например, в строке

mov     ax,2+1

содержится шесть лексем: первая именованная лексема MOV, затем именованная лексема AX (они разделены пробелами), запятая – специальная лексема, именованная лексема 2, символ плюс – специальная лексема и, наконец, именованная лексема 1. Добавление дополнительных пробелов в данную строку не повлияет на то, как это будет воспринимать препроцессор. Однако удаление пробелов между MOV и AX создаст новую именованную лексему.

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

db '2+2'

имеются две лексемы – за именованной лексемой следует строка в кавычках. Тем не менее, если кавычка не является первым символом лексемы, а стоит где-то внутри, она не имеет какого-то особого значения (кавычки не являются сами по себе специальными символами). Таким образом:

Jule's:

Только обычная именованная лексема, за которой следует символ «двоеточие».

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

Если за обратным слешем не следует лексемы, то он интерпретируется как символ продолжения строки, и лексемы на следующей строке присоединяются к лексемам текущей строки. Таким способом множество строк исходного текста могут сформировать для препроцессора одну строку.

Таким образом мы теперь понимаем, что строки, которые обрабатывает препроцессор, в действительности представляют собой последовательность лексем, а не просто текст. И такая последовательность появляется двумя путями – или из исходного программного текста или генерируется из макроинструкции. Т.е. мы имеем два вида генератора строк: генератор, читающий исходный текст (считыватель) и генератор, создающий строки на основе макроинструкции (обработчик макроинструкций).

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

Директива FIX обеспечивает текстовую замену (когда лексема может заменяться некоторой последовательностью лексем), подобно директивам EQU или DEFINE, однако эти определения и замены осуществляются на стадии чтения исходного текста, когда строки готовятся для обработки препроцессором. Таким образом вы можете сделать определенные замены, до того, как начнется работа препроцессора.

Поскольку замена, определяемая директивой FIX осуществляется на стадии чтения, то она может реально пригодится для некоторых синтаксических настроек, в том случае, когда некоторые текстовые конструкции, используемые программистом в исходном файле, обрабатываются препроцессором уже как нечто совсем другое. В официальном руководстве содержится пример использования FIX для определения другого способа выделения макроинструкций – использование лексемы ENDM вместо закрывающей скобки. Это один из не многих примеров, когда такая возможность может реально пригодится.

Обработчик макроинструкций имеет больше специальных команд и операторов, чем считыватель. Он не только заменяет параметры макроинструкции их значениями и обрабатывает некоторые специальные команды, такие как COMMON или FORWARD, но и осуществляет конкатенацию лексем, соединенных оператором # или конвертирует лексему в строку, при наличии оператора `. Эти операторы не распознаются считывателем и, таким образом, они работают только внутри макросов. Что касается обратного слеша, то он удаляется из начала лексемы, когда обработчик макроинструкции помещает ее во вновь генерируемую строку. Если в начале лексемы стоит несколько обратных слеша, то удаляется только первый, таким образом лексема будет обрабатываться много раз при обработке макроинструкции, пока наконец не будут удалены все обратные слеши и ни откроется другой символ.

Еще одна команда, предназначенная обработчику макроинструкций – директива LOCAL. Она сообщает, что определяется новые макропараметры в дополнение к обычным параметрам, и что величина, которая присваивается каждому из них, должна быть уникальной лексемой, генерируемой каждый раз, когда обрабатывается макроинструкция. Затем они замещаются этими величинами в каждом месте текста макроинструкции, где они встречаются, подобно тому, как это осуществляется с обычными параметрами. Таким образом, например, обработчик макроинструкций будет генерировать новые уникальные значения меток, каждый раз, когда они используются. Если имя локального параметра совпадает с именем обычного параметра, то он замещается локальным параметром.

macro Testing a
{
  db `a,13,10
  local a
  db `a,13,10
}

Testing one

Ассемблируя представленный выше пример и взглянув сгенерированный текст, вы увидите, какие значения будут присвоены параметру, определенному с помощью директивы LOCAL.

Есть еще одна важная деталь, демонстрируемая данным примером: обработчик макроинструкции выполняет преобразование лексемы только после того, как заменит параметры их значениями. Тоже правило применимо операции конкатенации над лексемами.

С другой стороны, как уже говорилось, оператор преобразования действует только на единичную лексему. Если параметр замещается последовательностью более, чем в одно слово, оператор ` воздействует только на первое из них. Это возможно трудно для понимания, например, выше представленный макрос не будет работать корректно, если параметр будет представлять собой последовательность отдельных слов, поскольку только первое из них будет преобразовано в строку, и таким образом, содержимое для директивы DB будет не корректным. Но это как раз тот случай, когда можно использовать директиву IRPS.


Обработка индивидуальных лексем

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

Таким образом любая последовательность величин, которая передается в макрос, может быть разобрана и обработана по частям (лексемам). Например, таким образом можно обойти проблему, когда оператор преобразования лексемы может конвертировать за раз только одну лексему. Этот макрос обрабатывает все лексемы, передаваемые через параметр и выводит их при ассемблировании:

macro Tokens sequence
{
  irps token, sequence
  \{
    display \`token,32
  \}
}

Tokens eax+ebx*2+1

Отметим, что оператор преобразования, должен быть экранирован, иначе он будет обрабатываться при развертывании внешнего макроса и IRPS не
«увидит» его (вместо этого в строке просто появится слово «token»).

Представленный выше пример не будет работать, если последовательность в последней строке модифицировать, добавив запятую – просто потому, что запятая будет рассматриваться как разделитель, отделяющий различные значения для параметров. Конечно, можно выделять параметр, который содержит запятую заключая величину в угловые скобки (< и >), но возможно также модифицировать макрос, чтобы он работал даже когда запятые используются в качестве разделителей:

macro Tokens [sequence]
{
  common irps token, sequence
  \{
    display \`token,32
  \}
}

Tokens mov ax,2+1

Директива COMMON берет все отдельные величины последовательности, переданные через параметр, и соединяет их со всеми разделителями в одну цепочку, которая в этом случае точно соответствует оригинальной строке. Но есть еще одно слабое место в макросах. Оператор преобразования ничего не делает, когда лексема является строкой в кавычках. Так что при выводе в данном макросе нет способа передать, является данная лексема единой строкой (в кавычках) или нет. Это можно решить, используя условное ассемблирование.

macro Tokens [sequence]
{
  common irps token, sequence
  \{
    if '' eqtype token
      display "'",token,"'",32
    else
      display \`token,32
    end if
  \}
}

Но такое решение немного похоже на смесь яблок с апельсинами. Оператор EQTYPE выполняется на стадии ассемблирования и позволяет различать несколько классов синтаксических структур, которые распознаются ассемблером. Например, оператор может отделить вещественные числа от имен регистров. В частности, оператор различает строку в кавычках, потому что это отдельный класс синтаксических элементов в fasm – и по этой причине EQTYPE может быть использован здесь, чтобы определить, когда лексема представляет собой строку в кавычках.

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


Условный препроцессинг

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

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

Шаблон с другой стороны также подчиняется строгим правилам. В нем содержаться элементы, которые определяют какие лексемы или группы лексем должны быть во втором параметре. Эти элементы делятся на два вида: элементы, которые требуют точного соответствия и элементы типа «шаблон поиска».

Любой символ (исключая =) и любая строка в кавычках являются элементами точного соответствия. Также перед любой лексемой (даже лексемой, которая должна совпасть буквально в любом случае) может стоять знак =, чтобы указать на буквальное соответствие элемента. В частности, == и =, могут быть использованы для сравнения знака равно и запятой, соответственно, так как их нельзя использовать напрямую, поскольку они используются шаблоне в качестве служебных символов.

Все примеры ниже используют шаблоны, составленные из элементов для буквального соответствия, чтобы получить позитивный результат по отношению к величине, которая стоит после запятой.

match +,+
{ display "special character is matched as-is",13,10 }

match 'a','a'
{ display "the same with quoted string",13,10 }

match =a,a
{ display "the name token must be preceded with =",13,10 }

match=a=+='a' , a+'a'
{ display "and = may be actually used with any token",13,10 }

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

В простейшем случае, если шаблон представляет собой некоторую именованную величину, он будет соответствовать величине, которая идет за запятой, если в ней есть хотя бы одна лексема. Следовательно, этот факт можно использовать для выполнения блока MATCH, при условии, что величина не является пустой.

macro check value
{
  match any,value
  \{ display "the value is not empty" \}
}

В этом примере директива MATCH встраивается в другой макрос (поэтому ее скобки должны быть экранированы) и может, таким образом, проверить что передается в качестве параметра, поскольку параметры заменяются их значениями во время разворачивания макроса препроцессором. Так что, когда строки, полученные при развертывании внешнего макроса, обрабатываются препроцессором, начинается интерпретация директивы MATCH, которая получает это самое значение от макроса, помещаемое после запятой.

Это не означает, что директива MATCH полезна только, когда она внутри макроса. Директива MATCH может быть удобна, и сама по себе, благодаря тому, что строковые константы в заголовке директивы, заменяются их значениями, перед тем как директива будет выполнена.

define X

match any,X
{ not preprocessed }

define X +

match any,X
{ display `any }

В представленном выше примере первая директива MATCH сравнивает величину, которая в действительности пуста, потому что символическая константа получает пустое значение до того, как выполнится MATCH. Поскольку шаблон должен сравниваться с чем-либо и условие таким образом не выполняется - блок MATCH не выполняется. Прежде чем выполнится вторая директива MATCH, символической константе присваивается значение, и в этом случае величина не пуста, таким образом шаблон соответствует значению X и становится равным этому же значению.

Единственная возможность получить положительный результат, если значение после запятой пусто, это использовать пустой шаблон, поскольку элементу шаблона должно быть поставлено не пустое значение из величины после запятой. С другой стороны, любая последовательность элементов шаблона стремится найти соответствие в наибольшем количестве лексем. Если шаблон состоит из двух или более элементов, то каждый элемент, кроме последнего ставится в соответствие одной лексеме из выражения справа (и положительный результат, таким образом возможен только в том случае, если выражение справа от запятой состоит по крайней мере из такого же количества лексем, что и шаблон). Рассмотрим пример:

match car cdr, 1+2+3
{
  db car
  db cdr
}

Здесь мы имеем в шаблоне два элемента (лексемы) и они оба теоретически могут получить соответствующее парное значение из последовательности справа. Однако, согласно алгоритму, которому придерживается fasm первый элемент шаблона получит одну лексемы, оставив все остальное второму элементу. Таким образом директива DB в первом случае получит “1”, тогда как вторая директива DB получит “+2+3”.

Добавляя некоторые элементы, требующие точного совпадения, между лексемами, можно добавить дополнительные условия в процесс выполнения директивы MATCH, но общих правил вполне достаточно, чтобы узнать результат, как в данном примере:

match first:rest, 1+2:3+4:5+6
{
  db first
  dd rest
}

Здесь первому элементу шаблона ставится в соответствие все справа от запятой до двоеточия (которое сравнивается буквально), поскольку это максимально-возможный вариант соответствия. Следовательно, для директивы DB мы получаем “1+2”, тогда как для директивы DD получается весь остаток справа от первого двоеточия, т.е. “3+4:5+6” (отметим, что синтаксис DD это допускает, как сегментный адрес и смещение, поэтому такое выражение ассемблируется корректно).

Буквальное сравнение строк в кавычках, хотя и менее полезно, чем другие случаи, но может быть использовано для специфической цели определения, является величина параметра строкой или нет. Макрос из предыдущей главы может быть переписан для использования в условном препроцессинге, как это показано ниже.

Отметим возможность не обычного использования директивы LOCAL (см. ниже) (не важно, относится она к внешнему макросу или внутреннему макроопределению - IRPS) - поскольку она не экранирована, она обрабатывается при развертывании внешнего макроса и не выдает результат в генерируемый внешний файл внутренней макрокомандой, таким образом не нарушает синтаксис:

macro Tokens [sequence]
{
  common irps token, sequence
  local output
  \{
    output equ \`token,32
    match \`token,token
    \\{ output equ "'",token,"'",32 \\}
    display output
  \}
}

Поскольку оператор преобразования не действует на строку в кавычках, конвертируемая лексема, т.о., будет равна своему оригиналу тогда и только тогда, когда оригинал представляет собой строку в кавычках. Чтобы проверить это условие в директиве MATCH в качестве шаблона берется преобразуемая лексема, что дает гарантию того, что каждый раз будет осуществляться буквальное сравнение (поскольку шаблон всегда будет строкой в кавычках). Таким образом содержимое блока директивы MATCH (который должен быть дважды экранирован, потому что находится внутри двух макроопределений) будет обрабатываться только в том случае, если лексемы представляют собой строки в кавычках.


Обработка синтаксических конструкций пользователя

Благодаря шаблонам поиска, директива MATCH может делать гораздо больше, нежели просто проверять простые условия. Можно использовать ее для разбора некоторых, определяемых пользователями конструкций и, следовательно, определять макроинструкции с более гибкой структурой аргументов.

Допустим мы хотим определить макрос, который позволял бы писать "let al=4" вместо "mov al,4". Обычно макроинструкции принимают параметры, разделенные запятой, так что макрос "let" определен так, что "al=4" будет восприниматься как один параметр. Следовательно, нам необходимо встроить директиву MATCH в макрос, чтобы распознать подобную структуру в параметре и разбить ее на две части:

macro let param
{
  match dest==src,param
  \{
     mov dest,src
  \}
}

Поскольку символ = имеет специальное значение в шаблоне MATCH, конструкция == должна быть использована, чтобы найти буквальное соответствие для =. Таким образом MATCH даст позитивный результат при условии, что параметр макроса имеет структуру, когда перед знаком = и после него что-то стоит. В противном случае макрос не сработает. Но мы можем добавить еще синтаксиса, в виде дополнительной директивы MATCH следующим образом:

macro let param
{
  match dest==src,param \{ mov dest,src \}
  match dest++,param \{ inc dest \}
}

Эта версия не только позволяет генерировать "mov al,4" вызовом "let al=4", но также позволяет писать "let al++", получая команду "inc al". Но теперь рассмотрим возможность дополнительно писать "let al+=4", чтобы в результате получить "add al,4". Мы могли бы просто добавить еще одну строку к макросу, представленному выше:

match dest+==src,param \{ add dest,src \}

Но несмотря на то, что данная строка обеспечивает ADD инструкцию, если мы напишем «let al+=4", шаблон "dest==src" сработает для этого случая с присвоением "dest" значения "al+" и мы получим ошибочную команду "mov al+,4", которая будет сгенерирована.

Чтобы решить данную проблему нам необходимо учесть информацию, что данная структура уже воспринята и предотвратить, чтобы другие директивы MATCH сработали в этом случае. Каждая директива MATCH независима, так что для того, чтобы учесть такой случай нужно задействовать строковую переменную. Первое решение, которое приходит на ум, представлено ниже. Отметим, что нам надо распознать шаблон "dest+==src" в начале, чтобы "al+=4" воспринималось со структурой +=, а не с единичным символом =.

macro let param
{
  local status
  define status 0
  match dest+==src,param
  \{
     add dest,src
     define status 1
  \}
  match =0,status
  \{
     match dest==src,param
     \\{
 mov dest,src
 define status 1
     \\}
  \}
}

Но это можно сделать проще. Вложенные директивы MATCH можно заменить одной:

match =0 dest==src  , status param
  \{
     mov dest,src
     define status 1
  \}

Запятая, которая отделяет шаблон от сравниваемого текста, обрамлена с двух сторон пробелами, чтобы было видно, что реально сравнивается. Так как первая лексема в шаблоне, которая сравнивается непосредственно, равно 0, а первая лексема в выражении справа равна или 0 или 1 – единственное возможное значение, то блок MATCH может выполниться только в том случае, если лексема status будет равна 0. Тогда оставшийся текст, который зависит от параметров макроса, сравнивается с "dest==src".

Полный текст макроса может иметь, т.о. вид:

macro let param*
{
  local status
  define status 0
  match =0 dest+==src , status param
  \{
     add dest,src
     define status 1
  \}
  match =0 dest==src , status param
  \{
     mov dest,src
     define status 1
  \}
  match =0 dest++ , status param
  \{
     inc dest
     define status 1
  \}
  match =0 any , status param
  \{
     err "SYNTAX ERROR"
  \}
}

Каждый из шаблонов содержит =0 для сравнения с переменной status справа от запятой, даже в первой директиве MATCH, что излишне, однако создает единообразие в представленном фрагменте. Последняя директива MATCH осуществляет проверку на тот случай, если все предыдущие директивы MATCH не распознали синтаксис, и сообщает об ошибке. Проверка на случай пустого параметра не нужна, так как после имени параметра в заголовке макроса указан знак *. В противном случае нам пришлось бы добавить еще одну директивы (на этот раз последнюю) MATCH, для проверки случая пустого параметра.

Использования директивы ERR требует некоторых пояснений. Эта команда обрабатывается на стадии ассемблирования (так что она может и не сработать, если будет находиться в блоке IF), но вызывает мгновенное прекращение процесса обработки, как только она встречается ассемблеру, даже если это происходит в какой-то промежуточной стадии процесса разрешения кода. Следовательно, цель этой директивы сообщить об неустранимой ошибке; в данном случае синтаксической. Директива ERR не имеет дополнительных правил, касающихся ее аргументов – она прерывает ассемблирование с сообщением об ошибке не обращая внимание на наличие дополнительного текста в строке. В выше представленном примере в строке вставлена дополнительная текстовая константа, чтобы прояснить о какой проблеме идет речь – fasm всегда выводит строку, вызвавшую ошибку, так что это сообщение будет представлено на консоли.


Принципы проектирования или чем отличается Fasm?

Содержание
Принципы проектирования или чем отличается Fasm?

Целью данной статьи является описание основных идей, которые повлияли на Fasm за все время его развития. Первоначальное пректирование указывает направление, в котором программа может развиваться, и несколько ограничивает возможное расширение возможностей Fasm. Я написал этот текст в марте 2005 года, чтобы объяснить, как Fasm дошел до того места, где он находится, и каковы были причины такого большого решения, которое я принял.


1. С чего все началось - TASM против NASM

Когда я выучил язык ассемблера, я использовал исключительно Turbo Assembler от Borland, но это был коммерческий продукт, и у меня не было личной копии. Вот почему я заинтересовался новым продуктом Netwide Assembler, который был бесплатным и даже с открытым исходным кодом. Но, хотя мне понравились некоторые из его идей, я был в целом разочарован отсутствием многих функций, к которым я привык при использовании TASM. Поэтому я отказался от этого и даже не начал использовать NASM ни для одного из моих проектов. Вместо этого я попытался с успехом написать свой собственный ассемблер (на самом деле я написал два, а fasm - последний, но я пропущу описание первого, так как он имеет тот же синтаксис и меньше функций), с возможностями, достаточными для сборки всех моих ранее написанных проектов с небольшими изменениями в исходных текстах.

Тогда должно быть очевидно, что синтаксис, который я выбрал для fasm, в основном имитировал тот, который я использовал при программировании с TASM, и важно отметить, что TASM предлагает два режима, с разными синтаксисами, первый и по умолчанию MASM-режим совместимости, а второй называется Ideal mode. Изучив основы ассемблера, я быстро переключился в Ideal mode, так как нашел его проще и менее запутанным.

Существуют две основные характеристики Ideal mode, которым я руководствовался при разработке синтаксиса для Fasm. Первый - это синтаксис для доступа к содержимому памяти. TASM с выбранным Ideal mode требует, чтобы такой операнд всегда был заключен в квадратные скобки, и они также гарантировали, что данный операнд всегда будет интерпретироваться как содержимое памяти - в то время как в режиме MASM квадратные скобки интерпретировались по-разному в различных ситуациях, что вызывает у меня ощущение хаоса. Поэтому я очень быстро привык к использованию квадратных скобок для обозначения операндов памяти, и я разработал то же правило синтаксиса для моего собственного ассемблера. NASM пошел в том же направлении и упростил его еще больше. В NASM, когда вы определяете переменную с каким-либо именем, это имя становится константой, равной адресу этой переменной. Поэтому каждая метка - это просто константа. Красиво и просто, но это была одна из тех вещей в NASM, которая мне не нравится. Потому что я привык к тому, что когда я определил некоторую переменную как байт:

alpha db 0

и затем попытался получить к ней доступ так:

mov [alpha],ax

TASM отказался бы принять это, потому что я пытался хранить некоторые большие данные в меньшей переменной. Эта особенность ловила много ошибок, и я чувствовал, что не могу от нее отказаться. Но мне все еще нравилась идея, что с меткой нужно обращаться как с константой, равной адресу, так как это сделало бы такие инструкции:

mov ax,alpha
mov ax,[alpha]

прямая аналогия:

mov ax,bx
mov ax,[bx]

и с таким синтаксисом очень просто и легко, например, настроить некоторый алгоритм для использования абсолютной адресации вместо основанной на регистре или наоборот. Следствием этого также стало избавление от оператора OFFSET, но я мог принять это изменение - этого было достаточно, чтобы заменить слово OFFSET пустой строкой во всех моих источниках. Однако в плоском ассемблере каждая метка, хотя и является на первый взгляд просто адресом, все же хранит информацию о том, какой тип переменной определяется за ней, и обеспечивает проверку размера, как у меня это было с TASM. Конечно, в программировании на ассемблере все еще нужен какой-то способ принудительного изменения размера, когда вы этого хотите. В TASM оператор переопределения размера должен быть помещен перед именем переменной внутри квадратных скобок. Но так как я следовал NASM, интерпретируя квадратные скобки как переменную (с адресом внутри, определяющим, что это), было более логично требовать оператора размера перед квадратными скобками, и это также согласуется с другой функцией, принятой из NASM, то есть любому операнду может предшествовать оператор размера, даже если он может быть избыточным. Но нет необходимости использовать эту функцию так часто, как с NASM, поскольку благодаря хранению информации о переменных типах fasm обычно может угадать размер - таким образом я получил то, что, по моему мнению, было лучшим из двух миров, и было первая веха в дизайне синтаксиса fasm. И мне все еще нужны были только небольшие изменения в моих источниках, чтобы преобразовать их в новый синтаксис, небольшой пример для сравнения:

mov [byte cs:0],0 ; TASM Ideal
mov byte [cs:0],0 ; fasm

Вторым характерным атрибутом синтаксиса, взятым из TASM Ideal mode, является размещение директив определения перед именем определяемого объекта. Это не относится к определениям данных, но такие директивы, как LABEL, MACRO или PROC, работали таким образом в Ideal mode, в то время как в режиме MASM имя всегда было перед директивой. Возможно, из-за некоторых предыдущих привычек языка более высокого уровня (например, Паскаля) мне больше нравился вариант Ideal mode.

Поэтому я скопировал синтаксис директив LABEL и MACRO из синтаксиса TASM Ideal mode с одним изменением: вместо того, чтобы заканчиваться директивой ENDM, содержимое макроинструкции должно быть заключено в фигурные скобки. Просто потому, что мне нравились брекеты, и их тоже было проще разбирать. Я также реализовал директиву LOCAL с тем же синтаксисом, что и в TASM, и таким образом реализовал все функции, которые использовал в TASM. Другие, более мощные возможности макроинстукций были реализованы намного позже, когда влияние TASM уже было утрачено и на его место вышли другие приоритеты проектирования (которые будут описаны ниже).

К списку вещей, которые были взяты из TASM, я мог бы также добавить ключевые слова USE16 и USE32, хотя TASM разрешал их только в объявлении сегмента, а fasm позволяет использовать их для переключения типа сгенерированного кода где угодно. Вот где появился второй принцип проектирования.


2. Гибкость - средства разработки ОС

Чтобы понять происхождение плоского ассемблера, также важно заметить, что я одновременно пытался разрабатывать некоторые ОС, и я разрабатывал fasm как инструмент, нацеленный в основном на эту цель. Вот почему было важно сделать его легко переносимым, и как только я закончил его, я портировал его в ОС, которую я разрабатывал, чтобы иметь возможность писать программы для него в своей родной среде. Это также может считаться причиной, по которой я написал fasm на ассемблере, однако это более вероятно, потому что я делал все свое программирование на ассемблере в эти дни - если бы я действительно предпочел какой-то язык высокого уровня, я бы сделал вместо этого какой-то самокомпилирующийся компилятор высокого уровня.

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

Также для целей разработки ОС я реализовал директиву ORG, которая немного отличается от оригинальной в TASM. Мне нужно было установить исходный адрес данного кода, но фактически не перемещая точку вывода в файле. Я думаю, что ответственность за загрузку кода в указанном им источнике должна лежать на программисте, как это делает DOS с программами .COM - это опять-таки важно при разработке ядра ОС, где у вас может быть несколько разных частей кода, которые будут помещены в разные места и могут быть решены по-разному. Директива ORG в моей версии позволяет спроектировать код для правильной работы при загрузке в указанном источнике, а его размещение в файле определяется порядком источника. Мой ассемблер, генерирующий код в плоском адресном пространстве, всегда выводил код точно в том же порядке, как это было определено в исходном коде. Так пришло название для него - flat assembler.

И по той же причине я изобрел совершенно новую функцию - директиву VIRTUAL. С TASM, когда я хотел получить доступ к некоторым структурам ядра ОС, которые я размещал по адресам, отличным от адресов в пространстве кода ядра, мне приходилось вычислять адреса вручную (обычно определяя цепочки констант, где каждая из них была равна предыдущей, увеличенной на размер данных). Моя новая директива позволяла объявлять структуры по заданному адресу, не вводя никаких фактических данных в вывод. Некоторые другие применения директивы VIRTUAL были изобретены гораздо позже, изначально это было только это.

Выход Fasm по умолчанию был простым двоичным файлом, поскольку он был наиболее удобным для программирования ОС и позволял также создавать программы .COM. Но вскоре я добавил также возможность вывода перемещаемого формата, который я разработал для своей операционной системы (я удалил эту функцию до официальных выпусков). Однако формат вывода был выбран не с помощью параметров командной строки, а с помощью директивы - это была идея, совершенно отличная от того, что предлагали другие ассемблеры, прямое следствие нового принципа, с которым я пришел.


3. Тот же источник, тот же вывод

Проблема с параметрами командной строки, выбирающими вариант вывода в случае ассемблирования, состоит в том, что данный код в любом случае, скорее всего, будет собираться и выполняться правильно только тогда, когда выбран тот же вывод, который имел в виду программист при написании этого кода. Также я вспомнил много случаев, когда у меня был источник для TASM, написанный кем-то другим, и чтобы правильно его скомпилировать, я должен был следовать указаниям, приведенным в комментарии в начале источника, и просто переписать все ключи командной строки, как описано там. И я подумал: почему бы просто не заставить ассемблер искать такие опции в исходном коде, чтобы никто не столкнулся с проблемой перекомпиляции? Так появился принцип SSSO - все настройки, которые могут повлиять на вывод ассемблера, выбираются только из источника, а источника всегда достаточно, чтобы сгенерировать именно тот файл, который был задуман программистом. Следствием идеи SSSO было также то, что независимо от того, какая версия fasm (учитывая порты для разных операционных систем), она всегда генерировала один и тот же выходной файл, поэтому, когда вы написали программу для DOS, версия fasm для Linux все равно сделает тот же DOS исполняемый файл из такого источника.

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

Тем не менее, все еще возможно, что один и тот же исходный файл будет собран по-разному в другой среде, потому что он может включать в себя некоторые другие файлы, и их содержимое и доступность могут варьироваться от компьютера к компьютеру и от системы к системе. Пути к файлам должны соответствовать правилам для данной операционной системы, и то, какой файл загружается, может зависеть также от переменных среды, поскольку fasm позволяет им раскрываться в значениях пути. Чтобы избавиться от этой зависимости от среды, ассемблеру придется отказаться от функций включения файлов. Ну, я знаю одного ассемблера, который выбрал этот путь, но это не то, что я мог бы даже рассмотреть для моего ассемблера. Таким образом, мое обоснование заключается в том, что системные файловые пути определяют, какой источник (составленный из, возможно, множества разных файлов) ассемблер получит для окончательной обработки, но после этого начинает действовать принцип SSSO.


4. Разрешение кода

Была еще одна особенность Turbo Assembler, которую я хотел иметь в своем ассемблере: оптимизировать размер смещений с помощью нескольких проходов, чтобы определить, какие смещения могут подходить для более короткого диапазона, а какие - нет. Чтобы сделать эту возможность возможной, мне пришлось сделать метки - которые с точки зрения программиста на самом деле являются константами - переменные времени сборки, которые постоянно обновляются при каждом проходе, чтобы отражать изменения кода в связи с оптимизации. И по этой причине мне пришлось выполнять обработку структур, таких как IF или REPEAT, использующие выражения, которые могут зависеть от значения таких меток, во время этих проходов, а не ранее. Поэтому в fasm директива IF не влияет на обработку макроинструкций или других директив, интерпретируемых препроцессором - это может смущать людей, начинающих изучать синтаксис fasm, но было действительно необходимо для корректного разрешения таких исходников как:

if alpha > 100
   ; some code here
end if

Так как при этом проверяется значение некоторой метки, которое может меняться в зависимости от прохода, правдивость условия может также меняться в зависимости от прохода, что может привести к цепочке еще более сложных изменений. Основное правило для плоского ассемблера всегда заключалось в том, что он не может выдавать код, который не разрешен полностью и надежно. Так что если есть хоть малейшее подозрение, что какое-то значение могло быть использовано при генерации кода с другим значением, чем оно в итоге получилось, Fasm делает больше проходов, пока все не совпадает. Этот процесс можно описать как попытку решить сложное уравнение с помощью итерационных приближений. Конечно, иногда решения не существует, как в данном случае:

alpha:
        if alpha = beta
           db 0
        end if
beta:

В таком случае ассемблер будет делать все больше и больше проходов, никогда не приближаясь ни к какому решению. Но поскольку существует ограничение на возможное количество проходов, встроенное в ассемблер, он, в конце концов, выйдет с сообщением об ошибке, в котором будет указано, что "code cannot be generated".

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

Знание того, как Fasm разрешает код, важно для понимания специфических самонезависимых исходных текстов. Рассмотрим такой пример:

if ~ defined alpha
   alpha:
end if

Если предположить, что эта метка не определена больше нигде в исходном коде, то в первый проход ассемблер, конечно, выполнит блок и определит метку так, как можно было бы ожидать. Но во втором проходе он предсказывает, что метка была определена (так как она была определена в предыдущем проходе) и пропустит этот блок. Это приведет к "мертвой петле" и остановится на границе проходов с ошибкой. Для того чтобы fasm корректно разрешил источник, нужно сделать это следующим образом:

if ~ defined alpha | defined got_alpha_here
   alpha:
   got_alpha_here = 1
end if

Таким образом, на первом проходе блок собирается, потому что метка еще не определена, а на более поздних проходах блок собирается из-за константы, которая отмечает, что этот блок был собран на предыдущем проходе и, следовательно, должен быть собран снова.

Для сопоставления значений прогнозируемого и фактического значения метки ассемблер конечно не может позволить определить метку в более чем одном месте. Это, однако, не относится к константам, определенным с помощью оператора =, который, в отличие от их имени, может быть переопределен, но в этом случае ассемблер просто запрещает прямую ссылку на них (что означает использование значения символа в источнике раньше, чем он получает на самом деле определены), и тогда прогнозирование не требуется. Но если константа определена только в одном месте, прямые ссылки допускаются так же, как и с любым другим типом метки.

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


5. Сложные решения с простыми особенностями

Этот последний принцип развился позже, когда - после выхода Windows версии плоского ассемблера - возникла необходимость разрешить некоторые синтаксисы более высокие уровня. Я боялся, что добавление множества новых возможностей, которые изначально не планировалось, может привести к непредсказуемым взаимодействиям между существующим и новым, и это было последнее, чего я хотел в своем ассемблере, когда одним из моих главных правил было заставить его всегда решать код логичным, однозначным образом. Поэтому вместо написания целого набора новых функций для этой цели я пытался реализовать их в виде макроинструкций, расширяя возможности препроцессора только в том случае, если хорошее макрорешение для данной задачи не может быть достигнуто без таких расширений. Но даже при добавлении новой функции, я всегда делал это в активном режиме, в первую очередь желая убедиться, что она не будет взаимодействовать с существующими. И всегда старался найти самое простое расширение, какую-то действительно низкоуровневую функцию, которая могла бы быть применена для решения множества различных проблем.

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

Но все же этот подход имеет некоторые сильные преимущества. Строительные блоки определены простым способом, и эти функции легко поддерживать, и в то же время их можно использовать для построения очень сложных конструкций, которые могут даже определять новые синтаксисы или предназначаться для разных машин, на которых был написан fasm. за. Я надеялся, что смогу поддерживать это простое и легкое ядро ??fasm, в то время как другие люди могут создавать пакеты расширенных макроинструкций для различных конкретных целей. И за эти годы это видение было по крайней мере частично реализовано - некоторые участники на доске объявлений уже создают пакеты, более сложные, чем любые макроинструкции, которые я написал сам, и делают с ними некоторые удивительные вещи. И это дает мне чувство выполненного долга, когда я вижу, как другие используют мой ассемблер для создания вещей, которые являются более впечатляющими, чем все, что я делал до сих пор.


Заключение

Конечно, этот текст далек от полноты с точки зрения описания конструкции fasm. Но он показывает основные направления и должен быть достаточен для объяснения большинства сделанных мною решений. В любом случае, причина всего этого также в том, что я держу проект fasm как "видение одного человека", подчеркивая усилия по сохранению общей логики и последовательности. Я надеюсь, что этот текст поможет другим понять мои мотивы и видение.


Введение и обзор flat assembler g

Содержание
Введение и обзор flat assembler g

Введение в flat assembler g, которое показывает предназначение этого движка и демонстрирует, как построить ассемблер для любой архитектуры процессора с помощью макроинструкций.


Что такое flat assembler g?

Это ассемблерный движок, разработанный как преемник того, который используется в fasm 1, одном из признанных ассемблеров для процессоров x86. Это голый движок, который сам по себе не способен распознавать и кодировать инструкции любого процессора, однако он может стать ассемблером для любой архитектуры процессора. Он имеет язык макроинструкций, который существенно улучшен по сравнению с языком fasm 1, и позволяет легко реализовать кодирование команд в виде настраиваемых макроинструкций.

Исходный код этого инструмента может быть скомпилирован с помощью fasm 1, но также можно использовать сам fasmg для его компиляции. Источник содержит предложения, которые включают различные заголовочные файлы в зависимости от используемого ассемблера. Когда fasmg компилируется сам, он использует предоставленный набор заголовков, реализующих инструкции x86 и форматы с синтаксисом, в основном совместимым с fasm 1.

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

Чтобы продемонстрировать, как могут быть реализованы наборы команд различных архитектур, приведем несколько примеров программ для микроконтроллеров 8051 и AVR. Они оставались простыми, и поэтому они не обеспечивают полную основу для программирования таких процессоров, хотя они могут обеспечить прочную основу для создания таких сред.

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


Как это работает?

Основная функция fasmg - генерировать выходные данные, определенные инструкциями в исходном коде. Учитывая одну строку текста, как показано ниже, ассемблер будет генерировать один байт с указанным значением:

db 90h

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

macro int number
        if number = 3
                db 0CCh
        else
                 db 0CDh, number
        end if
end macro

int 20h         ; генерирует два байта

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

macro jmpi target
        if target-($+2) < 80h & target-($+2) >= -80h
                db 0EBh
                db target-($+1)
        else
                db 0E9h
                dw target-($+2)
        end if
end macro

        jmpi start
        db 'some data'
start:

"jmpi", определенный выше, производит код инструкции перехода, как в архитектуре 8086. Такой код содержит относительное смещение цели перехода, хранящееся либо в одном байте, либо в 16-битном слове. Относительное смещение вычисляется как разница между адресом цели и адресом следующей инструкции. Специальный символ "$" предоставляет адрес текущей инструкции и используется для вычисления относительного смещения и определения того, может ли он поместиться в один байт.

Поэтому код, сгенерированный "jmpi start" в приведенном выше примере, зависит от значения адреса, помеченного как "start", а это, в свою очередь, зависит от длины вывода всех предшествующих ему инструкций, включая упомянутый прыжок. Это создает цикл зависимостей, и ассемблеру нужно найти решение, которое удовлетворяет всем ограничениям, созданным исходным текстом. Это было бы невозможно, если бы ассемблер был просто императивным интерпретатором. Таким образом, его язык в некоторых аспектах декларативен.

Поиск решения для таких круговых зависимостей может напоминать решение уравнения, и даже можно построить пример, где fasmg действительно способен решить одно из них:

x = (x-1)*(x+2)/2-2*(x+1)
db x

Циклическая ссылка была сведена здесь к единственному определению, которое ссылается на себя, чтобы построить значение. fasmg способен найти решение в этом случае, хотя во многих других он может потерпеть неудачу. Метод, используемый этим ассемблером, состоит в том, чтобы выполнить несколько проходов над исходным текстом, а затем попытаться предсказать все значения с помощью собранных таким образом знаний. Этот подход в большинстве случаев достаточно хорош для сборки машинных кодов, но редко достаточен для решения сложных уравнений, и приведенный выше пример является одним из исключений.


Каков способ разбора аргументов инструкции?

Не все инструкции имеют простой синтаксис, как в предыдущих примерах. Чтобы помочь в обработке аргументов, которые могут содержать специальные конструкции, fasmg предоставляет несколько способных инструментов, продемонстрированных ниже на примерах, реализующих несколько выбранных инструкций процессора Z80. Правила, регулирующие использование представленных функций, приведены в руководстве.

Когда инструкция имеет очень небольшой набор допустимых аргументов, каждый из них может быть обработан отдельно с помощью конструкции "match" :

macro EX? first,second
        match (=SP?), first
                match =HL?, second
                        db 0E3h
                else match =IX?, second
                        db 0DDh,0E3h
                else match =IY?, second
                        db 0FDh,0E3h
                else
                        err "incorrect second argument"
                end match
        else match =AF?, first
                match =AF'?, second
                        db 08h
                else
                        err "incorrect second argument"
                end match
        else match =DE?, first
                match =HL?, second
                        db 0EBh
                else
                        err "incorrect second argument"
                end match
        else
                err "incorrect first argument"
        end match
end macro

EX (SP),HL
EX (SP),IX
EX AF,AF'
EX DE,HL

Символ "?" появляется во многих местах, чтобы отметить имена как нечувствительные к регистру, и все эти вхождения могут быть удалены для дальнейшего упрощения примера.

Когда набор возможных значений аргумента больше, но имеет некоторые закономерности, текстовые замены могут быть определены для замены некоторых символов тщательно подобранными конструкциями, которые затем могут быть распознаны и проанализированы:

A? equ [:111b:]
B? equ [:000b:]
C? equ [:001b:]
D? equ [:010b:]
E? equ [:011b:]
H? equ [:100b:]
L? equ [:101b:]

macro INC? argument
        match [:r:], argument
                db 100b + r shl 3
        else match (=HL?), argument
                db 34h
        else match (=IX?+d), argument
                db 0DDh,34h,d
        else match (=IY?+d), argument
                db 0FDh,34h,d
        else
                err "incorrect argument"
        end match
end macro

INC A
INC B
INC (HL)
INC (IX+2)

У этого подхода есть черта, которая не всегда может быть желательной: он позволяет использовать выражение типа "[:0:]" непосредственно в аргументе. Но можно предотвратить использование синтаксиса таким образом, используя префикс в конструкции "match" :

REG.A? equ [:111b:]
REG.B? equ [:000b:]
REG.C? equ [:001b:]
REG.D? equ [:010b:]
REG.E? equ [:011b:]
REG.H? equ [:100b:]
REG.L? equ [:101b:]

macro INC? argument
        match [:r:], REG.argument
                db 100b + r shl 3
        else match (=HL?), argument
                db 34h
        else match (=IX?+d), argument
                db 0DDh,34h,d
        else match (=IY?+d), argument
                db 0FDh,34h,d
        else
                err "incorrect argument"
        end match
end macro

В случае аргумента, структурированного как "(IX+d)", иногда может быть желательно разрешить другие алгебраически эквивалентные формы выражения, такие как "(d+IX)" или "(c+IX+d)". Вместо того чтобы разбирать каждый возможный вариант по отдельности, можно позволить ассемблеру оценивать выражение, обрабатывая выбранный символ особым образом. Когда символ объявляется как "элемент", он не имеет значения, а когда он используется в выражении, он рассматривается алгебраически как переменный член в многочлене.

element HL?
element IX?
element IY?

macro INC? argument
        match [:r:], REG.argument
                db 100b + r shl 3
        else match (a), argument
                if a eq HL
                        db 34h
                else if a relativeto IX
                        db 0DDh,34h,a-IX
                else if a relativeto IY
                        db 0FDh,34h,a-IY
                else
                        err "incorrect argument"
                end if
        else
                err "incorrect argument"
        end match
end macro

INC (3*8+IX+1)

virtual at IX
        x db ?
        y db ?
end virtual

INC (y)

Существует небольшая проблема с приведенной выше макроинструкцией. Параметр может содержать любой текст, и когда такое значение помещается в выражение, оно может вызвать неустойчивое поведение. Например, если бы "INC (1/0)" был обработан, он превратил бы выражение "a eq HL" в "1/0 eq HL", и это логическое выражение является правильным и истинным, даже если аргумент был искажен. Такой неприятный побочный эффект является следствием макроинструкций, работающих по простому принципу подстановки текста (и лучший способ избежать таких проблем-использовать вместо этого CALM). Здесь, чтобы предотвратить это, локальная переменная может быть использована в качестве прокси-сервера, содержащего значение аргумента:

macro INC? argument
        match [:r:], REG.argument
                db 100b + r shl 3
        else match (a), argument
                local value
                value = a
                if value eq HL
                        db 34h
                else if value relativeto IX
                        db 0DDh,34h,a-IX
                else if value relativeto IY
                        db 0FDh,34h,a-IY
                else
                        err "incorrect argument"
                end if
        else
                err "incorrect argument"
        end match
end macro

Существует дополнительное преимущество такой прокси-переменной, благодаря тому, что ее значение вычисляется до того, как макроинструкция начинает генерировать какой-либо вывод. Когда выражение содержит символ типа "$", оно может давать различные значения в зависимости от того, где оно вычисляется, и использование прокси-переменной гарантирует, что взятое значение является тем, которое было получено при вычислении аргумента перед генерацией кода инструкции.

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

element register
element A? : register + 111b
element B? : register + 000b
element C? : register + 001b
element D? : register + 010b
element E? : register + 011b
element H? : register + 100b
element L? : register + 101b

element HL?
element IX?
element IY?

macro INC? argument
        local value
        match (a), argument
                value = a
                if value eq HL
                        db 34h
                else if value relativeto IX
                        db 0DDh,34h,a-IX
                else if value relativeto IY
                        db 0FDh,34h,a-IY
                else
                        err "incorrect argument"
                end if
        else match any more, argument
                err "incorrect argument"
        else
                value = argument
                if value eq value element 1 & value metadata 1 relativeto register
                        db 100b + (value metadata 1 - register) shl 3
                else
                        err "incorrect argument"
                end if
        end match
end macro

Паттерн "any more" предназначен для перехвата любого аргумента, содержащего сложное выражение, состоящее из более чем одного токена. Это предотвращает использование синтаксиса типа "INC A+0" или "INC A+B-A". Но в случае некоторых наборов инструкций включение такого ограничения может зависеть от личных предпочтений.

Условие "value eq value element 1" гарантирует, что значение не содержит никаких терминов, кроме имени регистра. Даже если аргумент вынужден содержать не более одного токена, все равно возможно, что он имеет сложное значение, например, если бы существовали такие определения, как "X = A + B" или "Y = 2 * A". И "INC X", и "INC Y" заставили бы оператор "element 1" возвращать значение "A", которое отличается от значения, проверяемого в любом случае.

Если инструкция принимает переменное число аргументов, простой способ распознать ее различные формы-объявить аргумент с модификатором "&", чтобы передать полное содержимое аргументов в "match":

element CC

NZ? := CC + 000b
Z?  := CC + 001b
NC? := CC + 010b
C?  := CC + 011b
PO  := CC + 100b
PE  := CC + 101b
P   := CC + 110b
M   := CC + 111b

macro CALL? arguments&
        local cc,nn
        match condition =, target, arguments
                cc = condition - CC
                nn = target
                db 0C4h + cc shl 3
        else
                nn = arguments
                db 0CDh
        end match
        dw nn
end macro

CALL 0
CALL NC,2135h

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


Как обрабатываются метки?

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

В некоторых вариантах языка ассемблера может быть желательно разрешить метке предшествовать инструкции без дополнительного ":" между ними. Затем необходимо создать маркированную макроинструкцию, которая после определения метки передает обработку исходной макроинструкции с тем же именем:

struc INC? argument
        .:
        INC argument
end struc

start   INC A
        INC B

Это должно быть сделано для каждой инструкции, которая должна допускать такой синтаксис. Достаточно простого цикла, подобного следующему:

iterate instruction, EX,INC,CALL
        struc instruction? argument
                .: instruction argument
        end struc
end iterate

Каждая встроенная инструкция, определяющая данные, уже имеет помеченный вариант.

Определяя помеченную инструкцию, которая имеет "?" вместо имени, можно перехватить каждую строку, начинающуюся с идентификатора, который не является известной инструкцией и поэтому считается меткой. Следующий вариант позволит метке без ":" начинать любую строку в исходном тексте (он также обрабатывает особые случаи, так что метки следуют с ":" или с"=", а значение все равно будет работать):

struc ? tail&
        match :, tail
                .:
        else match : instruction, tail
                .: instruction
        else match == value, tail
                . = value
        else
                .: tail
        end match
end struc

Очевидно, что больше нет необходимости определять какие-либо конкретные маркированные макроструктуры, когда применяется глобальный эффект такого рода. Вариант должен быть выбран в зависимости от типа синтаксиса, который должен быть разрешен.

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

struc ? tail&
        match :, tail
                label . at $ shr 1
        else match : instruction, tail
                label . at $ shr 1
                instruction
        else
                . tail
        end match
end struc

Значение текущего адреса, используемого для определения меток, может быть изменено с помощью "org". Если метки необходимо отличать от абсолютных значений, то для формирования адреса можно использовать символ, определенный с помощью "element":

element CODEBASE
org CODEBASE + 0

macro CALL? argument
        local value
        value = argument
        if value relativeto CODEBASE
                db 0CDh
                dw value - CODEBASE
        else
                err "incorrect argument"
        end if
end macro

Чтобы определить метки в адресном пространстве, которые не будут отражены в выходных данных, следует объявить "виртуальный" блок. Следующий пример подготавливает макроинструкции "DATA" и "CODE" для переключения между генерацией программных инструкций и меток данных. На выход пойдут только коды команд:

element DATA
DATA_OFFSET = 2000h
element CODE
CODE_OFFSET = 1000h

macro DATA?
        _END
        virtual at DATA + DATA_OFFSET
end macro

macro CODE?
        _END
        org CODE + CODE_OFFSET
end macro

macro _END?
        if $ relativeto DATA
                DATA_OFFSET = $ - DATA
                end virtual
        else if $ relativeto CODE
                CODE_OFFSET = $ - CODE
        end if
end macro

postpone
        _END
end postpone

CODE

Блок "postpone" используется здесь для обеспечения того, чтобы "виртуальный" блок всегда закрывался правильно, даже если исходный текст заканчивается определениями данных.

В среде, подготовленной приведенным выше образцом, любая инструкция могла бы отличить метки данных от меток, определенных в программе. Например, инструкция ветвления может быть сделана так, чтобы принять аргумент, являющийся либо меткой внутри программы, либо абсолютным значением, но запретить любую метку данных:

macro CALL? argument
        local value
        value = argument
        if value relativeto CODE
                db 0CDh
                dw value - CODE
        else if value relativeto 0
                db 0CDh
                dw value
        else
                err "incorrect argument"
        end if
end macro

DATA

variable db ?

CODE

routine:

В этом контексте будет разрешена либо "CALL routine", либо "CALL 1000h", в то время как "CALL variable" - нет.

Когда метки имеют значения, которые не являются абсолютными числами, можно генерировать перемещения для инструкций, которые их используют. Специальный "virtual" блок может использоваться для хранения смещений значений внутри программы, которые необходимо переместить при изменении ее базы:

virtual at 0
        Relocations::
        rw RELOCATION_COUNT
end virtual

RELOCATION_INDEX = 0

postpone
        RELOCATION_COUNT := RELOCATION_INDEX
end postpone

macro WORD? value
        if value relativeto CODE
                store $ - CODE : 2 at Relocations : RELOCATION_INDEX shl 1
                RELOCATION_INDEX = RELOCATION_INDEX + 1
                dw value - CODE
        else
                dw value
        end if
end macro

macro CALL? argument
        local value
        value = argument
        if value relativeto CODE | value relativeto 0
                db 0CDh
                word value
        else
                err "incorrect argument"
        end if
end macro

Затем можно получить доступ к таблице перемещений, созданной таким образом, с помощью "load". Следующие две строки могут быть использованы для того, чтобы поместить таблицу целиком где-то в выводе:

load RELOCATIONS : RELOCATION_COUNT shl 1 from Relocations : 0
dw RELOCATIONS

"load" считывает всю таблицу в одну строку, а затем "dw" записывает ее в вывод (дополняется кратным слову, но в этом случае строка никогда не требует такого заполнения).

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

element MOD.HIGH
element MOD.LOW

HIGH? equ MOD.HIGH +
LOW? equ MOD.LOW +

macro BYTE? value
        if value relativeto MOD.HIGH + CODE
                ; register HIGH relocation
                db (value - MOD.HIGH - CODE) shr 8
        else if value relativeto MOD.LOW + CODE
                ; register LOW relocation
                db (value - MOD.LOW - CODE) and 0FFh
        else if value relativeto MOD.HIGH
                db (value - MOD.HIGH) shr 8
        else if value relativeto MOD.LOW
                db (value - MOD.LOW) and 0FFh
        else
                db value
        end if
end macro

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

BYTE HIGH address
BYTE LOW address

Такой подход позволяет легко включить синтаксис с модификаторами в любую инструкцию, которая внутренне использует "byte" макроинструкцию при генерации кода.


Как можно параллельно генерировать несколько секций файла?

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

Естественным подходом является определение содержания вспомогательного раздела в "virtual" блоке и копирование его в соответствующее положение на выходе с помощью одной операции. Когда "virtual" блок помечен, его можно повторно открыть несколько раз, чтобы добавить к нему больше данных.

        include '8086.inc'
        org     100h
        jmp     CodeSection

DataSection:

        virtual
                Data::
        end virtual

        postpone
                virtual Data
                        load Data.OctetString : $ - $$ from $$
                end virtual
        end postpone

        db Data.OctetString

CodeSection:

        virtual Data
                Hello db "Hello!",24h
        end virtual

        mov     ah,9
        mov     dx,Hello
        int     21h

        virtual Data
                ExitCode db 37h
        end virtual

        mov     ah,4Ch
        mov     al,[ExitCode]
        int     21h

Это приводит к относительно простому синтаксису даже без помощи каких-либо дополнительных макросов.

Другой метод может заключаться в том, чтобы поместить части раздела в макросы и выполнить их все в нужном месте в исходном коде. Недостатком такого подхода является то, что отслеживание ошибок в определениях может стать немного громоздким.

Методы, которые позволяют легко добавлять к разделу, сгенерированному параллельно, также могут быть очень полезны для создания структур данных, таких как таблицы перемещения. Вместо команд "store", использовавшихся ранее при демонстрации концепции, обычные директивы данных можно было бы использовать внутри вновь открытого "virtual" блока для создания записей перемещения.


Какие есть варианты для разбора других видов синтаксиса?

В некоторых случаях команда, которую ассемблер должен разобрать, может начинаться с чего-то другого, чем имя инструкции или метка. Может быть, перед именем стоит специальный символ, например "." или "!", или это совершенно другой вид конструкции. Тогда необходимо использовать "macro ?" перехватывать целые строки исходного текста и обрабатывать любой специальный синтаксис такого рода.

Например, если бы требовалось разрешить команду, написанную как ".CODE", было бы невозможно реализовать ее непосредственно как макроинструкцию, потому что начальная точка заставляет символ интерпретироваться как локальный, а глобально определенная инструкция никогда не могла бы быть выполнена таким образом. Перехватывающая макроинструкция обеспечивает решение:

macro ? line&
        match .=CODE?, line
                CODE
        else match .=DATA?, line
                DATA
        else
                line
        end match
end macro

Строки, содержащие текст ".CODE" или ".DATA", обрабатываются здесь таким образом, что вызывают глобальную макроинструкцию с соответствующим именем, в то время как все остальные перехваченные строки выполняются без изменений. Этот метод позволяет отфильтровать любой специальный синтаксис и позволить ассемблеру обрабатывать обычные инструкции как обычно.

Иногда нетрадиционный синтаксис ожидается только в определенной области исходного текста, например, внутри блока с определенными границами. Затем макроинструкция синтаксического анализа должна быть применена только в этом месте и удалена с помощью "purge", когда блок заканчивается:

macro concise
        macro ? line&
                match =end =concise, line
                        purge ?
                else match dest+==src, line
                        ADD dest,src
                else match dest-==src, line
                        SUB dest,src
                else match dest==src, line
                        LD dest,src
                else match dest++, line
                        INC dest
                else match dest--, line
                        DEC dest
                else match any, line
                        err "syntax error"
                end match
        end macro
end macro

concise
        C=0
        B++
        A+=2
end concise

Макроинструкция, определенная таким образом, не перехватывает строки, содержащие директивы, управляющие потоком сборки, такие как "if" или "repeat", и они все еще могут свободно использоваться внутри такого блока. Это изменилось бы, если бы декларация была в форме "macro ?! line&. Такой вариант перехватывал бы каждую строкубез исключения.

Другим вариантом для перехвата специальных команд может быть использование "structure ?" перехватывать только те строки, которые не начинаются с известной инструкции (начальный символ затем обрабатывается как метка). Поскольку этот тест тестирует только неизвестные команды, он должен вызывать меньше накладных расходов на сборку:

struc (head) ? tail&
        match .=CODE?, head
                CODE tail
        else
                head tail
        end match
end struc

Все эти подходы скрывают тонкую ловушку. Метка, определенная с помощью ":", может сопровождаться другой инструкцией в той же строке. Если эта следующая инструкция (которая здесь становится скрытой в параметре "tail") является директивой управления типа "if", то помещение ее в предложение "else" приведет к нарушению вложенности блоков управления. Возможное решение состоит в том, чтобы каким-то образом вызвать содержимое "tail" вне блока "match". Одним из способов может быть вызов специального макроса:

struc (head) ? tail&
        local invoker
        match .=CODE?, head
                macro invoker
                        CODE tail
                end macro
        else
                macro invoker
                        head tail
                end macro
        end match
        invoker
end struc

Более простой вариант - вызвать исходную строку напрямую и, когда требуется переопределение, заставить ее игнорироваться с помощью другого перехватчика строк (после удаляя себя сразу):

struc (head) ? tail&
        match .=CODE?, head
                CODE tail
                macro ? line&
                        purge ?
                end macro
        end match
        head tail
end struc

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


Как определить инструкцию, разделяющую имя с одной из основных директив?

Может случиться так, что язык вообще может быть легко реализован с помощью макросов, но он должен включать команду с тем же именем, что и одна из директив ассемблера. Хотя можно переопределить любую инструкцию с помощью макроса, сами макросы могут потребовать доступа к исходной директиве. Чтобы одно и то же имя вызывало другую инструкцию в зависимости от контекста, реализованный язык может быть интерпретирован в пространстве имен, содержащем переопределяющий макрос, в то время как все макросы, требующие доступа к исходной директиве, должны были бы временно переключиться в другое пространство имен, где оно не было переопределено. Это потребовало бы, чтобы каждый такой макрос упаковывал свое содержимое в блок "namespace".

Но есть еще один трюк, связанный с тем, как тексты макропараметров или символьных переменных сохраняют контекст, в котором символы внутри них должны интерпретироваться (это включает базовое пространство имен и родительскую метку для символов, начинающихся с точки).

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

macro definitions end?
        namespace embedded
        struc LABEL? size
                match , size
                        .:
                else
                        label . : size
                end match
        end struc
        macro E#ND? name
                end namespace
                match any, name
                        ENTRYPOINT := name
                end match
                macro ?! line&
                end macro
        end macro
end macro

definitions end

start LABEL
END start

Параметр, заданный макросу "definitions", может показаться, что он ничего не делает, поскольку он заменяет каждый экземпляр "end" точно таким же словом - но текст, который исходит из параметра, снабжен дополнительной информацией о контексте, и этот атрибут затем сохраняется, когда текст становится частью нового макроса. Благодаря этому макрос "LABEL" может использоваться в пространстве имен, где инструкция "end" имеет другое значение, но экземпляры "end" в его теле все еще ссылаются на символ во внешнем пространстве имен.

В этом примере параметр был сделан нечувствительным к регистру, и таким образом он заменил бы даже оператор "END" в "macro", который должен определять символ во "embedded" пространстве имен. По этой причине идентификатор был разделен с помощью оператора конкатенации, чтобы предотвратить его распознавание в качестве параметра. Это не было бы необходимо, если бы параметр был чувствителен к регистру (как обычно).

Тот же эффект может быть достигнут за счет использования символьных переменных вместо макропараметров, с помощью "match" для извлечения текста символьной переменной:

define link end
match end, link
        namespace embedded
        struc LABEL? size
                match , size
                        .:
                else
                        label . : size
                end match
        end struc
        macro END? name
                end namespace
                match any, name
                        ENTRYPOINT := name
                end match
                macro ?! line&
                end macro
        end macro
end match

start LABEL
END start

Это не сработало бы без передачи текста через символическую переменную, потому что параметры, определенные директивами управления, такими как "match", не добавляют контекстную информацию к тексту, если она уже не была там.

CALM инструкции позволяют по-другому подходить к такого рода проблемам. Если настроенный набор команд определен полностью в форме CALM, им может даже не понадобиться доступ к исходным директивам управления. Однако, если инструкция должна собрать директиву, которая может быть недоступна, символическая переменная, переданная в "assemble", должна быть определена с соответствующим контекстом для символа инструкции.


Как преобразовать макроинструкцию в CALM?

Классическая макроинструкция состоит из строк текста, которые предварительно обрабатываются (путем замены имен параметров их соответствующими значениями) каждый раз, когда вызывается инструкция, и эти предварительно обработанные строки передаются ассемблеру. Например, эта макроинструкция генерирует только одну строку для ассемблера, и она делает это, заменяя "value" текстом, заданным единственным аргументом инструкции:

macro octet value*
        db value
end macro

CALM инструкцию можно рассматривать как настроенный препроцессор, который должен быть написан на специальном языке. Он способен использовать различные команды для обработки аргументов и генерировать строки для ассемблера. На базовом уровне он также способен имитировать то, что делает стандартный препроцессор - с помощью команды "arrange". После предварительной обработки строки она также должна явно передать ее ассемблеру с помощью команды "assemble" :

calminstruction octet value*
        arrange value, =db value
        assemble value
end calminstruction

Это дает тот же результат, что и исходная макроинструкция, поскольку она выполняет тот же вид предварительной обработки. Однако, в отличие от текста макроинструкции, шаблон, заданный для "arrange", должен явно указывать, какие маркеры имен должны быть заменены их значениями, а какие (с добавлением "=") должны быть оставлены нетронутыми. Токены, которые копируются из шаблона, лишаются любой контекстной информации, точно так же, как текст макроинструкции обычно не несет никакой информации (в то время как значения, полученные из аргументов, сохраняют контекст распознавания, в котором была запущена инструкция).

Это самый простой метод преобразования, и простая последовательность команд "arrange" и "assemble" может быть сделана для генерации тех же строк, что и исходная макроинструкция. Но есть одно исключение - когда "local" команда выполняется макроинструкцией, она создает предварительно обработанный параметр со специальным значением, указывающим на символ в пространстве имен, уникальный для данного экземпляра инструкции.

macro pointer
        local next
        dd next
    next:
end macro

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

global_uid = 0

calminstruction pointer
        compute global_uid, global_uid + 1
        local command
        arrange command, =dd =next#global_uid
        assemble command
        arrange command, =next#global_uid:
        assemble command
end calminstruction

Здесь "arrange" задается переменная, имеющая числовое значение, и она должна заменить его текстом. Это работает только тогда, когда значение является плановым неотрицательным числом, в таком случае "arrange" преобразует его в текстовый токен, содержащий десятичное представление этого числа. Поэтому строки, передаваемые ассемблеру, будут содержать идентификаторы типа "next#1".

В то время как увеличение глобального счетчика может быть сделано путем подготовки стандартной команды ассемблера типа "global_uid = global_uid + 1" с помощью "arrange" и передачи ее ассемблеру, команда "compute" позволяет сделать это непосредственно в спокойном процессоре. Более того, тогда на него не влияет ничего, что изменяет контекст сборки. Если бы команда была определена как безусловная и использовалась внутри пропущенного блока IF, "compute" все равно выполняло бы свою задачу, потому что выполнение CALM команд - как и стандартная предварительная обработка - выполняется независимо от основного потока сборки. Кроме того, ссылки на "global_uid" всегда указывают на один и тот же символ - тот, который был в области видимости, когда была определена и скомпилирована команда CALM. Поэтому увеличение значения с помощью "compute" является более надежным и предсказуемым.

Аналогичным образом сборка строки, определяющей метку, может быть заменена командой "publish". Здесь сначала необходимо вычислить значение метки (которое должно быть равно адресу после сборки строки, содержащей "dd"), поскольку "publish" выполняет только присвоение значения символу:

global_uid = 0

calminstruction pointer
        compute global_uid, global_uid + 1
        local symbol, command
        arrange symbol, =next#global_uid
        arrange command, =dd symbol
        assemble command
        local address
        compute address, $
        publish symbol:, address
end calminstruction

Поскольку CALM инструкция сама по себе условна, "publish" внутри тоже фактически условна. Поэтому он корректно работает как замена ассемблерных строк с меткой.

Хотя глобальный счетчик имеет несколько преимуществ, он может быть нарушен, поэтому иногда использование локального счетчика может быть предпочтительнее. Однако локальное пространство имен CALM инструкции обычно не недоступно извне, поэтому немного сложнее дать начальное значение такому счетчику. Одним из способов может быть проверка того, был ли счетчик уже инициализирован каким-то значением с помощью команды "take" :

calminstruction pointer
        local id
        take id, id
        jyes increment
        compute id, 0
    increment:
        compute id, id + 1
        local symbol, command
        arrange symbol, =next#id
        arrange command, =dd symbol
        assemble command
        local address
        compute address, $
        publish symbol:, address
end calminstruction

Но это добавляет команды, которые выполняются каждый раз, когда вызывается инструкция. Лучшее решение использует возможность определения пользовательских инструкций, обрабатываемых во время определения CALM инструкции:

calminstruction calminstruction?.init? var*, val:0
        compute val, val
        publish var, val
end calminstruction

calminstruction pointer
        local id
        init id, 0
        compute id, id + 1
        local symbol, command
        arrange symbol, =next#id
        arrange command, =dd symbol
        assemble command
        local address
        compute address, $
        publish symbol:, address
end calminstruction

Пользовательский оператор "init" вызывается в то время, когда определена CALM инструкция (он не генерирует никаких команд для выполнения определенной инструкцией - он сам должен был бы использовать команды "assembly" для генерации инструкций для компиляции). Ему присваивается имя переменной из локальной области действия инструкции CALM, и он использует "publish" для присвоения этой переменной начального числового значения.

Чтобы инициализировать локальную переменную с символическим значением, достаточно даже более простой пользовательской инструкции:

calminstruction calminstruction?.initsym? var*, val&
        publish var, val
end calminstruction

Текст аргумента "val" несет в себе контекст распознавания определения инструкции CALM, содержащей оператор "initsym", поэтому он позволяет подготовить текст для "assembly", содержащий ссылки на локальные символы:

calminstruction be32? value
        local command
        initsym command, dd value
        compute value, value bswap 4
        assemble command
end calminstruction

Опять же, после того как эта структура скомпилирована, она содержит только две фактические команды, "compute" и "assemble", а значение локального символа "command" - это текст, который интерпретируется в том же локальном контексте и относится к тому же символу "value", что и "compute".

Этот пример также демонстрирует еще одно преимущество CALM перед стандартными макроинструкциями: его строгая семантика предотвращает различные виды нежелательного поведения, которое допускается простой заменой текста. Текст "value" будет вычисляться "compute" как числовое подвыражение, сигнализирующее об ошибке при любом неожиданном синтаксисе. Поэтому должно быть выгодно обрабатывать аргументы полностью с помощью CALM команд и использовать только "assemble" для конечных простых утверждений.


Flat assembler g. Руководство пользователя

Содержание
Flat assembler g. Руководство пользователя

Этот документ описывает синтаксис flat assembler g с основными примерами. Она была написана с предположением, что ее будут читать последовательно и в любой момент она использует только те понятия и конструкции, которые были введены ранее. Однако следует иметь возможность сразу перейти к интересующему читателя разделу, а затем вернуться к более ранним частям только тогда, когда это необходимо для того, чтобы лучше понять более поздние.


0. Запуск ассемблера

Для запуска ассемблера из командной строки необходимо указать хотя бы один параметр - имя исходного файла, а при необходимости и второй - имя конечного файла. Если сборка прошла успешно, то сгенерированный вывод записывается в место назначения и отображается краткая сводка, в противном случае отображается информация об ошибках. Максимальное количество представленных ошибок можно контролировать с помощью дополнительного переключателя -e (по умолчанию отображается не более одной ошибки). Переключатель -p управляет максимальным числом проходов, которые ассемблер собирается выполнить. По умолчанию это ограничение равно 100. Переключатель -r позволяет установить предел стека рекурсии, то есть максимально допустимую глубину ввода макроинструкций и включения дополнительных исходных файлов. Переключатель -v может включить отображение всех строк из этого стека при сообщении об ошибке (по умолчанию ассемблер пытается выбрать только те строки, которые, вероятно, являются наиболее информативными, но эта простая эвристика не всегда может быть правильной). Если переключатель -v используется со значением 2, то он дополнительно заставляет все сообщения, отображаемые командами из исходного текста, отображаться в режиме реального времени (при каждом последовательном проходе). Переключатель -i позволяет вставить любую команду в начало обрабатываемого источника.


1. Фундаментальные правила синтаксиса

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

Текст исходной строки делится на синтаксические единицы, называемые лексемами. Существует ряд специальных символов, которые сами по себе становятся отдельными токенами. Любой из перечисленных ниже символов является такой синтаксической единицей:

+-/*=<>()[]{}:?!,.|&~#`\

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

Числа отличаются от имен тем, что они начинаются либо с десятичной цифры, либо с символа $, за которым следует любая шестнадцатеричная цифра. Это означает, что токен может считаться числовым, даже если он не является допустимым числом. Чтобы быть правильным, он должен быть одним из следующих: десятичное число (необязательно с буквой d в конце), двоичное число, за которым следует буква b, восьмеричное число, за которым следует буква o или q, или шестнадцатеричное число, либо перед которым стоит $ или 0x, либо за которым следует символ h. Поскольку первая цифра шестнадцатеричного числа может быть буквой, может потребоваться добавить к ней цифру ноль, чтобы сделать ее узнаваемой как число. Например, 0Ah - это допустимое число, а Ah - это просто имя.


2. Идентификаторы символов

Любое имя может стать определенным символом, если ему присвоено какое-то значение. Одним из простейших способов создания символа с заданным значением является использование команды = :

a = 1

Команда : определяет метку, то есть символ со значением, равным текущему адресу в сгенерированном выводе. В начале исходного текста этот адрес всегда равен нулю, поэтому, когда следующие две команды являются первыми в исходном файле, они определяют символы, имеющие одинаковые значения:

first:
second = 0

Метки, определенные с помощью команды :, являются специальными конструкциями на языке ассемблера, поскольку они позволяют любой другой команде (включая другое определение метки) следовать в той же строке. Это единственный вид команды, который позволяет это.

То, что стоит перед символом : или = в таком определении, является идентификатором символа. Это может быть простое имя, как в приведенных выше примерах, но оно также может содержать некоторые дополнительные модификаторы, описанные ниже.

Когда имя в определении символа имеет добавленный к нему символ ? (без пробелов между ними), символ нечувствителен к регистру (в противном случае он был бы определен как чувствительный к регистру). Это означает, что значение такого символа может быть обозначено (как в выражении справа от символа = именем, являющимся любым вариантом исходного имени, отличающимся только в случае букв. Однако только случаи 26 букв английского алфавита могут отличаться.

Можно определить чувствительный к регистру символ, который вступает в конфликт с нечувствительным к регистру. Тогда символ, чувствительный к регистру, имеет приоритет, и более общий символ используется только тогда, когда соответствующий символ, чувствительный к регистру, не определен. Это можно исправить с помощью модификатора ? , так как он всегда означает, что имя, за которым он следует, относится к символу без учета регистра.

tester? = 0
tester = 1
TESTER = 2
x = tester       ; x = 1
y = Tester       ; y = 0
z = TESTER       ; z = 2
t = tester?      ; t = 0

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

space:
space.x = 1
space.y = 2
space.color:
space.color.r = 0
space.color.g = 0
space.color.b = 0

Любое из имен в такой цепочке может необязательно сопровождаться символом ?, чтобы отметить, что оно относится к символу без учета регистра. Если ? вставляется в середину имени (фактически разбивая его на отдельные токены), то такой идентификатор считается синтаксической ошибкой.

Когда идентификатор начинается с точки (другими словами, когда имя родительского символа пусто), он ссылается на символ в пространстве имен самой последней регулярной метки, определенной перед текущей строкой. Это позволяет переписать приведенный выше пример следующим образом:

space:
.x = 1
.y = 2
.color:
.color.r = 0
.color.g = 0
.color.b = 0

После того как метка "space" определена, она становится самой последней определенной нормальной меткой, поэтому следующий ".x" относится к символу "space.x", а затем ".color" относится к "space.color".

Команда namespace , за которой следует идентификатор символа, изменяет базовое пространство имен для раздела исходного текста. Он должен быть сопряжен с командой end namespace позже в источнике, чтобы отметить конец такого блока. Это может быть использовано для того, чтобы снова переписать вышеприведенный образец по-другому:

space:
namespace space
        x = 1
        y = 2
        color:
        .r = 0
        .g = 0
        .b = 0
end namespace

Когда имени не предшествует точка, и как таковому оно не имеет явного указания, в каком пространстве имён находится символ, ассемблер ищет определённый символ в текущем пространстве имён, а если его нет, то в последовательных пространствах имён родительских символов, начиная с пространства имён, содержащего родительский символ текущего пространства имён. Если не найдено определенного символа с таким именем, то предполагается, что имя относится к символу в текущем пространстве имен (и если только не существует ?, символ после такого имени, предполагается, что символ чувствителен к регистру). Определение, не указывающее пространство имен, в котором должен быть создан новый символ, всегда создает новый символ в текущем базовом пространстве имен.

global = 0
regional = 1
namespace regional
        regional = 2            ; regional.regional = 2
        x = global              ; regional.x = 0
        regional.x = regional   ; regional.regional.x = 2
        global.x = global       ; global.x = 0
end namespace

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

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

Если идентификатор представляет собой всего лишь одну точку, то по приведенным выше правилам он относится к самой последней метке, которая не начиналась с точки. Это может быть применено для переписывания предыдущего примера еще одним способом:

space:
namespace .
        x = 1
        y = 2
        color:
        namespace .
                r = 0
                g = 0
                b = 0
        end namespace
end namespace

Он также демонстрирует, как разделы пространства имен могут быть вложены друг в друга.

# может быть вставлен в любое место внутри идентификатора без изменения его значения. Когда "#" является единственным символом, разделяющим два токена имени, это приводит к тому, что они интерпретируются как одно имя, образованное путем объединения токенов.

variable = 1
varia#ble = var#iable + 2       ; variable = 3

Это также может быть применено к числам.

Внутри блока, определенного с помощью namespace, изначально нет метки, которая считалась бы базовой для идентификаторов, начинающихся с точки (однако метка, служившая этой цели ранее, возвращается к использованию после end namespace). То же самое происходит и в начале исходного текста, до того, как была определена какая-либо метка. Это связано с несколькими дополнительными правилами, касающимися использования точек в идентификаторах.

Когда идентификатор начинается с точки, но нет метки, которая была бы для него родительской, идентификатор ссылается на потомка специального символа, который находится в текущем пространстве имен, но не имеет имени. Если идентификатор начинается с последовательности из двух или более точек, то он относится к потомку аналогичного безымянного символа, но является отличным для любого заданного числа точек. В то время как пространство имен, к которому обращаются с одной начальной точкой, меняется каждый раз, когда определяется новая обычная метка, специальное пространство имен, к которому обращаются с двумя или более точками в начале идентификатора, остается прежним:

first:
        .child = 1
        ..other = 0
second:
        .child = 2
        ..another = ..other

В этом примере значение идентификатора ".child" меняется от места к месту, но идентификатор "..other" означает везде одно и то же.

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

namespace base
        ..other = 1
end namespace

result = base.#..other

Символ # был вставлен в последний идентификатор для лучшей читаемости, но простая последовательность из трех точек сделала бы то же самое.

Безымянный символ, который содержит специальное пространство имен, сам может быть доступен, когда идентификатор заканчивается последовательностью из двух или более точек - благодаря правилу, что идентификатор, который заканчивается точкой, ссылается на родительский символ пространства имен, к которому можно было бы получить доступ, если бы после этой точки было имя. Таким образом, в контексте предыдущего примера "base..." (или "base.#..") будет ссылаться на безымянный родитель пространства имен, в котором находится символ "other", и это будет тот же символ, который идентифицируется простым .. внутри пространства имен символа "base".

Любой идентификатор может быть дополнен символом ?, и такой модификатор имеет эффект, когда он используется в контексте, где идентификатор может означать что-то другое, чем метка или переменная, подлежащая определению. Этот модификатор затем подавляет любую другую интерпретацию. Например, идентификатор, начинающийся с ? , не будет рассматриваться как инструкция, даже если это первый символ в строке. Это можно использовать для определения переменной, которая имеет общее имя с существующей командой:

?namespace = 0

Если такой модифицированный идентификатор используется в месте, где он вычисляется и не определяется, он все равно ссылается на тот же символ, на который он будет ссылаться в определении. Поэтому, если идентификатор также не использует точку, он всегда ссылается на символ в текущем пространстве имен.

Число может быть использовано в роли имени внутри идентификатора, но не тогда, когда оно помещено в начало, потому что тогда оно считается буквальным значением. Это ограничение также можно обойти, поставив перед идентификатором знак ? .


3. Определения основных символов

Когда символ определяется как метка, он должен быть единственным определением этого символа во всем источнике. Значение, присвоенное символу таким образом, может быть доступно из любого места в источнике, даже до того, как метка фактически определена. Когда символ используется до того, как он определен (это часто называют прямой ссылкой), ассемблер пытается правильно предсказать значение символа, делая несколько проходов над исходным текстом. Только когда все предсказания оказываются верными, ассемблер генерирует конечный результат.

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

Когда символ определяется с помощью команды =, он может иметь несколько определений такого рода. Такой символ называется переменной, и при его использовании осуществляется доступ к значению из его последнего определения. Символ, определенный с помощью такой команды, также может иметь прямую ссылку, но только тогда, когда он определен ровно один раз во всем источнике и как таковой имеет одно однозначное значение.

a = 1           ; a = 1
a = a + 1       ; a = 2
a = b + 1       ; a = 3
b = 2

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

x = (x-1)*(x+2)/2-2*(x+1)       ; x = 6 or x = -1

Символ := определяет постоянное значение. Он может быть использован вместо =, чтобы гарантировать, что данный символ определен ровно один раз и что на него можно ссылаться вперед.

=: определяет символ переменной, такой как =, но он отличается тем, как он обрабатывает предыдущее значение (если таковое существует). В то время как = отбрасывает предыдущее значение, =: сохраняет его, чтобы позже его можно было вернуть с помощью команды "восстановить".:

a = 1
a =: 2          ; preserves a = 1
a = 3           ; discards a = 2 and replaces it with a = 3
restore a       ; brings back a = 1

restore может сопровождаться несколькими идентификаторами символов, разделенными запятыми, и оно отбрасывает последнее определение каждого из них. Не считается ошибкой использовать restore с символом, который не имеет активного определения (либо потому, что он никогда не был определен, либо потому, что все его определения уже были отброшены ранее). Если символ обрабатывается с помощью команды restore, он становится переменной и никогда не может быть переадресован. По этой причине restore не может быть применено к константам.

Ключевое слово label, за которым следует идентификатор символа, является альтернативным способом определения метки. В этой основной форме он эквивалентен определению, сделанному с помощью : , но занимает целую строку. Однако с помощью этой команды можно обеспечить дополнительные настройки для определенной метки. За идентификатором может опционально следовать маркер : , а затем дополнительное значение, связанное с этой меткой (обычно обозначающее размер помеченной сущности). Ассемблер имеет ряд встроенных констант, определяющих различные размеры для этой цели, но это значение также может быть предоставлено в виде простого числа.

label character:byte
label char:1

Символ : может быть опущен в пользу простого пробела, но он рекомендуется для ясности. После идентификатора и необязательного размера может следовать ключевое слово at , а затем значение, которое должно быть присвоено метке вместо текущего адреса.

label wchar:word at char

Встроенные константы размера эквивалентны следующему набору определений:

byte? = 1       ; 8 bits
word? = 2       ; 16 bits
dword? = 4      ; 32 bits
fword? = 6      ; 48 bits
pword? = 6      ; 48 bits
qword? = 8      ; 64 bits
tbyte? = 10     ; 80 bits
tword? = 10     ; 80 bits
dqword? = 16    ; 128 bits
xword? = 16     ; 128 bits
qqword? = 32    ; 256 bits
yword? = 32     ; 256 bits
dqqword? = 64   ; 512 bits
zword? = 64     ; 512 bits

Ключевое слово element, за которым следует идентификатор символа, определяет специальную константу, которая не имеет фиксированного значения и может использоваться в качестве переменной в линейных полиномах. За идентификатором может опционально следовать маркер : , а затем значение, связанное с этим символом, называемое метаданными элемента.

element A
element B:1

Метаданные, присвоенные символу, могут быть извлечены с помощью специального оператора, определенного в следующем разделе.


4. Значения выражений

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

+, - и * выполняют стандартные арифметические операции над целыми числами (+ и - также могут использоваться в унарной форме - только с одним аргументом). / и mod выполняют деление с остатком, давая частное или остаток соответственно. Из этих арифметических операторов mod имеет самый высокий приоритет (он вычисляется первым), * и / идут следующими, в то время как + и - вычисляются последними (даже в их унарных вариантах). Операторы с одинаковым приоритетом вычисляются слева направо. Круглые скобки могут использоваться для заключения вложенных выражений, когда требуется другой порядок операций.

xor, and и or выполняют побитовые операции над числами. xor - это сложение битов (исключающее или), and - умножение битов, а or - включающее или (логическая дизъюнкция). Эти операторы имеют более высокий приоритет, чем любые арифметические операторы.

shl и shr выполняют битовый сдвиг первого аргумента на количество битов, указанное вторым. shl сдвигает биты влево (в сторону высших степеней двойки), в то время как shr сдвигает биты вправо (в сторону нуля), отбрасывая биты, которые попадают в дробный диапазон. Эти операторы имеют более высокий приоритет, чем другие двоичные побитовые операции.

not, bsf и bsr являются унарными операторами с еще более высоким приоритетом. not инвертирует все биты числа, в то время как bsf и bsr ищут самый низкий или самый высокий бит набора соответственно и дают индекс этого бита в результате.

Все операции над числами выполняются так, как если бы они выполнялись над бесконечными бинарными представлениями этих чисел. Например, bsr с отрицательным числом в качестве аргумента не дает действительного результата, так как такое число имеет бесконечную цепочку битов набора, простирающуюся к бесконечности, и как таковое не содержит старшего бита набора (это сигнализируется как ошибка).

Оператор bswap позволяет создать строку байтов, содержащую представление числа в обратном порядке байтов (big endian). Вторым аргументом этого оператора должна быть длина в байтах требуемой строки. Этот оператор имеет тот же приоритет, что и операторы shl и shr .

Когда строковое значение используется в качестве аргумента для любой из операций над числами, оно рассматривается как последовательность битов и автоматически преобразуется в положительное число (расширенное нулевыми битами до бесконечности). Старшие символы последовательной строки соответсвуют старшим битам числа.

Чтобы преобразовать число обратно в строку, можно использовать унарный оператор string. Этот оператор имеет наименьший возможный приоритет, поэтому, когда он предшествует выражению, все его значения вычисляются до преобразования. Когда требуется преобразование в противоположном направлении, достаточно простого унарного + , чтобы строка стала числом.

Длина строки может быть получена с помощью унарного оператора lengthhof . Этот оператор может быть применен только к строке, и он является одним из операторов с наивысшим приоритетом.

Когда в выражении используется символ, определенный командой element , результатом может быть линейный полином в переменной, представленной символом. Только простые арифметические операции разрешены на терминах многочлена, и он должен оставаться линейным - так, например, разрешается только умножать многочлен на число, но не на другой многочлен.

Существует несколько операторов с высоким приоритетом, которые позволяют извлекать информацию о членах линейного полинома. Полином должен быть первым аргументом, а индекс термина - вторым. Оператор element извлекает переменную полиномиального члена (с коэффициентом единицы), оператор scale извлекает коэффициент (число, на которое умножается переменная), а оператор metadata возвращает метаданные, связанные с переменной.

Когда второй аргумент является индексом, превышающим индекс последнего члена многочлена, все три оператора возвращают ноль. Когда второй аргумент равен нулю, element и scale дают информацию о постоянном члене - element возвращает число 1, а scale возвращает значение постоянного члена.

element A
linpoly = A + A + 3
vterm = linpoly scale 1 * linpoly element 1     ; vterm = 2 * A
cterm = linpoly scale 0 * linpoly element 0     ; cterm = 3 * 1

Оператор metadata с нулевым индексом возвращает размер, связанный с первым аргументом. Это значение является определенным только тогда, когда первым аргументом является символ, имеющий связанный с ним размер (или арифметическое выражение, содержащее такой символ), в противном случае оно равно нулю. Существует дополнительный унарный оператор sizeof, который дает то же значение, что и metadata 0 .

label table : 256
length = sizeof table   ; length = 256

elementof, scaleof и metadataof - это варианты операторов element, scale и metadata с противоположным порядком аргументов. Поэтому, когда sizeof используется в выражении, это эквивалентно написанию 0 metadataof на его месте. Эти операторы имеют еще более высокую прецендентность, чем их аналоги, и являются правоассоциативными.

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

elementsof - это еще один унарный оператор с наивысшим приоритетом, он подсчитывает количество переменных членов линейного полинома.

Выражение также может содержать литеральное значение, определяющее число с плавающей запятой. Такое число должно быть в десятичной системе счисления, оно может содержать символ . в качестве десятичного знака и может сопровождаться символом е, а затем десятичным значением показателя степени (необязательно перед + или - для обозначения знака показателя степени). Если присутствует . или е, за ним должна следовать по крайней мере одна цифра. Символ f может быть добавлен в конце такого литерального значения. Если число не содержит ни ., ни e, то окончательное f -это единственный способ убедиться, что оно рассматривается как число с плавающей запятой, а не как простое десятичное целое число.

Числа с плавающей запятой обрабатываются ассемблером в двоичной форме. Их диапазон и точность, по крайней мере, так же высоки, как и в самом длинном формате с плавающей запятой, который ассемблер способен произвести на выходе.

Основные арифметические операции могут иметь число с плавающей запятой в качестве любого из аргументов, но тогда ни один из аргументов не может содержать нескалярных (линейных полиномиальных) членов. Результатом такой операции всегда является число с плавающей запятой.

Унарный оператор float может использоваться для преобразования целочисленного значения в число с плавающей запятой. Этот оператор имеет самый высокий приоритет.

trunc - это еще один унарный оператор с наивысшим приоритетом, и он может быть применен к числам с плавающей запятой. Он извлекает целочисленную часть числа (это усечение к нулю), и результат всегда является простым целым числом, а не числом с плавающей запятой. Если аргумент уже был простым целым числом, эта операция оставляет его неизменным.

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

Также допускается использование числа с плавающей запятой в качестве первого аргумента операторов shl и shr . Затем число умножается или делится на степень двух, заданную вторым аргументом.


5. Классы символов

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

Все типы определений, описанные в предыдущих разделах, создают символы класса выражений. label и restore являются примерами встроенных символов, принадлежащих классу инструкций.

В любом пространстве имен допускается, чтобы символы разных классов имели одно и то же имя, например, можно определить инструкцию с именем shl , в то время как существует также оператор с тем же именем - но оператор принадлежит классу выражений.

Возможно даже, что одна строка содержит один и тот же идентификатор, означающий разные вещи в зависимости от ее положения:

?restore = 1
restore restore ; remove the value of the expression-class symbol

Третий класс символов - это помеченные инструкции. Символ, принадлежащий к этому классу, может быть распознан только тогда, когда первый идентификатор команды не является инструкцией - в этом случае первый идентификатор становится меткой к инструкции, определенной вторым. Если мы рассматриваем = как особый вид идентификатора, он может служить примером помеченной инструкции.

Ассемблер содержит встроенные символы всех классов. Их имена всегда нечувствительны к регистру, и они могут быть переопределены, но удалить их невозможно. Когда все значения такого символа удаляются с помощью команды типа restore, встроенное значение сохраняется.

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

xor?.mask? := 10101010b
a = XOR.MASK    ; symbol in the namespace of built-in case-insensitive "XOR"

label?.test? := 0
a = LABEL.TEST  ; undefined unless "label?" is defined

Здесь пространство имен, содержащее test, принадлежит символу класса выражений, а не существующей инструкции label. Когда нет символа класса выражения, который соответствовал бы спецификатору LABEL, выбирается пространство имен, которое принадлежало бы чувствительному к регистру символу такого имени. Таким образом, test не найден, поскольку он был определен в другом пространстве имен - в нечувствительной к регистру label .


6. Генерация данных

Инструкция db позволяет генерировать байты данных и помещать их в выходные данные. За ним должно следовать одно или несколько значений, разделенных запятыми. Когда значение числовое, оно определяет один байт. Когда значение является строкой, оно помещает строку байтов в вывод.

db 'Hello',13,10        ; generate 7 bytes

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

db 4 dup 90h            ; generate 4 bytes
db 2 dup ('abc',10)     ; generate 8 bytes

Когда специальный идентификатор, состоящий из одиночного символа ? , используется в качестве значения в аргументах db , он резервирует один байт. Это продвигает адрес в выводе, куда будут помещены следующие данные, но зарезервированные байты не генерируются сами, если за ними не следуют какие-то другие данные. Поэтому, если байты зарезервированы в конце вывода, они не увеличивают размер сгенерированного файла. Такие данные называются неинициализированными, в то время как все обычные данные называются инициализированными.

Инструкция rb резервирует количество байтов, указанное ее аргументом.

db ?                    ; reserve 1 byte
rb 7                    ; reserve 7 bytes

Каждая встроенная инструкция, которая генерирует данные (традиционно называемая директивой данных), сопряжена с помеченной инструкцией с тем же именем. Такая команда в дополнение к генерации данных определяет метку по адресу генерируемых данных, с соответствующим размером, равным размеру единицы данных, используемой этой инструкцией. В случае db и rb этот размер равен 1.

some db sizeof some     ; generate a byte with value 1

dw , dd , dp , dq , dt , ddq , dqq и ddqq - это инструкции, аналогичные db с различными размерами блока данных. Порядок байтов в пределах одной сгенерированной единицы всегда является little-endian. Когда строка байтов предоставляется в качестве значения любой из этих инструкций, сгенерированные данные расширяются нулевыми байтами до длины, кратной единице данных. rw , rd , rp , rq , rt , rdq , rqq и rdqq - это инструкции, которые резервируют определенное количество единиц данных. Размеры блоков, связанные со всеми этими инструкциями, перечислены в таблице 1.

Инструкции dw , dd , dq , dt и ddq допускают числа с плавающей запятой в качестве единиц данных. Любое такое число затем преобразуется в формат с плавающей запятой, соответствующий заданному размеру.

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

emit 2: 0,1000,2000      ; generate three 16-bit values

Инструкция file считывает данные из внешнего файла и записывает их в выходные данные. Аргумент должен быть строкой, содержащей путь к файлу, за ним может необязательно следовать : и числовое значение, указывающее смещение внутри файла, затем за ним может следовать запятая и числовое значение, указывающее, сколько байтов нужно скопировать.

file 'data.bin'                 ; insert entire file
excerpt file 'data.bin':10h,4   ; insert selected four bytes

Таблица 1 Директивы данных
Size (bytes) | Generate data | Reserve data
1 | db file | rb
2 | dw | rw
4 | dd | rd
6 | dp | rp
8 | dq | rq
10 | dt | rt
16 | ddq | rdq
32 | dqq | rqq
64 | ddqq | rdqq
* | emit |


7. Условное ассемблирование

Инструкция if вызывает ассемблирование блока исходного текста только при определенном условии, заданном логическим выражением, являющимся аргументом этой инструкции. Команда else if в следующих строках завершает предыдущий условно ассемблированный блок и открывает новый, собранный только тогда, когда предыдущие условия не были выполнены, а новое условие (аргумент else if ) истинно. Команда else завершает предыдущий условно собранный блок и начинает блок, который ассемблируется только тогда, когда ни одно из предыдущих условий не было истинным. Команда end if должна использоваться для завершения всей конструкции. Внутри может быть много или ни одной команды else if и не более одной команды else.

Логическое выражение - это отдельная синтаксическая сущность от базовых выражений, которые были описаны ранее. Логическое выражение состоит из логических значений, связанных с логическими операторами. Логические операторы: унарные ~ для отрицания, & для конъюнкции и | для альтернативы. Отрицание вычисляется первым, в то время как & и | просто вычисляются слева направо, без приоритета друг над другом.

Логическое значение в его простейшей форме может быть базовым выражением, тогда оно соответствует истинному условию тогда и только тогда, когда его значение не является постоянным нулем. Другой способ создания логического значения - сравнение значений двух базовых выражений с одним из следующих операторов: = (равно), < (меньше), > (больше), <= (меньше или равно), >= (больше или равно), <> (не равно).

count = 2
if count > 1
        db '0'
        db count-1 dup ',0'
else if count = 1
        db '0'
end if

Когда линейные многочлены сравниваются таким образом, логическое значение справедливо только тогда, когда они сопоставимы, то есть они отличаются только постоянным членом. В противном случае условие, подобное равенству, не является ни универсально истинным, ни универсально ложным, поскольку оно зависит от значений, заменяемых переменными, и ассемблер сигнализирует об этом как об ошибке.

Оператор relativeto создает логическое значение, которое истинно только тогда, когда разность сравниваемых значений не содержит переменных термов. Поэтому его можно использовать для проверки того, сопоставимы ли два линейных полинома - условие relativeto верно только тогда, когда оба сравниваемых полинома имеют одинаковые переменные члены.

Поскольку логические выражения вычисляются лениво, можно создать единственное условие, которое не вызовет ошибки, когда многочлены не сопоставимы, но будет сравнивать их, если они сопоставимы:

if a relativeto b & a > b
        db a - b
end if

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

Оператор eq сравнивает два базовых выражения и создает логическое значение, которое истинно только тогда, когда их значения одного типа и равны. Этот оператор можно использовать для проверки того, является ли значение определенной строкой, определенным числом с плавающей запятой или определенным линейным полиномом. Он может сравнивать значения, которые не сопоставимы с оператором = .

Оператор defined создает логическое значение в сочетании с базовым выражением, которое следует за ним. Это условие справедливо, если выражение не содержит символов, не имеющих доступного определения. Выражение проверяется только на наличие его компонентов, оно не обязательно должно иметь вычислимое значение. Это может быть использовано для проверки того, был ли определен символ класса expression, но поскольку символ может быть доступен через прямую ссылку, это условие может быть истинным даже тогда, когда символ определен позже в исходном коде. Если это нежелательно, вместо этого следует использовать оператор definite , поскольку он проверяет, были ли все символы в следующем базовом выражении определены ранее.

Базовое выражение, которое следует за defined , также может быть пустым, и тогда условие тривиально выполняется. Это не относится к definite .

Оператор used формирует логическое значение, если за ним следует один идентификатор. Это условие истинно, если значение указанного символа было использовано в любом месте источника.

assert - это инструкция, которая сигнализирует об ошибке, когда условие, заданное ее аргументом, не выполняется.

assert a < 65536


8. Макроинструкции

Команда macro позволяет определить новую инструкцию в виде макроинструкции. Блок исходного текста между командами macro и end macro становится текстом макроинструкции, и эта последовательность строк собирается вместо исходной команды, которая начинается с идентификатора команды, определенной таким образом.

macro null
        db 0
end macro

null            ; "db 0" is assembled here

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

macro lower name,value
        name = value and 0FFh
end macro

lower a,123h    ; a = 23h

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

Когда имя параметра определено, за ним может следовать символ ? , чтобы обозначить, что он нечувствителен к регистру, аналогично имени в идентификаторе символа. Между именем и ? не должно быть пробелов. За определением параметра также может следовать * , чтобы обозначить, что ему требуется значение, которое не является пустым, или, альтернативно, символ :, за которым следует значение по умолчанию, которое присваивается параметру вместо пустого, когда никакое другое значение не предусмотрено.

macro prepare name*,value:0
        name = value
end macro

prepare x       ; x = 0
prepare y,1     ; y = 1

Если аргумент макроинструкции должен содержать символ запятой, то весь аргумент должен быть заключен между символами < и > (они не становятся частью значения). Если внутри такого значения встречается другой символ < , он должен быть сбалансирован соответствующим символом > внутри того же значения.

macro data name,value
        name:
        .data db value
        .end:
end macro

data example, <'abc',10>

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

macro id first,rest&
        dw first
        db rest
end macro

id 2, 7,1,8

Когда имя параметра должно быть заменено его значением и ему предшествует символ ` (без каких-либо пробелов между ними), текст значения вставляется в строку в кавычках, и эта строка заменяет как символ ` , так и имя параметра.

macro text line&
        db `line
end macro

text x+1        ; db 'x+1'

local - это команда, которая может использоваться только внутри макроинструкции. За ним следует одно или несколько имен, разделенных запятыми, и он объявляет, что имена из этого списка должны в контексте текущей макроинструкции интерпретироваться как принадлежащие специальному пространству имен, связанному с этой макроинструкцией, а не текущему базовому пространству имен. Это позволяет создавать уникальные символы при каждом вызове макроинструкции. Такое объявление определяет дополнительные параметры с указанными именами и, следовательно, влияет только на использование тех имен, которые следуют в той же макроинструкции. Объявление одного и того же имени как локального несколько раз в одной и той же макроинструкции не дает никакого дополнительного эффекта.

macro measured name,string
        local top
        name db string
        top: name.length = top - name
end macro

measured hello, 'Hello!'        ; hello.length = 6

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

Символ, который является локальным для макроинструкции, никогда не считается самой последней меткой, которая является базовой для символов, начинающихся с точки. Более того, его потомственное пространство имен отключено от основного дерева символов, поэтому, если бы команда namespace использовалась с локальным символом в качестве аргумента, символы из основного дерева больше не были бы видны (включая все именованные инструкции ассемблера, даже такие команды, как end namespace ).

Точно так же, как символ выражения может быть переопределен и ссылаться на свое предыдущее значение в определении нового, макроинструкции также могут быть переопределены и использовать предыдущее значение этого символа инструкции в своем тексте:

macro zero
        db 0
end macro

macro zero name
        label name:byte
        zero
end macro

zero x

И точно так же, как и другие символы, макроинструкция может иметь прямую ссылку, когда она определена ровно один раз во всем источнике.

Команда purge отбрасывает определение символа точно так же, как и restore , но делает это для символа класса инструкций. Он ведет себя так же, как "восстановление" во всех других аспектах. Макроинструкция может удалить свое собственное определение с помощью purge .

Макроинструкция может использовать свое собственное значение рекурсивным способом, но во избежание непреднамеренной бесконечной рекурсии эта функция доступна только в том случае, если макроинструкция помечена как таковая, следуя за ее идентификатором символом : .

macro factorial: n
        if n
                factorial n-1
                result = result * (n)
        else
                result = 1
        end if
end macro

В дополнение к разрешению рекурсии такая макроинструкция ведет себя как константа. Его нельзя переопределить, и к нему нельзя применить purge .

Макроинструкция, в свою очередь, может определять другую макроинструкцию или несколько. Блоки, обозначенные macro и end macro , должны быть правильно вложены друг в друга, чтобы такое определение было принято ассемблером.

macro enum enclosing
        counter = 0
        macro item name
                name := counter
                counter = counter + 1
        end macro
        macro enclosing
                purge item,enclosing
        end macro
end macro

enum x
        item a
        item b
        item c
x

Когда требуется, чтобы макроинструкция генерировала непарную команду macro или end macro, это можно сделать с помощью специальной команды esc . Его аргумент становится частью макроинструкции, но не учитывается при подсчете вложенных пар macro и end macro .

macro xmacro name
        esc macro name x&
end macro

xmacro text
        db `x
end macro

Если esc помещается внутри вложенного определения, оно не обрабатывается до тех пор, пока не будет определена самая внутренняя макроинструкция. Это позволяет поместить определение, содержащее esc , в другую макроинструкцию без необходимости повторять esc для каждого уровня вложенности.

Когда за идентификатором макроинструкции в ее определении следует символ ! , он определяет безусловную макроинструкцию. Это особый вид символа класса инструкций, который вычисляется даже в тех местах, где сборка приостановлена - например, внутри условного блока, условие которого ложно, или внутри определения другой макроинструкции. Это позволяет определить инструкции, которые могут быть использованы там, где в противном случае потребовалось бы прямо указать end if или end macro , как в следующем примере:

macro proc name
        name:
        if used name
end macro

macro endp!
        end if
        .end:
end macro

proc tester
        db ?
endp

Если макроинструкция endp в приведенной выше выборке не был определен как безусловный, и блок начинающийся со слов if пропускался, макроинструкция не оценивалась, и это привело бы к ошибке, потому что end if пропал бы.

Следует отметить, что команда end выполняет инструкцию, идентифицируемую по ее аргументу в дочернем пространстве имён нечувствительного к регистру end. Поэтому команда типа end if можно было бы альтернативно вызвать с помощью end.if , и можно переопределить любую такую инструкцию, переопределив символ в end? пространство имен. Более того, любая инструкция, определенная в рамках end? пространство имён может быть вызвано со словом end . Этот слегка модифицированный вариант вышеприведенного образца позволяет использовать эти факты:

macro proc name
        name:
        if used name
end macro

macro end?.proc!
        end if
        .end:
end macro

proc tester
        db ?
end proc

Аналогичное правило применяется к команде else и инструкциям в пространстве имен else? .

Когда идентификатор, состоящий из одиночного символа ? , используется в качестве символа инструкции в определении макроинструкции, он определяет специальную инструкцию, которая затем вызывается каждый раз, когда строка, подлежащая сборке, не содержит безусловной инструкции, и полный текст строки становится аргументами этой макроинструкции. Этот специальный символ также может быть определен как безусловная инструкция, и тогда он вызывается для каждой следующей строки без исключения. Это позволяет полностью переопределить процесс сборки на отдельных участках текста. В следующем примере определяется макроинструкция, которая позволяет определить блок комментариев, пропуская все строки текста до тех пор, пока он не встретит строку с содержимым, равным аргументу, заданному для comment .

macro comment? ender
        macro ?! line&
                if `line = `ender
                        purge ?
                end if
        end macro
end macro

comment ~
         Any text may follow here.
~

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


9. Маркированные макроинструкции

Команда struct позволяет определить помеченную инструкцию в виде макроинструкции. За исключением того, что такое определение должно быть закрыто с помощью end struct вместо end macro, эти макроинструкции определяются так же, как и с помощью команды macro. Помеченная команда вычисляется, когда первый идентификатор команды не является инструкцией, а второй идентификатор относится к классу помеченных команд:

struc some
        db 1
end struc

get some        ; "db 1" is assembled here

Внутри помеченных макроинструкций идентификаторы, начинающиеся с точки, больше не относятся к пространству имён ранее определённой обычной метки. Вместо этого они относятся к пространству имён меток, которыми была маркирована инструкция.

struc POINT
        label . : qword
        .x dd ?
        .y dd ?
end struc

my POINT        ; defines my.x and my.y

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

Для более легкого использования этой функции другие синтаксисы могут быть определены с помощью макроинструкций, как в этом примере:

macro struct? definition&
        esc struc definition
                label . : .%top - .
                namespace .
end macro

macro ends?!
                        %top:
                end namespace
        esc end struc
end macro

struct POINT vx:?,vy:?
        x dd vx
        y dd vy
ends

my POINT 10,20

Команда restruc аналогична команде purge, но она оперирует символами из класса помеченных инструкций. Аналогично, команда mvstruct - это то же самое, что и mvmacro , но для помеченных инструкций.

Как и в случае с macro , можно использовать идентификатор, состоящий из одиночного символа ? с struct . Он определяет специальную маркированную макроинструкцию, которая вызывается каждый раз, когда первый символ строки не распознается как инструкция. Все, что следует за этим первым идентификатором, становится аргументами для помеченной макроинструкции. В следующем примере эта функция используется для улавливания любых осиротевших меток (тех, за которыми не следует ни один символ) и обработки их как обычных, а не вызывающих ошибку. Он достигает этого, делая : значением по умолчанию для параметра def .:

struc ? def::&
        . def
end struc

orphan
regular:
assert orphan = regular

Подобно macro , этот специальный вариант не переопределяет безусловные помеченные инструкции, если только он сам не является безусловным.

Хотя . обеспечивает эффективный метод доступа к символу метки, иногда он может потребоваться для обработки фактического текста метки. Для этого может быть определен специальный параметр и его имя должно быть вставлено в скобки перед именем помеченной макроинструкции:

struc (name) SYMBOL
        . db `name,0
end struc

test SYMBOL


10. Символьные переменные и контекст распознавания

equ - это встроенная маркированная инструкция, которая определяет символ класса выражений с символическим значением. Такое значение может содержать любой текст (даже пустой), и когда оно используется в выражении, это эквивалентно вставке текста его значения вместо его идентификатора, с эффектом, подобным оценке параметра макроинструкции.

Это может привести к другим результатам, чем при использовании стандартной переменной, определенной с помощью = следующем в показано как примере:

numeric = 2 + 2
symbolic equ 2 + 2
x = numeric*3           ; x = 4*3
y = symbolic*3          ; y = 2 + 2*3

В то время как x присваивается значение 12, значение y равно 8. Это показывает, что использование таких символов может привести к непреднамеренным взаимодействиям, и поэтому определения этого типа следует избегать, если это действительно необходимо.

equ допускает переопределения и сохраняет предыдущее значение символа аналогично команде =: , поэтому более раннее значение может быть возвращено с помощью команды restore. Для замены символического значения (аналогично тому, как = перезаписывает обычное значение) вместо equ следует использовать команду reequ .

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

first:
        .x = 1
        link equ .x
        .x = 2
second:
        .x = 3
        db link         ; db 2

Следует отметить, что тот же процесс применяется к аргументам любой макроинструкции, когда они становятся предварительно обработанными параметрами. Если во время выполнения макроинструкции контекст меняется, то идентификаторы в тексте параметров по-прежнему ссылаются на те же символы, что и в строке, вызвавшей инструкцию:

x = 1
namespace x
        x = 2
end namespace
macro prodx value
        namespace x
                db value*x
        end namespace
end macro
prodx x         ; db 1*2

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

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

Если текст, следующий за equ , содержит идентификаторы известных символьных переменных, то каждая из них заменяется своим содержимым и именно такой обработанный текст присваивается вновь определенному символу.

define - это обычная инструкция, которая также создает символическое значение, но в отличие от equ она не вычисляет символические переменные в назначенном тексте. За ним должен следовать идентификатор определяемого символа, а затем текст значения.

Разница между equ и define часто не заметна, потому что при использовании в конечном выражении символьные переменные оцениваются вложенно до тех пор, пока не останутся только используемые составляющие выражений. Возможное использование define заключается в создании ссылки на другую символическую переменную, как показано в следующем примере:

        a equ 0*
        x equ -a
        define y -a
        a equ 1*
        дб х 2 ; дб -0*2
        db y 2 ; db -1*2

Другие варианты использования define появятся в последующих разделах, с введением других инструкций, которые работают с символическими значениями.

define, как и equ , сохраняет предыдущее значение символа. redefine - это вариант этой инструкции, который отбрасывает более раннее значение, аналогично reequin .

Обратите внимание, что, хотя символьные переменные относятся к классу выражений символов, их состояние не может быть определено с помощью операторов типа defined, definite или used , поскольку логическое выражение вычисляется так, как если бы каждая символьная переменная была заменена текстом соответствующего значения. Поэтому оператор, за которым следует идентификатор символьной переменной, будет применен к содержимому этой переменной, каким бы оно ни было. Например, если создается символьная переменная, которая является ссылкой на обычный символ, то любой оператор типа defined , за которым следует идентификатор указанной символьной переменной, будет определять статус связанного символа, а не связывающей переменной.


11. Повторяющиеся блоки инструкций

Инструкция repeat позволяет собрать блок инструкций несколько раз, причем количество повторений определяется значением ее аргумента. Блок инструкций должен заканчиваться командой end repeat. Вместо слова repeat можно использовать синоним rept .

a = 2
repeat a + 3
        a = a + 1
end repeat
assert a = 7

Инструкция while вызывает повторную сборку блока инструкций до тех пор, пока условие, указанное в ее аргументе, является истинным. Его аргумент должен быть логическим выражением, как аргумент для if или assert. Блок должен быть закрыт командой end while .

a = 7
while a > 4
        a = a - 2
end while
assert a= 3

%- это специальный параметр, который предварительно обрабатывается внутри повторяющегося блока инструкций и заменяется десятичным числом, представляющим собой число текущего повторения (начиная с 1). Он работает аналогично параметру макроинструкции, поэтому он заменяется его значением перед обработкой фактической команды и поэтому его можно использовать для создания идентификаторов символов, содержащих число как часть имени:

repeat 16
        f#% = 1 shl %
end repeat

В приведенном выше примере определяются символы от f1 до f16 со значениями, являющимися последовательными степенями двух.

Команда repeat может иметь дополнительные аргументы, разделенные запятыми, каждый из которых содержит имя дополнительных параметров, специфичных для данного блока. За каждым из имен может следовать символ : и выражение, указывающее базовое значение, с которого параметр начнет подсчет повторений. Это позволяет легко изменить предыдущий образец, чтобы определить диапазон символов от f0 до f15:

repeat 16, i:0
        f#i = 1 shl i
end repeat

%%- это еще один специальный параметр, значение которого равно общему количеству запланированных повторений. Этот параметр не определен внутри блока while. В следующем примере он используется для создания последовательности байтов со значениями, уменьшающимися от 255 до 0:

repeat 256
        db %%-%
end repeat

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

s = x/2
repeat 100
        if x/s = s
               break
        end if
        s = (s+x/s)/2
end repeat

Приведенный выше пример пытается найти квадратный корень из значения символа x, который, как предполагается, определен в другом месте. Его можно легко переписать, чтобы выполнить ту же задачу с помощью while вместо repeat:

s = x/2
while x/s <> s
        s = (s+x/s)/2
        if % = 100
                break
        end if
end while

Инструкция iterate (с синонимом irp) повторяет блок инструкций при повторении списка значений, разделенных запятыми. Первым аргументом для iterate должно быть имя параметра, за которым следует запятая, а затем список значений. Во время каждой итерации параметр получает одно из значений из списка.

iterate value, 1,2,3
        db value
end iterate

Как и в случае аргумента макроинструкции, значение параметра, содержащего запятые, должно быть заключено в символы < и>. Также можно заключить первый аргумент для iterate с < и >, чтобы определить несколько параметров. Затем список значений делится на раздел, содержащий столько значений, сколько есть параметров, и каждая итерация работает с одним таким разделом, присваивая каждому параметру соответствующее значение:

iterate <name,value>, a,1, b,2, c,3
        name = value
end iterate

Имя параметра также может, как и в случае макроструктур, сопровождаться *, чтобы требовать, чтобы параметр имел значение, которое не является пустым, или : и значение по умолчанию. Если оператор iterate заканчивается запятой, за которой не следует ничего другого, он не интерпретируется как дополнительное пустое значение, чтобы поместить пустое значение в конец списка, необходимо использовать пустое заключающее<>.

Инструкция break плюс параметры % и %% могут использоваться внутри блока iterate с теми же эффектами, что и в случае repeat.

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

iterate value, 1,2,3
        indx 1+%%-%
        db value
end iterate

С помощью indx можно даже перемещать представление повторяющихся значений много раз во время одного повторения. В следующем примере вся обработка выполняется во время первого повторения итерированного блока, а затем используется инструкция break для предотвращения дальнейших итераций:

iterate str, 'alpha','beta','gamma'
        repeat %%
                dw offset#%
        end repeat
        repeat %%
                indx %
                offset#% db str
        end repeat
        break
end iterate

Параметры, определенные с помощью iterate, не присоединяют контекст к итерированным значениям, но и не удаляют исходный контекст, если он уже присоединен к тексту аргументов. Таким образом, если значения, заданные для iterate, сами были созданы из другого параметра, который сохранил исходный контекст для идентификаторов символов (например, параметр макроинструкции), то этот контекст сохраняется, но в противном случае iterate определяет просто подстановку обычного текста.

Параметры, определенные инструкциями типа iterate или repeat, обрабатываются везде в тексте связанного блока, но с некоторыми ограничениями, если блок определяется частично текстом макроструктуры и частично в других местах. В этом случае параметры доступны только в тех частях блока, которые определены в том же месте, что и начальная команда.

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

Повторяющиеся инструкции вместе с if принадлежат к группе, называемой директивами управления. Это инструкции, которые управляют потоком сборки. Каждый из них определяет свой собственный блок подчиненных инструкций, закрытый соответствующей командой end, и если эти блоки вложены друг в друга, это всегда должно быть правильное вложение - внутренний блок всегда должен быть закрыт перед внешним. Таким образом, все управляющие директивы являются безусловными инструкциями - они распознаются, даже если они находятся внутри пропущенного блока.

postpone - это еще одна директива управления, которая вызывает сборку блока инструкций позже, когда весь следующий исходный текст уже обработан.

dw final_count
postpone
        final_count = counter
end postpone
counter = 0

Приведенный выше пример откладывает определение символа final_count до тех пор, пока не будет обработан весь источник, чтобы он мог получить доступ к конечному значению переменной counter.

Сборка исходного текста, следующая за postpone, включает в себя сборку любых дополнительных блоков, объявленных с postpone, поэтому, если таких блоков несколько, они собираются в обратном порядке. Тот, который был объявлен последним, собирается первым, когда достигается конец исходного текста.

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

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

var equ 1
var equ 2
var equ 3
var reequ 4
irpv param, var
        db param
end irpv

В приведенном выше примере есть три итерации со значениями 1, 2 и 4.

irpv может эффективно преобразовать значение символьной переменной в параметр, и это может быть полезно само по себе, потому что символьная переменная вычисляется только в выражениях внутри аргументов инструкций (помеченных или нет), в то время как параметры предварительно обрабатываются во всей строке перед началом любой обработки команды. Это позволяет, например, переопределить регулярное значение, связанное символьной переменной:

x = 1
var equ x
irpv symbol, var
        indx %%
        symbol = 2
        break
end irpv
assert x = 2

Комбинация indx и break была добавлена к приведенному выше образцу, чтобы ограничить итерацию последним значением символьной переменной. В следующем разделе будет представлено лучшее решение той же проблемы.

Когда переменная, переданная в irpv, имеет значение, которое не является символическим, параметру присваивается текст, который при вычислении выдает то же значение. Когда значение является положительным числом, параметр заменяется его десятичным представлением (аналогично тому, как обрабатывается параметр%), в противном случае параметр заменяется идентификатором прокси-символа, содержащего значение из стека.

Директива outscope доступна во время обработки любой макроструктуры и изменяет команду, которая следует в той же строке. Если команда вызывает определение каких-либо параметров, они создаются не в контексте обрабатываемой в данный момент макроструктуры, а в контексте исходного текста, который ее вызвал.

macro irpv?! statement&
        display 'IRPV wrapper'
        esc outscope irpv statement
end macro

Это позволяет не только безопасно обернуть некоторые директивы управления в макроинструкции, но и создавать дополнительные настраиваемые языковые конструкции, определяющие параметры для блока текста. Поскольку outscope должен присутствовать в тексте конкретной макроструктуры, которая его требует, рекомендуется использовать его в сочетании с esc, как в приведенном выше примере, это гарантирует, что он обрабатывается таким же образом, даже если все определение помещено в другую макроструктуру.


12. Соответствие параметров

match - это директива управления, которая приводит к тому, что ее блок инструкций собирается только тогда, когда текст, указанный ее вторым аргументом, соответствует шаблону, заданному первым. Текст отделяется от шаблона символом запятой и включает в себя все, что следует за этим разделителем до конца строки.

Каждый специальный символ (за исключением , и =, которыеимеют определенное значение в шаблоне) сопоставляется буквально - он должен быть сопряжен с идентичным маркером в тексте. В следующем примере содержимое первого блока собрано, а содержимое второго - нет.

match +,+
        assert 1        ; positive match
end match

match +,-
        assert 0        ; negative match
end match

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

match a[b], 100h[3]
        dw a+b          ; dw 100h+3
end match

Имя параметра в шаблоне может иметь дополнительный символ ?, чтобы указать, что это имя без учета регистра.

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

match =a==a, a=8
        db a            ; db 8
end match

Если за = следует маркер имени с прикрепленным к нему символом ?, этот элемент сопоставляется буквально, но без учета регистра:

match =a?==a, A=8
        db a            ; db 8
end match

Когда в шаблоне много подстановочных знаков, каждый последующий сопоставляется с как можно меньшим количеством токенов, и последний берет то, что осталось. Если подстановочные знаки следуют друг за другом без каких-либо буквально совпадающих элементов между ними, первый из них сопоставляется только с одним маркером, а второй - с оставшимся текстом:

match car cdr, 1+2+3
        db car          ; db 1
        db cdr          ; db +2+3
end match

В приведенном выше примере сопоставленный текст должен содержать не менее двух маркеров, поскольку для каждого подстановочного знака требуется, чтобы хотя бы один маркер не был пустым. В следующем примере есть дополнительные ограничения, но применяются те же общие правила, и первый подстановочный знак потребляет как можно меньше:

match first:rest, 1+2:3+4:5+6
        db `first       ; db '1+2'
        db 13,10
        db `rest        ; db '3+4:5+6'
end match

В то время как любые пробелы рядом с подстановочным знаком игнорируются, наличие или отсутствие пробелов между буквально совпадающими элементами имеет значение. Если такие элементы не имеют пробелов между ними, их аналоги также не должны содержать пробелов между ними. Но если в шаблоне есть пробел между элементами, он не накладывает никаких ограничений на использование пробелов в соответствующем тексте - он может присутствовать или нет.

match ++,++
        assert 1        ; positive match
end match

match ++,+ +
        assert 0        ; negative match
end match

match + +,++
        assert 1        ; positive match
end match

match + +,+ +
        assert 1        ; positive match
end match

Наличие пробелов в тексте становится обязательным, если шаблон содержит символ =, за которым следует пробел:

match += +, ++
        assert 0        ; negative match
end match

match += +, + +
        assert 1        ; positive match
end match

Команда match аналогична команде if в том, что она позволяет использовать else или else match для создания выборки блоков, из которых выполняется только один:

macro let param
        match dest+==src, param
                dest = dest + src
        else match dest-==src, param
                dest = dest + src
        else match dest++, param
                dest = dest + 1
        else match dest--, param
                dest = dest + 1
        else match dest==src, param
                dest = src
        else
                assert 0
        end match
end macro

let x=3                 ; x = 3
let x+=7                ; x = x + 7
let x++                 ; x = x + 1

Можно даже смешивать условия if и match в последовательности блоков else. Вся конструкция должна быть закрыта командой end, соответствующей тому, какой из двух был использован последним:

macro record text
        match any, text
                recorded equ `text
        else if RECORD_EMPTY
                recorded equ ''
        end if
end macro

match способно распознавать символьные переменные, и перед началом сопоставления их идентификаторы в тексте второго аргумента заменяются соответствующими значениями (точно так же, как они заменяются в тексте, следующем за командой equ ):

var equ 2+3

match a+b, var
        db a xor b
end match

Это означает, что match можно использовать вместо irpv для преобразования последнего значения символьной переменной в параметр. Пример из предыдущего раздела, где irpv использовался с break для выполнения только одной итерации по последнему значению, можно переписать, чтобы вместо этого использовать match :

x = 1
var equ x
match symbol, var
        symbol = 2
end match
assert x = 2

Разница между ними заключается в том, что irpv выполнит свой блок даже для пустого значения, в то время как в случае match блок else должен быть добавлен для обработки пустого текста.

Когда оценка символьных переменных в сопоставленном тексте нежелательна, символ, созданный с помощью define, может использоваться в качестве прокси для сохранения текста, поскольку замена не является рекурсивной:

macro drop value
        local temporary
        define temporary value
        match =A, temporary
                db A
                restore A
        else
                db value
        end match
end macro

A equ 1
A equ 2

drop A
drop A

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

Директива rawmatch (с синонимом rmatch) очень похожа на match, но она работает с необработанным текстом второго аргумента. Он не только не оценивает символьные переменные, но и лишает текст любого дополнительного контекста, который он мог бы нести.

struc has instruction
        rawmatch text, instruction
                namespace .
                        text
                end namespace
        end rawmatch
end struc

define x
x has a = 3
assert x.a = 3

В приведенном выше примере идентификатор a будет интерпретироваться в контексте, эффективном для строки, вызывающей макроинструкцию has, если он не был преобразован обратно в необработанный текст с помощью rmatch.


13. Области вывода

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

org 100h
start:                  ; start = 100h

$ - это встроенный символ класса выражений, который всегда равен значению текущего адреса. Поэтому определение константы со значением, указанным символом $, эквивалентно определению метки в той же точке:

org 100h
start = $               ; start = 100h

Символ $$ всегда равен основанию текущего адресного пространства, поэтому в области, начинающейся с org, он имеет то же значение, что и базовый адрес из аргумента org. Разница между $ и$$, таким образом, является текущей позицией относительно начала области:

org 2000h
db 'Hello!'
size = $ - $$           ; size = 6

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

Инструкция section аналогична инструкции org, но она дополнительно урезает все зарезервированные данные, которые предшествуют ей, аналогично тому, как неинициализированные данные не записываются в выходные данные, когда они находятся в конце файла. Таким образом, за section могут следовать инициализированные определения данных, не вызывая инициализации ранее зарезервированных данных нулями и записи в выходные данные. В этом примере только первый из трех зарезервированных буферов фактически преобразуется в обнуленные данные и записывается в выходные данные, поскольку за ним следуют некоторые инициализированные данные. Второй обрезается из-за section, а третий обрезается, так как он находится в конце файла:

data1 dw 1
buffer1 rb 10h          ; zeroed and present in the output

org 400h
data dw 2
buffer2 rb 20h          ; not in the output

section 1000h
data3 dw 3
buffer3 rb 30h          ; not in the output

$% - это встроенный символ, равный смещению в выходном файле, при котором были бы сгенерированы инициализированные данные, если бы они были определены в этот момент. Символ $%% - это текущее смещение в выходном файле. Эти два значения различаются только тогда, когда они используются после того, как некоторые данные были зарезервированы - $% тогда больше, чем $%%, на длину неинициализированных данных, которые были бы сгенерированы в выходные данные, если бы за ними следовали какие-то инициализированные.

db 'Hello!'
rb 4
position = $%%          ; position = 6
next = $%               ; next = 10

Значения в комментариях приведенного выше примера предполагают, что источник не содержит других инструкций, генерирующих выходные данные.

virtual создает специальную область вывода, которая не записывается в основной выходной файл. Такая область должна находиться между командами virtual и end virtual, и после ее закрытия выходной генератор возвращается в область, в которой он ранее работал, с положением и адресом, такими же, как и перед открытием virtual блока. Это также позволяет вложить virtual блоки друг в друга.

Когда virtual не имеет аргумента, базовый адрес этой области совпадает с текущим адресом во внешней области. Аргумент virtual может иметь форму ключевого слова at, за которым следует выражение, определяющее базовый адрес для закрытой области:

int dw 1234h
virtual at int
        low db ?
        high db ?
end virtual

Вместо или в дополнение к такому аргументу virtual также может сопровождаться ключевым словом as и строкой, определяющей расширение дополнительного файла, в котором инициализированное содержимое области будет храниться в конце успешной сборки.

Инструкция load определяет значение переменной, загружая строку байтов из данных, сгенерированных в области вывода. За ним должен следовать идентификатор определяемого символа, затем, необязательно, символ : и количество байтов для загрузки, затем ключевое слово from и адрес загружаемых данных. Этот адрес может быть указан в двух режимах. Если это просто числовое выражение, то это адрес в текущей области. В этом случае загруженные байты должны быть уже сгенерированы, поэтому загрузка возможна только из пространства между адресами $$ и$.

virtual at 100h
        db 'abc'
        load b:byte from 101h   ; b = 'b'
end virtual

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

Другой вариант load нуждается в специальной метке, которая создается с помощью :: вместо : . Такая метка имеет значение, которое нельзя использовать напрямую, но его можно использовать с инструкцией load для доступа к данным области, в которой была определена эта метка. Затем адрес для load должен быть указан как метка области, за которой следует : , а затем адрес в этой области:

virtual at 0
        hex_digits::
        db '0123456789ABCDEF'
end virtual
load a:byte from hex_digits:10  ; a = 'A'

Этот вариант load может получить доступ к данным, которые генерируются позже, даже в пределах текущей области:

area::
db 'abc'
load sub:3 from area:$-2        ; sub = 'bcd'
db 'def'

Инструкция store может изменять уже сгенерированные данные в области вывода. За ним должно следовать значение (автоматически преобразованное в строку байтов), затем, необязательно, символ :, за которым следует количество байтов для записи (когда этот параметр отсутствует, длина строки определяется размером, связанным с адресом), затем ключевое слово at и адрес данных для замены в одном из тех же двух режимов, которые разрешены load. Однако store не имеет права изменять данные, которые еще не были сгенерированы, и любая область, которая была затронута store, становится переменной областью, запрещая также load заранее считывать данные из такой области.

В следующем примере используется комбинация load и store для шифрования всего содержимого текущей области с помощью простой операции xor.:

db 'Text'
key = 7Bh
repeat $-$$
        load a : byte from $$+%-1
        store a xor key : byte at $$+%-1
end repeat

Если окончательные данные области, которые были изменены store, должны быть прочитаны ранее в источнике, это может быть достигнуто путем копирования этих данных в другую область, которая не была бы ограничена таким образом. Это аналогично определению константы с конечным значением некоторой переменной:

load char : byte from const:0

virtual
        var::
        db 'abc'
        .length = $
end virtual

store 'A' : byte at var:0

virtual
        const::
        repeat var.length
                load a : byte from var:%-1
                db a
        end repeat
end virtual

Метка области может быть передана с помощью load, но она никогда не может быть передана с помощью store, даже если она ссылается на текущую выходную область.

virtual инструкция может иметь в качестве единственного аргумента существующую метку области. Этот вариант позволяет расширить ранее определенный и закрытый блок дополнительными данными. Метка области должна ссылаться на блок, который был создан ранее в источнике с помощью virtual. Любое определение данных в расширяющемся блоке будет иметь тот же эффект, как если бы это определение присутствовало в исходном virtual блоке.

virtual at 0 as 'log'
        Log::
end virtual

virtual Log
        db 'Hello!',13,10
end virtual

Если метка области используется в выражении, она образует переменный член линейного полинома. Метаданными такого термина является строка :: , позволяющая определить, что метка области использовалась для формирования значения, поскольку метаданные терминов, созданных с помощью element, всегда являются числовыми.

Существует дополнительный вариант директив load и store, который позволяет считывать и изменять уже сгенерированные данные в выходном файле с учетом простого смещения в этом выходном файле. Этот вариант распознается, когда за ключевым словом at или from следует символ :, а затем значение смещения.

checksum = 0
repeat $%
        load a : byte from : %-1
        checksum = checksum + a
end repeat

Инструкция restartout отказывается от всех выходных данных, сгенерированных до этого момента, и начинает заново с пустого. Необязательный аргумент может указывать базовый адрес недавно запущенной области вывода. Когда restartout не имеет аргумента, текущий адрес сохраняется, используя его в качестве основы для новой области.

Инструкции org, section и restartout не могут использоваться внутри virtual блока, они могут только разделять области, которые входят в выходной файл.


14. Управление исходником и выводом

Инструкция include считывает исходный текст из другого файла и обрабатывает его, прежде чем продолжить работу в текущем источнике. Его аргументом должна быть строка, определяющая путь к файлу (формат пути может зависеть от операционной системы). Если между инструкцией и аргументом есть ! , другой файл считывается и обрабатывается безоговорочно, даже если он находится внутри пропущенного блока (безусловные инструкции из другого файла могут быть распознаны).

include 'macro.inc'

Дополнительный аргумент может быть дополнительно добавлен (отделен от пути запятой), и он интерпретируется как команда, выполняемая после того, как файл был прочитан и вставлен в исходный поток, непосредственно перед обработкой первой строки.

Инструкция eval берет последовательность байтов, определенную ее аргументами, обрабатывает ее как исходный текст и собирает ее. Аргументы представляют собой либо строки, либо числовые значения отдельных байтов, разделенные запятыми. В следующем примере eval используется для генерации определений символов, названных последовательными буквами алфавита:

repeat 26
        eval 'A'+%-1,'=',`%
end repeat

assert B = 2

Инструкция display вызывает запись последовательности байтов в стандартный вывод рядом с сообщениями, генерируемыми ассемблером. За ним должны следовать строки или числовые значения отдельных байтов, разделенные запятыми. В следующем примере используется repeat 1 для определения параметра с десятичным представлением вычисленного числа, а затем отображается в виде строки:

macro show description,value
        repeat 1, d:value
                display description,`d,13,10
        end repeat
end macro

show '2^64=',1 shl 64

Инструкция err сигнализирует об ошибке в процессе сборки с пользовательским сообщением, указанным в ее аргументе. Он допускает те же аргументы, что и директива display.

if $>10000h
        err 'segment too large'
end if

Директива format позволяет настроить дополнительные параметры, касающиеся основного вывода. В настоящее время единственным доступным выбором является format binary, за которым следует ключевое слово as и строка, определяющая расширение выходного файла. Если имя выходного файла не указано в командной строке, он создается из пути к основному исходному файлу путем удаления расширения и присоединения нового расширения, если таковое определено.

format binary as 'com'

Директива format, аналогично end, использует идентификатор, который следует за ней, чтобы найти инструкцию в дочернем пространстве имен символа format, не учитывающего регистр. Единственная встроенная инструкция, которая находится в этом пространстве имен - это binary, но дополнительные инструкции могут быть определены в виде макроструктур.

Встроенный символ __time__ (с устаревшим синонимом %t) имеет постоянное значение метки времени, отмечающей момент времени, когда сборка была запущена.

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

__source__ - это еще один встроенный символ, значение которого представляет собой строку, содержащую имя основного исходного файла.

Директива retaincomments переключает ассемблер на обработку точки с запятой как обычного маркера и, следовательно, не удаляет комментарии из строк перед обработкой. Это позволяет использовать точки с запятой в таких местах, как шаблон MATCH.

retaincomments
macro ? line&
        match instruction ; comment , line
                virtual
                        comment
                end virtual
                instruction
        else
                line
        end match
end macro

var dd ?  ; bvar db ?

Директива isolatelines запрещает ассемблеру впоследствии комбинировать строки, прочитанные из исходного текста, когда разрыву строки предшествует обратная косая черта.

Директива removecomments возвращает поведение точек с запятой по умолчанию, а директива combinelines позволяет объединять строки из исходного текста как обычно.


15. Инструкции CALM

Директива calminstruction позволяет определять новые инструкции в виде скомпилированных последовательностей специализированных команд. В отличие от обычных макроинструкций, которые работают по прямому принципу текстовой подстановки, спокойные (скомпилированные макро-инструкции, подобные ассемблированию) способны выполнять множество операций без прохождения какого-либо текста через стандартный цикл предварительной обработки и сборки. Это обеспечивает более точный контроль, лучшую обработку ошибок и более быстрое выполнение.

Все ссылки на символы в тексте, определяющем CALM инструкцию, фиксируются во время определения. Как следствие, любые символы, локальные для инструкции CALM, являются общими для всех ее исполняемых экземпляров (например, последовательные экземпляры могут видеть значения локальных символов, оставленных предыдущими). Чтобы помочь в повторном использовании этих ссылок, команды в CALM обычно работают с переменными, регулярно переписывая символы с новыми значениями.

Оператор calminstruction следует тем же правилам, что и объявление macro, включая такие параметры, как модификатор ! для определения безусловной инструкции, * для пометки требуемого аргумента, : для придания ему значения по умолчанию и &, чтобы указать, что последний аргумент потребляет весь оставшийся текст в строке.

Однако, поскольку команда CALM работает вне стандартного цикла предварительной обработки и сборки, ее аргументы не становятся параметрами предварительной обработки. Вместо этого они являются локальными символьными переменными, получающими новые значения при каждом вызове инструкции.

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

В определении CALM обучения идентифицируются только утверждения его специализированного языка. Начальным символом каждой строки должно быть простое имя без модификаторов, и она распознается как допустимая инструкция только в том случае, если символ без учета регистра с таким именем находится в пространстве имен команд CALM (которое для целей настройки доступно как пространство имен, привязанное к символу calminstruction без учета регистра). Если такая именованная инструкция не найдена, начальное имя может стать меткой, если за ним следует :, затем оно обрабатывается как чувствительный к регистру символ, принадлежащий специализированному классу. Символы этого класса распознаются только при использовании в качестве аргументов для команд перехода (описано ниже).

Оператор end calminstruction необходимо использовать, чтобы закрыть определение и вернуть нормальный режим сборки. Это не обычная команда end, а инструкция с одинаковым именем в пространстве имен CALM, которая принимает в качестве аргумента только calminstruction.

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

calminstruction please? cmd&
 assemble cmd
end calminstruction

please display 'Hi!'

Команда match во многом похожа на стандартную директиву с тем же именем. Его первым аргументом должен быть шаблон, следующий тем же правилам, что и для директивы match. Второй аргумент должен быть идентификатором символьной переменной, текст которой будет сопоставлен с шаблоном. Маркеры имен в шаблоне (за исключением тех, которые сделаны буквальными с символом =) рассматриваются как имена переменных, в которые должны быть помещены соответствующие части текста, если совпадение будет успешным. Та же переменная, которая является источником текста, также может использоваться в шаблоне в качестве переменной для записи. Когда совпадения нет, все переменные остаются незатронутыми.

calminstruction please? cmd&
        match (cmd), cmd
        assemble cmd
end calminstruction

please(display 'Hi!')

Было ли совпадение успешным, также можно проверить с помощью условного перехода jyes или jno после команды match. Прыжок jyes выполняется только тогда, когда матч удался.

calminstruction please? cmd&
        match =do? =not? cmd, cmd
        jyes done
        assemble cmd
    done:
end calminstruction

please do not display 'Bye!'

Для дальнейшего управления потоком обработки команда jump позволяет прыгать безоговорочно, а с помощью команды exit можно в любой момент прекратить обработку команды (эта команда не принимает аргументов).

В то время как символы, используемые для аргументов инструкции, являются неявно локальными, другие идентификаторы могут стать фиксированными ссылками на глобальные символы, если они считаются доступными во время определения (поскольку в инструкции CALM все такие ссылки рассматриваются как используемые, а не как определяемые). Такая команда, как match, может затем записываться в глобальную переменную.

define comment

calminstruction please? cmd&
        match cmd //comment, cmd
        assemble cmd
end calminstruction

please display 'Hi!' // 3
db comment                      ; db 3

Чтобы принудительно обработать символ как локальный, следует использовать команду local, за которой следует одно или несколько имен, разделенных запятыми.

calminstruction please? cmd&
        local comment
        match cmd //comment, cmd
        assemble cmd
end calminstruction

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

Если шаблон в инструкции имеет символ ? сразу после имени подстановочного знака, это не влияет на то, как идентифицируется символ (независимо от того, является ли используемый символ нечувствительным к регистру, зависит от того, что присутствует в локальной области в момент определения инструкции). Вместо этого изменение имени подстановочного знака с помощью ? позволяет сопоставить его с пустым текстом.

Поскольку исходный текст для match в этом варианте задается только одним идентификатором, этот синтаксис позволяет иметь больше аргументов. Необязательный третий аргумент для match может содержать пару скобочных символов. Затем любой элемент подстановочного знака должен быть сопоставлен с текстом, в котором такие скобки правильно сбалансированы.

calminstruction please? cmd&
        local first, second
        match first + second, cmd, ()
        jyes split
        assemble cmd
        exit
    split:
        assemble first
        assemble second
end calminstruction

please display 'H',('g'+2) + display '!'

Скобки, выбранные третьим аргументом, не должны использоваться нигде в шаблоне.

Команда arrange похожа на обратную команду match, она может создавать текст, содержащий значения одной или нескольких переменных. Первый аргумент определяет переменную, в которой будет храниться сконструированный текст, в то время как второй аргумент представляет собой шаблон, сформированный таким же образом, как и для match (за исключением того, что ему не нужно предшествовать запятой с =, чтобы он был включен в аргумент). Все маркеры без имен, кроме = и маркеров, которым предшествует =,буквально копируются в сконструированный текст, и они не несут с собой никакого контекста распознавания. Маркеры имен, которые не являются буквальными с помощью =, являются трактатами как имена переменных, символические значения которых помещаются на их место в сконструированный текст.

calminstruction addr? arg
        local base, index
        match base[index], arg
        local cmd
        arrange cmd, =dd base + index
        assemble cmd
end calminstruction

addr 8[5]                       ; dd 8 + 5

При правильно выбранных шаблонах arrange можно использовать для копирования символьного значения из одной переменной в другую или для присвоения ей фиксированного значения (даже пустого).

Если переменная, используемая в шаблоне, оказывается, имеет числовое значение вместо символьного, если это неотрицательное число без дополнительных членов, оно преобразуется в десятичный токен, хранящийся в сконструированном символьном значении (операция, которая за пределами инструкций CALM потребует использования трюка repeat 1 ):

digit = 4 - 1

calminstruction demo
        local cmd
        arrange cmd, =display digit#0h
        assemble cmd
end calminstruction

demo                            ; display 3#0h

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

Таким образом, compute может использоваться не только для вычисления заранее определенного выражения, но и для анализа и вычисления выражения из текста символьной переменной (например, из аргумента инструкции) или комбинации того и другого:

a = 0

calminstruction low expr*
        compute a, expr and 0FFh
end calminstruction

low 200 + 73                    ; a = 11h

Поскольку символьная переменная оценивается как вложенное выражение, ее использование здесь не имеет побочных эффектов, которые были бы вызваны простой заменой текста.

Команда check аналогична команде if. Она вычисляет условие, определенное логическим выражением, которое следует за ним, и соответственно устанавливает флаг результата, который может быть проверен с помощью команды jyes или jno. Значения символьных переменных обрабатываются как числовые подвыражения (они могут не содержать никаких операторов, специфичных для логического выражения).

calminstruction u8range? value
        check value >= 0 & value < 256
        jyes ok
        local cmd
        arrange cmd, =err 'value out of range'
        assemble cmd
    ok:
end calminstruction

u8range -1

Команда publish позволяет присвоить значение символу, определяемому текстом, содержащимся в переменной. Это позволяет определить символ с именем, созданным с помощью команды типа arrange, или имя, которое было передано в аргументе инструкции. Первым аргументом должна быть символьная переменная, содержащая идентификатор определяемого символа, вторым аргументом должна быть переменная, содержащая присваиваемое значение (символическое или числовое). За первым аргументом может следовать символ :, указывающий на то, что символ должен быть постоянным, или ему может предшествовать :, чтобы значение укладывалось поверх предыдущего (чтобы предыдущее можно было вернуть с помощью директивы restore).

calminstruction constdefine? var
        local val
        arrange val,
        match var=  val, var
        publish var:, val
end calminstruction

constdefine plus? +

Приведенная выше инструкция позволяет определить символическую константу, что невозможно со стандартными директивами ассемблера.

Цель команды transform состоит в замене идентификаторов символьных переменных (или констант) их значениями в заданном тексте, что является той же операцией, что и директива equ, когда она подготавливает значение для присвоения. Аргументом для transform должна быть символьная переменная, значение которой будет обработано таким образом, а затем заменено преобразованным текстом.

calminstruction (var) constequ? val
        transform val
        publish var:, val
end calminstruction

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

calminstruction prepasm? cmd&
    loop:
        transform cmd
        jyes loop               ; warning: may hang on cyclic references
        assemble cmd
end calminstruction

Флаг результата изменяется только некоторыми командами, такими как check, match или transform. Другие команды сохраняют его неизменным.

При необходимости transform может иметь два аргумента, второй из которых указывает пространство имен. Идентификаторы в тексте, заданном первым аргументом, затем интерпретируются как символы в этом пространстве имен независимо от их исходного контекста.

stringify - это команда, которая преобразует текст переменной в строку и записывает его в ту же переменную (заданную единственным аргументом). Эта операция аналогична операции, выполняемой оператором ` при предварительной обработке.

calminstruction (var) strcalc? val
        compute val, val        ; compute expression
        arrange val, val        ; convert result to a decimal token
        stringify val           ; convert decimal token to string
        publish var, val
end calminstruction

p strcalc 1 shl 1000
display p

В то время как большинство команд, доступных для CALM инструкций, заменяют значения переменных при записи в них, take - это команда, которая позволяет работать со стеками значений. Он удаляет самое верхнее значение исходного символа (заданное вторым аргументом) и передает его символу назначения (первому аргументу), помещая его поверх любых существующих значений. Аргумент назначения может быть пустым, в таком случае значение полностью удаляется, и операция аналогична директиве restore. Эта команда обновляет флаг результата, чтобы указать, было ли какое-либо значение для удаления. Если символ назначения совпадает с исходным, флаг результата можно использовать для проверки наличия доступного значения, не влияя на него.

calminstruction reverse? cmd&
        local tmp, stack
    collect:
        match tmp=,cmd, cmd
        take stack, tmp
        jyes collect
    execute:
        assemble cmd
        take cmd, stack
        jyes execute
end calminstruction

reverse display '!', display 'i', display 'H'

Символ, доступ к которому осуществляется как к месту назначения, так и к источнику с помощью команды take, никогда не может быть перенаправлен, даже если это возможно в противном случае.

Определение макроструктур в пространстве имен calminstruction без учета регистра позволяет добавлять настраиваемые команды в язык инструкций CALM. Однако они должны быть определены как нечувствительные к регистру, чтобы быть признанными таковыми.

macro calminstruction?.asmarranged? variable*, pattern&
        arrange variable, pattern
        assemble variable
end macro

calminstruction writeln? text&
        asmarranged text, =display text,10
end calminstruction

writeln 'Next!'

Такие дополнительные команды могут даже быть определены как сами CALM инструкции:

calminstruction calminstruction?.initsym? variable*,value&
        publish variable, value
end calminstruction

calminstruction show? text&
        local command
        initsym command, display text
        stringify text
        assemble command
end calminstruction

show :)

Команда initsym в этом примере используется для присвоения текста локальной символьной переменной в момент определения команды show. Подобно local (и в отличие от stringify и assembly), он не создает никакого фактического кода, который был бы выполнен при вызове инструкции show. Аргументы initsym сохраняют свой исходный контекст, поэтому символы в тексте, присвоенном переменной command, интерпретируются как в локальном пространстве имен инструкции show. Это позволяет команде display получить доступ к text, даже если он является локальным для инструкции CALM и поэтому обычно виден только в области определения show. Это похоже на использование define для формирования символических ссылок.


А кто у нас молодец?

Содержание
А кто у нас молодец?
 Previous

Если что, заходите к нам на огенек в телеграмм-чат https://t.me/ChatAssembler (на входе антиботовый тест).

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

Версия перевода fasmg на весну 2021 года.


Содержание
Содержание
 Next

1. Глава 1 Введение
1.1 Обзор компилятора
1.1.1 Системные требования
1.1.2 Использование компилятора
1.1.3 Клавиатурные команды в редакторе
1.1.4 Параметры редактора
1.1.5 Компилятоция из командной строки
1.1.6 Сообщения компилятора командной строки
1.1.7 Форматы вывода
1.2 Синтаксис ассемблера
1.2.1 Синтаксис инструкций
1.2.2 Определение данных
1.2.3 Константы и метки
1.2.4 Числовые выражения
1.2.5 Переходы и вызовы
1.2.6 Установки размера

2. Глава 2 Описание инструкций
2.1 Инструкции архитектуры x86
2.1.1 Инструкции перемещения данных
2.1.2 Инструкции преобразования типов
2.1.3 Двоичные арифметические инструкции
2.1.4 Десятичные арифметические инструкции
2.1.5 Логические инструкции
2.1.6 Инструкции передачи управления
2.1.7 Инструкции ввода-вывода
2.1.8 Строковые операции
2.1.9 Инструкции управления флагами
2.1.10 Условные операции
2.1.11 Разные инструкции
2.1.12 Системные инструкции
2.1.13 Инструкции FPU
2.1.14 Инструкции MMX
2.1.15 Инструкции SSE
2.1.16 SSE2 instructions
2.1.17 SSE3 instructions
2.1.18 AMD 3DNow! instructions
2.1.19 Инструкции длинного x86-64 режима
2.1.20 SSE4 instructions
2.1.21 AVX instructions
2.1.22 AVX2 instructions
2.1.23 Auxiliary sets of computational instructions
2.1.24 AVX-512 instructions
2.1.25 Other extensions of instruction set
2.2 Директивы управления
2.2.1 Числовые константы
2.2.2 Условное ассемблирование
2.2.3 Повторение блоков инструкций
2.2.4 Адресные пространства
2.2.5 Другие директивы
2.2.6 Множественные проходы
2.3 Директивы препроцессора
2.3.1 Включение файлов-исходников
2.3.2 Символьные константы
2.3.3 Макроинструкции
2.3.4 Структуры
2.3.5 Повторение макроинструкций
2.3.6 Условный препроцессинг
2.3.7 Порядок обработки
2.4 Директивы форматирования
2.4.1 MZ
2.4.2 PE
2.4.3 COFF
2.4.4 ELF

3. Глава 3 Программирование под Windows
3.1 Основные заголовки
3.1.1 Структуры
3.1.2 Импорт
3.1.3 Процедуры (32-bit)
3.1.4 Процедуры (64-bit)
3.1.5 Настройки процедур
3.1.6 Экспорт
3.1.7 COM
3.1.8 Ресурсы
3.1.9 Текстовые кодировки
3.2 Расширенные заголовки
3.2.1 Параметры процедур
3.2.2 Структурирование исходников

4. Понимание fasm
Ассемблер как компилятор и ассемблер как интерпретатор
Уровень исполнения и уровень интерпретации
Использование fasm в качестве чистого интерпретатора
Разрешение кода
Препроцессор
Макроинструкции
Непосредственные макроинструкции
Лексемы и генератор строк
Обработка индивидуальных лексем
Условный препроцессинг
Обработка синтаксических конструкций пользователя

5. Принципы проектирования или чем отличается Fasm?
1. С чего все началось - TASM против NASM
2. Гибкость - средства разработки ОС
3. Тот же источник, тот же вывод
4. Разрешение кода
5. Сложные решения с простыми особенностями
Заключение

6. Введение и обзор flat assembler g
Что такое flat assembler g?
Как это работает?
Каков способ разбора аргументов инструкции?
Как обрабатываются метки?
Как можно параллельно генерировать несколько секций файла?
Какие есть варианты для разбора других видов синтаксиса?
Как определить инструкцию, разделяющую имя с одной из основных директив?
Как преобразовать макроинструкцию в CALM?

7. Flat assembler g. Руководство пользователя
0. Запуск ассемблера
1. Фундаментальные правила синтаксиса
2. Идентификаторы символов
3. Определения основных символов
4. Значения выражений
5. Классы символов
6. Генерация данных
7. Условное ассемблирование
8. Макроинструкции
9. Маркированные макроинструкции
10. Символьные переменные и контекст распознавания
11. Повторяющиеся блоки инструкций
12. Соответствие параметров
13. Области вывода
14. Управление исходником и выводом
15. Инструкции CALM

"А кто у нас молодец?." "Я молодец"