BAULA1 (1110624), страница 18
Текст из файла (страница 18)
Программист доволен: теперь текст его программы значительно сократился, и программа стала более понятной. Таким образом, можно приблизить уровень языка Ассемблер (как мы говорили, это язык низкого уровня) к языку высокого уровня (например, Паскалю). В этом, как мы уже говорили, и состоит одно из назначений механизма макроопределений и макрокоманд – поднять уровень языка, в котором они используются.
Далее, однако, программист может заметить, что некоторые макрокоманды работают не совсем хорошо. Например, на место макрокоманды с допустимым форматом параметров
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
Это макрорасширение макропроцессор "со спокойной совестью" подставит на место нашей макрокоманды. Конечно, позже, на втором этапе, когда Ассемблер будет анализировать правильность программы, для команды add ax,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
mov al,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 (обычно он связан с дисплеем).