Страница с сайта Владислава Пирогова Ассемблер и не только.


Такие разные макросы


Введение




Makros (макрос) - греческое слово, обозначающее "большой" или "длинный". В Кратком оксфордском словаре английского языка сказано, что от него происходит префикс macro. В литературе по программированию macro обычно употребляется как синоним слова "макрокоманда".

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

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

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

Общие положения



Макросы делятся на макроопределения (macro definition или просто macro) и макровызовы (macro instruction или macrocode).

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

Признаком макровызова является имя макроопределения и список параметров, если они используются. В процессе компиляции, обнаружив любое имя, MASM ищет его в таблицах имен, описанных в компилируемой программе и в своих таблицах, зарезервированных имен. Если имя соответствует макроопределению, то выполняется макроподстановка (macro substitution). При этом фрагменты ассемблерного кода, содержащиеся в теле макроопределения, включаются в текст программы, сразу компилируются и при отсутствии ошибок передаются в формируемый объектный модуль. В таких случаях макроподстановку иногда называют макрорасширением (macro expansion). Если же тело макроопределения не содержит ассемблерного кода, то в тексте программы и в объектном коде не остается никаких следов от его выполнения (пример 6 данной статьи).

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

По функциональному назначению макроопределения можно разделить на две основные категории:

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

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

В дистрибутивном пакете MASM 6.0 имеется каталог INCLUDE, в котором собраны файлы с макроопределениями различного назначения. Например, файлы bios.inc и dos.inc содержат макроопределения запросов функций BIOS и DOS, соответственно, а файл macros.inc содержит макросы, выполняющие различные вспомогательные действия, в том числе и обработку строк текста. В том же каталоге находятся файлы с другими макроопределениями.

В дистрибутивном пакете MASM32 имеется каталог MACROS, в котором находятся файлы usemacro.inc и macros.asm, содержащие макроопределениями запросов функций, выполняемых 32-x разрядными операционными системами семейства WINDOWS. Если запросы функций BIOS и DOS программируются сравнительно просто, то программировать запросы системных функций WIDOWS, без использования готовых макроопределений просто нецелесообразно.

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

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

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

Макросы для работы со стеком

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

Исходный вариант макроопределений, выполняющих сохранение в стеке и восстановление из стека параметров команд общего назначения, приведен в примере 1. Тексты макросов различаются только одной строкой. У PushArg в ней записана заготовка команды push par, а у PopArg - заготовка команды pop par.

Пример 1. Макроопределения PushArg и PopArg

; Сохранение аргументов в стеке
PushArg macro arg ; заголовок макроопределения
local par ; описание локальной переменной par
; цикл формирования последовательности команд push arg[i]
irp par,; par = arg[i] - имя очередного аргумента
push par ; push arg[i] - заготовка формируемой команды push
endm ; конец цикла (действия директивы irp)
endm ; конец макроопределения
; Выборка аргументов из стека
PopArg macro arg ; заголовок макроопределения
local par ; описание локальной переменной par
; цикл формирования последовательности команд pop arg[i]
irp par,; par = arg[i] - имя очередного аргумента
pop par ; pop arg[i] - заготовка формируемой команды pop
endm ; конец цикла (действия директивы irp)
endm ; конец макроопределения
Описание макроопределения открывает директива MACRO, в данном случае после нее указан один параметр arg, но он является списком имен операндов команд push или pop. Завершает макроопределение директива ENDM, кроме того, она используется для указания конца директив IRP.

Описание local par вводит локальную символьную переменную par. Оно не является обязательным и применяется только для указания локального характера переменных, используемых в данном макросе. Если такое указание не требуется, то описание переменных можно опустить.

Директива IRP является заголовком тела цикла, а завершает его директива ENDM. При каждом повторе цикла символьной переменной par присваивается имя очередного операнда из списка arg. При отсутствии ошибок цикл повторяется до окончания списка. Запись означает, что arg является параметром макроопределения.

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

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

Пример 2. Варианты макровызовов определений примера 1

PushArg ; сохранение содержимого регистров
PopArg ; восстановление содержимого регистров
Несколько замечаний относительно списка параметров:

