FASM Preprocessor Tutorial by Vid

Руководство по препроцессору FASM

    Содержание

  1. Об этом документе
  2. Общие понятия
    1. Что такое препроцессор
    2. Комментарии
    3. Перенос строки
    4. Директива INCLUDE
  3. Присваивания
    1. Директива EQU
    2. Директива RESTORE
  4. Простые макросы без аргументов
    1. Определение простых макросов
    2. Вложенные макросы
    3. Директива PURGE. Отмена определения макроса
    4. Поведение макросов
  5. Макросы с фиксированным количеством аргументов
    1. Макросы с одним аргументом
    2. Макросы с несколькими аргументами
    3. Директива LOCAL
    4. Оператор объединения #
    5. Оператор `
  6. Макросы с групповыми аргументами
    1. Определение макросов с групповым аргументом
    2. Директива COMMON
    3. Директива FORWARD
    4. Директива REVERSE
    5. Комбинирование директив управления группами
    6. Директива LOCAL в макросах с групповыми аргументами
    7. Макросы с несколькими групповыми аргументами
  7. Условный препроцессинг
    1. Оператор EQ
    2. Оператор EQTYPE
    3. Оператор IN
  8. Структуры
  9. Оператор FIX и макросы внутри макросов

1. Об этом документе

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

Если Вам что-нибудь из туториала покажется непонятным, пожалуйста, напишите на форум FASM, форум WASM, автору или переводчику.

2. Общие понятия

2.1. Что такое препроцессор

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

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

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

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

2.2. Комментарии

Подобно большинству ассемблеров, комментарии в FASM начинаются с точки с запятой ;. Всё, что следует за этим символом до конца строки игнорируется и удаляется из исходника.

К примеру, исходный текст

; заполним 100h байтов адресуемых EDI нулями
xor	eax, eax	; обнуляем eax
mov	ecx, 100h/4
rep	stosd
после препроцессора превращается в

xor	eax,eax
mov	ecx,100h/4
rep	stosd

ПРИМЕЧАНИЕ: ; можно рассматривать как директиву препроцессора, удаляющую текст начиная с этого символа до конца строки.

ПРИМЕЧАНИЕ: Строка, полностью состоящая из комментария не будет удалена. Она становится пустой строкой (см. пример выше). Это будет важно в дальнейшем.

2.3. Перенос строки (Line Break)

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

Например:

db	1, 2, 3,\
    4, 5, 6,\
    7, 8, 9
будет преобразовано в:
db	1,2,3,4,5,6,7,8,9

Конечно, \ в составе текстовой строки или комментария не вызовет объединения строк. Внутри текстовой строки этот символ воспринимается как обычный ASCII символ (как и всё остальное заключённое между кавычками ' или "). Комментарии же удаляются без анализа того, что в них написано.

В строке после символа \ могут быть только пробелы или комментарии.

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

db	1, 2, 3,\
;	4,5,6,\   - закомментировано
    7, 8, 9
преобразуется в:
db	1, 2, 3
    7, 8, 9
и вызовет ошибку. Выход из положения - помещать символ \ до комментария:
db	1, 2, 3,\
\;	4,5,6     - правильно закомментировано
    7, 8, 9
в результате будет:
db	1, 2, 3, 7, 8, 9
как мы и хотели.

2.4. Директива INCLUDE

Синтаксис:
include	'file name'
Эта директива вставляет содержимое файла file name в исходный текст. Вставленный текст, естественно, тоже будет обработан препроцессором. Имя файла (и путь к нему, если он есть) должны быть заключены в кавычки " или апострофы '.

Например:

include	'file.asm'
include	'HEADERS\data.inc'
include	'..\lib\strings.asm'
include	'C:\config.sys'

Можно также использовать переменные окружения ОС, помещая их имена между символами %:

include	'%FASMINC%\win32a.inc'
include	'%SYSTEMROOT%\somefile.inc'
include	'%myproject%\headers\something.inc'
include	'C:\%myprojectdir%\headers\something.inc'

3. Присваивания (Equates)

3.1. Директива EQU

Простейшая команда препроцессора.

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

Например:

count	equ 10  ; это команда препроцессора
mov	ecx, count
преобразуется в:
mov	ecx, 10

Ещё пример:

mov	eax, count
count	equ 10
mov	ecx, count
преобразуется в:
mov	eax, count
mov	ecx,10
потому что препроцессор заменит count только после директивы equ.

Даже это работает:

10	equ 11
mov	ecx, 10
после обработки препроцессором, получим:
mov	ecx, 11
Обратите внимание, name1 может быть любым идентификатором. Идентификатор - это всего лишь набор символов, завершаемый пробелом (space), символом табуляции (tab), концом строки (EOL), комментарием ;, символом переноса строки \ или оператором, включая операторы ассемблера и/или специальные символы вроде , или }.

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

Например:

10	equ 11, 12, 13
db	10
получим:
db	11, 12, 13

3.2. Директива RESTORE

Можно заставить препроцессор прекратить заменять идентификаторы, определённые директивой EQU. Это делает директива RESTORE

Синтаксис:
restore	name1
name1 - это идентификатор определённый ранее в директиве EQU. После этой команды name больше не будет заменяться на name2.

Например:

mov	eax, count
count	equ 10
mov	eax, count
restore	count
mov	eax, count
получим:
mov	eax, count
mov	eax, 10
mov	eax, count
Обратите внимание, что для определений сделанных директивой EQU работает принцип стека. То есть, если мы два раза определим один и тот же идентификатор используя EQU, то после однократного использования RESTORE значение идентификатора будет соответствовать определённому первой директивой EQU.

Например:

mov	eax, count
count	equ 1
mov	eax, count
count	equ 2
mov	eax, count
count	equ 3
mov	eax, count
restore	count
mov	eax, count
restore	count
mov	eax,count
restore	count
mov	eax,count
получим:
mov	eax, count
mov	eax, 1
mov	eax, 2
mov	eax, 3
mov	eax, 2
mov	eax, 1
mov	eax, count
Если попытаться выполнить RESTORE большее количество раз, чем было сделано EQU, никаких предупреждений выдано не будет. Значение идентификатора будет неопределенно.

Например:

mov	eax, count
restore	count
mov	eax, count
получим:
mov	eax, count
mov	eax, count

4. Простые макросы без аргументов

4.1. Определение простых макросов

Использую EQU можно делать наиболее простые замены в исходном тексте при обработке препроцессором. Большими возможностями обладают макросы. Командой MACRO можно создавать собственные инструкции.

Синтаксис:
macro	name
{
; тело макроса
}
Когда препроцессор находит директиву macro, он определяет макрос с именем name. Далее, встретив в исходном тексте строку, начинающуюся с name, препроцессор заменит name на тело макроса - то, что указано в определении между скобочками { и }. Имя макроса может быть любым допустимым идентификатором, а тело макроса - всё, что угодно, за исключением символа }, который означает завершение тела макроса.

Например:

macro	a
{
    push eax
}
xor	eax, eax
    a
будет заменено на:
xor	eax, eax
push	eax

Или:

macro	a
{
push	eax
}
macro	b
{
push	ebx
}
b
a
получим:
push	ebx
push	eax

Разумеется, макросы не обязательно оформлять так, как выше, можно делать и так:

macro	push5 {push	dword 5}
push5
получим:
push	dword 5

Или:

macro	push5 {push	dword 5
}
с тем же самым результатом. Скобочки можете размещать как хотите.

4.2. Вложенные макросы

Макросы могут быть вложенными один в другой. То есть, если мы переопределим макрос, будет использовано последнее определение. Но если в теле нового определения содержится тот же макрос, то будет использовано предыдущее определение. Посмотрите пример:
macro	a {	mov	ax, 5}

macro	a
{
    a
    mov	bx, 5
}

macro	a
{
    a
    mov	cx, 5
}

    a
в результате получим:
    mov	ax, 5
    mov	bx, 5
    mov	cx, 5

Или такой пример:

macro	a {1}

a

macro	a {
    a
    2 }

a

macro	a {
    a
    3 }

a
получим:
    1

    1
    2

    1
    2
    3

4.3. Директива PURGE. Отмена определения макроса

Как и в случае с директивой EQU, можно отменить определение макроса. Для этого используется директива PURGE с указанием имени макроса.

Синтаксис:
purge	name
Пример:
a
macro	a {1}
a
macro	a {2}
a
purge	a
a
purge	a
a
получим:
a
1
2
1
a
Если применить PURGE к несуществующему макросу, ничего не произойдёт.

4.4. Поведение макросов

Имя макроса будет заменено его телом не только в том случае, если оно расположено в начале строки. Макрос может находиться в любом месте исходного текста, где допустима мнемоника инструкции (например, add или mov). Всё потому, что основное предназначение макросов - имитировать инструкции. Единственное исключение из этого правила - макросы недопустимы после префиксов инструкций (rep).

Пример:

macro	CheckErr
{
    cmp	eax, -1
    jz	error
}

    call	Something
a:	CheckErr	; здесь макросу предшествует метка, всё Ок.
получим:
    call	Something
a:	cmp	eax,-1
    jz	error

Пример №2:

macro	stos0
{
    mov	al, 0
    stosb
}
    stos0		;это место инструкции, будет замена.
here:	stos0 		;это тоже место инструкции.
    db	stos0	;здесь инструкции не место, замены не будет.
получим:
    mov	al, 0
    stosb
here:	mov	al, 0
    stosb
    db	stos0

Возможно переопределять (overload) инструкции посредством макросов. Так как препроцессор ничего об инструкциях не знает, он позволяет использовать мнемонику инструкции в качестве имени макроса:

macro	pusha
{
    push eax ebx ecx edx ebp esi edi
}
macro	popa
{
    pop edi esi ebp edx ecx ebx eax
}
эти 2 новые инструкции будут экономить по 4 байта в стеке, так как не сохраняют ESP (правда, занимают побольше места, чем реальные инструкции :). Всё же, переопределение инструкций не всегда хорошая идея - кто-нибудь читая Ваш код может быть введён в заблуждение, если он не знает, что инструкция переопределена.

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

macro	use32
{
    align	4
    use32
}

macro	use16
{
    align	2
    use16
}

5. Макросы с фиксированным количеством аргументов

5.1. Макросы с одним аргументом

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

Синтаксис:
macro	name argument { тело макроса }

Например:

macro	add5 where
{
    add	where, 5
}

    add5	ax
    add5	[variable]
    add5	ds
    add5	ds+2
получим:
    add	ax, 5
    add	[variable], 5
    add	ds, 5	;такой инструкции не существует
            ;но препроцессор это не волнует.
            ;ошибка появится на стадии ассемблирования.
    add	ds+2,5	;ошибка синтаксиса, как и ранее
            ;определится при анализе синтаксиса (parsing).
(разумеется, комментарии в результате работы препроцессора не появятся :)

5.2. Макросы с несколькими аргументами

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

macro	movv where, what
{
    push	what
    pop	where
}

movv	ax, bx
movv	ds, es
movv	[var1], [var2]
преобразуется в:
    push	bx
    pop	ax

    push	es
    pop	ds

    push	[var2]
    pop	[var1]
Если несколько аргументов имеют одно и тоже имя, то будет использован первый из них :).

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

macro	pupush a1, a2, a3, a4
{
    push	a1 a2 a3 a4
    pop	a4 a3 a2 a1
}

    pupush	eax, dword [3]
получим:
    push	eax dword [3]
    pop	dword [3] eax

Если в аргументе макроса необходимо указать запятую ,, необходимо аргумент заключить в скобочки из символов < и >.

macro	safe_declare name, what
{
    if used	name
        name	what
    end if}

safe_declare	var1, db 5
safe_declare	array5, <dd 1,2,3,4,5>
safe_declare	string, <db "привет, я просто строка",0>
получим:
if used	var1
    var1	db 5
end if

if used	array5
    array5	dd 1,2,3,4,5
end if

if used	string
    string	db "привет, я просто строка",0
end if

Конечно же, можно использовать символы < и > и внутри тела макроса:

macro	a arg {db arg}
macro	b arg1,arg2 {a <arg1,arg2,3>}
b	<1,1>,2
получим:
db	1,1,2,3

5.3. Директива LOCAL

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

macro	pushstr string
{
    call	behind ;помещаем в стек адрес string и переходим к behind
    db	string, 0
behind:
}
но если использовать такой макрос 2 раза, то и метка behind будет объявлена дважды, что приведёт к ошибке. Эта проблема решается объявлением локальной метки behind. Это и делает директива LOCAL.

Синтаксис:
local		label_name

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

macro	pushstr string
{
  local	behind
    call	behind
    db	string,0
behind:
}

pushstr	'aaaaa'
pushstr	'bbbbbbbb'
call	something
На самом деле, behind заменяется на behind?XXXXXXXX, где XXXXXXXX - какой-то шестнадцатеричный номер генерируемый препроцессором. Последний пример может быть преобразован к чему-то вроде:
    call	behind?00000001
    db	'aaaaa', 0
behind?00000001:
    call	behind?00000002
    db	'bbbbbbbb', 0
behind?00000002:
    call	something

Заметьте, Вы не сможете напрямую обратиться к метке содержащей ?, так как это специальный символ в FASM, поэтому он и используется в локальных метках. К примеру, aa?bb рассматривается как идентификатор aa, специальный символ ? и идентификатор bb.

Если Вам нужно несколько локальных меток - не проблема, их можно указать в одной директиве LOCAL, разделив запятыми ,:

macro	pushstr string  ;делает то же, что и предыдущий макрос
{
  local	addr, behind
    push	addr
    jmp	behind
addr	db	string,0
behind:
}

Всегда хорошо бы начинать все локальные метки макросов с двух точек .. - это значит, что они не будут менять текущую глобальную метку. К примеру:
macro	pushstr string
{
  local	behind
    call	behind
    db	string, 0
behind:
}

MyProc:
    pushstr	'aaaa'
.a:
будет преобразовано в:
MyProc:
    call	behind?00000001
    db	'aaaa', 0
behind?00000001:
.a:
в результате получим метку behind?00000001.a вместо MyProc.a. Но если в примере выше behind заменить на ..behind, текущая глобальная метка не изменится и будет определена метка MyProc.a:
macro	pushstr string
{
  local	..behind
    call	..behind
    db	string,0
..behind:
}

MyProc:
    pushstr	'aaaa'
.a:

5.4. Оператор объединения #

У макроязыка FASMа есть ещё одна возможность - манипуляции с идентификаторами. Делается это оператором #, который объединяет два идентификатора в один. К примеру, a#b становится ab, а aaa bbb#ccc ddd -- aaa bbbccc ddd.

Оператор # может быть использован только внутри тел макросов, а объединение символов происходит после замены аргументов макроса параметрами. Так что его можно использовать для создания новых идентификаторов из переданных в макрос параметров:

macro	string name, data
{
  local	..start
..start:
name db data,0
sizeof.#name =  $ - ..start
}

string	s1,'нудные макросы'
string	s2,<'а вот и я',13,10,'заставлю тебя их видеть во сне'>
получим:
..start?00000001:
s1	db 'нудные макросы',0
sizeof.s1 = $ - ..start?00000001

..start?00000002:
s2	db 'а вот и я',13,10,'заставлю тебя их видеть во сне',0
sizeof.s2 = $ - ..start?00000002
так что для всех строк, создаваемых этим макросом будет определён идентификатор sizeof.имя строки, равный количеству байт строки.

Оператор # способен так же объединять символьные строки:

macro	debug name
{
    db 'name: '#b,0
}
debug	'1'
debug	'foobar'
будет:
db	'name: 1',0
db	'name: foobar',0
Это полезно при передаче аргументов из макроса в макрос:
macro	pushstring string
{
  local	..behind
    call	..behind
    db	string,0
..behind:}

macro	debug string
{
    push	MB_OK
    push 0 ;empty caption
    pushstring	'debug: '#string	;принимает один аргумент
    push	0				;нет окна-предка
    call	[MessageBox]
}

Обратите внимание, нельзя использовать # совместно с идентификаторами, определёнными local, так как local обрабатывается препроцессором раньше, чем #. Из-за этого подобный код работать не будет:

macro	a arg
{
  local	name_#arg
}

a	foo

5.5. Оператор `

