Современные решения

для защиты Windows приложений

и восстановления исходного кода

Препроцессор NASM

NASM содержит мощный макропроцессор, поддерживающий условное ассемблирование, многоуровневое включение файлов, две формы макро-определений (однострочные и многострочные) и механизм "контекстного стека", расширяющий возможности макро-определений (далее – макросы). Все директивы препроцессора начинаются со знака %.

4.1 Однострочные макросы

4.1.1 Обычный способ: %define

Однострочные макросы описываются при помощи директивы препроцессора %define. Определения работают подобно языку С; т.е. вы можете сделать что-то наподобие

%define ctrl 0x1F & 
%define param(a,b) ((a)+(a)*(b)) 
          mov byte [param(2,ebx)], ctrl 'D'

что будет развернуто в

          mov byte [(2)+(2)*(ebx)], 0x1F & 'D'

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

%define a(x) 1+b(x) 
%define b(x) 2*x 
          mov ax,a(8)

будет обработан как и ожидается — mov ax,1+2*8, несмотря на то, что при описании a макрос b еще не определен.

Макросы, описываемые конструкцией %define, регистрочувствительны: после %define foo bar только foo будет развернуто в bar, но никак не Foo или FOO. При использовании вместо %define конструкции %idefine ('i' от слова 'insensitive') вы можете сразу описать все варианты комбинации строчных и прописных букв в имени макроса, поэтому %idefine foo bar будет развертывать в bar не только foo, но и Foo, FOO, fOO и т.п.

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

%define a(x) 1+a(x) 
          mov ax,a(3)

макрос a(3) будет развернут единожды, т.е. в конструкцию 1+a(3) и дальнейшее развертывание производиться не будет. Данная возможность может быть полезна: в параграфе 8.1 имеется соответствующий пример.

Вы можете перегружать однострочные макросы: если вы напишете

%define foo(x) 1+x 
%define foo(x,y) 1+x*y

препроцессор, подсчитав передаваемые вами параметры, корректно обработает оба типа вызова макроса: foo(3) будет развернуто в 1+3, в то время как foo(ebx,2) — в 1+ebx*2. В то же время, если вы напишете

%define foo bar

последующие описания foo будут запрещены: макрос без параметров не допускает описание макроса с тем же именем, но с параметрами, и наоборот.

Несмотря на это, переопределение однострочных макросов не запрещается: вы можете легко описать макрос как

%define foo bar

и затем, в том же самом исходном файле, переопределить его как

%define foo baz

Когда макрос foo будет вызван, он развернется в соответствии с самым поздним своим описанием. Это полезно в основном при описании однострочных макросов с конструкцией %assign (см. параграф 4.1.4).

Вы можете переопределить однострочный макрос при помощи ключа '-d' командной строки NASM: см. параграф 2.1.8.

При описании однострочных макросов вы можете объединять строки при помощи псевдо-оператора %+. Например:

%define _myfunc _otherfunc 
%define cextern(x) _ %+ x 
cextern (myfunc)

После первого развертывания третья строка примет вид "_myfunc". При дальнейшей обработке препроцессор развернет эту строку в "_otherunc".

4.1.2 Однострочные макросы раннего связыванияs: %xdefine

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

%assign ofs 0 
%macro arg 1 
        %xdefine %1 dword [esp+ofs] 
        %assign ofs ofs+4 
%endmacro 
        arg  a 
        arg  b

Если в этом примере вместо %xdefine мы используем %define, оба макроса будут развернуты в одно и то же значение dword [esp+8], что неверно. При использовании конструкции %xdefine макрос разворачивается во время его определения, поэтому a будет развернуто в dword [esp+0], а b — в dword [esp+4].

Макрос %xdefine имеет нечувствительный к регистру эквивалент %ixdefine, работающий так же, как и %idefine по отношению к %define.

4.1.3 Отмена определения макроса: %undef

Команда %undef удаляет однострочные макросы. Например, следующая последовательность:

%define foo bar 
%undef foo 
        mov eax, foo

будет развернута в инструкцию mov eax, foo, так как после %undef макрос foo больше не определен.