работа со стеком организована по принципу "первым пришел, последним ушел", поэтому для восстановления сохраненных значений параметров их имена должны располагаться в списках PushArg и PopArg в противоположном порядке;
это не мешает вам при выталкивании из стека сознательно изменить последовательность параметров или указать другие имена;
в списках можно указывать имена любых операндов, использование которых допускают команды push и pop, а не только регистров общего назначения, чтобы подчеркнуть это обстоятельство, макросы имеют имена PushArg и PopArg;
для сохранения содержимого сразу всех регистров общего назначения, лучше использовать не макросы, а команды pusha и popa, это сократит не только исходный текст, но и объектный модуль и несколько ускорит выполнение задачи.
Изменения макроопределений. Текст примера 1 достаточно прост и вполне корректен, тем не менее, в него можно внести некоторые полезные изменения.

Изменение 1. Директива irp морально устарела, начиная с MASM 6.0, появилась ее более привычная запись FOR <тело цикла> ENDM. Директива for обладает некоторыми преимуществами перед irp, но в данном случае их действия одинаковы.

Для использования новой директивы в обоих вариантах примера 1 замените строку irp par, на строку for par, . Логика выполнения макроопределений при такой замене не изменяется.


Замечание
Существуют директивы irpc и forc. Они отличаются от irp и for тем, что при каждом повторе цикла из строки выбирается только один символ.

Изменение 2. В макровызовах примера 2 список имен операндов заключается в угловые скобки. Если вы хотите избавиться от них, то надо внести небольшое изменение в текст примера 1.

Параметры макроопределений могут иметь атрибуты. В данном случае нас интересует только один из них, а именно VARARG. Он указывает на то, что параметр является списком имен, отделенных друг от друга запятыми. Для использования этого атрибута первые строки обоих вариантов примера 1 надо записать так:

PushArg macro arg:vararg ; заголовок макроопределения PushArg
PopArg macro arg:vararg ; заголовок макроопределения PopArg
Теперь при записи макровызовов угловые скобки не нужны, но если вы их укажите, то ошибки не будет.

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


Изменение 3. Можно так изменить текст макроопределения PushArg, что при вызове PopArg не потребуется указание списка операндов. Для этого надо запомнить имена сохраненных в стеке операндов, а затем использовать их при восстановлении из стека.

Полный текст соответствующего примера находится в файле macros.inc в каталоге Include дистрибутивного пакета MASM версии 6.0. Он состоит из трех макроопределений @SaveRegs, @RestoreRegs, @ArgRev и описания глобальной текстовой переменной pregs (имена взяты из файла macros.inc).

Макроопределение @SaveRegs отличается от PushArg только тем, что перед формированием команд push список фактических параметров, макровызова копируется в переменную pregs с помощью строковой директивы CATSTR (см ниже).

Макроопределение @RestoreRegs вызывается без параметров. При его выполнении используется вспомогательное макроопределение @ArgRev. Оно инвертирует список параметров, сохраненный в переменной pregs, и передает его макросу @RestoreRegs, для формирования группы команд pop. Способ инверсии списка параметров показан в примере 6.

Работа с новыми регистрами. У микропроцессоров семейства Pentium появилось сначала восемь 64-х разрядных регистров, имеющих имена от MM0 до MM7, а затем еще восемь 128-ми разрядных регистров с именами от XMM0 до XMM7. Они доступны только новым инструкциям групп MMX, SSE1 и SSE2. Инструкциям общего назначения, к которым относятся push и pop, новые регистры недоступны. Поэтому при работе со стеком пересылка данных выполняется с помощью новых инструкций.

В примере 3 приведены макроопределения для сохранения и восстановления содержимого регистров mmx. Вместо push и pop в текст программы вставляются две команды. Одна из них корректирует указатель стека на размер операнда (8 байтов), а другая выполняет пересылку из регистра в стек или в обратном направлении. Для пересылки 64-х разрядных операндов, вместо обычной команды mov используется MOVQ из группы mmx.

Пример 3. Макроопределения PushMmx и PopMmx