Существует оператор, преобразующий идентификатор в символьную строку. Он так же может быть использован только внутри макросов:

macro	proc name
{
name:
    log	`name	;log - макрос, принимающий параметр-строку
}

proc	DummyProc
получим:
DummyProc:
    log	'DummyProc'
Пример посложнее, с использованием #
macro	proc name
{
name:
    log 'начинается подпрограмма: '#`name
}

proc	DummyProc
retn
proc	Proc2
retn
будет:
DummyProc:
log	'начинается подпрограмма: DummyProc'
retn

Proc2:
log	'начинается подпрограмма: Proc2'
retn

6. Макросы с групповыми аргументами

6.1. Определение макросов с групповым аргументом

У макросов могут быть так называемые групповые аргументы. Это позволяет использовать переменное количество аргументов. При определении макроса, групповой аргумент заключается в квадратные скобочки [ и ]:

Синтаксис:
macro	name arg1, arg2, [grouparg]
{
; тело макроса
}
Среди аргументов в определении макроса, групповой аргумент должен быть последним. Групповой аргумент может содержать несколько значений:
macro	name arg1,arg2,[grouparg] {}
name	1,2,3,4,5,6
В этом примере значение arg1 будет 1, arg2 - 2, а grouparg - 3,4,5,6

6.2. Директива COMMON

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