Отмена определения предопределенных макросов может быть осуществлена при помощи ключа '-u' командной строки NASM: см. параграф 2.1.9.

4.1.4 Переменные препроцессора: %assign

Альтернативным способом определения однострочных макросов является использование директивы %assign (и ее нечувствительного к регистру эквивалента %iassign, отличающегося от %assign тем же самым, чем %idefine отличается от %define).

Данная директива используется для определения однострочного макроса без параметров, но включающего числовое значение. Это значение может быть задано в форме выражения и обрабатывается оно только один раз — при обработке директивы %assign.

Как и в случае %define, макрос, определенный при помощи %assign, может быть позднее переопределен, например следующая строка

%assign i i+1

увеличивает числовое значение макроса.

%assign полезна для контроля завершения препроцессорных циклов %rep: пример этого см. в параграфе 4.4. Другие примеры применения %assign можно найти в параграфе 7.4 и парарафе 8.1.

Выражение, передаваемое %assign, является критическим (см. параграф 3.7) и должно на выходе давать просто число (не включая в себя различные перемещающиеся ссылки наподобие адресов кода и данных).

4.2 Многострочные макросы: %macro

Многострочные макросы в большинстве своем похожи на макросы в MASM и TASM: определение многострочного макроса в NASM выглядит похоже.

%macro prologue 1 
          push ebp 
          mov ebp,esp 
          sub esp,%1 
%endmacro

Здесь определяется как макрос С-подобная функция prologue: теперь вы можете вызвать макрос следующим образом:

myfunc:   prologue 12

что будет развернуто в три строки кода

myfunc:   push ebp 
          mov ebp,esp 
          sub esp,12

Число 1 после имени макроса в строке %macro определяет число параметров, которые ожидает получить макрос prologue. Конструкция %1 внутри тела макроса ссылается на первый передаваемый параметр. В случае макросов, принимающих более одного параметра, ссылка на них осуществляется как %2,%3 и т.д.

Многострочные макросы, как и однострочные, чувствительны к регистру символов, за исключением случая, когда вы примените альтернативную директиву %imacro.

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

%macro silly 2 
%2:       db %1 
%endmacro 
          silly 'a', letter_a    ; letter_a:  db 'a' 
          silly 'ab', string_ab  ; string_ab: db 'ab' 
          silly {13,10}, crlf    ; crlf:      db 13,10

4.2.1 Перегрузка многострочных макросов

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

%macro prologue 0 
          push ebp 
          mov ebp,esp 
%endmacro

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

Иногда вам может захотеться "перегрузить" процессорные инструкции; например, если вы определите

%macro push 2 
          push %1 
          push %2 
%endmacro

позже вы сможете написать

          push ebx               ; эта строка не вызов макроса! 
          push eax,ecx           ; а это - вызов

По умолчанию NASM будет выдавать предупреждение при обработке первой из этих строк, так как push теперь определена как макрос и вызывается с числом параметров, определения для которого нет. При этом будет сгенерирован корректный код, однако ассемблер даст предупреждение. Данное предупреждение можно отключить при помощи ключа командной строки -w-macro-params (см. параграф 2.1.12).

4.2.2 Локальные метки в макросах

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

%macro retz 0 
          jnz %%skip 
          ret 
%%skip: 
%endmacro

После этого вы можете вызывать этот макрос столько раз, сколько нужно, и при этом в каждом вызове вместо метки %%skip NASM будет подставлять разные "реальные" имена. NASM создает имена в форме ..@2345.skip, где число 2345 изменяется при каждом вызове макроса. Префикс ..@ предотвращает пересечение имен локальных меток макросов с механизмом обычных локальных меток, описанным в параграфе 3.8. В связи с этим вам нужно избегать определения собственных меток в такой форме (префикс ..@, затем число, затем точка), иначе они могут совпасть с локальными метками макросов.

4.2.3 Поглощающие параметры макросов

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

          writefile [filehandle],"Привет, фуфел!",13,10

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

%macro writefile 2+ 
          jmp %%endstr 