; Сохранение в стеке содержимого регистров mmx
PushMmx macro regs64:vararg ; заголовок макроопределения
; цикл формирования последовательности команд
for par, ; par=regs64[i] очередное имя регистра
sub esp, 8 ; команда коррекции указателя стека movq [esp], par ; заготовка команды пересылки в стек
endm ; конец действия директивы for
endm ; конец макроопределения
; Восстановление из стека содержимого регистров mmx
PopMmx macro regs64:vararg ; заголовок макроопределения
;цикл формирования последовательности команд
for par, ; par=regs64[i] очередное имя регистра
movq par, [esp] ; заготовка команды пересылки из стека
add esp, 8 ; команда коррекции указателя стека
endm ; конец действия директивы for
endm ; конец макроопределения
В примере 3 циклы организованы с помощью директивы for, а список параметров специфицирован как vararg, для исключения угловых скобок, ограничивающих список операндов макровызова. Вариант макровызовов показан в примере 4.

Пример 4. Вызовы макроопределений примера 3

PushMmx mm0, mm1, mm2 ; сохранение содержимого трех регистров
PopMmx mm2, mm1, mm0 ; восстановление содержимого трех регистров
Обращаем ваше внимание на то, что в данном случае список макровызова может содержать только имена 64-х разрядных регистров, но не переменных.

128-ми разрядные регистры. При сохранении или восстановлении содержимого 128-ми разрядных регистров xmm указатель стека должен изменяться на 16 байтов, а для пересылки данных используется команда MOVUPS. Она входит в группу инструкций SSE1 и пересылает не выровненные данные, расположенные в памяти начиная с любого адреса. Для получения новых макроопределений надо переименовать макросы примера 3, а тела циклов для сохранения и выборки записать так:

;при сохранении в стеке содержимого регистров xmm
sub esp, 16 ; команда коррекции указателя стека
movups [esp], par ; заготовка команды пересылки в стек
; при восстановлении из стека содержимого регистров xmm
movups par, [esp] ; заготовка команды пересылки из стека
add esp, 16 ; команда коррекции указателя стека
MASM 6.0 не компилирует новые инструкции микропроцессоров Pentium. Поэтому при работе с макросами, приведенными в примерах 3 и 4, надо либо расширять возможности MASM 6.0 с помощью макроопределений, выполняющих компиляцию новых инструкций, либо использовать версию MASM32 6.14.

Стек числовых регистров. В состав всех микропроцессоров семейства Pentium входит специализированный процессор, выполняющий вычисления с вещественными числами (FPU). Исполняемые им инструкции работают со специальными регистрами, сгруппированными в стек и имеющими имена от st(0) до st(7). Регистр st(0) или просто st находится в верхушке стека и обязательно участвует во всех операциях. Числовые регистры недоступны командам push и pop, поэтому для обмена данными со стеком общего назначения используются инструкции FPU.

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

В примере 5 приведены макроопределения для сохранения и восстановления заданного количества числовых регистров (от 1 до 8).

Пример 5. Макроопределения PushFlt и PopFtt

; Сохранение в стеке содержимого числовых регистров
PushFltr macro num ; заголовок макроопределения
repeat num ; начало цикла формирования команд
sub esp, 8 ; команда коррекции указателя стека
fstp qword ptr [esp] ; команда копирования содержимого st в стек
endm ; конец директивы repeat
endm ; конец макроопределения
; Восстановление из стека содержимого числовых регистров
PopFltr macro num ; заголовок макроопределения
repeat num ; начало цикла формирования команд
fld qword ptr [esp] ; команда копирования из стека в регистр st
add esp, 8 ; команда коррекции указателя стека
endm ; конец директивы repeat
endm ; конец макроопределения

В данном случае невозможно указать список имен числовых регистров, поэтому в примере 5 для организации циклов формирования команд использована директива REPEAT. При каждом выполнении цикла исходное значение num уменьшается на 1, если результат отличен от 0, то цикл повторяется.
Тело цикла состоит из двух команд и заканчивается директивой ENDM. В командах fstp и fld явно указывается размер операнда (qword ptr). В отличие от предыдущих примеров, в данном случае используются готовые команды, не требующие настройки.

Количество освобождаемых при сохранении или занимаемых при восстановлении числовых регистров указывается в макровызове, например PushFltr 4, или PopFltr 4.