macro	string [grp]
{
  common
  db	grp,0
}

string	'aaaaaa'
string	'line1',13,10,'line2'
string	1,2,3,4,5
получим:
db	'aaaaaa',0
db	'line1',13,10,'line2',0
db	1,2,3,4,5,0

6.3. Директива FORWARD

Аргументы можно обрабатывать и по-отдельности. Для этого служит директива FORWARD. Часть тела макроса после этой директивы обрабатывается препроцессором для каждого аргумента из группы:

macro	a arg1,[grparg]
{
forward
    db	arg1
    db	grparg
}

a	1,'a','b','c'
a	-1, 10, 20
будет:
db	1
db	'a'
db	1
db	'b'
db	1
db	'c'

db	-1
db	10
db	-1
db	20
Директива FORWARD работает по умолчанию для макросов с групповыми аргументами, так что предыдущий пример можно сделать так:
macro	a arg1,[grparg]
{
    db	arg1
    db	grparg
}

6.4. Директива REVERSE

REVERSE - это аналог FORWARD, но обрабатывает группу аргументов в обратном порядке - от последнего к первому:
macro	a arg1,[grparg]
{
reverse
    db	arg1
    db	grparg
}

a	1,'a','b','c'
получим:
db	1
db	'c'
db	1
db	'b'
db	1
db	'a'