%%str:    db %2 
%%endstr: mov dx,%%str 
          mov cx,%%endstr-%%str 
          mov bx,%1 
          mov ah,0x40 
          int 0x21 
%endmacro

то приведенный выше пример вызова макроса writefile будет работать как нужно: текст перед первой запятой [filehandle] используется в качестве первого параметра и развернется, когда встретится ссылка %1, весь последующий текст объединится в %2 и расположится после db.

Поглощающая природа макроса указывается в NASM при помощи знака (+) после количества параметров в строке %macro.

При определении поглощающего макроса вы тем самым говорите NASM, как он должен разворачивать любое число параметров свыше явно указанного; в приведенном случае, например, он будет знать, что должен делать при вызове writefile с 2,3,4 или большим числом параметров. NASM также будет это учитывать при перегрузке макросов и не позволит вам определить другую форму writefile, принимающую к примеру 4 параметра.

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

          writefile [filehandle], {"Привет, фуфел!",13,10}

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

В параграфе 5.2.1 приведен более "продвинутый" способ написания рассмотренного макроса

4.2.4 Параметры макросов по умолчанию

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

%macro die 0-1 "Полный кабздец твоей проге, фуфел!" 
          writefile 2,%1 
          mov ax,0x4c01 
          int 0x21 
%endmacro

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

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

%macro foobar 1-3 eax,[ebx+2]

то он может быть вызван с числом параметров от одного до трех; при этом параметр %1 всегда берется из строки вызова макроса, параметр %2 (если не задан) примет значение eax, а параметр %3 (если не задан) — значение [ebx+2].

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

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

%macro die 0-1+ "Полный кабздец твоей проге, фуфел!",13,10

Максимальное число параметров может быть неограниченным, что обозначается как *. В этом случае, конечно невозможно предусмотреть полный набор параметров по умолчанию. Примеры такого типа макросов см. в параграфе 4.2.6.

4.2.5 %0: Счетчик макро-параметров

Параметр %0 возвращает числовую константу, представляющую собой количество передаваемых в макрос параметров. Он может использоваться как аргумент для %rep (см. параграф 4.4) с целью перебора всех параметров макроса. Примеры см. в параграфе 4.2.6 ниже.

4.2.6 %rotate: "Вращение" параметров макросов

Unix-программисты знакомы с командой shift оболочки, позволяющей "сдвигать" переданные шелл-скриптом аргументы ($1, $2 и т.д.) так, что аргумент, имевший до этого ссылку $2, получает ссылку $1, а имевший ссылку $1, становится недоступен.

NASM обеспечивает подобный механизм при помощи %rotate. Как видно по имени, эта команда отличается от команды Unix тем, что параметры не теряются: "выталкиваемые" с левого конца списка аргументов, они появляются на правом, и наоборот.

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

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

%macro multipush 1-* 
%rep %0 
          push %1 
%rotate 1 
%endrep 
%endmacro

Этот макрос вызывает инструкцию PUSH для каждого аргумента, слева-направо. Начинается это дело с сохранения первого аргумента, %1, затем вызывается %rotate для перемещения всех аргументов на один шаг влево, так что изначально второй аргумент становится доступен как %1. Повторение данной процедуры для всего набора аргументов (достигается это передачей %0 как аргумента для %rep) позволяет сохранить каждый из них.

Обратите внимание на использование звездочки (*) в качестве максимального числа параметров. Это означает, что число передаваемых макросу multipush параметров не ограничено.

Удобно иметь под рукой и обратный макрос, извлекающий регистры из стека, особенно если он не требует передавать ему аргументы в обратном порядке относительно PUSH. В идеале вы будете помещать в текст программы вызов макроса multipush, затем копировать эту строку, вставлять ее туда, где требуется извлечение из стека и заменять имя вызываемого макроса на multipop. И этот макрос будет извлекать регистры со стека в порядке, обратном тому, в каком их туда поместили.

Все это может быть реализовано при помощи следующего определения:

%macro multipop 1-* 
%rep %0 
%rotate -1 
          pop %1 