Инверсия списка параметров. В некоторых случаях может возникнуть необходимость перед записью в стек изменить порядок параметров на противоположный тому, в котором они перечислены в списке макровызова. Например, при компиляции директивы INVOKE, MASM анализирует язык, на котором составлена основная программа (Си, Бейсик, Фортран или Паскаль) и, в зависимости от этого, выбирает прямой или обратный способ записи в стек параметров подпрограммы.

Список параметров является строкой текста, а для манипуляций со строками в MASM 6.0 были введены следующие четыре директивы:

CatStr - объединение строк;
InStr - определение позиции символа в строке;
SizeStr - определение количества символов в строке;
SubStr - выделение подстроки.

Их можно использовать и как операторы в символьных выражениях, в таком случае перед именем указывается символ @ - @CatStr, @InStr, @SizeStr, @SubStr.

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

Пример 6. Инверсия списка параметров

InvList MACRO arglist ; заголовок макроопределения
LOCAL txt, arg ; описание локальных переменных
txt TEXTEQU <> ; очистка текстовой переменной
% FOR arg, arglist; начало цикла перестановки параметров
txt CATSTR , , txt; запись в txt очередного параметра и запятой
ENDM ; конец цикла перестановки параметров
txt SUBSTR txt, 1, @SizeStr(%txt) - 1; удаление последней запятой в txt
txt CATSTR , txt, > ; окружение содержимого txt угловыми скобками
EXITM txt ; завершение выполнения с передачей имени txt
ENDM ; конец макроопределения
Обратите внимание на то, что в тексте примера 6 нет ни одной ассемблерной команды, следовательно, при его вызове текст программы не изменяется. На основе указанного при вызове списка параметров, макроопределение примера 6 формирует в переменной txt инвертированный список. Предположим, что в качестве параметра arglist передается следующий текст: , после выполнения макроса переменная txt будет содержать инвертированный список, а именно: .

В исходе строка txt пустая. При каждом повторе цикла перестановки строковая директива CATSTR добавляет в ее начало имя очередного элемента списка arglist и запятую после него. После окончания цикла перестановок в конце строки txt окажется лишняя запятая, которую надо исключить.

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

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

Покажем, применение описанного макроса для записи параметров в стек в обратном порядке. Текст соответствующего макроопределения приведен в примере 7. Его основное отличие от примера 1 заключается в том, что в заголовке цикла вместо исходного списка сохраняемых параметров записан макровызов определения примера 6. В процессе компиляции MASM сначала исполняет макрос InvList, преобразующий исходный список параметров, а затем формирует последовательность команд push.

Пример 7. Запись параметров в стек в обратном порядке

PushArg MACRO argum:VARARG ; заголовок макроопределения
local prm ; описание локальной переменной
% FOR prm, InvList (<>); заголовок цикла с вызовом InvList
push prm ; заготовка формируемой команды
ENDM ; конец действия директивы for
ENDM ; конец макроопределения
Пусть вас не смущает обилие скобок при вызове InvList. Круглые скобки ограничивают список параметров, в данном случае он состоит из одного элемента. Две угловые скобки нужны потому, что argum является параметром внешнего макроопределения (PushArg), который специфицирован как vararg.


Замечание
В примерах 6 и 7 перед директивой for указан знак процента (%). Обычно он означает, что текст интерпретируется как выражение.


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

Макросы для компиляции команд



За последние несколько лет сменилось три поколения микропроцессоров семейства Pentium. У процессоров каждого поколения и у разных моделей одного поколения появлялось достаточно много новых инструкций. Например, по сравнению с Pentium III, у Pentium 4 прибавилось сразу 144 инструкции!

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

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

Команда без операндов. Во многих руководствах встречается макроопределение для компиляции инструкции CPUID, которую выполняют все модели микропроцессоров семейства Pentium. Ее могут компилировать только версии MASM, исполняющие директиву .586, в противном случае надо использовать макрос, приведенный в примере 8.

Пример 8. Компиляция инструкции cpuid

CPUID MACRO ; заголовок макроопределения
db 0Fh, 0A2h ; объектный код инструкции cpuid
ENDM ; конец макроопределения
Макровызовом этого определения является запись в исходном тексте программы команды cpuid, вместо нее в текст программы будет включена директива db 0Fh, 0A2h.

Команда имеет операнды. Подходящих для наших целей одноадресных команд нет, поэтому выберем такую группу двухадресных команд, операнды которых адресуются наиболее просто.