6.5. Комбинирование директив управления группами

3 вышеупомянутые директивы могут разделять тело макроса на блоки. Каждый блок обработается препроцессором после предыдущего. Например:
macro	a [grparg]
{
  forward
    f_#grparg:	;оператор объединения
  common
    db	grparg
  reverse
    r_#grparg:
}

a	1,2,3,4
будет:
f_1:
f_2:
f_3:
f_4:
db	1,2,3,4
r_4:
r_3:
r_2:
r_1:

6.6. Директива LOCAL в макросах с групповыми аргументами

У локальных меток в макросах есть ещё одно полезное свойство. Если директива LOCAL находится внутри блока FORWARD или REVERSE, то уникальное имя метки сгенерируется для каждого аргумента из группы, и в последующих блоках FORWARD и/или REVERSE для каждого аргумента будет использована соответствующая ему метка:
macro	string_table [string]
{
  forward		;таблица указателей на строки
    local	addr	;локальная метка для строки
    dd	addr		;указатель на строку
  forward		;строки
    addr	db string,0	;создаём и завершаем нулём
}

string_table	'aaaaa','bbbbbb','5'
получим:
dd	addr?00000001
dd	addr?00000002
dd	addr?00000003
addr?00000001	db 'aaaaa',0
addr?00000002	db 'bbbbbb',0
addr?00000003	db '5',0
Другой пример с блоком REVERSE:
macro	a [x]
{
forward
  local	here
  here	db x
reverse
  dd	here
}