%endrep 
%endmacro
Данный макрос начинает работу с поворота своих аргументов вправо, поэтому исходный последний аргумент становится аргументом %1. Затем этот аргумент извлекается из стека, список аргументов снова поворачивается вправо и теперь аргументом %1 становится аргумент, бывший в начале предпоследним. Таким образом, аргументы извлекаются из стека в обратном порядке.

4.2.7 Объединение параметров макросов

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

%macro keytab_entry 2 
keypos%1 equ $-keytab 
          db %2 
%endmacro 
keytab: 
          keytab_entry F1,128+1 
          keytab_entry F2,128+2 
          keytab_entry Return,13

Это будет развернуто следующим образом:

keytab: 
keyposF1 equ $-keytab 
          db 128+1 
keyposF2 equ $-keytab 
          db 128+2 
keyposReturn equ $-keytab 
          db 13

Точно также можно присоединять текст к другому концу параметра, написав %1foo.

Если вам требуется присоединить к параметру макроса цифру, например для определения меток foo1 и foo2 при передаче параметра foo, вы не можете написать %11, так как это будет воспринято как одиннадцатый параметр макроса. Вместо этого вы должны написать %{1}1, где первая единица (определяющая номер параметра макроса) будет отделена от второй (представляющей собой присоединяемый к параметру текст).

Объединение может быть применено и к другим встраиваемым объектам препроцессора, таким как локальные метки макросов (параграф 4.2.2) и контекстно-локальные метки (параграф 4.6.2). В любом случае, неопределенность синтаксиса может быть разрешена путем заключения всего, находящегося после знака % и перед присоединяемым текстом, в фигурные скобки: %{%foo}bar прицепит текст bar к действительному имени локальной метки %%foo. (Это вообще-то излишне, так как форма, используемая NASM для генерации реальных имен локальных макро-меток подразумевает, что и %{%foo}bar, и %%foobar будут развернуты в одно и то же, однако возможность существует).

4.2.8 Коды условий в качестве параметров макросов

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

Однако более полезной возможностью является ссылка на макро-параметр вида %-1, которую NASM будет разворачивать как обратный код условия. Так, макрос retz, описанный в параграфе 4.2.2, может быть заменен макросом условного возврата более общего вида:

%macro retc 1 
          j%-1 %%skip 
          ret 
%%skip: 
%endmacro

Данный макрос может быть теперь вызван как retc ne, что будет развернуто в инструкцию условного перехода JE, или как retc po, что будет преобразовано в переход JPE.

Ссылка %+1 на макро-параметр может вполне спокойно интерпретировать аргументы CXZ и ECXZ как правильные коды условий; однако если передать эту лабуду ссылке %-1, будет сообщено об ошибке, так как обратных кодов условий к таким параметрам не существует.

4.2.9 Подавление развертывания макросов в листинге

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

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

%macro foo 1.nolist

Или так:

%macro bar 1-5+.nolist a,b,c,d,e,f,g,h

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

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

%if<условие> 
; некоторый код, ассемблируемый только при выполнении <условия>
%elif<условие2> 
; ассемблируется, если <условие> не выполняется, а выполняется <условие2> 
%else 
; ассемблируется, если и <условие>, и <условие2> не выполняются 
%endif

Оператор %else необязателен, так же как и оператор %elif. Если нужно, вы можете использовать более одного оператора %elif.

4.3.1 %ifdef: Проверка присутствия однострочного макроса

Начинающийся со строки %ifdef MACRO условно-ассемблируемый блок будет обрабатываться только в том случае, если определен однострочный макрос MACRO. Если он не определен, вместо этого будут обрабатываться блоки %elif и %else (если они есть).

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

          ; выполнение некоторой функции 
%ifdef DEBUG 
          writefile 2,"Функция выполнена полностью.",13,10 
%endif 
          ; выполнение чего-нибудь еще

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

Для осуществления обратной проверки (отсутствия определения макроса) вы можете использовать вместо %ifdef оператор %ifndef. Вы можете также тестировать наличие определения макроса в блоках %elif при помощи операторов %elifdef и %elifndef.

4.3.2 %ifctx: Проверка контекстного стека