У микропроцессора Pentium Pro появились две группы команд условной пересылки CMOVcc и FCMOVcc. Первая группа относится к категории команд общего назначения. Инструкции второй группы исполняет процессор FPU, они применяются при программировании вычислений с плавающей точкой. В записи конкретной команды вместо маленьких букв cc подставляется мнемоническое обозначение условия, например, CMOVNE или FCMOVB.

Компилировать инструкции этих групп могут только версии MASM, начиная с 6.12 (исполняющие директиву .686), в противном случае надо применять специальные макроопределения. Мы опишем макроопределения для компиляции инструкций группы FCMOVcc. Операнды инструкций этой группы могут находиться только в числовых регистрах, что упрощает анализ и преобразование их имен.

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

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

Пример 9. Мета-макро определения инструкций FCMOVcc

FPIDEF MACRO OPNAME, OPCODE1, OPCODE2 ; мета-макроопределение
OPNAME MACRO OP1, OP2 ; заготовка основного определения
; Проверка имени первого операнда
LOCAL fvsym, reg ; описание локальных переменных
IFDIFI , IFDIFI , ; сравнение операндов
.ERR1 %OUT 'Error: First operand must be st'
ENDIF ; конец тела второй директивы IFDIFI
ENDIF ; конец тела первой директивы IFDIFI
; Проверка имени второго операнда
reg SUBSTR , 4, 1 ; выделение номера регистра
fvsym SUBSTR ,1+reg*5,5 ; выбор эталона
IFDIFI fvsym, ; сравнение OP2 с эталоном имени
.ERR2
%OUT 'Error: Second operand must be st(i)'
ENDIF ; конец тела директивы IFDIFI
; Запись кода команды в объектный модуль
db opcode1, opcode2+reg
ENDM ; конец макроопределения opname
ENDM ; конец макроопределения fpidef
; Список вызовов макроопределения FPIDEF
FPIDEF FCMOVB, 0DAh, 0C0h ; пересылка если меньше
FPIDEF FCMOVE, 0DAh, 0C8h ; пересылка если равно
FPIDEF FCMOVBE, 0DAh, 0D0h ; пересылка если меньше или равно
FPIDEF FCMOVU, 0DAh, 0D8h ; пересылка если неупорядочены
FPIDEF FCMOVNB, 0DBh, 0C0h ; пересылка если больше или равно
FPIDEF FCMOVNE, 0DBh, 0C8h ; пересылка если не равно
FPIDEF FCMOVNBE,0DBh, 0D0h ; пересылка если больше
FPIDEF FCMOVNU, 0DBh, 0D8h ; пересылка если упорядочены

Предположим, что в тексте программы встретилась команда "fcmove st, st(4)". MASM ищет любые имена в своих списках зарезервированных символов и в таблицах имен, описанных в компилируемой программе. Обнаружив имя fcmove в списке, приведенном в конце текста примера 9, он формирует макровызов "fpidef fcmove, 0DAh, 0C8h". Макрос fpidef, в свою очередь, вместо OPNAME формирует и исполняет макроопределение "fcmove st, st(4)", которое компилирует команду и вставляет в текст программы директиву "db 0DAh, 0CCh".

Макроопределение OPNAME проверяет имена операндов, формирует код второго операнда в переменной reg и включает в текст программы указанную выше директиву db, описывающую код команды.

Имя первого операнда проверяется сравнением с его допустимыми образцами st или st(0). Для того чтобы результат не зависел от регистра, на котором набраны буквы имени (st, ST, sT, St), сравнение выполняет директива IFDIFI (если различаются). Буква I в конце ее имени указывает на то, что перед сравнением операндов коды всех букв имен приводятся к одному регистру.

Для проверки второго операнда в переменную reg с помощью директивы SUBSTR помещается четвертый символ его имени. Если имя указано правильно, то это будет цифра от 0 до 7. Затем вторая директива SUBSTR копирует в переменную fvsym образец правильной записи имени второго операнда из списка допустимых имен. Наконец, директива IFDIFI сравнивает содержимое fvsym с именем второго операнда. При несовпадении выдается сообщение об ошибке и компиляция прекращается. В случае совпадения в текст программы вставляется код команды.