a	1,2,3
будет:
here?00000001	db 1
here?00000002	db 2
here?00000003	db 3
dd	here?00000003
dd	here?00000002
dd	here?00000001
Как видно, метки используется с соответствующими аргументами и в FORWARD и в REVERSE блоках.

6.7. Макросы с несколькими групповыми аргументами

Возможно использовать и несколько групповых аргументов. В этом случае определение макроса не будет выглядеть как:
macro	a [grp1],[grp2]
так как тут не ясно какой аргумент какой группе принадлежит. Исходя из этого делают так:

Синтаксис:
macro	a [grp1,grp2]
В этом случае каждый нечётный аргумент относится к группе grp1, а каждый чётный - к grp2:
macro	a [grp1,grp2]
{
  forward
    l_#grp1:
  forward
    l_#grp2:
}

a	1,2,3,4,5,6
будет:
l_1:
l_3:
l_5:
l_2:
l_4:
l_6:
Или ещё:
macro	ErrorList [name,value]
{
  forward
    ERROR_#name = value
}

ErrorList \
    NONE,0,\
    OUTOFMEMORY,10,\
    INTERNAL,20
получим:
ERROR_NONE = 0
ERROR_OUTOFMEMORY = 10
ERROR_INTERNAL = 20
Конечно же, может быть больше 2х групп аргументов:
macro	a [g1,g2,g3]
{
  common
    db	g1
    db	g2
    db	g3
}

a	1,2,3,4,5,6,7,8,9,10,11
будет:
db	1,4,7,10
db	2,5,8,11
db	3,6,9

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

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