Конструкция условного ассемблирования %ifctx ctxname предполагает обработку идущего вслед за ней кода только тогда, когда на вершине контекстного стека препроцессора находится имя ctxname. Как и в случае с %ifdef, поддерживаются также формы %ifnctx,%elifctx и %elifnctx.

Более подробно о контекстном стеке можно узнать в параграфе 4.6. а пример использования %ifctx приведен в параграфе 4.6.5.

4.3.3 %if: Проверка произвольных числовых выражений

Конструкция условного ассемблирования %if expr будет вызывать обработку последующего кода только в том случае, если выражение expr не нулевое. Примером использования данной конструкции может служить проверка выхода из препроцессорного цикла %rep: пример смотрите в параграфе 4.4.

Выражения, указываемые для %if, а также его эквивалента %elif, являются критическими (см. параграф 3.7).

%if расширяет обычный синтаксис выражений NASM, предусматривая набор операторов отношения, которые в выражениях обычно запрещены. Операторы =, <, >, <=, >= и <> проверяют на равенство, отношения "меньше чем", "больше чем", "меньше или равно", "больше или равно" и на неравенство соответственно. С-подобные формы == и != также поддерживаются и являются альтернативными формами = и <>. И наконец, имеются операторы низкого приоритета &&, ^^ и ||, производящие логические операции И, ИСКЛЮЧАЮЩЕЕ ИЛИ и ИЛИ. Они работают также, как логические операторы в С (за исключением того, что в С нет логического "ИСКЛЮЧАЮЩЕЕ ИЛИ"), то есть возвращают всегда 0 или 1 и обрабатывают любое ненулевое значение как 1 (например, ^^ возвращает 1 только если одно из его входных значений нулевое, а второе — ненулевое). Операторы отношения также возвращают 1 если условие истинно и 0 — если ложно.

4.3.4 %ifidn и %ifidni: Проверка на идентичность текста

Конструкция %ifidn text1,text2 будет вызывать ассемблирование последующего кода только в том случае, если текст аргументов text1 и text2 после развертывания однострочных макросов становится идентичным. Отличия в виде пробелов не считаются.

%ifidni подобна %ifidn, но нечувствительна к регистру символов.

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

%macro pushparam 1 
%ifidni %1,ip 
          call %%label 
%%label: 
%else 
          push %1 
%endif 
%endmacro

Как и большинство других %if-конструкций, %ifidn имеет эквивалент %elifidn и обратные формы %ifnidn и %elifnidn. Соответственно, %ifidni имеет эквивалент %elifidni и обратные формы %ifnidni и %elifnidni.

4.3.5 %ifid, %ifnum, %ifstr: Проверка типов символов

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

Конструкция условного ассемблирования %ifid, принимающая один параметр (который может быть пустым), обрабатывает последующий код только в том случае, если первый символ в параметре существует и является идентификатором. %ifnum работает аналогично, но проверяет символ на соответствие числовой константе; %ifstr тестирует на соответствие символа строке.

Например, описанный в параграфе 4.2.3 макрос writefile может быть расширен для использования преимуществ конструкции %ifstr следующим образом:

%macro writefile 2-3+ 
%ifstr %2 
          jmp %%endstr 
%if %0 = 3 
%%str:	  db %2,%3 
%else 
%%str:	  db %2 
%endif 
%%endstr: mov dx,%%str 
          mov cx,%%endstr-%%str 
%else 
	  mov dx,%2 
	  mov cx,%3 
%endif 
          mov bx,%1 
          mov ah,0x40 
          int 0x21 
%endmacro

После этого макрос writefile может "справиться" со следующими двумя своими вызовами:

          writefile [file], strpointer, length 
          writefile [file], "Привет!", 13, 10

В первом случае strpointer используется в качестве адреса уже объявленной строки, а length — как длина этой строки. Во втором случае макросу передается строка, которую макрос объявляет и получает ее адрес и длину самостоятельно.

Обратите внимание на использование %if внутри %ifstr: это нужно для определения того, передано ли макросу 2 аргумента (в этом случае строка — просто константа и ей достаточно db %2) или больше (в этом случае все аргументы кроме первых двух объединяются в %3 и тогда уже требуется db %2,%3).