Дополнение к примеру 9. При выполнении инструкций группы FCMOVcc проверяется состояние разрядов регистра флагов EFLSGS. Обычным инструкциям FPU регистр флагов недоступен. Поэтому разработчики Pentium Pro ввели 4 новые операции, которые помещают результат сравнения операндов в разряды EFLAGS. Эти четыре инструкции имеют тот же формат, что и инструкции группы FCMOVcc. Поэтому для их компиляции можно использовать макроопределение примера 9, просто добавьте в конец его текста следующие пять строчек:

; Дополнение списка вызовов макроопределения FPIDEF
FPIDEF FCOMI, 0DBh, 0F0h ; простое сравнение операндов
FPIDEF FCOMIP, 0DFh, 0F0h ; тоже, но с освобождением st
FPIDEF FUCOMI, 0DBh, 0E8h ; сравнение неупорядоченных операндов
FPIDEF FUCOMIP, 0DFh, 0E8h ; тоже, но с освобождением st

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

Операнд в оперативной памяти. У новых двухадресных инструкций групп MMX, SSE1 и SSE2 один операнд находится в регистре mmx или xmm, а другой либо в регистре, либо в оперативной памяти. В большинстве случаев в оперативной памяти находится второй операнд, исключением являются инструкции пересылки. Они позволяют перемещать данные как из памяти в регистры, так и в обратном направлении.

Составить макроопределения для анализа и преобразования имен новых регистров mmx и xmm несложно. Намного сложнее выполнять анализ и преобразование адресов операндов, находящихся в оперативной памяти. При работе с 32-х разрядными адресами код операнда содержит переменное количество байтов, а в его мнемонической записи допускается использование арифметических и логических выражений. В качестве примера приведем три вполне корректные формы записи адресов операндов:

dword ptr [ebx + 8]; [eax + 4*ecx + 32]; 16[ebx][eax*4];

Очевидно, что для анализа и преобразования подобных выражений нужен разбор всех допустимых случаев, при этом текст макроопределения становится слишком большим и трудно обозримым. Пример макроса, обрабатывающего ограниченный набор способов записи адресов операндов, приведен в Интернет на моем сайте http://www.macro.aaanet.ru/codes.html Но и при разумных ограничениях текст макроса остается достаточно длинным, поэтому некоторые авторы выбирают другой путь. К их числу относится Агнер Фог, разработанные им макросы для компиляции новых инструкций находятся в Интернет, на сайте http://www.agner.org/assem

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

Как уже говорилось, начиная с Pentium Pro, микропроцессоры семейства Pentium поддерживают группу CMOVcc, состоящую из 16-ти инструкций условной пересылки. При работе с версиями MASM 6.0 - 6.11 для компиляции инструкций этой группы нужно макроопределение, текст которого приведен в примере 10.

Пример 10. Макроопределение для компиляции инструкций CMOVcc

CMOVDEF MACRO OPNAME, CCODE ; мета-макро определение
OPNAME MACRO DST, SRC:VARARG ; заготовка макроопределения
LOCAL X, Y ; описание локальных переменных (меток)
X: ; сохранение адреса начала команды
BSF DST, SRC ; компиляция псевдокоманды
Y: ; сохранение адреса конца команды
ORG X+1 ; возврат ко второму байту кода операции
DB CCODE ; изменение кода второго байта команды
ORG Y ; восстановление текущего адреса
ENDM ; конец макроопределения OPNAME
ENDM ; конец макроопределения CMOVDEF
; Начало списка инструкций группы вызовов CMPVDEF
CMOVDEF CMOVO, 40h ; пересылка если переполнение
CMOVDEF CMOVNO, 41h ; пересылка если нет переполнения
CMOVDEF CMOVB, 42h ; пересылка если меньше
CMOVDEF CMOVNB, 43h ; пересылка если больше или равно
; и так далее вплоть до инструкции CMOVG с кодом 4Fh

Текст примера 6.10 состоит из мета-макро определения CMOVDEF и неполного списка его макровызовов с указанием имен 4-х первых инструкций группы CMOVcc и соответствующих им кодов операций.