Как известно, оператор if обрабатывается во время ассемблирования. Это значит, что условие в этом операторе проверяется после обработки исходного текста препроцессором. Именно это обеспечивает работу некоторых логических операций.

Я не буду рассказывать о деталях времени ассемблирования (логических операциях вроде &, | и т.п.) - RTFM. Я лишь расскажу об операторах проверки условия используемых препроцессором.

7.1. Оператор EQ

Простейший логический оператор - это EQ. Он всего лишь сравнивает два идентификатора - одинаковы ли их значение. Значение abcd eq abcd - истина, а abcd eq 1 - ложь и так далее.. Это полезно для сравнения символов, которые будут обработаны препроцессором:
STRINGS	equ ASCII
if STRINGS eq ASCII
    db	'Oh yeah',0
else if STRINGS eq UNICODE
    du	'Oh yeah',0
else
    display	'unknown string type'
end if
после обработки препроцессором, это примет вид:
if ASCII eq ASCII
    db	'Oh yeah',0
else if ASCII eq UNICODE
    du	'Oh yeah',0
else
    display	'unknown string type'
end if
Здесь только первое условие (ASCII eq ASCII) выполняется, так что будет ассемблировано только db 'Oh yeah',0

Другой вариант:

STRINGS	equ UNICODE	  ;разница здесь, UNICODE вместо ASCII
if STRINGS eq ASCII
    db	'Oh yeah',0
else if STRINGS eq UNICODE
    du	'Oh yeah',0
else
    display 'unknown string type'
end if
получим:
if UNICODE eq ASCII
    db	'Oh yeah',0
else if UNICODE eq UNICODE
    du	'Oh yeah',0
else
    display 'unknown string type'
end if
Тут уже первое условие (UNICODE eq ASCII) будет ложно, второе (UNICODE eq UNICODE) - верно, будет ассемблироваться du 'Oh yeah',0.

Несколько лучшее применение этого - проверка аргументов макросов, вроде:

macro	item type,value
{
if type eq BYTE
    db	value
else if type eq WORD
    dw	value
else if type eq DWORD
    dd	value
else if type eq STRING
    db	value,0
end if
}

item	BYTE,1
item	STRING,'aaaaaa'
будет:
if BYTE eq BYTE
    db	1
else if BYTE eq WORD
    dw	1
else if BYTE eq DWORD
    dd	1
else if BYTE eq STRING
    db	1,0
end if

if STRING eq BYTE
    db	'aaaaaa'
else if STRING eq WORD
    dw	'aaaaaa'
else if STRING eq DWORD
    dd	'aaaaaa'
else if STRING eq STRING
    db	'aaaaaa',0
end if
ассемблироваться будут только 2 команды:
db	1
db	'aaaaaa',0

Подобно всем другим операторам препроцессора, EQ может работать с пустыми аргументами. Это значит, что, например, if eq верно, а if 5 eq - ложно и т.п.

Пример макроса:

macro	mov dest,src,src2
{
if src2 eq
    mov	dest, src
else
    mov	dest, src
    mov	src, src2
end if
}
здесь, если есть третий аргумент, то будут ассемблироваться 2 последних команды, если нет - то только одна первая.

7.2. Оператор EQTYPE

Ещё один оператор - EQTYPE. Он определяет, одинаков ли тип идентификаторов.
    Существующие типы:
  • отдельные строки символов, заключённые в кавычки (те, которые не являются частью численных выражений)
  • вещественные числа (с плавающей точкой)
  • любые численные выражения, например, 2+2 (любой неизвестный символ будет рассматриваться как метка, так что он будет считаться подобным выражением)
  • адреса - численные выражения в квадратных скобках (учитывая оператор размерности и префикс сегмента)
  • мнемоники инструкций
  • регистры
  • операторы размерности
  • операторы NEAR и FAR
  • операторы USE16 и USE32
  • пустые аргументы (пробелы, символы табуляции)
Пример макроса, который позволяет использовать переменную в памяти в качестве счётчика в инструкции SHL (например shl ax, [myvar]):
macro	shl dest, count
{
  if count eqtype [0]		;если count - ячейка памяти
    push	cx
    mov	cl, count
    shl	dest, cl
    pop	cx
  else				;если count другого типа
    shl	dest, count	;просто используем обычную shl
  end if
}

    shl	ax, 5
byte_variable	db 5
    shl	ax, [byte_variable]
получится:
if 5 eqtype [0]
    push	cx
    mov	cl, 5
    shl	ax, cl
    pop	cx
