Макросредства языка Ассемблер.
Сейчас мы переходим к изучению очень важной и сложной темы – макросредств в языках программирования.[62] С этим понятием мы будем знакомиться постепенно, используя примеры из макросредств нашего языка Ассемблера. Здесь следует подчеркнуть, что для полного изучения макросредств Ассемблера обязательно требуется изучение учебника [5].
Заметим, что мы не случайно будем изучать именно макросредства языка Ассемблера, а не макросредства языков программирования высокого уровня (Паскаля, С и т.д.). Всё дело в том, что макросредства в языках высокого уровня чаще всего примитивны (не развиты) и не обеспечивают тех возможностей, которые мы должны изучить в макросредствах. Именно поэтому для уровня университетского образования приходится изучать достаточно развитые макросредства языка Ассемблер.
Макросредства по существу являются алгоритмическим языком, встраиваемым в некоторый другой язык программирования, который в этом случае называется макроязыком. Например, изучаемые нами макросредства встроены в язык Ассемблера, который называется Макроассемблером. Таким образом, программа на Макроассемблере содержит в себе запись двух алгоритмов: один на макроязыке, а второй – собственно на языке Ассемблера.
Как мы знаем из курса первого семестра, у каждого алгоритма должен быть свой исполнитель. Исполнитель алгоритма на макроязыке называется макропроцессором, а исполнителем алгоритма на Ассемблере является, в конечном счете, компьютер. (Не надо путать макропроцессор с процессором компьютера: макропроцессор – это программа, а не часть аппаратуры ЭВМ). Результатом работы макропроцессора (этот исполнитель работает первым) является программный модуль на "чистом" языке Ассемблера, без макросредств. Иногда говорят, что макропроцессор генерирует модуль на Ассемблере. На рис. 12.1 показана схема работы этих двух исполнителей (их часто называют общим именем – Макроассемблер). Программные модули пользователя на этом рисунке заключены в прямоугольники, а системные программы – в прямоугольники с закруглёнными углами.
Работа макропроцессора по обработке макросредств программного модуля называется макропроцессированием. Изучение макросредств языка Ассемблера мы начнём с уже знакомых нам макрокоманд. До сих пор мы говорили, что при обработке программы на Ассемблере на место макрокоманды по определённым правилам подставляется некоторый набор предложений языка Ассемблер. Теперь пришло время подробно изучить, как это делается.
Каждое предложение Ассемблера, являющееся макрокомандой, имеет, как мы знаем, обязательное поле – код операции, который является именем макрокоманды. Именно по коду операции макропроцессор будет определять, что это именно макрокоманда, а не какое-нибудь другое предложение языка Ассемблер. Коды операций макрокоманд являются именами пользователя, а все остальные коды операций – служебными именами.[63] Кроме того, у макрокоманды есть (возможно, пустой) список фактических параметров: [64]
<имя макрокоманды> [<список фактических параметров>]
Итак, каждая макрокоманда имеет имя. Обработка макрокоманды макропроцессором начинается с того, что он, просматривая текст модуля от данной макрокоманды вверх, ищет специальную конструкцию Макроассемблера, которая называется макроопределением (на жаргоне программистов – макросом). Каждое макроопределение имеет имя, и поиск заканчивается, когда макропроцессор находит макроопределение с тем же именем, что и у макрокоманды. Здесь надо сказать, что макропроцессор не считает ошибкой, если в программе будут несколько одноимённых макроопределений, он выбирает из них первое, встреченное при просмотре программы вверх от макрокоманды. Говорят, что новое макроопределение переопределяет одноимённое макроопределение, описанное ранее.
Макроопределение в нашем Макроассемблере имеет следующий синтаксис:
<имя> macro [<список формальных параметров>]
Тело макро-
определения
Endm
Первая строка является директивой – заголовком макроопределения, она определяет его имя и, возможно, список формальных параметров. Список формальных параметров – это (возможно пустая) последовательность имён, разделённых запятыми. Тело макроопределения – это набор (возможно пустой) предложений языка Ассемблера (среди них могут быть и предложения, относящиеся к макросредствам языка). Заканчивается макроопределение директивой endm (обратите внимание, что у этой директивы нет метки, как, скажем, у директивы конца описания процедуры).
Макроопределение может находиться в любом месте программы до первой макрокоманды с таким же именем, но хорошим стилем программирования считается описание всех макроопределений в начале программного модуля. Итак, каждой макрокоманде должно быть поставлено в соответствие макроопределение с таким же именем, иначе в программе фиксируется синтаксическая ошибка. Макроассемблер допускает вложенность одного макроопределения внутрь другого (в отличие от процедур, вложенность которых на Ассемблере, как мы уже знаем, не допускается), однако это редко используется в практике программирования.
Далее, как мы знаем, в макрокоманде на месте поля операндов может задаваться список фактических параметров. Как видим, здесь просматривается большое сходство с механизмом процедур в языке Паскаль, где в описании процедуры мог задаваться список формальных параметров, а в операторе процедуры – список фактических параметров. Однако на этом сходство между Паскалем и Макроассемблером заканчивается, и начинаются различия.
Каждый фактический параметр макрокоманды является строкой символов (возможно пустой). Хорошим аналогом являются строки типа String в Турбо-Паскале, однако, фактические параметры не заключаются в апострофы. Фактические параметры, если их более одного, разделяются запятыми или пробелами. Если фактический параметр расположен не в конце списка параметров и является пустой строкой, то его позиция выделяется запятой, например:
Mymacro X,,Y; Три параметра, второй пустой
Mymacro ,A B; Три параметра, первый пустой
Как видим, в отличие от Паскаля, все параметры макроопределения одного типа – это строки символов, другими словами, всегда есть соответствие по типу между фактическими и формальными параметрами. Далее, в Макроассемблере не должно соблюдаться соответствие в числе параметров: формальных параметров может быть как меньше, так и больше, чем фактических. Если число фактических и формальных параметров не совпадает, то макропроцессор выходит из этого положения совсем просто. Если фактических параметров больше, чем формальных, то лишние (последние) фактические параметры отбрасываются, а если фактических параметров не хватает, по недостающие (последние) фактические параметры считаются пустыми строками символов.
Рассмотрим теперь, как макропроцессор обрабатывает (выполняет) макрокоманду. Сначала, как мы уже говорили, он ищет соответствующее макроопределение, затем начинает передавать фактические параметры (строки символов, возможно пустые) на место формальных параметров (имён). В Паскале, как мы знаем, существуют два способа передачи параметров – по значению и по ссылке. В Макроассемблере реализован другой способ передачи фактических параметров макрокоманды в макроопределение, его нет в Паскале. Этот способ называется передачей по написанию (иногда – передачей по имени). При таком способе передачи параметров все имена формальных параметров в теле макроопределения заменяются соответствующими им фактическими параметрами (строками символов).[65]
Далее начинается просмотр тела макроопределения и поиск в нём предложений, относящихся к макросредствам, например, макрокоманд. Все предложения в макроопределении, относящиеся к макросредствам, обрабатываются макропроцессором так, что в результате получается набор предложений на "чистом" языке Ассемблера, который называется макрорасширением. Последним шагом в обработке макрокоманды является подстановка полученного макрорасширения на место макрокоманды, это действие называется макроподстановкой. На рис. 12.2 показана схема обработки макрокоманды.
Параметры | ||
Макрокоманда | Макроопределение | |
Макроподстановка | Макрорасширение | |
Рис. 12.2. Схема обработки макрокоманды. |
Из рассмотренного механизма обработки макрокоманд вытекает главное применение этого макросредства при программировании на Ассемблере. Как можно заметить, если нам необходимо выполнить в программе некоторое достаточно сложное действие, можно идти двумя путями. Во-первых, можно написать процедуру и вызывать её, передавая ей фактические параметры. Во-вторых, можно написать макроопределение, в теле которого реализовать нужное нам действие, и обращаться к этому макроопределению по соответствующей макрокоманде, также передавая необходимые параметры.
В дальнейшем мы сравним эти два метода, а пока отметим, что написание макроопределений – это хороший способ повысить уровень языка программирования. Действительно, макрокоманда по синтаксису практически ничем не отличается от команд Ассемблера, но может задавать весьма сложное действие. Вспомним, например, макрокоманду inint для ввода целого значения. Соответствующее ей макроопределение по своим функциям похоже на процедуру Read языка Паскаль и реализует достаточно сложный алгоритм по преобразованию вводимых символов в значение целого числа. С точки же зрения программиста в языке Ассемблера как бы появляется новая машинная команда, предназначенная для ввода целых чисел. Говорят, что при помощи макросредств можно расширить язык Ассемблера, как бы вводя в него новые команды, необходимые программисту.
Теперь пришло время написать наше собственное простое макроопределение и на его основе продолжить изучение работы макропроцессора. Предположим, что в программе на Ассемблере приходится неоднократно выполнять оператор присваивания вида z:=x+y, где x,y и z – целочисленные операнды размером в слово. В общем случае для реализации этого оператора присваивания необходимы три команды Ассемблера, например:
mov ax,X
add ax,Y
mov Z,ax
Естественно, что программисту было бы более удобно, если бы в языке Ассемблера существовала трёхадресная команда, которая реализовывала бы такой оператор присваивания, например, команда с именем Sum:
Sum Z,X,Y; Z:=X+Y
Потребуем, чтобы первый операнд этой команды мог иметь форматы r16 и m16, а второй и третий – форматы i16,m16 и r16. Такой команды, как мы знаем, в нашем компьютере нет, но можно создать новую макрокоманду, которая работала бы так, как нам надо. Для этого можно написать, например, такое макроопределение:
Sum macro Z,X,Y
mov ax,X
add ax,Y
mov Z,ax
Endm
Вот теперь, если в нашей программе есть, например, описания переменных
A dw ?
B dw ?
C dw ?
и надо выполнить присваивание C:=A+B, то программист может записать это в виде одного предложения Ассемблера – макрокоманды
Sum C,A,B
Увидев такую макрокоманду, макропроцессор (а он работает раньше Ассемблера),[66] найдёт соответствующее макроопределение с именем Sum и построит следующее макрорасширение:
mov ax,A
add ax,B
mov C,ax
Это макрорасширение и будет подставлено в текст нашей программы вместо макрокоманды Sum C,A,B (произойдёт макроподстановка макрорасширения на место макрокоманды).
Программист доволен: теперь текст его программы значительно сократился, и программа стала более понятной. Таким образом, можно приблизить уровень языка Ассемблер (как мы говорили, это язык низкого уровня) к языку высокого уровня (например, Паскалю). В этом, как мы уже говорили, и состоит одно из назначений механизма макроопределений и макрокоманд – поднять уровень языка, в котором они используются.
Далее, однако, программист может заметить, что некоторые макрокоманды работают не совсем хорошо. Например, на место макрокоманды с допустимым форматом параметров
Sum C,ax,B
будет подставлено макрорасширение
mov ax,ax
add ax,B
mov C,ax
Первая команда в этом макрорасширении, хотя и не влияет на правильность алгоритма, но явно лишняя и портит всю картину. Естественно, что нам хотелось бы убрать из макрорасширения первую команду, если второй операнд макрокоманды является регистром ax. Другими словами, мы бы хотели делать условную макрогенерацию и давать макропроцессору указания вида "если выполняется такое-то условие, то вставляй в макрорасширение вот эти предложения Ассемблера, а иначе – не вставляй". На языке Паскаль такие указания мы записывали в виде условных операторов. Ясно, что и в макроязыке (а, как мы говорили, это тоже алгоритмический язык) тоже должны допускаться аналогичные условные макрооператоры.
В Макроассемблере условные макрооператоры принадлежат к средствам так называемой условной компиляции. Легко понять смысл этого названия, если вспомнить, что при выполнении таких макрооператоров меняется вид компилируемой программы на Ассемблере, в ней появляются те или иные группы предложений. Мы изучим только самые употребительные макрооператоры, которые будем использовать в наших примерах, для полного изучения этой темы необходимо обратиться к учебнику [5].
Итак, мы хотим вставить в наше макроопределение условный макрооператор с таким смыслом:
"Если второй параметр X не идентичен (не совпадает) с именем регистра ax, то тогда необходимо вставить в макрорасширение предложение mov ax,X ".
На Макроассемблере наше макроопределение с именем Sum в этом случае будет иметь такой вид:
Sum macro Z,X,Y
ifdif <X>,<ax>
mov ax,X
Endif
add ax,Y
mov Z,ax
Endm
Поясним работу этого условного макрооператора. Так как его аргументы – строки символов, т.е. он проверяет совпадение или несовпадение двух строк текста, то надо как-то задать эти строки. В качестве ограничителей строки в Макроассемблере выбраны угловые скобки, так что выражение <ax> эквивалентно записи 'ax' в Паскале. Таким образом, семантику нашего условного макрооператора на языке Паскаль можно записать как
if 'X'<>'ax' then
Вставить в макрорасширение mov ax,X
Вся тонкость здесь, однако, состоит в том, что на место имени формального параметра X внутрь кавычек подставляется строка – второй фактический параметр макрокоманды Sum.
Изучая дальше наше макроопределение можно заметить, что и на место, например, макрокоманды
Sum ax,ax,13
подставится макрорасширение
add ax,13
mov ax,ax
с лишней последней строкой. Чтобы это исправить, нам придётся снова изменить наше макроопределение, например, так:
Sum macro Z,X,Y
ifdif <X>,<ax>
mov ax,X
Endif
add ax,Y
ifdif <Z>,<ax>
mov ax,Z
Endif
Endm
Вот теперь на место макрокоманды
Sum ax,ax,13
будет подставляться ну о-очень хорошее макрорасширение
add ax,13
Дальнейшее изучение нашего макроопределения, однако, выявит новую неприятность: макрокоманда
Sum ax,Y,ax
порождает неправильное макрорасширение
mov ax,Y
add ax,ax
Это, конечно, не то же самое, что ax:=ax+Y. Мы можем справиться с этой новой проблемой, снова усложнив наше макроопределение, например, так:
Sum macro Z,X,Y
ifidn <ax>,<Y>
add ax,X
Else
ifdif <ax>,<X>
mov ax,X
Endif
add ax,Y
Endif
ifdif <Z>,<ax>
mov Z,ax
Endif
Endm
В новой версии нашего макроопределения мы использовали вложенные условные макрооператоры. Первый из них с именем ifidn сравнивает свои аргументы-строки текста и вырабатывает значение True, если они идентичны (равны). Как и в условном операторе языка Паскаль, в условном макрооператоре может присутствовать ветвь else, которая выполняется, если при сравнении строк получается значение false. Обязательно проверьте, что для нашей последней макрокоманды Sum ax,Y,ax сейчас тоже получается правильное макрорасширение.
Не нужно, конечно, думать, что теперь мы написали идеальное макроопределение и все проблемы решены. Первая неприятность, которая нас подстерегает, связана с самим механизмом сравнения строк на равенство в условных макрооператорах. Рассмотрим, например, что будет, если записать в нашей программе макрокоманду
Sum AX,X,Y
На её место будет подставлено макрорасширение
mov ax,X
add ax,Y
mov AX,ax
с совершенно лишней последней строкой. Причина здесь в том, что макропроцессор, естественно, считает строки <AX> и <ax> не идентичными, со всеми вытекающими отсюда последствиями. Другая трудность подстерегает нас, если мы попытаемся использовать в нашей программе, например, макрокоманду
Sum ax,bx,dl
После обработке этой макрокоманды будет построено макрорасширение
mov ax,bx
add ax,dl
Это макрорасширение макропроцессор "со спокойной совестью" подставит на место нашей макрокоманды. Конечно, позже, на втором этапе, когда Ассемблер будет анализировать правильность программы, для команды addax,dl зафиксируется ошибка – несоответствие типов операндов. Это очень важный момент – ошибка зафиксирована не при обработке макрокоманды Sum ax,bx,dl , как происходит при обработке синтаксически неправильных обычных команд Ассемблера, а позже и уже при анализе не макрокоманды, а макрорасширения. В этом отношении наша макрокоманда уступает обычным командам Ассемблера. Нам бы, конечно, хотелось, чтобы диагностика об ошибке (и лучше на русском языке) выдавалась уже на этапе обработки макрокоманды макропроцессором. Немного позже мы научимся, как это делать.
Итак, мы обратили внимание на две трудности, которые, как Вы догадываетесь, можно преодолеть, снова усложнив наше макроопределение. Мы, однако, сделаем это не на примере макрокоманды суммирования двух чисел, а на примерах других, более сложных, задач.
Рассмотрим теперь типичный ход мыслей программиста при разработке нового макроопределения. Как мы знаем, для того, чтобы в Ассемблере выполнить вывод текстовой строки, расположенной в сегменте данных, например,
T db 'Строка для вывода$'
необходимо загрузить адрес начала этой строки на регистр dx и выполнить макрокоманду outstr:
mov dx,offset T
Outstr
Ясно, что это не совсем то, что хотелось бы программисту, ему было бы удобнее выводить строку текста, например, так:
outtxt'Строка для вывода'
Осознав такую потребность, программист решает написать новое макроопределение, которое позволяет именно так выводить текстовые строки. Проще всего построить новое макроопределение outtxt на базе уже существующего макроопределения outstr, например, так:
outtxt macro X
local L,T
jmp L
T db X
db '$'
L: push ds; запоминание ds
push cs
pop ds; ds:=cs
push dx; сохранение dx
mov dx,offset T
Outstr
pop dx; восстановление dx
pop ds; восстановление ds
Endm
Обратите внимание, что второй фактический параметр (строку символов) – наше макроопределение располагает внутри макрорасширения (т.е. в сегменте кода). А так как макрокоманда outstr "думает", что выводит текст из сегмента данных, то мы временно совместили сегменты данных и кода, загрузив в регистр ds значение регистра cs.
В этом макроопределении мы использовали новое макросредство – директиву
local L,T
Эта директива объявляет имена L и T локальными именами макроопределения outtxt. Как и локальные имена, например, в языке Паскаль, они не видны извне макроопределения, следовательно, в других частях этого программного модуля также могут использоваться эти имена. Директива local для макропроцессора имеет следующий смысл. При каждом входе в макроопределение локальные имена, перечисленные в этой директиве, получают новые уникальные значения. Обычно макропроцессор выполняет это совсем просто: при первом входе в макроопределение заменяет локальные имена L и T, например, на имена ??0001 и ??0002, при втором входе – на имена ??0003 и ??0004 и т.д. (учтите, что в Ассемблере символ ? относится к буквам и может входить в имена).
Назначение директивы local становится понятным, когда мы рассмотрим, что будет, если эту директиву убрать из нашего макроопределения. В этом случае у двух макрорасширений макрокоманды outtxt будут внутри одинаковые метки L и T, что повлечёт за собой ошибку, которая будет зафиксирована на следующем этапе, когда Ассемблер станет переводить программу на объектный язык.
В качестве следующего примера рассмотрим такую проблему. Мы выводим значения знаковых целых чисел, используя макрокоманду outint. Эта макрокоманда, однако, позволяет выводить целые значения только форматов r16,m16 и i16. Если программисту необходимо часто выводить целые числа ещё и в форматах r8,m8 и i8, то он, естественно, захочет написать для себя новое макроопределение, которое обеспечивает такие более широкие возможности. Используя макроопределение outint как базовое, мы напишем новое макроопределение с именем oint. Ниже приведён вид этого макроопределения.
oint macro X
local K
ifb <X>
%out Нет аргумента в oint!
Err
Exitm
Endif
push ax
K=0
irp i,<al,ah,bl,bh,cl,ch,dl,dh,
AL,AH,BL,BH,CL,CH,DL,DH,
Al,Ah,Bl,Bh,Cl,Ch,Dl,Dh,
aL,aH,bL,bH,cL,cH,dL,dH>
ifidn <i>,<X>
K=1
Endif
Endm
if K EQ 1 or type X EQ byte
push ax
moval,X
Cbw
outint ax
pop ax
Else
outint X
Endif
Endm
В макроопределении oint используется много новых макросредств, поэтому мы сейчас подробно прокомментируем его работу. Вслед за заголовком макроопределения находится уже знакомая нам директива с объявлением локального имени K, затем располагается условный макрооператор с именем ifb, который вырабатывает значение true, если ему задан пустой параметр X (пустая строка символов).
Директива Ассемблера %out предназначена для вывода во время компиляции диагностики об ошибке, текст диагностики программист располагает сразу вслед за первым пробелом после имени директивы %out. Таким образом, программист может задать свою собственную диагностику, которая будет выведена при обнаружении ошибки в макроопределении. В нашем примере диагностика "Нет аргумента в oint!" выводится, если программист забыл задать аргумент у макрокоманды oint. Эта диагностика выводится на так называемое устройство стандартного вывода (stdout), а её копия – на устройство стандартой диагностики об ошибках (stderr).
После того, как макроопределение обнаружит ошибку в своих параметрах, у программиста есть две возможности. Во-первых, можно считать выданную диагностику предупредительной, и продолжать компиляцию программы с последующим получением (или, как говорят, генерацией) объектного модуля. Во-вторых, можно считать обнаруженную ошибку фатальной, и запретить генерацию объектного модуля (Ассемблер, однако, будет продолжать проверку остальной части программы на наличие других ошибок).
В нашем макроопределении мы приняли второе решение и зафиксировали фатальную ошибку, о чём предупредили Ассемблер с помощью директивы .err. Получив эту директиву, Ассемблер вставит в протокол своей работы (листинг) диагностику о фатальной ошибке, обнаруженной в программе, эта ошибка носит обобщённое название forced error (т.е. ошибка, "навязанная" Ассемблеру Макропроцессором). Копия сообщения о фатальной ошибке посылается и в стандартный вывод stderr (обычно он связан с дисплеем).
После выдачи директивы .err дальнейшая обработка макроопределения не имеет накакого смысла, и мы прервали эту обработку, выдав макропроцессору директиву exitm. Эта директива прекращает процесс построения макрорасширения,[67] и в нём остаются только те строки, которые попали туда до выполнения директивы exitm. Например, если вызвать наше макроопределение макрокомандой oint без параметра, то будет получено такое макрорасширение: [68]
Нет аргумента в oint!
Err
Именно оно и будет подставлено на место ошибочной макрокоманды без параметра. На этом примере мы показали, как программист может предусмотреть свою собственную реакцию и диагностику на ошибку в параметрах макрокоманды. Обратите внимание, что реакция на ошибку в макрокоманде производится именно на этапе обработки самой макрокоманды, а не позже, когда Ассемблер будет анализировать полученное макрорасширение.
Следующая директива Макроассемблера
K=0
является макрооператором присваивания и показывает использование нового важного понятия из макросредств нашего Ассемблера – так называемых переменных периода генерации. Это достаточно сложное понятие, и сейчас мы начнём разбираться, что это такое.
Ещё раз напомним, что макросредства по существу являются алгоритмическим языком, поэтому полезно сравнить эти средства, например, с таким алгоритмическим языком, как Паскаль. Сначала мы познакомились с макроопределениями и макрокомандами, которые являются аналогами соответственно описаний процедур и операторов процедур Паскаля. Затем мы изучили некоторые из условных макрооператоров, являющихся аналогами условных операторов Паскаля, а теперь пришла очередь заняться аналогами операторов присваивания, переменных и циклов языка Паскаль в наших макросредствах.
Макропеременные в нашем макроязыке называются переменными периода генерации. Такое название призвано подчеркнуть время существования этих переменных: они порождаются только на период обработки исходного программного модуля на Ассемблере и генерации объектного модуля.
Как и переменные в Паскале, переменные периода генерации в Макроассемблере бывают глобальные и локальные. Глобальные переменные уничтожаются только после построения всего объектного модуля, а локальные, как всегда, после выхода из того макросредства, в котором они порождены. В нашем Макроассемблере различают локальные переменные периода генерации макроопределения (они порождаются при входе в макроопределение и уничтожаются после построения макрорасширения) и локальные переменные – параметры макроцикла с именем irp (они уничтожаются после выхода из этого цикла). В нашем последнем макроопределении локальной является переменная периода генерации с именем K, о чём объявлено в директиве local, и переменная периода генерации с именем i, которая является локальной в макроцикле irp.
Переменные периода генерации могут принимать целочисленные или (в особых случаях) строковые значения. В нашем Ассемблере нет специальной директивы (аналога описания переменных var в Паскале), при выполнении которой порождаются переменные периода генерации (т.е. им отводится место в памяти макропроцессора). У нас переменные периода генерации порождаются автоматически, при присваивании им первого значения. Так, в нашем макроопределении локальная переменная периода генерации с именем K порождается при выполнении макрооператора присваивания K=0, при этом ей, естественно, присваивается нулевое значение.
Следующая важная компонента макросредств Ассемблера – это макроциклы (которые, конечно, должны быть в макросредствах, как в любом "солидном" алгоритмическом языке высокого уровня).[69] В нашем макроопределении мы использовали один из видов макроциклов с именем irp. Этот макроцикл называется циклом с параметром и имеет такой синтаксис (параметр цикла мы назвали именем i):
irp i,<список цикла>
тело цикла
Endm
Параметр цикла является локальной в этом цикле переменной периода генерации, которая может принимать строковые значения. Список цикла (он заключается в угловые скобки) является последовательностью (возможно пустой) текстовых строк, разделённых запятыми (напомним, что в макропроцессоре строки не заключаются в апострофы). В нашем последнем макроопределении такой список макроцикла
<al,ah,bl,bh,cl,ch,dl,dh,AL,AH,BL,BH,CL,CH,DL,DH,
Al,Ah,Bl,Bh,Cl,Ch,Dl,Dh,aL,aH,bL,bH,cL,cH,dL,dH>
Этот список содержит 32 двухбуквенные текстовые строки. Вообще говоря, его необходимо записывать в виде одного предложения Макроассемблера (что возможно, так как максимальная длина предложения в Ассемблере около 130 символов), но в нашем примере мы для удобства изобразили его в две строки. При написании текста макроопределения oint мы изобразили этот список даже в виде четырёх строк, что, конечно, тоже неправильно и сделано только для удобства восприятия нашего примера.
Выполнение макроцикла с именем irp производится по следующему правилу. Сначала переменной цикла присваивается первое значение из списка цикла, после чего выполняется тело цикла, при этом все вхождения в это тело параметра цикла заменяются на текущее значение этой переменой периода генерации. После этого параметру цикла присваивается следующее значение из списка цикла и т.д. Так, в нашем примере тело цикла будет выполняться 32 раза, при этом переменная i будет последовательно принимать значение строк текста al,ah,bl и т.д.
Как можно заметить, целью выполнения макроцикла в нашем примере является присваивание переменной периода генерации K значение единицы, если параметр макрокоманды совпадает по написанию с именем одного из коротких регистров компьютера, причём это имя может задаваться как большими, так и малыми буквами алфавита в любой комбинации. Это позволяет распознать имя короткого регистра, как бы его ни записал пользователь, и присвоить переменной K значение единица, в противном случае переменная K сохраняет нулевое значение.
Далее в макроопределении расположен условный макрооператор нового для нас вида, который, однако, наиболее похож на условный оператор языка Паскаль:
if <логическое выражение>
ветвь then
Else
ветвь else
Endif
На этом примере мы познакомимся с логическими выражениями макропроцессора. Эти выражения весьма похожи на логические выражения Паскаля, только вместо логических констант true и false используются соответственно целые числа 1 и 0, а вместо знаков операций отношения – мнемонические двухбуквенные имена, которые перечислены ниже:
EQ вместо =
NE вместо <>
LT вместо <
LE вместо <=
GT вместо >
GE вместо >=
Таким образом, заголовок нашего условного макрооператора
if K EQ 1 or type X EQ byte
эквивалентен такой записи на Паскале
if (K=1)or (type X = byte) then
Заметим, что в Паскале нам необходимо использовать круглые скобки, так как операция отношения = имеет меньший приоритет, чем операция логического сложения or. В Макроассемблере же, наоборот, операции отношения (EQ,GT и т.д.) имеют более высокий приоритет, чем логические операции (or,and и not), а так как оператор type имеет больший приоритет, чем оператор EQ, то круглые скобки не нужны. По учебнику [5] Вам необходимо обязательно изучить уровни приоритета всех операторов Ассемблера.
Таким образом, наш условный макрооператор после вычисления логического выражения получает значение true, если K=1 (т.е. параметр макрокоманды – это короткий регистр r8) или же для случая, когда type X EQ byte (т.е. параметр макрокоманды имеет формат m8). В остальных случаях (для параметра форматов r16,m16) логическое выражение имеет значение false. Когда это логическое выражение равно true, наше макроопределение вычисляет и помещает на регистр ax целочисленное значение, подлежащее выводу. И так как теперь это значение имеет формат r16, то для его вывода можно использовать уже известную нам макрокоманду outint, а для значения false просто выводить значение параметра X.[70]
Необходимо также заметить, что операции отношения LT,GT,LE и GE, как правила, рассматривают свои операнды как беззнаковые значения. Исключением является случай, когда макропроцессор "видит", что некоторой переменной периода генерации явно присвоено отрицательное значение. Например, рассмотрим следующий фрагмент программы:
L: mov ax,ax; Чтобы была метка, type L=-1
K=type L; Макропроцессор "видит" беззнаковое K=0FFFFh
if K LT 0; Берётся K=0FFFFh > 0 => ЛОЖЬ !
. . .
K=-1; Макропроцессор "видит" знаковое K=-1
if K LT 0; Берётся K=-1 < 0 => ИСТИНА !
. . .
Как видим, здесь вопрос весьма запутан, его надо тщательно изучить по учебнику [5].
Не следует, конечно, думать, что мы написали совсем уж универсальное макроопределение для вывода любых челых чисел, которое всегда выдаёт либо правильный результат, либо диагностику об ошибке в своих параметрах. К сожалению, наше макроопределение не будет выводить значения аргументов форматов m8 и m16, если эти аргументы заданы без имён, по которым можно определить их тип, например, вызов oint [bx] ,будет считаться ошибочным. Это связано с тем, что ошибку вызовет оператор type [bx].
Кроме того, например, при вызове с помощью макрокоманды
oint --8
будет получено макрорасширение
mov ax,--8
outintax
(т.к. type –-8 = 0). К сожалению, наш макропроцессор не предоставляет хороших средств, позволяющих выявить синтаксические ошибки такого рода. Показанные выше ошибки будут выявлены уже компилятором с Ассемблера при анализе полученного макрорасширения.
Далее, нам важно понять принципиальное отличие переменных языка Ассемблера и переменных периода генерации. Так, например, переменная Ассемблера с именем X может, например, определяться предложением резервирования памяти
X dw 13
В то время как переменная периода генерации с именем Y – макрооператором присваивания
Y = 13
Главное – это уяснить, что эти переменные имеют разные и непересекающиеся времена существования. Переменные периода генерации существуют только во время компиляции исходного модуля с языка Ассемблер на объектный язык и заведомо уничтожаются до начала счёта, а переменные Ассемблера, наоборот, существуют только во время счёта программы (до выполнения макрокоманды finish). Некоторые студенты не понимают этого и пытаются использовать переменную Ассемблера на этапе компиляции, например, пишут такой неправильный условный макрооператор
if X EQ 13
Это сразу показывает, что они не понимают суть дела, так как на этапе компиляции хотят анализировать значение переменной X, которая будет существовать только во время счёта программы.
В следующем примере мы покажем, как макроопределение может обрабатывать макрокоманды с переменных числом фактических параметров. Задачи такого рода часто встают перед программистом. Пусть, например, в программе надо часто вычислять максимальное значение от нескольких знаковых целых величин в формате слова. Для решения этой задачи можно написать макроопределение, у которого будет только один формальный параметр, на место которого будет передаваться список (возможно пустой) фактических параметров. Такой список в нашем Макроассемблере заключается в угловые скобки. Пусть, например, макроопределение должно вычислить и поместить на регистр ax максимальное значение из величин bx,X,-13,cx, тогда нужно вызвать это макроопределение с помощью такой макрокоманды (дадим этой макрокоманде имя maxn):
maxn <bx,X,-13,cx>
Здесь один фактический параметр, который, однако, является списком, содержащим четыре "внутренних" параметра. Мы будем также допускать, чтобы некоторые параметры из этого списка опускались (т.е. задавались пустыми строками). При поиске максимума такие пустые параметры будем просто отбрасывать. Далее необходимо договориться, что будет делать макроопределение, если список параметров вообще пуст. В этом случае можно, конечно, выдавать диагностику о фатальной ошибке и запрещать генерацию объектного модуля, но мы поступим более "гуманно": будем в качестве результата выдавать самое маленькое знаковое число (это 8000h в шестнадцатеричной форме записи). Ниже приведён возможный вид макроопределения для решения этой задачи. Наше макроопределение с именем maxn будет вызывать вспомогательное макроопределение с именем cmpax. Это вспомогательное макроопределение загружает на регистр ax максимальное из двух величин: регистра ax и своего единственного параметра X.
maxn macro X
mov ax,8000h; MinInt
irp i,<X>
ifnb <i>
cmpax i
Endif
Endm
Endm
; Вспомогательное макроопределение
cmpax macro X
local L
cmp ax,X
jge L
mov ax,X
L:
Endm
Поясним работу макроопределения maxn, однако сначала, как мы обещали ранее, нам надо существенно уточнить правила передачи фактического параметра (строки символов) на место формального параметра. Дело в том, что некоторые символы, входящие в строку-фактический параметр, являются для макропроцессора служебными и обрабатываются по-особому (такие символы называются в нашем макроязыке макрооператорами). Ниже приведено описание наиболее интересных макрооператоров, полностью их необходимо изучить по учебнику [5].
§ Если фактический параметр заключён в угловые скобки, то они считаются макрооператорами, их обработка заключается в том, что они отбрасываются при передаче фактического параметра на место формального.
§ Символ восклицательного знака (!) является макрооператором, он удаляется из фактического параметра, но при этом блокирует (иногда говорят – экранирует) анализ следующего за ним символа на принадлежность к служебным символам (т.е. макрооператорам). Например, фактический параметр <ab!!+!>> преобразуется в строку ab!+>, именно эта строка и передаётся на место формального параметра. Это один из способов, как можно передать в качестве параметров сами служебные символы.
§ В том случае, если комментарий начинается с двух символов ;; вместо одного, то это макрокомментарий, такой комментарий не переносится в макрорасширение.
§ Символ & является макрооператором, он удаляется макропроцессором из обрабатываемого предложения (заметим, что из двух следующих подряд символов & удаляется только один). Данный символ играет роль лексемы – разделителя, он позволяет выделять в тексте имена формальных параметров макроопределения и переменных периода генерации. Например, пусть в программе есть такой макроцикл
K=1
irp i,<l,h>
K=K+1
mov a&i,X&K&i
Endm
После обработки этого макроцикла Макрокпроцессор подставит на это место в текст программы следующие строки:
mov al,X2l
mov ah,X3h
§ Символ % является макрооператором, он предписывает макропроцессору вычислить следующее за ним арифметическое выражение и подставить значение этого выражения вместо знака %. Например, после обработки предложений
N equ 5
K=1
M equ %(3*K+1)>N
Будут получены предложения
N equ 5
M equ 4>N
Разберём теперь выполнение макроопределения maxn для макрокоманды
maxn <-13,,bx,Z>
На место формального параметра X будет подставлена строка символов -13,,bx,Z. Таким образом, макроцикл принимает следующий вид
irp i,<-13,,bx,Z>
ifnb <i>
cmpax i
Endif
Endm
В теле этого макроцикла располагается условный макрооператор с именем ifnb, он проверяет свой параметр и вырабатывает значение true, если этот параметр не является пустой строкой символов. Таким образом, получается, что в теле макроцикла выполняется условный макрооператор, который только для непустых элементов из списка цикла вызывает вспомогательное макроопределение cmpax. Единственным назначением этого вспомогательного макроопределения является локализация в нём метки L, которая таким образом получает уникальное значение для каждого параметра из списка цикла.
Упражнение. Напишите макроопределение maxn без использования вспомогательного макроопределения. Надо сразу сказать, что для этого требуется хорошее знание языка Ассемблера.
Макроопределения с переменным числом параметров являются достаточно удобным средством при программировании многих задач. Аналогичный механизм (но с совершенно другой реализацией) есть в языке высокого уровня С, который допускает написание функций с переменным числом фактических параметров. Правда, для функций языка С фактических параметров должно быть не менее одного, и значение этого первого параметра должно как-то задавать общее число фактических параметров. Вы будете изучать язык С в следующем семестре.
Теперь мы познакомимся с использованием макросредств для настройки макроопределения на типы передаваемых ему фактических параметров. Здесь имеется в виду, что, хотя с точки зрения макропроцессора фактический параметр – это просто строка символов, но с точки зрения Ассемблера у этого параметра может быть тип. Например, в Ассемблере это может быть длинная или короткая целая переменная, константа и т.д. и ясно, что для обработки операндов разных типов требуются и различные команды.
Как мы помним, для языка Паскаль должно соблюдаться строгое соответствие между типами фактических и формальных параметров процедур. А как быть программисту, если, например, ему надо написать функцию для поиска максимального элемента массива, причём необходимо, чтобы в качестве фактического параметра этой функции можно было бы передавать массивы разных типов (с целыми, вещественными, символьными, логическими и т.д. элементами)? На языках высокого уровня хорошо решить эту задачу практически невозможно.[71]
Сейчас мы увидим, что макросредства предоставляют программисту простое и элегантное решение, позволяющее настраивать макроопределение на тип фактических параметров. Предположим, что нам в программе необходимо суммировать массивы коротких и длинных целых чисел. Для реализации такого суммирования можно написать макроопределение, например, с таким заголовком:
SumMas macro X,N
В качестве первого параметра этого макроопределения можно задавать массивы коротких или длинных знаковых целых чисел, длина массива указывается во втором параметре. Другими словами, первый операнд макрокоманды SumMas может быть формата m8 или m16, а второй – формата m16 или i16. Сумма должна возвращаться на регистре ax (т.е. наше макроопределение – в некотором смысле функция). Допускается, чтобы второй параметр был опущен, в этом случае по умолчанию считается, что в массиве 100 элементов. Для простоты наше макроопределение не будет сохранять и восстанавливать используемые регистры, а переполнение при сложении будем игнорировать. Кроме того, не будем проверять допустимость типа второго параметра, например, передачу в качестве второго параметра короткого регистра r8 или какой-нибудь метки программы. Ниже показан возможный вариант такого макроопределения.
SumMas macro X,N
Local K,L
ifb <X>
%out Нет массива!
Err
Exitm
Endif
ifb <N>
%out Берём длину=100
mov cx,100
Else
mov cx,N
Endif
if type X LT 1 or type X GT 2
%out Плохой тип массива!
Err
Exitm
Endif
K=word
if type X EQ byte
K=byte
Endif
lea bx,X
xor dx,dx; Сумма:=0
if K EQ byte
L: mov al,[bx]
Cbw
add dx,ax
Else
L: add dx,[bx]
Endif
add bx,K
loop L
mov ax,dx
Endm
Как видим, наше макроопределение настраивается на тип переданного массива и оставляет в макрорасширении только команды, предназначенные для работы с элементами массива именно этого требуемого типа. Заметьте также, что вся эта работа по настройке на нужный тип параметров производится до начала счёта (на этапе компиляции), то есть на машинном языке получается эффективная программа, не содержащая никаких лишних команд.
Упражнение. Объясните, для чего предназначено показанное ниже макроопределение и как к нему следует обращаться:
BegProc macroR
push bp
mov bp,sp
irp i,<R>
push i
Endm
Endm
В качестве ещё одного примера рассмотрим следующую задачу. Пусть программисту на Ассемблере необходимо много раз вызывать различные процедуры со стандартным соглашением о связях, передавая им каждый раз досточно большое количество параметров. Естественно, программист хочет автоматизировать процесс вызова процедуры, написав макроопределение, которой позволяет вызывать процедуры почти так же компактно, как и в языках высокого уровня. Например, пусть в Паскале есть описание процедуры с заголовком
Procedure P(var X:Mas; N:integer; var Y:integer);
Такую процедуру можно, например, вызвать оператором процедуры P(X,400,Y) . В Ассемблере для фактических параметров, описанных, например, так:
X dw 400 dup (?)
Y dw ?
вызов процедуры, как мы знаем, будет, например, производится последовательностью команд:
mov ax,offset X
push ax
mov ax,400
push ax
mov ax,offset Y
push ax
call P
Для автоматизации вызова процедур напишем такое макроопределение:
CallProc macro Name,Param
irp i,<Param>
mov ax,i
push ax
Endm
call Name
Endm
Вот теперь вызов нашей процедуры можно производить одной макрокомандой
CallProc P,<<offset X>,400,<offset Y>>
Разумеется, это выглядит не так красиво, как в Паскале, но здесь уж ничего не поделаешь, хотя для любителей макрооператоров Ассемблера можно предложить и альтернативный вызов нашего макроопрелеления, не перегруженный угловыми скобками:
CallProc P,<offset! X,400,offset! Y>
Как бы то ни было, главная наша цель достигнута – вызов процедуры стал производиться в одно предложение Ассемблера. Заметим, что пустой список фактических параметров можно, как и в Паскале, полностью опускать, например
CallProc F
Посмотрим теперь, как разработчик нашего макроопределения сможет проконтролировать, что в качестве первого параметра передано непустое имя некоторой процедуры (вообще говоря, метки). Проще всего проверить, что переданный параметр не пустой, как мы уже знаем, это можно выполнить, используя условный макрооператор
ifb <Name>
%out Пустое имя процедуры
Err
Exitm
Endif
Несколько сложнее обстоит дело, если наш программист захочет проверить, что в качестве первого параметра передано именно имя, а не какая-нибудь недопустимая строка символов. Поставленную задачу можно решить, например, с помощью такого фрагмента на Макроассемблере:
Nom=1; Номер символа в имени
irpc i,<Name>
Err=1; Признак ошибки в очередном символе имени
if Nom EQ 1
irpc j,<abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOQPRSTUVWXYZ_?@#$>
ifidn <i>,<j>
Err=0
Endif
Endm
Else
irpc j,<abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOQPRSTUVWXYZ_?@#$0123456789>
ifidn <i>,<j>
Err=0
Endif
Endm
Endif
if Err EQ 1
exitm; Выход из цикла irpc
Endif
Endm
if Err EQ 1
%out Плохой символ i в имени Name
Err
exitm; Выход из макроопределения
Endif
Для анализа символов фактического параметра на принадлежность заданному множеству символов мы использовали макроцикл irpc. Выполнение этого макроцикла очень похоже на выполнение макроцикла irp, за исключением того, что параметру цикла каждый раз присваивается очередной один символ, входящий в список цикла. В нашем примере в списке цикла мы указали в первом операторе irpc все символы, с которых может начинаться имя в Ассемблере (это латинские буквы и некоторые символы, приравнивающиеся к буквам). Во втором операторе irpc из нашего примера мы к буквам просто добавили 10 цифр. Заметим, что, как уже отмечалось ранее, каждое предложение надо записывать в одну строку, мы разбили каждый из макроциклов irpc на две строки исключительно для удобства чтения нашего примера, что, конечно же, будет классифицировано Ассемблером как синтаксическая ошибка.
Итак, теперь мы убедились, что в качестве первого параметра наше макроопределение получило некоторое имя. Но является ли полученное нами имя именно именем процедуры (или, в более общем случае, именем команды)? Чтобы выяснить это, в наше макроопределение можно вставить проверку типа полученного имени, например, так:
if type Name NE -1 and type Name NE -2
%out Name не имя процедуры!
Err
Exitm
Endif
Некоторые характеристики имени можно получить также, применив к этому имени одноместный оператор Ассемблера .type. Результатом работы этого оператора является целое значение в формате байта (i8), при этом каждый бит в этом байте, если он установлен в "1", указывает на наличие некоторой характеристики имени. Ниже приведены номера некоторых битов в байте, которое этот оператор вырабатывает, будучи применённым к своему имени-операнду (напомним, что биты в байте нумеруются справа-налево, начиная с нуля):
Так, например, для имени, описанного в Ассемблере как
X dw ?
оператор .type X = 00100001b = 3310. Полностью про этот оператор можно прочитать в учебнике [5], а мы на этом закончим наше краткое знакомство с возможностью макросредств языка Ассемблер.
Дата добавления: 2015-10-05; просмотров: 2154;