Предположим, что в тексте программы встретилась команда "cmovb cx, [bx+2]". MASM находит имя инструкции в списке CMOVDEF и вызывает одноименное мета-макро определение с параметрами "CMOVB, 42h". Оно, в свою очередь формирует и выполняет макроопределение "cmovb cx, [bx+2]", в котором производится подмена имени компилируемой инструкции.

Для подмены взята инструкция BSF, ее исполняют микропроцессоры семейства Intel, начиная с модели 386, и могут компилировать все версии MASM, начиная с 5.1. Таким образом, реально компилируется команда "bsf cx, [bx+2]", в результате MASM получается объектный код "0F BC 4F 02". Остается заменить в этом коде содержимое второго байта (BC) на 42, т. е. сформировать код "0F 42 4F 02", соответствующий исходной команде "cmovb cx, [bx+2]".

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

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

Начиная с Intel 386, микропроцессоры могут работать в 16-ти разрядном (реальном) и в 32-х разрядном (защищенном) режимах. Независимо от установленного режима у команд могут быть как 16-ти, так и 32-х разрядные операнды и адреса. Если разрядность операнда или адреса не совпадает с разрядностью установленного режима работы процессора, то у команды появляются один или два префикса. Префикс размера операнда имеет код 66h, а префикс размера адреса имеет код 67h.

При наличии префиксов текущий адрес, сохраненный в примере 10 в переменной X, не соответствует адресу начала кода операции, а содержимое адреса X+1 не является вторым байтом кода операции и его изменять нельзя. Как поступать в таких случаях?

MASM определяет разрядность режима работы микропроцессора исходя из модели памяти, указанной в директиве .MODEL. Допустимы следующие имена моделей: TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE, FLAT. Из них только модель FLAT соответствует 32-х разрядному режиму работы микропроцессора.

Если компилируемые инструкции работают только с 16-ти разрядными операндами и адресами, то вы можете выбрать любую модель, кроме FLAT. И наоборот, если инструкции работают только с 32-х разрядными операндами и адресами, то надо выбрать модель FLAT. Только при выполнении этих условий у команд отсутствуют префиксы, и макроопределение примера 10 является корректным.

Если по каким-то причинам указанное требование невыполнимо, то надо произвести предварительную компиляцию и получить листинг программы. По листингу уточняется наличие и количество префиксов перед командой BSF и вносится изменение в текст макроопределения примера 10. В зависимости от количества префиксов адрес в директиве ORG X+1 изменяется на X+2 или X+3. Ничего лучшего предложить нельзя.

2. Запись адреса операнда, находящегося в оперативной памяти, может состоять из нескольких слов или выражений, разделенных пробелами. При компиляции таких записей с применением макроопределения примера 10 будет выдано аварийное сообщение "слишком много операндов". Для того чтобы в подобных случаях не возникало аварийной ситуации запись адреса в команде надо заключить в угловые скобки, например, . Сказанное не распространяется на пробелы, используемые при записи индексных выражений, заключенных в квадратные скобки.

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

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

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

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

Пример 11. Управление компиляцией макроопределения

IFDEF @version ; если переменная @version определена, то
IF @version GE 612 ; если версия MASM 6.12 или выше, то
; MASM поддерживает инструкции группы P6
.686 ; разрешение компиляции инструкций группы P6
SUPPORT EQU 1 ; определение переменной SUPPORT
ENDIF ; конец действия директивы IF
ENDIF ; конец действия директивы IFDEF
IFNDEF SUPPORT ; если переменная SUPPORT не определена, то
; здесь располагается текст макроопределения для компиляции новых
; инструкции из группы P6, например, полный текст примера 9 или 10.
ENDIF ; конец действия директивы IFNDEF SUPPORT
В примере 6.11 проверяется поддержка MASM инструкций общего назначения, входящих в группу P6. Если вы собираетесь компилировать новые инструкции MMX, то, не изменяя версию, замените в тексте примера 6.11 директиву .686 на .mmx. При компиляции инструкций групп SSE надо изменить номер версии на 614, и вместо директивы .686 указать .xmm.

Замечание
Директивы .686, .mmx и .xmm разрешают компиляцию разных категорий инструкций микропроцессоров, поэтому их можно использовать совместно, но директива .686 должна быть указана первой. По крайней мере, так надо поступать при работе с версией MASM 6.15.


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

Павел Соколенко.