else
    shl	ax, 5
end if

byte_variable	db 5

if [byte_variable] eqtype [0]
    push	cx
    mov	cl, [byte_variable]
    shl	ax, cl
    pop	cx
else
    shl	ax, [byte_variable]
end if
в результате обработки условий конечный результат будет:
    shl	ax, 5
byte_variable	db 5
    push	cx
    mov	cl, [byte variable]
    shl	ax, cl
    pop	cx
Заметьте, что shl ax, byte [myvar] не будет работать с этим макросом, так как условие byte [variable] eqtype [0] не выполняется. Читаем дальше.

Когда мы сравниваем что-то посредством EQTYPE, то это что-то может быть не только единичным идентификатором, но и их комбинацией. В таком случае, результат eqtype истина, если не только типы, но и порядок идентификаторов совпадают. К примеру, if eax 4 eqtype ebx name - верно, так как name - это метка, и её тип - численное выражение.

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

macro	mov dest,src
{
  if dest src eqtype [0] [0]
    push	src
    pop	dest
  else
    mov	dest,src
  end if
}

mov	[var1], 5
mov	[var1], [var2]
преобразуется препроцессором в:
if [var1] 5 eqtype [0] [0]	;не верно
    push	5
    pop	[var1]
else
    mov	[var1],5
end if

if [var1] [var2] eqtype [0] [0]  ;верно
    push	[var2]
    pop	[var1]
else
    mov	[var1], [var2]
end if
и будет ассемблировано в:
    mov	[var1], 5
    push	[var2]
    pop	[var1]
Хотя более удобно для восприятия реализовать макрос используя логический оператор И - &:
macro	mov dest,src
{
  if (dest eqtype [0]) & (src eqtype [0])
    push	src
    pop	dest
  else
    mov	dest, src
  end if
}
Пример с использованием EQTYPE с четырьмя аргументами приведён для демонстрации возможностей, обычно проще использовать в таких случаях &. Кстати, в качестве аргументов, возможно использовать некорректные выражения - достаточно, чтобы лексический анализатор распознал их тип. Но это не является документированным, так что не будем этот обсуждать.

7.3. Оператор IN

Бывают случаи, когда в условии присутствует слишком много EQ:
macro	mov a,b
{
  if (a eq cs) | (a eq ds) | (a eq es) | (a eq fs) | \
     (a eq gs) | (a eq ss)
    push	b
    pop	a
  else
    mov	a, b
  end if
}
Вместо применения множества логических операторов ИЛИ - |, можно использовать специальный оператор IN. Он проверяет, присутствует ли идентификатор слева, в списке идентификаторов справа. Список должен быть заключён в скобочки < и >, а идентификаторы в нём разделяются запятыми ,:
macro	mov a,b
{
  if a in <cs,ds,es,fs,gs,ss>
    push	b
    pop	a
  else
    mov	a, b
  end if
}
Это так же работает для нескольких идентификаторов (как и EQ):
if dword [eax] in <[eax], dword [eax], ptr eax, dword ptr eax>

8. Структуры

В FASM, структуры практически тоже самое, что и макросы. Определяются они посредством директивы STRUC:

Синтаксис:
struc	name arguments	{ тело структуры }

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

struc	a {db 5}
a
это не будет работать. Структуры распознаются только после меток, как здесь:
struc	a {db 5}
name	a
подобно макросу, это преобразуется препроцессором в:
db	5

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

struc	a {.local:}

name1	a
name2	a
будет:
name1.local:
name2.local:

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

struc	rect left,right,top,bottom  ;аргументы как у макроса
{
    .left	dd left
    .right	dd right
    .top	dd top
    .bottom	dd bottom
}

r1 rect 0,20,10,30
r2 rect ?,?,?,?
получим:
r1.left		dd 0
r1.right	dd 20
r1.top		dd 10
r1.bottom	dd 30

r2.left		dd ?
r2.right	dd ?
r2.top		dd ?
r2.bottom	dd ?

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

Существуют хитрый приём, позволяющий не указывать аргументы, если они равны 0:

struc	ymmv arg
{
    .member	dd arg+0
}

y1	ymmv 0xACDC
y2	ymmv
будет:
y1.member	dd 0xACDC+0
y2.member	dd +0
Как говорилось ранее, если значение аргумента не указанно, то в теле макроса или структуры вместо него ничего не подставляется. В этом примере + используется или как бинарный (то есть с двумя операндами), или как унарный (с одним операндом) оператор.