Для всех трех конструкций %ifid, %ifnum и %ifstr существуют соответствующие версии %elifXXX, %ifnXXX и %elifnXXX.

4.3.6 %error: Сообщения об ошибках, определяемых пользователем

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

%ifdef SOME_MACRO 
; производятся некоторые настройки 
%elifdef SOME_OTHER_MACRO 
; производятся другие настройки 
%else 
%error Не определены ни SOME_MACRO, ни SOME_OTHER_MACRO. 
%endif

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

4.4 Препроцессорные циклы: %rep

Несмотря на то,что префикс TIMES в NASM весьма удобен, он не может быть использован в многострочных макросах, потому как обрабатывается уже после полного разворачивания последних. Вследствие этого, NASM предусматривает другую форму циклов, работающих на уровне препроцессора, а именно %rep.

Директивы %rep и %endrep (%rep принимает числовой аргумент или выражение; %endrep не принимает никаких аргументов) используются для заключения в них куска кода, который при этом реплицируется столько раз, сколько указано препроцессору:

%assign i 0 
%rep 64 
          inc word [table+2*i] 
%assign i i+1 
%endrep

Этот пример будет генерировать 64 инструкции INC; инкрементируя каждое слово в памяти от [table] до [table+126].

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

fibonacci: 
%assign i 0 
%assign j 1 
%rep 100 
%if j > 65535 
%exitrep 
%endif 
          dw j 
%assign k j+i 
%assign i j 
%assign j k 
%endrep 
fib_number equ ($-fibonacci)/2

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

4.5 Подключение других файлов

Препроцессор NASMа, используя очень похожий на С синтаксис, позволяет подключать к текщему исходнику другие файлы. Это осуществляется при помощи директивы %include:

%include "macros.mac"

Эта строка включит файл macros.mac в исходный файл, содержащий директиву %include.

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

Стандартная идиома С, предотвращающая многократное включение одного и того же файла, точно также срабатывает и в NASM: если файл macros.mac имеет форму

%ifndef MACROS_MAC 
%define MACROS_MAC 
; какие-то определения и объявления 
%endif

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

При помощи ключа командной строки -p (см. параграф 2.1.7) вы можете подключить файл даже не используя явным образом директиву %include в файле-потребителе.

4.6 Контекстный стек

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

NASM обеспечивает данный уровень гибкости при помощи контекстного стека. Препроцессор поддерживает стек контекстов, каждый из которых характеризуется именем. Добавление нового контекста в стек осуществляется директивой %push, а извлечение из стека — директивой %pop. Вы можете определять метки, являющиеся локальными по отношению к определенному контексту в стеке.

4.6.1 %push и %pop: Создание и удаление контекста

Директива %push используется для создания нового контекста и помещения его на вершину контекстного стека. Эта директива требует указания одного аргумента, а именно имени контекста. Например:

%push foobar

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

Директива %pop, не требующая аргументов, удалает самый верхний контекст из стека и разрушает его вместе с любыми связанными с ним метками.

4.6.2 Контектно-локальные метки

Точно так же, как использование %%foo вводит локальную по отношению к определенному макросу метку, конструкция %$foo используется для определения метки, локальной по отношению к контексту на вершине контекстного стека. Таким образом, пример с циклом REPEAT – UNTIL может быть реализован следующим образом:

%macro repeat 0 
%push repeat 
%$begin: 
%endmacro

%macro until 1 
          j%-1 %$begin 
%pop 
%endmacro

Вызовы могут производиться, например, так:

          mov cx,string 
          repeat 
          add cx,3 
          scasb 
          until e

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

Если вам требуется определить или получить доступ к меткам, локальным к контексту, находящемуся ниже вершины стека, вы можете использовать %$$foo, или %$$$foo для еще более "глубокого" контекста и т.д.

4.6.3 Контекстно-локальные однострочные макросы

NASM позволяет определять однострочные макросы, локальные по отношению к определенному контексту. Например,

%define %$localmac 3

