Учитывая практическую важность сказанного в состав Макроассемблера, начиная с MASM 6.0, включены специальные средства для описания подпрограмм и оформления их вызова. В данном разделе приведен краткий обзор этих средств и пример их использования. При этом автор стремился выделить наиболее важные вопросы, ответ на которые не всегда очевиден.
В полный комплект поставки MASM 6.0 и последующих версий входит подробный HELP, содержащий описание директив, операторов и прочих атрибутов языка. Работая с Макроассемблером, вы всегда можете получить дополнительные сведения по интересующему вас вопросу.
Алгоритмические языки созданы и создаются для решения различных классов или категорий задач, поэтому они принципиально отличаются друг от друга. Здесь нас интересуют только те различия, которые относятся к действиям, связанным с вызовом процедур. Основная часть вызова процедуры в большинстве алгоритмических языков имеет следующий вид:
имя_процедуры (список_параметров)
Однако соответствующая ей последовательность команд зависит от конкретных особенностей компилятора. Прежде всего, в именах процедуры и параметров могут различаться или не различаться заглавные и строчные буквы. Далее, параметры могут записываться в стек в порядке их перечисления в списке или в обратном порядке. Количество параметров в списке может быть фиксированным или переменным. Наконец, параметры могут удаляться из стека при возврате из процедуры или в вызывающем модуле.
В табл. В.З перечислены особенности компиляторов, свойства которых может учитывать Макроассемблер при компиляции программных модулей.
|
С |
Sy |
St |
В |
F |
Р |
Различаются
строчные и заглавные |
Да |
Да |
Да |
Нет |
Нет |
Нет |
Строчные буквы
в именах преобразуются |
Нет |
Нет |
Нет |
Да |
Да |
Да |
Параметры записываются
в стек в порядке их перечисления в списке |
Нет |
Нет |
Нет |
Да |
Да |
Да |
Параметры записываются
в стек в обратном |
Да |
Да |
Да |
Нет |
Нет |
Нет |
Параметры удаляются
в процедуре при выполнении команды ret |
Нет |
Нет |
* |
Да |
Да |
Да |
Параметры удаляются
в вызывающем модуле (команда add sp, N) |
Да |
Да |
* |
Нет |
Нет |
Нет |
Допустим список
параметров переменной длины (тип varagr) |
Да |
Да |
Да |
Нет |
Нет |
Нет |
В табл. В.З использованы следующие сокращения имен языков: С — Си, Sy — Syscall, St — Stdcall, В — Basic (Бейсик), F — Fortran (Фортран), Р — Pascal (Паскаль). Пояснять смысл имен Си, Бейсик, Фортран и Паскаль нет необходимости. Слова Syscall и Stdcall не соответствуют ни одному из конкретных языков, их точное назначение в HELP не описано, об этом можно только догадываться. Звездочки в столбце St обозначают, что место удаления параметров зависит от формы задания их списка. При обычной форме вызова их удаляет процедура, а если количество параметров переменное, то они удаляются в вызывающем модуле.
Для того чтобы Макроассемблер учитывал при компиляции программного модуля перечисленные в табл. В.З особенности, у директив MODEL, PROC и PROTO (см. ниже) появился новый спецификатор Langtype. Его допустимыми значениями являются имена С, Syscall, Stdcall, Basic, Fortran и Pascal. Имя должно быть указано явно, умолчаний не существует.
Замечание
При описании названий языков в тексте книги мы также указываем русскую
транскрипцию их названий, но в записи параметров на языке Макроассемблера
используются только латинские имена.
Полное описание процедуры
По-прежнему допустима сокращенная форма описания процедуры, при которой указывается только ее тип far или near Однако для того чтобы Макроассемблер взял на себя оформление текста процедуры, необходимо ее полное описание, которое имеет следующий вид:
метка PROC тип [спецификаторы] [, список_параметров]
Тип процедуры желательно указывать всегда, допустимые типы far, farie, far32, near, nearlG И near32. Имена far И near (без Цифр) используются в тех случаях, когда тип процедуры соответствует типу сегмента, в котором она расположена. Тип сегмента, в свою очередь, зависит от модели памяти и директив .386, .486, .586.
Спецификаторы указывают тип языка (описан выше), доступность для других модулей (private, Public, Export) и аргументы пролога и эпилога. По умолчанию выбирается тип языка, описанный в директиве MODEL, общедоступная процедура public и стандартная форма пролога и эпилога, которая описана ниже.
Важно
Разделителями между именами типа и спецификаторов могут быть только пробелы.
Первая запятая является признаком конца спецификаторов и начала списка
параметров. При несоблюдении этого правила Макроассемблер выдает различные
сообщения об ошибках и прерывает компиляцию модуля.
Список параметров по своему назначению аналогичен последовательности директив EQU, описанных в предыдущем разделе и позволяет использовать в основном тексте процедуры имена вместо ссылок на регистр bр. При его обработке Макроассемблер вычисляет смещение каждого параметра относительно регистра bp и подставляет нужные величины в команды подпрограммы. Если вы посмотрите листинг модуля, то увидите значения смещений для всех перечисленных в списке имен.
Параметры должны иметь уникальные имена, отличающиеся от других имен, описанных в данном модуле. После каждого имени указывается символ "двоеточие" и размер параметра (word, dword и т. п.). Если это не сделано, то по умолчанию размер параметра зависит от разрядности сегмента, в котором расположена процедура, — word для 16-разрядных и dword для 32-разрядных сегментов.
Таким образом, в полном описании процедуры появились два наиболее важных элемента — тип языка и список фактических параметров. Они содержат исчерпывающую информацию о способе компиляции процедуры.
Пролог и эпилог
В процессе компиляции подпрограммы Макроассемблер включает в объектный модуль две группы команд, одна из которых называется прологом (prologue), а другая эпилогом (epilogue). Пролог вставляется перед основным текстом подпрограммы, а эпилог перед командой ret. Вставляемые команды вам уже известны.
Стандартный вариант пролога содержит следующие две команды:
push bp ; сохранение исходного содержимого bx
mov bp, sp ; bx = адрес верхушки стека
Соответственно эпилогом являются команды
pop bp ; восстановление содержимого регистра bx
ret N ; изменение команды ret зависит от langtype
Если значением Langtype является Basic, Fortran или Pascal, то при формировании эпилога к команде ret будет добавлен суммарный размер параметров в байтах для очистки стека при возврате из подпрограммы. Если значением Langtype является С, то команда ret не изменяется, а в текст вызывающего модуля вставляется команда add sp, N.
Обычно команды пролога и эпилога отсутствуют в листинге. Для того чтобы они в нем оказались в начале текста модуля, укажите директиву .Listaii (точка перед именем обязательна).
Пролог и эпилог можно как исключить, так и расширить. Для исключения пролога в текст модуля перед описанием сегмента включается директива
OPTION PROLOGUE: NONE
Аналогичная директива существует и для эпилога, но ее использовать не обязательно. Макроассемблер вставляет эпилог только перед командой ret, поэтому для исключения эпилога достаточно использовать имена retn, retf или ret N, в зависимости от обстоятельств. Обратите внимание, наличие или отсутствие эпилога не влияет на включение команды add sp, N, поскольку она расположена в другом программном модуле.
В пролог можно добавить сохранение в стеке содержимого используемых регистров, а в эпилог — их восстановление. Для этого в описание процедуры включается спецификатор USES, а после него перечисляются используемые в подпрограмме регистры.
Замечание
Разделителем между именами регистров является пробел, указание запятых
не допустимо.
Если процедура содержит свой сегмент данных, то в пролог можно включить команды переопределения содержимого регистра ds, при этом в эпилог добавляется команда, восстанавливающая из стека исходное значение регистра ds. Для выполнения этих действий в описание процедуры включается спецификатор <ioadds> (угловые скобки обязательны).
Таким образом, пролог и эпилог позволяют не записывать в основном тексте вспомогательные действия, выполняемые при входе в процедуру и выходе из нее. Целесообразность включения команд пролога и эпилога решается в каждом конкретном случае с учетом назначения процедуры.
Оформление процедуры cnvindec. В комплект поставки Макроассемблера входят исходные тексты программных модулей, ил тюстрирующие различные случаи применения директивы PROC. Тем не менее автор счел целесообразным показать, что изменится в тексте процедуры cnvindec (см. пример В.5), если в ее описание включить полную форму директивы PROC. Измененный текст приведен в примере В.7.
Пример В.7. Измененный текст процедуры cnvindec
.LI STALL разрешаем печатать все
subr SEGMENT word public ' su эг ' ; начало сегмента s,ubr
.386 задаем тип микропроцессора
dten dd 10 константа для умножения на 10
cnvindec PROC FAR PASCAL USES e Зх fs si, address :dword
Ifs si, address fs:si = адрес начала строки текста
mov address, 0 result = 0 очистка результата
cnvloop: xor eax, eax очистка еах
lods byte ptr fs : [si] al = очередной символ строки
cmp al, '0' код символа меньше кода цифры 0 ?
jb endcnv -> да, конец формирования числа
cmp al, '9' код символа больше кода цифры 9 ?
ja endcnv -> да, конец формирования числа
sub al, 30h вычитаем код цифры 0
xchg eax, address переставляем еах и result
mul cs :dten edx: eax = result * 10
add address, eax result = result + eax
jmp short cnvloop -> на начало цикла преобразования
endcnv : pop si восстанавливаем содержимое si
pop fs восстанавливаем содержимое fs
pop edx восстанавливаем содержимое edx
pop bp восстанавливаем содержимое bp
retf возврат из подпрограммы
cnvindec ENDP конец блока процедуры
subr ENDS конец сегмента subr
END конец текста модуля
В тексте примера В.7 отсутствуют директивы PUBLIC и ASSUME. Первая из них не нужна потому, что при полном описании процедура, по умолчанию, является общедоступной. Директива ASSUME необходима только при использовании компилятора MASM 5.1, а данный пример предназначен для компиляции на более поздних версиях, которые не требуют указания этой директивы. Зато в тексте модуля появилась новая директива .Listaii, она нужна для того, чтобы Макроассемблер включил в листинг команды пролога.
В основном тексте процедуры отсутствуют ссылки на регистр bp, вместо них используется имя параметра address. Если вы посмотрите листинг, то увидите, что ему соответствует тип Dword и значение [bp+6].
В пролог, кроме двух стандартных команд, включены команды, выполняющие сохранение в стеке регистров edx, fs и si. Эпилог в данном случае исключен, поскольку использована команда retf, а не ret. Поэтому команды, восстанавливающие содержимое регистров si, fs, edx и bp, включены в текст процедуры, первая из них имеет метку endcnv.
Замечание
Процедура примера В. 7 предназначена для вызова из программных модулей,
составленных на языке ассемблера. Это объясняется тем, что сформированное
число возвращается в стеке, именно по этой причине в тексте процедуры
использована команда retf, исключающая вставку эпилога. Если вызывающий
модуль составлен на алгоритмическом языке, то взять результат из стека,
без специальных ухищрений, невозможно. Проще внести изменения в основной
текст процедуры, позволяющие вызывать ее из модуля, составленного на любом
языке.
Первый вариант таких изменений заключается в том, что перед выходом из процедуры сформированное число помещается в регистр еах. Для этого пять последних команд текста процедуры заменяются следующими:
endcnv: raov еах, address ; копируем результат в еах
ret ; стандартная форма команды возврата
В данном случае выполнение процедуры завершает команда ret, поэтому Макроассемблер вставит перед ней эпилог, а к команде ret добавит операнд, равный 4, для выталкивания параметров из стека.
Обоснованием этого варианта является то, что в алгоритмических языках функция возвращает результат в регистре еах (или ах). Например, в модуле, составленном на Фортране, возможна такая форма вызова данной функции:
argument = cnvindec (string)
Описанный вариант применим, если результатом является только одно число. В общем случае у процедуры появляются дополнительные параметры, содержащие адреса для записи результатов вычислений. В конце раздела В. 4 было сказано, что если выходные параметры заданы в виде адресов, то при возврате из процедуры допустимо их удаление из стека.
При описании примера В.5 говорилось, что в регистре al передается код символа, являющегося ограничителем числа. Если он используется в вызывающем модуле, то к процедуре примера В.7 надо добавить еще один параметр, содержащий адрес для записи кода символа.
Таким образом, при составлении основного текста процедуры важно учитывать, что именно передается в качестве параметра — значение переменной или ее адрес. От этого будут зависеть действия, выполняемые как в самой процедуре, так и в вызывающем модуле.
Директива вызова процедуры
Для вызова процедур, ориентированных на передачу параметров в стеке, предназначена специальная директива:
Invoke имя_процедуры [, список_параметров]
Список, если он указан, содержит фактические параметры, имена которых должны быть явно описаны в вызывающем модуле или объявлены в директиве EXTERN. Параметры отделяются от имени процедуры и друг от друга запятыми. В качестве параметров могут использоваться регистры, пары регистров, имена и адреса переменных. При этом в любом случае размеры фактических параметров должны соответствовать размеру формальных параметров, перечисленных в описании процедуры.
Если в списке указано имя переменной, то процедуре передается
ее значение. Если указано имя регистра, то передается его содержимое.
Для передачи содержимого пары регистров между их именами дважды указывается
символ "двоеточие". Например, если формальный параметр описан
как двойное слово и предназначен для передачи адреса, то в качестве фактического
параметра можно указать пару регистров DS: -.si.
Для передачи адреса переменной перед ее именем указывается ключ ADDR.
Если формальный параметр описан как двойное слово, то процедуре передается
сегмент, в котором описана переменная и ее смещение (адрес) относительно
начала сегмента. Если формальный параметр описан как слово, то процедуре
передается только сегмент, в котором расположена переменная, в некоторых
случаях это может пригодиться на практике.
При передаче адреса вместо имени переменной может быть указано выражение в форме, позволяющей Макроассемблеру однозначно определить адрес (смещение) переменной в сегменте, например, ADDR iinbuf+2.
При обработке директивы invoke Макроассемблер вставляет
в объектный модуль группу команд, выполняющих размещение параметров в
стеке, а после них команду вызова процедуры. Обычно результат подстановки
в листинге отсутствует. Чтобы увидеть его в начале текста модуля, надо
поместить директиву .Listaii (точка обязательна).
Последовательность записи параметров в стек зависит от имени, указанного
в качестве Langtype в описании процедуры. Если указаны имена Basic, Fotran
или Pascal, то параметры записываются в стек в порядке их перечисления
в списке директивы invoke. Если же указаны имена С, Syscall или Stdcall,
то параметры записываются в стек в обратном порядке (см. табл. В.З). Это
обеспечивает соответствие способов записи и использования передаваемых
в стеке параметров.
Вызов cnvindec
В качестве примера покажем, как может выглядеть вызов процедуры, описанной в примере В.7. Для этого в вызывающем модуле указывается следующая директива:
Invoke cnvindec, ADDR linbuf
Предполагается, что имя linbuf соответствует буферу, содержащему строку цифр в коде ASCII (см. раздел В.З). Если linbuf расположена в сегменте данных, то директива преобразуется в следующие команды:
push ds ; запись содержимого ds
push offset linbuf ; запись адреса linbuf
call cnvindec ; вызов подпрограммы cnvindec
Процедура содержит один параметр, поэтому последовательность команд, записывающих величины в стек, не зависит от языка.
Замечание
Описание процедуры обязательно должно предшествовать ее вызову директивой
Invoke. Поэтому если вызывающая часть и процедура находятся в одном программном
модуле, то текст процедуры должен быть расположен перед текстом вызывающей
части. Это требование остается в силе независимо от того, в одном или
в разных сегментах программного модуля описаны процедура и вызывающая
часть.
Прототип процедуры
Если процедура и вызывающий модуль готовятся независимо друг от друга и объединяются только при компоновке задачи, то выполнить указанное выше условие невозможно. В таком случае в текст вызывающего модуля включается прототип процедуры. Для его описания существует специальная директива, имеющая следующий формат:
имя_процедуры PROTO тип [langtype] [, список_параметров ]
Для составления прототипа нужен либо исходный текст процедуры, либо исчерпывающее описание способа вызова соответствующего ей объектного или библиотечного модуля, либо образец прототипа, взятый из другого программного модуля, вызывающего данную процедуру.
Рассмотрим конкретный случай. Предположим, что процедура,
описанная в примере В. 7, подготовлена в виде объектного модуля. Для создания
ее прототипа надо взять из примера В. 7 строку, содержащую директиву PROC,
заменить слово PROC на PROTO и исключить описание сохраняемых в стеке
регистров (USES edx fs si).
В результате получится следующий прототип:
cnvindec PROTO FAR PASCAL, addressrdword
Из этого примера видно, что для получения прототипа из описания директивы PROC исключаются только те спецификаторы, которые нужны при компиляции процедуры и не используются при компиляции вызывающего модуля.
При компиляции вызывающего модуля Макроассемблеру недоступно описание процедуры, поэтому имя процедуры, ее тип и значение Langtype он "принимает на веру", но имена параметров проверяет обязательно.
В списке PROTO указываются имена формальных параметров. Они не должны совпадать с именами переменных, описанных в вызывающем модуле. В противном случае Макроассемблер выдаст сообщение о повторном определении имени и прервет процесс компиляции. При успешной компиляции имена и размеры параметров включаются в объектный модуль и используются компоновщиком при сборке задачи. На стадии сборки известны описание процедуры и способ ее вызова и компоновщик проверяет их соответствие друг другу.
Заключение.
Практическая ценность директив PROC, PROTO и INVOKE состоит в том, что
при их использовании Макроассемблер самостоятельно учитывает особенности
компиляторов при формировании параметров в стеке и оформлении пролога
и эпилога процедуры. Это позволяет разрабатывать процедуры, не зависящие
от языка программирования, на котором составлен вызывающий модуль. Кроме
того, директива PROTO позволяет использовать в вызывающем модуле, составленном
на языке ассемблера, модули из библиотек, входящих в состав компиляторов
Си, Фортрана, Паскаля и Бейсика. Однако для этого вам должно быть доступно
описание этих библиотек.
В тексте данного раздела мы не упоминали об одной важной детали. Для успешной
разработки общедоступных процедур недостаточно знать только язык ассемблера.
Вы должны иметь представление не только о языке программирования, на котором
будет составлен вызывающий модуль, но и о возможностях конкретного компилятора
с этого языка. Все существующие компиляторы поддерживают расширенные версии
языков программирования, Макроассемблер не является исключением. А вот
в чем заключается это расширение, зависит от конкретной реализации компилятора.
И последнее. Навыки программирования приобретаются не при чтении книг,
а в процессе практической деятельности. Успехов вам на этом поприще, уважаемый
Читатель и Программист!