ПРИМЕЧАНИЕ: часто используется так же макрос или структура struct, которая определяется для расширения возможностей при определении структур. Не путайте struct и struc.

9. Оператор FIX и макросы внутри макросов

В стародавние времена, в FASMе отсутствовала одна полезная возможность - создавать макросы внутри других макросов. Например, что бы при развёртывании макроса был бы определён новый макрос. Что-то вроде гипотетичного:

macro	declare_macro_AAA
{
  macro AAA
  {
    db	'AAA',0
  }	;завершаем определение AAA
}	;завершаем определение declare_macro_AAA

Проблема в том, что когда макрос declare_macro_AAA обрабатывается препроцессором, первая найденная скобочка } считается завершением определения его, а не так как хотелось бы. Так же происходит и с другими символами и/или операторами (например, #, `, forward, local).

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

Синтаксис :
name1	fix name2

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

a	equ 10
b	fix 10
mov	ax, a
mov	bx, b
будет преобразован в:
mov	ax, 10
mov	bx, 10
Но при обработке такого кода:
equ	fix =
a	equ 10
mov	ax, a
в первой строк директива FIX скажет препроцессору поменять все EQU на =. Далее, перед обработкой следующей строки, препроцессор проверит, нет ли там пофиксеных идентификаторов. Так что в нашей второй строке equ будет заменено на =, и строка примет вид a = 10. Так что никакой другой обработки этой строки не будет выполнено. А значит, и третья строка не будет преобразовываться препроцессором, так как идентификатор a не будет определён директивой EQU. Результат всего этого будет такой:
a	= 10
mov	ax, a

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

macro	declare_macro_AAA
{
  macro AAA
  %_
    db	'aaa',0
  _%
}

%_ fix {
_% fix }

declare_macro_AAA
Здесь, препроцессор найдёт объявление макроса declare_macro_AAA и определит его, далее будет два FIX, и потом использование макроса declare_macro_AAA. Так что он преобразует это в:
macro	declare_macro_AAA
{
  macro	AAA
  %_
    db	'aaa',0
  _%
}

%_ fix {
_% fix }

macro	AAA
%_
    db	'aaa',0
_%
и теперь уже содержимое нового макроса будет обработано препроцессором. Далее будут заменены аргументы FIXов, и получится:
macro	declare_macro_AAA
{
  macro	AAA
  %_
    db	'aaa',0
  _%
}

macro	AAA
{
    db	'aaa',0
}
как мы и хотели.

Подобным образом можно пофиксить все остальные проблематичные вещи:

macro	declare_macro_TEXT
{
  macro	TEXT [arg]
  %_
    %forward
    db	%x arg
  _%
}

%_ fix {
_% fix }
%forward fix forward

declare_macro_TEXT

%x fix `

TEXT abc,def
В этом примере нужно обратить внимание на один момент: строка %x fix ` должна находиться после declare_macro_TEXT. Если б она находилась до, то %x было бы пофиксено во время развёртывания макроса, и тогда `arg приняло бы вид 'arg', следовательно макрос TEXT был бы объявлен так:
macro	TEXT [arg]
{
  forward
    db 'arg'	;строка не зависит от аргументов
}
Но, в нашем случае он будет:
macro	TEXT [arg]
{
  forward
    db `arg		;имена аргументов превращаются в строки
}
Этот пример показывает, как важно местонахождение FIX.

Иногда необходимо фиксить идентификаторы дважды:

macro m1
{
  macro m2
  %_
    macro m3 [arg]
    %%_
      db arg
    _%%
  _%
}

%%_ fix %_
_%% fix _%
%_ fix {
%_ fix }

m1
m2
m3
Символы фиксятся даже во время препроцессинга других FIX, так что код выше не будет работать, если порядок будет такой:
%_ fix {
%_ fix }
%%_ fix %_
_%% fix _%
В этом случае строка %%_ fix %_ была бы пофиксена сразу же после %_ fix {, так что все последующие %%_ сразу же преобразовались бы в }. То же самое и для _%% fix _%.

Я знаю, FIXы могут смутить, и хорошо бы понимать внутренние детали работы препроцессора, но они предоставляют очень большие возможности. Privalov делает FASM настолько мощным, на сколько это возможно, даже за счёт некоторого ущерба удобопонятности.

Заключение

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

TAJGA FASM Tutorial by vid - FASM preprocessor guide.
перевёл S.T.A.S. | последняя редакция: 22 июня 2004г.