будет определять однострочный макрос %$localmac, локальный к контексту на вершине стека. Само собой, после создания еще одного контекста директивой %push, данный макрос может быть доступен по имени %$$localmac.

4.6.4 %repl: Переименование контекста

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

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

%pop 
%push newname

на недеструктивную версию %repl newname.

4.6.5 Пример использования контекстного стека: Блок IF

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

%macro if 1 
    %push if 
    j%-1 %$ifnot 
%endmacro

%macro else 0 
    %ifctx if 
        %repl else 
        jmp %$ifend 
        %$ifnot: 
    %else 
        %error "Перед 'else' ожидается 'if' !" 
    %endif 
%endmacro

%macro endif 0 
    %ifctx if 
        %$ifnot: 
        %pop 
    %elifctx else 
        %$ifend: 
        %pop 
    %else 
        %error "Перед 'endif' ожидается 'if' или 'else'!" 
    %endif 
%endmacro

Данный код более устойчив, чем макросы REPEAT и UNTIL из параграфа 4.6.2, так как он использует условное ассемблирование для проверки правильного порядка следования макросов (например, нельзя перед if вызвать endif) и привлекает директиву %error, если порядок нарушен.

Кроме того, макрос endif способен справиться сразу с двумя разными условиями: следует ли он сразу за if или за else. Достигается это опять же за счет условного ассемблирования, при котором производятся разные действия в зависимости от того, что на вершине стека — if или else.

 

Макрос else должен сохранить контекст в стеке, чтобы метка %$ifnot, на которую ссылается if была той же самой, что и определенная в макросе endif, но в то же время он должен изменить имя контекста, чтобы endif знал, что тут поработал else. Он делает это при помощи %repl.

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

          cmp ax,bx 
          if ae 
            cmp bx,cx 
            if ae 
              mov ax,cx 
            else 
              mov ax,bx 
            endif 
          else 
            cmp ax,cx 
            if ae 
              mov ax,cx 
            endif 
          endif

Макросы блока IF совершенно спокойно обрабатывают вложенность посредством сохранения контекста, вводимого внутри if поверх контекста, описанного извне if; таким образом else и endif всегда ссылаются на последние if или else, не имеющие на этот момент пары.

4.7 Стандартные макросы

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

Большинство пользовательских директив ассемблера реализованы как макросы, вызывающие примитивные директивы; все они описываются в главе 5. Оставшийся набор стандартных макросов описан ниже.

4.7.1 __NASM_MAJOR__ и __NASM_MINOR__: Версия NASM

Однострочные макросы __NASM_MAJOR__ и __NASM_MINOR__ разворачиваются соответственно в старшую и младшую части номера версии NASM. Так, в NASM 0.96 __NASM_MAJOR__ будет определен как 0, а __NASM_MINOR__ — как 96.

4.7.2 __FILE__ и __LINE__: Имя файла и номер строки

Как и в препроцессоре С, NASM позволяет пользователю узнать имя файла и номер строки, содержащие текущую инструкцию. Макрос __FILE__ разворачивается в строковую константу, представляющую собой имя текущего входного файла (которое в ходе ассемблирования может изменяться, если используется директива %include), а __LINE__ разворачивается в числовую константу, означающую текущий номер строки во входном файле.

Эти макросы могут быть использованы, например, для передачи макросу отладочной информации, так как вызов __LINE__ внутри макроопределения (неважно, одно- или многострочного) будет возвращать номер строки макровызова, а не строки определения. Так, например, для определения в какой части кода наступает крах, пишется подпрограммка stillhere, которой передается в EAX номер строки, а на выходе получается что-то вроде "строка 155: я еще жива". Затем вы пишете макрос

%macro notdeadyet 0 
          push eax 
          mov eax,__LINE__ 
          call stillhere 
          pop eax 
%endmacro

и "утыкаете" ваш код вызовами notdeadyet до тех пор, пока не найдете точку краха.

4.7.3 STRUC и ENDSTRUC: Объявление структурных типов данных

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

STRUC принимает один параметр, являющийся именем типа данных. Данное имя описывается как символ со значением 0, затем к нему присоединяется суффикс _size и оно определяется как EQU с размером структуры. После того, как STRUC выполнена, вы описываете структуру данных путем определения полей при помощи семейства псевдо-инструкций RESB. В конце описания вы должны вызвать ENDSTRUC.

Например, для определения структуры mytype, содержащей двойное слово, слово, байт и строку, вы можете написать:

          struc mytype 
mt_long:  resd 1 
mt_word:  resw 1 
mt_byte:  resb 1 
mt_str:   resb 32 
          endstruc

Данный код определяет шесть символов: mt_long как 0 (смещение от начала структуры mytype до поля с двойным словом), mt_word как 4, mt_byte как 6, mt_str как 7, mytype_size как 39 и собственно mytype как ноль.

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

          struc mytype 
.long:    resd 1 
.word:    resw 1 
.byte:    resb 1 
.str:     resb 32 
          endstruc

Здесь описываются смещения к полям структуры в виде mytype.long, mytype.word, mytype.byte и mytype.str.

Так как NASM не имеет встроенной поддержки структур, он не поддерживает формы нотации как в языке С с использованием точки для ссылки на элементы структуры (за исключением нотации локальных меток), поэтому код наподобие mov ax,[mystruc.mt_word] будет ошибочным. Константа mt_word подобна любым другим константам, поэтому корректным синтаксисом в этом случае будет mov ax,[mystruc+mt_word] или mov ax,[mystruc+mytype.word].

4.7.4 ISTRUC, AT иIEND: Объявление экземпляров структур

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

mystruc:  istruc mytype 
          at mt_long, dd 123456 
          at mt_word, dw 1024 
          at mt_byte, db 'x' 
          at mt_str, db 'Привет, фуфел!', 13, 10, 0 
          iend

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

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

          at mt_str, db 123,134,145,156,167,178,189 
          db 190,100,0

В зависимости от личных предпочтений, вы можете также полностью пропустить код на строке AT и начать поле структуры со следующей строки:

          at mt_str 
          db 'Привет, фуфел!' 
          db 13,10,0

4.7.5 ALIGN и ALIGNB: Выравнивание данных

Макросы ALIGN и ALIGNB предоставляют удобный способ выравнивания кода или данных по словам, двойным словам, параграфам (16 байт) или другим границам. (В некоторых ассемблерах для этой цели служит директива EVEN). Синтаксис ALIGN и ALIGNB следующий:

          align 4                ; выравнивание по 4-байтной границе 
          align 16               ; выравнивание по параграфам 
          align 8,db 0           ; заполнение 0 вместо NOP 
          align 4,resb 1         ; выравнивание 4 в BSS (неиниц. данные) 
          alignb 4               ; эквивалент предыдущей строки

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

Если второй аргумент не задан, используется значение по умолчанию: NOP для ALIGN и RESB 1 для ALIGNB. Если второй аргумент задан, оба макроса становятся эквивалентными. Обычно вы должны использовать ALIGN в секциях кода и данных, а ALIGNB — в секции BSS. Тогда никакого второго аргумента не понадобится (кроме, конечно, специальных случаев).

Так как ALIGN и ALIGNB являются простыми макросами, проверки ошибок в них нет: они не могут сообщить вам о том, что переданный аргумент не является степенью двойки или что второй аргумент генерирует более одного байта кода. В любом таком случае они будут "молча делать плохие вещи".

ALIGNB (или ALIGN со вторым аргументом RESB 1) могут использоваться при определении структур:

          struc mytype2 
mt_byte:  resb 1 
          alignb 2 
mt_word:  resw 1 
          alignb 4 
mt_long:  resd 1 
mt_str:   resb 32 
          endstruc

Таким образом гарантируется, что члены структуры осмысленно выровнены относительно ее базы.

И последнее замечание: ALIGN и ALIGNB работают относительно начала секции, а не начала адресного пространства в конечном исполнимом файле. Например, выравнивание по параграфам в секциях, гарантирующих свое выравнивание только по двойным словам — пустая трата времени. NASM не в состоянии проверить, что характеристики выравнивания секции подходят для использования ALIGN или ALIGNB.



Перейти на содержание